Supersedes #1179 with a full diagnosis.
Symptom
Runs that publish a PR right after their change lands repeatedly come back as draft PRs — including work that is eventually merged — where non-draft PRs are expected. Superseded duplicates pile up: a draft PR is closed and re-created as another draft ("replacement") once its checks finish. The runs behave as if a freshly pushed commit can never be published non-draft.
How it shows up
- Commit a change and push the branch (the repo's pre-push hook already encourages verifying before pushing).
- Immediately run
gh pr create with a body that carries the required verification section.
- The write is refused; the sanctioned exit in the refusal is
--draft; the retried draft create succeeds. The run then ends as a caveated draft handoff, and because a draft cannot satisfy verified completion, the cycle tends to repeat as a replacement draft later.
This is deterministic in that flow, not exceptional: the push-triggered workflows start on the branch push (workflows configured with on: push and cancel-in-progress: true), so the exact head SHA's run is always queued or in progress at the moment the PR is created.
Harness error text
Non-draft create refused at the preflight while the head run is in flight:
PR publication preflight: the GitHub write was NOT performed. non-draft publication blocked: the exact head SHA still has a pending branch run. Inspect the exact head SHA before `gh pr create`; `gh pr checks --watch` after create is not this gate. Use --draft when readiness is unresolved.
Two neighboring refusals funnel to the same fallback:
PR publication preflight: observed local check has no successful completion: <command>. Rerun it successfully or publish a draft; write NOT performed.
PR publication preflight: claim review could not establish readiness from the committed source and tests; write NOT performed. Keep a draft while evidence is unresolved.
Root cause in code terms
publish_gate.observe gathers head evidence with a single gh run list --commit <head-sha> sample taken before the write, mapped through pr_publish.headStatusFromRunList.
pr_publish.decide classifies an in-flight run like failing evidence: pending/failed map to block (or draft_only when the failure is disclosed as base-reproduced), unknown blocks, and only none/passed allow. A queued run is therefore treated strictly worse than no run at all — none ("no pre-PR run", PR-only workflows) is explicitly allowed per ADR 0100 — even though a queued run is simply evidence that has not arrived yet.
- Because the natural order is push-then-create and push starts the runs, the refusal above fires on every ordinary publish. There is no re-read of run state before refusing, and no supported path to non-draft in that moment other than waiting outside the harness.
- Every refusal points at
--draft, and decide(draft = true, …) short-circuits to .allow — it skips the verification-section requirement, head evidence, the observed-local-check gate (State.unresolved is only consulted under !draft), and claim review. --draft is thus the only write that reliably succeeds once any gate has refused, which is why drafts become the default output rather than an explicit choice.
- The draft then cannot close the loop:
pr_verify.decision treats a draft receipt as unverified, pr_verify.completionGate defers completion ("draft: verified readiness requires a ready PR"), and attempt_completion stamps draft handoffs with "Draft handoff — CI is not verified.". Runs end as caveated drafts; a moved head or re-created PR restarts the cycle.
Expected behavior
A non-draft gh pr create immediately after pushing produces a non-draft PR while its first run is still going. The verification requirement stays enforced where it is decisive: pr_verify is armed at publication and completion requires current-head checks to pass (ADR 0120/0123 already guarantee this). Concluded failures and unreadable run evidence keep blocking the write. Drafts remain an explicit choice, not the harness's default output.
Where a fix could land
- Decision semantics: treat a queued/in-flight head run as evidence-not-yet-arrived (decide it like
none), keeping failed/unknown blocked and leaving verified completion as the enforcement point. This revises the in-flight part of ADR 0100 and needs an ADR record.
- Evidence gathering: re-read run state before refusing (or resolve the run first) so a refusal only happens on concluded evidence, not on an instantaneous sample.
- Refusal messaging: stop routing recoverable refusals (body/claim/local-check problems) to
--draft, so the unconditional draft bypass is not the advertised default exit.
All three can stand alone; the first two address the deterministic refusal, the third addresses the learned fallback.
Notes
--draft bypassing every check is consistent with ADR 0120's "draft is the unresolved-readiness fallback", but combined with refusals that fire on ordinary states it makes drafts the path of least resistance in every run.
- Body-verification parsing and claim review can also refuse recoverable inputs in practice; they share the same draft-pointing exit text.
Supersedes #1179 with a full diagnosis.
Symptom
Runs that publish a PR right after their change lands repeatedly come back as draft PRs — including work that is eventually merged — where non-draft PRs are expected. Superseded duplicates pile up: a draft PR is closed and re-created as another draft ("replacement") once its checks finish. The runs behave as if a freshly pushed commit can never be published non-draft.
How it shows up
gh pr createwith a body that carries the required verification section.--draft; the retried draft create succeeds. The run then ends as a caveated draft handoff, and because a draft cannot satisfy verified completion, the cycle tends to repeat as a replacement draft later.This is deterministic in that flow, not exceptional: the push-triggered workflows start on the branch push (workflows configured with
on: pushandcancel-in-progress: true), so the exact head SHA's run is always queued or in progress at the moment the PR is created.Harness error text
Non-draft create refused at the preflight while the head run is in flight:
Two neighboring refusals funnel to the same fallback:
Root cause in code terms
publish_gate.observegathers head evidence with a singlegh run list --commit <head-sha>sample taken before the write, mapped throughpr_publish.headStatusFromRunList.pr_publish.decideclassifies an in-flight run like failing evidence:pending/failedmap to block (ordraft_onlywhen the failure is disclosed as base-reproduced),unknownblocks, and onlynone/passedallow. A queued run is therefore treated strictly worse than no run at all —none("no pre-PR run", PR-only workflows) is explicitly allowed per ADR 0100 — even though a queued run is simply evidence that has not arrived yet.--draft, anddecide(draft = true, …)short-circuits to.allow— it skips the verification-section requirement, head evidence, the observed-local-check gate (State.unresolvedis only consulted under!draft), and claim review.--draftis thus the only write that reliably succeeds once any gate has refused, which is why drafts become the default output rather than an explicit choice.pr_verify.decisiontreats a draft receipt as unverified,pr_verify.completionGatedefers completion ("draft: verified readiness requires a ready PR"), andattempt_completionstamps draft handoffs with "Draft handoff — CI is not verified.". Runs end as caveated drafts; a moved head or re-created PR restarts the cycle.Expected behavior
A non-draft
gh pr createimmediately after pushing produces a non-draft PR while its first run is still going. The verification requirement stays enforced where it is decisive:pr_verifyis armed at publication and completion requires current-head checks to pass (ADR 0120/0123 already guarantee this). Concluded failures and unreadable run evidence keep blocking the write. Drafts remain an explicit choice, not the harness's default output.Where a fix could land
none), keepingfailed/unknownblocked and leaving verified completion as the enforcement point. This revises the in-flight part of ADR 0100 and needs an ADR record.--draft, so the unconditional draft bypass is not the advertised default exit.All three can stand alone; the first two address the deterministic refusal, the third addresses the learned fallback.
Notes
--draftbypassing every check is consistent with ADR 0120's "draft is the unresolved-readiness fallback", but combined with refusals that fire on ordinary states it makes drafts the path of least resistance in every run.