fix(skills): make /fixer loop until the backlog is drained and park on stall, not fixed counts - #2169
fix(skills): make /fixer loop until the backlog is drained and park on stall, not fixed counts#2169carlos-alm wants to merge 6 commits into
Conversation
…n stall, not fixed counts Two changes: - /fixer previously stopped after one batch of --count issues even when more qualifying open issues remained. It now loops across batches by default (Phase 3), re-fetching a fresh queue and continuing until no qualifying open issues remain or a 15-batch safety cap is hit. --once restores the old single-batch behavior. An empty queue is now reported as a clean stop (exit 0), with an explicit rule against wandering to other repos or inventing unrelated work to fill a batch. - Convergence rounds (2f) and drain passes (Phase 4) were capped at a fixed 3, parking/reporting a PR regardless of whether it was still making real progress. Both now track a per-round/per-pass progress signature and park only after 3 consecutive rounds/passes with zero measurable change (genuinely blocked), backed by a 15-round/15-pass safety cap for runaway cases. Fixes 2 bugs Greptile found on the first attempt at this change (#2165, reverted in #2167, Confidence Score 2/5): - The drain phase never actually removed a merged PR's number from parked.txt, so its length never shrank and a pass that successfully merged a PR was still recorded as making no progress — the drain could wrongly report the remaining PRs as blocked after 3 passes even though each pass was merging something. Step 3 (merge the lowest-ready PR) now explicitly removes it from parked.txt as part of the same step. - Phase 0's resume handling only checked whether state.json existed, so a fresh invocation after a fully-completed prior run inherited that run's stale batches-done count and empty queue.json, causing new issues to be silently skipped or the 15-batch cap to trigger prematurely. Resume now requires queue.json to be non-empty (real unfinished work); otherwise every run-scoped artifact is reset before starting.
| if [ "$PARKED_AFTER" -gt 0 ] && { [ "$DRAIN_STALL" -ge 3 ] || [ "$DRAIN_PASS" -ge 15 ]; }; then | ||
| echo "fixer: stopping drain — $([ "$DRAIN_STALL" -ge 3 ] && echo "3 consecutive passes with no merges (blocked)" || echo "15-pass safety cap reached"). $PARKED_AFTER PR(s) remain: report each as needing human review." | ||
| elif [ "$PARKED_AFTER" -gt 0 ]; then | ||
| echo "fixer: running another pass ($((DRAIN_PASS + 1)))" |
There was a problem hiding this comment.
Fixed in 66125b6 — the merge-lowest-ready-PR step in Phase 4 now also flips that issue's state.json entry from parked to merged (matched by PR number), in the same step that removes the PR from parked.txt. Traced through the logic to confirm: previously only parked.txt shrank, so Phase 5's final report kept listing a successfully-merged issue as parked and undercounted the merged total, exactly as described. (Note: this comment's diff anchor landed on the drain-stall stopping-condition block a few lines below the actual removal snippet — the concern described in the prose is what's fixed.)
| printf '%s\n' "score=${SCORE:-none};unanswered=$UNANSWERED;g3=$(git merge-base --is-ancestor origin/main HEAD && echo ok || echo behind);mergeable=$MERGEABLE;failed=$FAILED_CHECKS" \ | ||
| > .codegraph/fixer/gate-signature |
There was a problem hiding this comment.
There was a problem hiding this comment.
Fixed in b56330b — the gate signature now also folds in the current commit SHA (git rev-parse HEAD), a hash of the full concatenated Greptile review/comment text (not just the extracted N/5 digit), and the specific set of currently-unanswered comment ids (not just their count). Traced through it: a push landing before the score/checks catch up, a re-review that restates the same score for different underlying reasons, or a CI rerun that fails the same named check for a new cause will now all change the signature, so genuine activity between rounds is never misread as zero measurable progress.
| If `.codegraph/fixer/loop-decision` is `loop`, go back to Phase 1 and process another batch — do **not** re-run Phase 0 (the repo slug, test/lint commands, and `state.json` are already established for this run, and re-running Phase 0's resume-detection would misread the mid-run `state.json` as a crash to resume from). If it is `stop`, proceed to Phase: Drain Parked PRs. | ||
|
|
||
| **Exit condition:** `.codegraph/fixer/loop-decision` is `loop` (with a fresh `start-from` persisted and per-batch scratch state cleared) or `stop`. Every batch processed this run is recorded cumulatively in `state.json` — nothing from an earlier batch is lost or overwritten by a later one. |
There was a problem hiding this comment.
Fixed in af322d0 — both the queue-build (Phase 1) and the batch-completion check (Phase 3) now pass --author to 'gh issue list' so the author filter runs server-side instead of being applied client-side after a flat --limit 400 fetch, and the limit is raised to 1000 as a generous safety bound on one author's own backlog rather than a cap on the repo's total open-issue count. Confirmed this was a real gap: with the old client-side filter, issues by other authors interspersed in the 400-issue window could crowd out this author's own qualifying issues, letting REMAINING read 0 while qualifying issues still existed.
Greptile SummaryThe fixer skill now processes multiple issue batches and uses progress-based convergence and drain stopping conditions.
Confidence Score: 3/5The PR is not yet safe to merge because active reruns of the same named CI check can still be classified as repeated zero-progress rounds and cause a PR to be parked. The convergence signature stores failing checks only by name, while the parking logic treats three identical signatures as a stall; a rerun without a new commit or review event therefore remains indistinguishable from inactivity. Files Needing Attention: .claude/skills/fixer/SKILL.md Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
Start["Initialize or resume fixer state"] --> Queue["Build current issue batch"]
Queue --> Empty{"Queue empty?"}
Empty -->|Yes| Done["Exit successfully"]
Empty -->|No| Solve["Solve issues and converge PRs"]
Solve --> BatchCheck{"More qualifying issues?"}
BatchCheck -->|Yes, below cap| Queue
BatchCheck -->|No or --once| Drain["Drain parked PRs"]
Drain --> Report["Reconcile state and report outcomes"]
Reviews (2): Last reviewed commit: "fix: filter gh issue list by author serv..." | Re-trigger Greptile |
Merging a parked PR during the drain phase only removed its number from parked.txt; the corresponding state.json entry stayed status="parked", so Phase 5's final report undercounted merged issues and reported completed work as unfinished. Flip the entry to "merged" (matched by PR number) in the same step that removes it from parked.txt.
Merging a parked PR during the drain phase only removed its number from parked.txt; the corresponding state.json entry stayed status="parked", so Phase 5's final report undercounted merged issues and reported completed work as unfinished. Flip the entry to "merged" (matched by PR number) in the same step that removes it from parked.txt.
…#2169) The stall signature relied on aggregates (score, unanswered count, mergeable, failing check names) that can read identical across two rounds even when real activity happened: a push landing before the score/checks catch up, a re-review restating the same score for different reasons, or a rerun failing the same named check for a new cause. Fold in the current commit SHA, a hash of the full Greptile review text, and the specific set of unanswered comment ids so genuine progress is never misread as a stall.
…ation (#2169) Both the queue-build and the batch-completion check fetched --limit 400 open issues repo-wide and filtered by author client-side. In a repo with more than 400 open issues, entries belonging to the target author could fall outside that window, under-reporting the remaining backlog as drained. Pass --author to gh issue list so filtering happens server-side and raise the limit to a generous safety bound on one author's own queue.
|
Addressed all three findings from the Greptile review (Confidence Score 2/5):
All three were traced through standalone before fixing, per the review guidance. Lint and the full test suite (260 files / 4178 tests) pass locally. Ready for re-review. |
Summary
--countissues and stopped, even if more qualifying open issues remained. It now loops across batches by default (new Phase 3 — Continue the Batch Loop, or Proceed to Drain), re-fetching a fresh queue and continuing until no qualifying open issues remain or a 15-batch safety cap is hit.--oncerestores the old single-batch behavior.exit 0, not an error) — with an explicit rule against searching other repos or inventing unrelated work to fill a batch.This replaces #2165, reverted in #2167. Fixes the 2 issues Greptile found on #2165 (Confidence Score 2/5):
parked.txtshrinking, but nothing removed a PR's number from it after merging — so a pass that successfully merged a PR still looked stalled, and the drain could wrongly report the remaining PRs as blocked after 3 passes. Fixed: the "merge lowest-ready PR" step now explicitly removes that PR fromparked.txt.state.jsonexisted to decide "resume", so a fresh invocation after a fully-completed prior run inherited its stalebatches-donecount and emptyqueue.json— causing new issues to be silently skipped or the 15-batch cap to trigger early. Fixed: resume now requiresqueue.jsonto be non-empty (real unfinished work); otherwise every run-scoped artifact is reset before starting.Test plan
npm run lintclean (skill file is outsidesrc//tests/scope)/fixer --dry-runon a future run to confirm batch/round bookkeeping behaves as designed