Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ tests/genai/.genai_cache/
# Activity logs (local-only, generated by session_activity_logger)
.claude/logs/

# Headless run logs (drain-backlog.sh / triage-and-implement.sh)
logs/drain/
logs/scheduled/

# Python
venv/
*.pyc
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## [Unreleased]

### Added
- **`/implement` pre-flight check — fast-fail on already-merged issues** (Issue #936): `/implement --issues #N` now exits immediately with `BLOCKED` + `exit 1` when the target issue is already CLOSED or when any of the last 20 commits references `#N`. Previously the full pipeline ran to completion before discovering the issue was already done, wasting up to 10 minutes per occurrence. Two independent checks fire: (1) issue state check via the already-fetched `gh issue view` response (free; skipped silently when `gh` is unavailable per AC #5), (2) recent-commit grep via `git log --oneline -n 20 --grep "#N\b"` (always runs, even without `gh`). Pass `--force` to bypass both checks for intentional re-implementation of a closed issue. Free-text invocations without an issue reference are unaffected. `docs/PIPELINE-MODES.md` Mode Selection table updated to document the pre-flight behavior and `--force` override. 4 new structural regression tests in `tests/unit/commands/test_implement_preflight_issue_check.py`.

- **`install.sh --uninstall` — shell-only uninstall path** (Issue #951): Previously, the only documented removal path was `/sync --uninstall` (Python orchestrator), which requires a working Claude CLI. When Claude Code itself is broken or hooks are bricked, `/sync` cannot run. `install.sh` now accepts `--uninstall` (remove hooks and global registration), `--dry-run` (preview plan without mutating anything), and `--repos <list>` (also strip autonomous-dev hooks from named per-repo `.claude/settings.json` files). Two companion scripts — `plugins/autonomous-dev/scripts/uninstall_strip_repo_hooks.py` and `plugins/autonomous-dev/scripts/uninstall_unregister_plugin.py` — handle the per-repo hook stripping and plugin-registry cleanup respectively; both are self-contained (no imports from the plugin's lib tree) so they work even in a partially-broken install. A timestamped backup is created at `~/.claude/backups/uninstall-YYYYMMDD-HHMMSS/` before any mutation. `plugins/autonomous-dev/docs/TROUBLESHOOTING.md` gains an "Uninstalling autonomous-dev" section with a comparison table. `README.md` Uninstall section updated to document both paths. 15 new tests across `tests/unit/scripts/test_uninstall_strip_repo_hooks.py` (6), `tests/unit/scripts/test_uninstall_unregister_plugin.py` (5), and `tests/integration/test_install_sh_uninstall.py` (4).

### Security
Expand Down
2 changes: 2 additions & 0 deletions docs/PIPELINE-MODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ covers:
| **Batch (issues)** | `--issues <nums>` | Process GitHub issues with auto-worktree per issue |
| **Resume** | `--resume <run_id>` | Recover from auto-compact / crash mid-pipeline |

**Pre-flight check (Issue #936)**: When ARGUMENTS contains a single issue reference (`#N`), STEP 0 exits with `BLOCKED` and `exit 1` if the issue is already CLOSED or if a recent commit (last 20) references `#N`. This prevents burning pipeline time on already-merged work. Pass `--force` to skip the check (rare: intentional re-implementation of a closed issue). Free-text invocations (no `#N`) and `gh`-unavailable cases are unaffected.

**Auto-detection**: If no mode flag is given, `/implement` scans the feature description for signals:
- Fix keywords (`failing test`, `broken test`, `flaky test`) → suggests `--fix`
- Light keywords or file paths (`*.md`, `*.json`, `*.yaml`, docs-only) not matching security patterns → suggests `--light`
Expand Down
25 changes: 24 additions & 1 deletion plugins/autonomous-dev/commands/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,16 +212,39 @@ If ARGUMENTS contains an issue reference (`#NNN` or issue number), fetch the iss
```bash
ISSUE_NUMBER=$(echo "ARGUMENTS" | grep -oE '#?([0-9]+)' | head -1 | tr -d '#')
if [ -n "$ISSUE_NUMBER" ]; then
ISSUE_DATA=$(gh issue view "$ISSUE_NUMBER" --json title,body 2>/dev/null)
ISSUE_DATA=$(gh issue view "$ISSUE_NUMBER" --json title,body,state 2>/dev/null)
if [ $? -eq 0 ]; then
ISSUE_TITLE=$(echo "$ISSUE_DATA" | python3 -c "import sys,json; print(json.load(sys.stdin).get('title',''))")
ISSUE_BODY=$(echo "$ISSUE_DATA" | python3 -c "import sys,json; print(json.load(sys.stdin).get('body',''))")
ISSUE_STATE=$(echo "$ISSUE_DATA" | python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))")
fi
fi
```

Store `ISSUE_BODY` and `ISSUE_TITLE` as pipeline context. If `gh issue view` fails, proceed without issue body (ISSUE_BODY remains empty). Do NOT block the pipeline on fetch failure.

**Pre-flight: skip already-merged or already-addressed issues** (Issue #936):

```bash
# Pre-flight: skip if issue is already closed or recently referenced in commits.
# Issue #936: prevent burning pipeline time on already-merged work.
if [ -n "$ISSUE_NUMBER" ] && ! echo "ARGUMENTS" | grep -q -- "--force"; then
# Check 1: issue state (free — uses already-fetched ISSUE_STATE; AC #5: gh failures leave ISSUE_STATE empty, skip silently)
if [ -n "$ISSUE_STATE" ] && [ "$ISSUE_STATE" != "OPEN" ]; then
echo "BLOCKED: Issue #${ISSUE_NUMBER} is ${ISSUE_STATE}, not OPEN. Use --force to override (rare: re-implement intentionally)."
exit 1
fi
# Check 2: recent commit references (always runs, even if gh unavailable; AC #5)
RECENT_FIX=$(git log --oneline -n 20 --grep "#${ISSUE_NUMBER}\b" 2>/dev/null | head -3)
if [ -n "$RECENT_FIX" ]; then
echo "BLOCKED: Issue #${ISSUE_NUMBER} appears already addressed in recent commits:"
echo "$RECENT_FIX"
echo "Use --force to override."
exit 1
fi
fi
```

Activate pipeline state:
```bash
# Garbage-collect stale state files from prior crashed runs (Issue #1048)
Expand Down
160 changes: 160 additions & 0 deletions scripts/drain-all.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/usr/bin/env bash
# Serial single-issue drain of the open-issue backlog.
#
# Lists open issues, excludes epics + already-PR'd, runs /implement N for each
# in headless mode with full harness. Resumable: re-running picks up where it
# left off by skipping issues already attempted.
#
# Usage:
# bash scripts/drain-all.sh # drain everything
# bash scripts/drain-all.sh --phase1 # only the top auto-improvement cluster (~36 issues)
# bash scripts/drain-all.sh --dry # print queue, don't run
# bash scripts/drain-all.sh --label LABEL # filter by GH label
# bash scripts/drain-all.sh --limit 10 # stop after N issues
#
# Background-friendly: run with `nohup bash scripts/drain-all.sh &` and tail
# logs/drain-all/progress.log to watch.

set -euo pipefail
cd "$(dirname "$0")/.."

LOG_DIR="logs/drain-all"
STATE_DIR="$LOG_DIR/state"
PROGRESS_LOG="$LOG_DIR/progress.log"
mkdir -p "$STATE_DIR"

REPO="akaszubski/autonomous-dev"
DRY=0
PHASE1=0
LABEL_FILTER=""
LIMIT=0

while [[ $# -gt 0 ]]; do
case "$1" in
--dry) DRY=1; shift ;;
--phase1) PHASE1=1; shift ;;
--label) LABEL_FILTER="$2"; shift 2 ;;
--limit) LIMIT="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
-h|--help) sed -n '1,20p' "$0"; exit 0 ;;
*) echo "unknown flag: $1"; exit 2 ;;
esac
done

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$PROGRESS_LOG"; }

# Build the queue.
QUEUE_FILE="$LOG_DIR/queue.txt"

if (( PHASE1 )); then
log "Phase 1: using /triage top cluster"
python3 -c "
import sys, os, json
for _p in ('.claude/lib', 'plugins/autonomous-dev/lib', os.path.expanduser('~/.claude/lib')):
if os.path.isdir(_p):
sys.path.insert(0, _p); break
from issue_triage_analyzer import main
import io, contextlib
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
main(['--auto-improvement', '--repo', '$REPO', '--json'])
data = json.loads(buf.getvalue())
clusters = sorted(data, key=lambda c: -c['rank_score'])
print('\n'.join(str(n) for n in clusters[0]['issue_numbers']))
" > "$QUEUE_FILE"
else
log "Full drain: listing all open issues (excluding epics + PR-linked)"
GH_ARGS=(--repo "$REPO" --state open --limit 300 --json number,labels,closedByPullRequestsReferences)
if [[ -n "$LABEL_FILTER" ]]; then GH_ARGS+=(--label "$LABEL_FILTER"); fi
gh issue list "${GH_ARGS[@]}" \
--jq '.[] | select((.labels | map(.name) | contains(["epic"])) | not) | select((.closedByPullRequestsReferences | length) == 0) | .number' \
| sort -n > "$QUEUE_FILE"
fi

TOTAL=$(wc -l < "$QUEUE_FILE" | tr -d ' ')
log "Queue: $TOTAL issues"

if (( LIMIT > 0 )) && (( LIMIT < TOTAL )); then
head -n "$LIMIT" "$QUEUE_FILE" > "$QUEUE_FILE.limited" && mv "$QUEUE_FILE.limited" "$QUEUE_FILE"
TOTAL="$LIMIT"
log "Limited to first $LIMIT"
fi

if (( DRY )); then
log "DRY run — queue contents:"
cat "$QUEUE_FILE"
exit 0
fi

# Pre-flight checks.
if ! git diff --quiet || ! git diff --cached --quiet; then
log "ABORT: working tree dirty"
git status --short
exit 1
fi

if ! command -v claude >/dev/null; then
log "ABORT: claude CLI not on PATH"
exit 1
fi

# Drain loop.
i=0
DONE=0
FAILED=0
SKIPPED=0

while read -r n; do
i=$((i+1))
marker="$STATE_DIR/done-$n"
fail_marker="$STATE_DIR/failed-$n"

if [[ -f "$marker" ]]; then
SKIPPED=$((SKIPPED+1))
log "[$i/$TOTAL] #$n SKIP (already done)"
continue
fi
if [[ -f "$fail_marker" ]]; then
SKIPPED=$((SKIPPED+1))
log "[$i/$TOTAL] #$n SKIP (previously failed; rm $fail_marker to retry)"
continue
fi

log "[$i/$TOTAL] #$n START"
ts="$(date +%Y%m%d-%H%M%S)"
events="$LOG_DIR/${ts}-${n}.events.json"

# Refresh state: working tree must be clean before each iteration.
if ! git diff --quiet || ! git diff --cached --quiet; then
log " abort: tree became dirty between iterations — stopping"
exit 1
fi

rm -f /tmp/implement_pipeline_state.json /tmp/implement_pipeline_state.lock 2>/dev/null || true

if claude \
--print \
--permission-mode acceptEdits \
--output-format stream-json \
--include-hook-events \
--verbose \
--name "drain-all-${n}" \
--setting-sources user,project,local \
"/implement $n" \
> "$events" 2>>"$PROGRESS_LOG"; then
touch "$marker"
DONE=$((DONE+1))
log "[$i/$TOTAL] #$n OK"
else
touch "$fail_marker"
FAILED=$((FAILED+1))
log "[$i/$TOTAL] #$n FAIL (events: $events)"
fi

log " progress: $DONE done, $FAILED failed, $SKIPPED skipped, $((TOTAL - i)) remaining"
done < "$QUEUE_FILE"

log "==="
log "Drain complete. done=$DONE failed=$FAILED skipped=$SKIPPED total=$TOTAL"
log "Failed markers in $STATE_DIR/failed-*"
log "Retry failures by removing markers: rm $STATE_DIR/failed-*"
131 changes: 131 additions & 0 deletions scripts/drain-backlog.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env bash
# Headless drainer for /implement --batch — uses the full harness.
#
# Each batch runs in its own `claude -p` process: fresh context, full hook stack,
# all agents, all enforcement. No --bare, no dangerously-skip-permissions.
# Hooks decide what's allowed; we just don't pause for permission prompts on edits.
#
# Usage:
# bash scripts/drain-backlog.sh # run all batches in order
# bash scripts/drain-backlog.sh --dry # print plan only
# bash scripts/drain-backlog.sh --from 5 # resume from batch index 5
# bash scripts/drain-backlog.sh --only m0 # run a single named batch
# bash scripts/drain-backlog.sh --budget 25 # USD cap per batch (default 50)
#
# Output:
# logs/drain/<ts>-<idx>-<name>.log human-readable session log
# logs/drain/<ts>-<idx>-<name>.events.json stream-json event stream
# logs/drain/state.json resume marker

set -euo pipefail

cd "$(dirname "$0")/.."

LOG_DIR="logs/drain"
STATE_FILE="$LOG_DIR/state.json"
mkdir -p "$LOG_DIR"

# Ordered batches: name:csv-issues. Security first, big epics excluded.
BATCHES=(
"security:958,1040,1111,918,984,978,1018"
"ordering:935,957,936,940,973,1090,1106,1107,1109,1110"
"hook-fps:1001,1002,1031,1032,1038,917,1055"
"coordinator:980,987,988,990,1010,1095"
"plan-critic:903,1006,1073,920,873"
"install:945,952,894,1036,895,977,1108"
"m0:1045,1046,1047,1048"
"m2:1058,1059,1060,1014,1015,961,962,963"
"m3:974,975,976,1025,1027"
"m4m5:964,1022,1026,1044"
"periodic:1099,1100"
"intent:1070,1072,1074,1076,1078,1079"
"refactor:1000,1013,1016,1034"
"doc-drift:900,1061,915"
"misc:924,931,932,981,983,1009,955,956,959,965,912,913,914,916"
"new-feat:909,910,911,979"
)

DRY=0
FROM=0
ONLY=""
while [[ $# -gt 0 ]]; do
case "$1" in
--dry) DRY=1; shift ;;
--from) FROM="$2"; shift 2 ;;
--only) ONLY="$2"; shift 2 ;;
--budget) shift 2 ;; # accepted but ignored — subscription auth, no $ cap
-h|--help) sed -n '1,30p' "$0"; exit 0 ;;
*) echo "unknown flag: $1"; exit 2 ;;
esac
done

# Pre-flight: working tree must be clean (harness commits per batch).
if [[ -z "${SKIP_GIT_CHECK:-}" ]] && ! git diff --quiet || ! git diff --cached --quiet; then
echo "ERROR: working tree dirty. Commit/stash first, or set SKIP_GIT_CHECK=1."
git status --short
exit 1
fi

# Pre-flight: plugin must be loaded.
if ! claude plugin list 2>/dev/null | grep -q autonomous-dev; then
echo "WARN: autonomous-dev plugin not in 'claude plugin list'. Hooks may not fire."
fi

run_batch() {
local idx="$1" name="$2" issues="$3"
local ts; ts="$(date +%Y%m%d-%H%M%S)"
local base="$LOG_DIR/${ts}-${idx}-${name}"
local log="${base}.log"
local events="${base}.events.json"

echo "[$idx/${#BATCHES[@]}] batch=$name issues=$issues -> $log"

# Clear any stale pipeline state from a prior crashed run.
rm -f /tmp/implement_pipeline_state.json /tmp/implement_pipeline_state.lock 2>/dev/null || true

# Run the harness via headless claude. Hooks + agents + gates all active.
# --permission-mode acceptEdits: auto-approve file edits BUT hook decisions still block.
# --output-format stream-json + --include-hook-events: full diagnostic trail.
claude \
--print \
--permission-mode acceptEdits \
--output-format stream-json \
--include-hook-events \
--verbose \
--name "drain-${name}-${ts}" \
--setting-sources user,project,local \
"/implement --batch $issues" \
> "$events" 2> "$log" || {
echo " batch $name FAILED — see $log"
jq -n --arg name "$name" --arg ts "$ts" --arg log "$log" --arg status failed \
'{ts:$ts,name:$name,status:$status,log:$log}' \
>> "$LOG_DIR/state.json"
return 0 # don't abort the queue
}

# Extract last result line for summary.
jq -n --arg name "$name" --arg ts "$ts" --arg log "$log" --arg status ok \
'{ts:$ts,name:$name,status:$status,log:$log}' \
>> "$LOG_DIR/state.json"
echo " ok"
}

i=0
for entry in "${BATCHES[@]}"; do
i=$((i+1))
name="${entry%%:*}"
issues="${entry#*:}"

if [[ -n "$ONLY" && "$ONLY" != "$name" ]]; then continue; fi
if (( i <= FROM )); then continue; fi

if (( DRY )); then
echo "[$i/${#BATCHES[@]}] DRY: claude -p '/implement --batch $issues'"
continue
fi

run_batch "$i" "$name" "$issues"
done

echo
echo "Done. Logs: $LOG_DIR/ State: $STATE_FILE"
Loading
Loading