flip-ready fails when the pull request is already merged
Area: archon-deliver workflow pack — node flip-ready
The problem
flip-ready treats "the pull request is already merged or closed" as a failure of the node. It is
not one: a merged pull request is the outcome the node exists to produce, already reached by another
hand. The node fails, the failure is unrecoverable, and it takes the rest of the run with it.
The branch
.archon/workflows/sdlc/deliver/archon-deliver.yaml:599 (node flip-ready at :537):
if ! FLIP=$(gh pr ready "$PR_NUMBER" --repo "$ORIGIN_REPO" 2>&1); then
printf '%s\n' "flip-ready: the ready flip failed: $FLIP" >&2
exit 1
figh pr ready refuses a pull request that is not a draft. When the pull request was merged or closed
while the run was still in flight, that refusal is correct at the gh level and wrong at the node
level — the node reads "the command I ran returned non-zero" as "the delivery failed."
Observed, on a self-hosted install
deliver__flip-ready: Bash node 'deliver__flip-ready' failed [exit 1]:
flip-ready: the ready flip failed: X Pull request owner/repo#85 is closed.
Only draft pull requests can be marked as "ready for review"The pull request had been merged 5 minutes earlier, by the operator, while the ship run that created it was still in its deliver phase. Nothing was wrong with the work. The run reported a delivery failure for a delivery that had succeeded.
Why it is unrecoverable
Retrying re-runs gh pr ready against the same merged pull request and fails identically. There is
no input that makes the node succeed, and no operator action inside the run that clears it — the
state it objects to is terminal and outside its control.
The failure is not local
flip-ready's stdout is the delivered URL, and outcome binds it (archon-deliver.yaml:636-641):
- id: outcome
script: outcome
with:
pr_url: "$flip-ready.output"
depends_on: [flip-ready]The engine refuses the binding rather than passing a value from a failed producer:
Node 'outcome' failed before execution: Node 'outcome' binding 'delivered' reads
'$deliver__flip-ready.output', but node 'deliver__flip-ready' failed (...), so its output cannot be
trusted. A binding never falls back to 'if_skipped' for a failed producer — fix the failure, or
guard 'outcome' with a 'when:' condition that excludes the failed branch.That refusal is correct and is not the defect. outcome.py documents the guarantee it protects
— "a failed flip blocks this node instead of handing it a stale value". The defect is that
flip-ready classifies a non-failure as a failure, so a sound cascade fires on it. Fixing this by
relaxing the binding rule would trade a false failure for a false success.
The visible cost on the install above: the run's DAG failed, the producing loop's ship stage
reported every ship run failed, and the loop that drives the factory exited on it. One merged pull
request stopped an unattended automation loop until a human noticed.
Why it is worth solving
A workflow that cannot tolerate its own success being completed by someone else is not safe to run unattended, and unattended is the mode this pack exists for. The trigger is ordinary: an operator merges the pull request from the GitHub UI, a merge queue merges it, or a second run reaches the same pull request. None of those are error conditions, and all of them currently fail the run.
The cost is asymmetric. The false failure produces no useful signal — the artifact it names as delivered exists and is merged — while it destroys the signal that would matter, because the run that genuinely failed has to be told apart from the run that succeeded.
Why now
- The pack is being actively consolidated (
#3127is cited in this node's own comment at:620-624as a pending dependency cleanup), so the node is open for correction. flip-readyalready reads structured pull-request state two lines below the failure (archon-deliver.yaml:606,gh pr view --json isDraft). The missing read is astateread in the same idiom, not new machinery..archon/scripts/__tests__/flip-ready-check-read.test.tsalready drives this node's bash body with a stubbedghonPATH. The regression test for this is cheap and the seam exists.
The desired outcome
flip-ready distinguishes "the flip could not be performed because the pull request is already
terminal" from "the flip failed", and reports the first as the delivery it is.
Concretely, for a pull request that is already merged when the node runs, the node succeeds and emits
the delivered URL on stdout, so outcome reports a completed delivery. The other cases keep their
current strictness.
Invariants that must hold
- The engine's binding rule is not the fix. A failed producer must keep failing its dependents.
The change belongs in how
flip-readyclassifies its own outcome. - Classify on structured state, not on
gh's prose. Read the pull request'sstatewithgh pr view --json state,isDraftand branch onMERGED/CLOSED/OPEN. Do not matchis closedorOnly draft pull requestsin the message text — perAGENTS.md, classifying vendor output with a pattern is a last resort, and this vendor's wording is not a contract. The structured read is also what makes the case testable. - A genuine flip failure must still fail, loudly. Auth failure, network failure, a malformed
selector, a pull request that does not exist — all keep exiting non-zero with
gh's own words, as today. - Do not silently accept a closed-unmerged pull request as a delivery.
CLOSEDwithout merge is not a delivered artifact. Whether it fails the node or reports a structured non-delivery is the implementer's call, but it must not be reported as delivered. - Both reads the node already performs stay. The check-state preflight and the
isDraftread-back are load-bearing; the new state read is additional, not a replacement.
What acceptance looks like
- A test in
.archon/scripts/__tests__/drives the node's bash body with a stubbedghwheregh pr readyexits non-zero with the closed-pull-request message andgh pr view --json statereturnsMERGED. The node exits 0 and prints the pull request URL. - A second test covers the same refusal with
stateCLOSED: the node exits non-zero, and its message names the pull request's state rather than "the ready flip failed". - A third test keeps today's behaviour:
gh pr readyfailing for any other reason still exits 1. - The existing tests in
flip-ready-check-read.test.tsstill pass unchanged. bun run validategreen, with the test count stated.
Evidence
The failing branch, on dev.
$ grep -n "gh pr ready" .archon/workflows/sdlc/deliver/archon-deliver.yaml
599: if ! FLIP=$(gh pr ready "$PR_NUMBER" --repo "$ORIGIN_REPO" 2>&1); thenThe node already reads structured state, in the same body.
$ sed -n '606p' .archon/workflows/sdlc/deliver/archon-deliver.yaml
if ! draft=$(gh pr view "$PR_NUMBER" --repo "$ORIGIN_REPO" --json isDraft --jq .isDraft 2>/dev/null); thenoutcome.py states the guarantee the cascade protects.
$ sed -n '26,32p' .archon/workflows/sdlc/ship/scripts/outcome.py
... That choice is load-bearing twice. The value is proof of the flip,
not of creation: ... And a composed dependency on `deliver` resolves to its
returns node, so a failed flip blocks this node instead of handing it a
stale value, ...The test seam that already exists.
$ sed -n '4,8p' .archon/scripts/__tests__/flip-ready-check-read.test.ts
* The node's bash body is the unit under test: fixtures stub `flip-ready`'s
...
* failed check read must refuse before `gh pr ready` is invoked.The install that hit this runs a pinned earlier revision of this pack; the same body and the same
failure are present there, so the fix belongs on dev and reaches it on the next pin.
Source: coleam00/Archon