phase-pr pushes a commit after reading CI, so its green can describe a stale head #491
Labels
No labels
agent-ready
bug
contract
documentation
duplicate
enhancement
good first issue
help wanted
invalid
needs-human
needs-input
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
iterative-works/iw-cli#491
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?
iw phase-prreads the CI verdict, then creates and pushes a commit of its own. That push moves the pull request's head, so the green it records describes a commit the reviewer will not see.Observed
Opening PR 483 for IW-476 phase 2:
./iw phase-prwaited for CI and saw run 580 pass on the phase commit2ec9d276.chore(IW-476): update review-state for phase 02(3915100f).formatjob failed.0and writtenstatus: awaiting_reviewwithci_gated_sha: 2ec9d276.So the card claimed a green gate while
fj pr status 483showedCI / format ... Failure. The gate did not catch it; a human did.In the code
finish(core/commands/PhasePr.scala:224-249) dispatches on the verdict.CiVerdict.Green(_, sha)callshandOver(gatedSha = Some(sha)), which callsupdateReviewStateForReviewand thenpersist.persist(:333-345) commits the review-state file and pushes the phase branch.The
CiVerdict.Runningbranch at:230-233already guards against exactly this, and says so:The hazard is understood on the
Runningpath and reproduced on theGreenpath.Why it matters
The gate is the signal that a phase is safe to review, and in batch mode it is the signal that a phase is safe to merge. A gate that can report green over a red head does not do the one job it exists for.
Candidate fixes
Runningbranch already assumes.ci_gated_sha../iw ci-gatere-reads CI and stampsci_gated_shawithout pushing anything, which is why it was the safe way to repair PR 483's card by hand.Related, but distinct
ci_wait_starteddisables the wait budget.Recorded as IW-476 LESSONS.md entry 15.
Defect 2: the appearance budget is measured from a head that is gone
CiGate.startOfWait(core/model/CiGate.scala:414-436) takesci_wait_startedfrom the card and measures the whole wait from it, across invocations.emptyVerdict(:492-508) then spendsrunAppearanceBudgetMs(5 minutes) against that stamp. Nothing resets the stamp when the head moves, so a push made after the stamp gets a head with no budget left: the forge has not registered a run for it yet,shaMatchedis empty, and the gate answersNoRunForEvent— "CI did not run" — for a commit that is seconds old.Observed, IW-583 (Forgejo, 2026-09-21). A
batch-implementrun was killed while it waited on PR #588, leavingci_pendingwithci_wait_startedstamped. The session re-ranphase-pr. It pushed review-state commite3da7326, read CI at once, reported "CI did not run" and wroteci_failed. Forgejo run 890 fore3da7326was already queued; a later re-run waited normally and CI passed in about 20 minutes.Why it is the more urgent of the two.
BatchImplement.decideOutcomemapsci_failedtoPhaseOutcome.Fail, which halts the batch with no retry and no gate re-entry, and points the reader at job logs that do not exist for a pipeline that went green. On IW-583 only the live phase session noticed and re-ran the command; an unattended run would have stopped there. A falseci_failedis the sharpest remaining obstacle to running the loop without a person.The two defects share a cause — a verdict read against a head the gate did not measure — and they sit in the same gate code, so one branch fixes both. Defect 1 removes the push that moves the head; defect 2 makes the budget start at the head it is measuring.
Decision on the CLARIFY (Michal, 2026-09-21): F1
A green
phase-prmay leave the review-state card uncommitted. Write the card after the verdict, commit nothing, push nothing.Evidence that settled it:
dashboard/jvm/src/infrastructure/ReviewStateReader.scala), and every kanon plugin reader —BatchImplementation,PhaseRecovery,PhaseCatchUp,Preflight— reads disk. Thepersistscaladoc asserts the card must be published but names no consumer.phase-mergeandphase-advanceboth commit a dirty card locally before they need a clean tree, and IW-583 (PhaseAdvance.scala:228-236) has just hardened that path.9d577408deliberately removed.--batchdoes — was considered and rejected. A laterphase-prrun pushes the branch increatePrAndGatebefore the gate, so that local commit would ride out and move the head anyway. F1 cannot do that.Accepted cost: after every green
phase-prthe worktree carries an uncommittedreview-state.json. An agent may ask about it, and agit stashorreset --hardloses the green record;phase-mergethen does the full wait again, which is slower but safe.Note for the analysis
An earlier analysis of defect 1 alone is committed on branch
IW-491as56475c05. Reuse its findings rather than repeating the work. In particular it establishes, with evidence, that the redformatjob on run 581 was not caused by the card commit:scala-cli fmt --checkcrashed with SIGSEGV (exit 139) after scanning 0 of 512 files, recorded in45bca500and tracked as #497. The card commit only made the stale-head defect visible.Still live on
main(1ab91787), and this issue now carries a second defect that shares its cause. Both are in scope for the fix.Defect 1 — the push after the verdict (this issue as filed)
persist(core/commands/PhasePr.scala:595-608) commits the card and pushes the phase branch, and every path out of the gate calls it:handOver->reportOpenPullRequest->persiston green, andstop->persiston red.recordPullRequest(:581) writes the card before the wait but does not commit it, so the commit and the push still land after the verdict. The head the reviewer sees is not the head that was gated.dev-docs#241 is the same defect seen from the other side: on a forge whose CI triggers on the merge-request event, that bookkeeping push starts a second full pipeline that gates nothing and that nobody waits for. It lists the same first fix — push the card before gating — so one fix closes both.
Defect 2 — the appearance budget is measured from a head that is gone
CiGate.startOfWait(core/model/CiGate.scala:414-436) takesci_wait_startedfrom the card and measures the whole wait from it, across invocations.emptyVerdict(:492-508) then spendsrunAppearanceBudgetMs(5 min) against that stamp. Nothing resets the stamp when the head moves, so a push made after the stamp gets a head with no budget left: the forge has not registered a run for it yet,shaMatchedis empty, and the gate answersNoRunForEvent— "CI did not run" — for a commit that is seconds old.Observed, IW-583 (Forgejo, 2026-09-21). A
batch-implementrun was killed while it waited on PR #588, leavingci_pendingwithci_wait_startedstamped. The session re-ranphase-pr. It pushed review-state commite3da7326, read CI at once, reported "CI did not run" and wroteci_failed. Forgejo run 890 fore3da7326was already queued; a later re-run waited normally and CI passed in about 20 minutes.Why it matters more than it looks.
BatchImplement.decideOutcomemapsci_failedtoPhaseOutcome.Fail, which halts the batch with no retry and no gate re-entry, and points the reader at job logs that do not exist for a pipeline that went green. On IW-583 only the live phase session noticed and re-ran the command. An unattended run would have stopped there. So a falseci_failedis the sharpest remaining obstacle to running the loop without a person.What the fix has to settle
The card records the gate's outcome, so it is written after the gate; pushing it then is what moves the head. Pushing it before the gate fixes the gated-sha and the wasted pipeline, but the outcome write still has to reach the branch somehow. That choice — push the outcome with a
[skip ci]subject, hold it until the merge, or something else — is the design question this issue has to answer, not a detail of it.Whatever the answer, the appearance budget must start at the head it is measuring, not at a stamp an earlier head left behind.
#485 is closed: the premature-green half of this family was implemented in
0881a351/10fc4b09.