batch-implement: ci_pending/ci_fixing should poll for CI, not consume the retry budget #400
Labels
No labels
bug
contract
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
iterative-works/iw-cli#400
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The batch-implement orchestrator conflates two unrelated situations and draws
both from the same scarce retry budget:
implementing,review_failed,context_ready).or being fixed (
ci_pending,ci_fixing).core/model/BatchImplement.scalamaps both toPhaseOutcome.Recover:Recoverre-runs the implementer (the wrong tool for "CI is running") andconsumes one unit of the
--max-retriesbudget (default 1). So a phase thatlegitimately needs to wait for CI can fail with
Exhausted N recovery attempts; still in status 'ci_pending'— especially if anearlier transient already spent the budget.
Observed failure
Seen in the field (kanon
iw-plugin/commands/batch-implement.scaladriving awaterfall phase against a Forgejo/GitLab forge):
ceiling — fixed separately downstream), leaving status
implementing.implementing→Recover→ recovery attempt 1/1 (the only retry spent),which opened the phase MR and reached
ci_pending.ci_pending→Recoveragain, but 0 retries left → batch failed, eventhough CI was simply still running.
The downstream fix (making the session wait for the backgrounded CI watch)
makes this far less likely to trigger, but the design wart remains: a "CI is
running" status should never consume an implementation retry.
Root cause
There is no distinct "poll the forge until CI concludes" path. The orchestrator
only knows three moves — merge, re-dispatch the implementer (
Recover), or fail— so the CI-wait case is forced through
Recoverand is rate-limited by abudget meant for genuine recovery.
The plumbing to do this right already exists:
queryForgeState(and thephase-advancecatch-up logic) can poll the forge for MR/CI state withoutre-dispatching the implementer.
Proposed direction (not prescribing)
ci_pending/ci_fixingtheir own outcome (e.g.PhaseOutcome.AwaitCI)distinct from
Recover.queryForgeState) on an interval until CI concludes — merged → mark done,failed → hand back to claude to fix in-context, still running → keep waiting —
bounded by the per-phase
--claude-timeout, not by--max-retries.--max-retriesexclusively for genuine re-implementation recovery.Acceptance criteria
--max-retriesbudget and is not failed as "Exhausted recovery attempts"while CI is still in progress.
References
terminating the in-agent CI watch (fixed in kanon
batch-implement.scala).track a published surface).
core/model/BatchImplement.scaladecideOutcome; kanoniw-plugin/commands/batch-implement.scalaorchestration loop.