Skip to content

ci(unwedge): rule 3 — a merge_group run whose queue group is gone is cancelled; arming refuses --auto unless guard-tree is green on the current head (#3292, #3358) - #3403

Merged
noahgift merged 2 commits into
mainfrom
PMAT-3292-rule3-arming
Sep 16, 2026

Conversation

@noahgift

Copy link
Copy Markdown
Contributor

Follow-up to #3361 (rules 1+2, merged 17:33Z). Operator rulings 2026-09-16.

Rule 3 — orphaned merge_group run. A merge_group CI run whose (PR number, base oid) is absent from the live mergeQueue.entries is a zombie: measured today, run 35099392561 for pr-3266-2261757f… kept running and then held gate queued for 50 min after the queue had rebuilt that entry as pr-3266-f5d02deb…. orphan_verdict EVENT BRANCH ENTRIES_FILE → CANCEL only when the group is provably gone; LEAVE when present; REFUSE when the entries list is unreadable, wrong-shaped, or empty (an empty read would license cancelling the whole queue). Keyed on PR number and base oid because the zombie and its replacement share the PR number, and #3266's base oid is #3270's head oid. Rows O0a–O7 (13), fixture o1_mq_entries_live.json is a verbatim graphql answer; proved live against a fresh payload: the measured zombie → CANCEL, both live groups → LEAVE. Mutation: absence check disabled → O2 RED (exit 1), restored → 0.

Arming precondition — scripts/arm_pr_automerge.sh PR. Today #3278 was armed while its guard-tree was red, sat at queue position 1, and ejected the batch behind it. The helper reads the PR's current head, reads the check runs for that sha (never statusCheckRollup, the name-keyed surface the stale-check trap lives in), and runs gh pr merge --auto --squash only if guard-tree is success on that exact sha; otherwise refuse: guard-tree is <state> on <sha>, exit 3. Rows A1–A12 incl. success-on-an-older-sha-only → refuse. Proved live in all three polarities on #3366 (failure), #3361 (in_progress), #3363 (would-arm); used to arm #3396.

Self-tests: unwedge 53 rows ok, arm 12 rows ok; bashrs 0 errors on both scripts (warnings are the pre-existing SC2086 class). No runners API anywhere; capacity stays the fleet-bin.sh oracle.

🤖 Generated with Claude Code

noahgift and others added 2 commits September 16, 2026 20:50
…roup is gone

Run 35099392561 built gh-readonly-queue/main/pr-3266-2261757fd8a4... from
13:02:23Z to 14:30:59Z -- eighty-eight minutes -- after the queue had already
discarded that entry and rebuilt #3266 on base f5d02de. GitHub does not cancel
the run on a discarded ref: it keeps building, draws runners, and then holds
`gate` queued for fifty minutes on a concurrency group, computing a verdict
nothing can ever read.

THE KEY IS (PR NUMBER, BASE OID), AND BOTH HALVES ARE LOAD-BEARING. A merge
queue ref is `gh-readonly-queue/<base>/pr-<N>-<40 hex>` and the 40 hex IS the
entry's `baseCommit.oid` -- read live from both sides and committed as the
fixture (o1_mq_entries_live.json is the verbatim answer to MQ_QUERY, so the
field names in the predicate cannot drift from the ones GitHub returns).
Matching on the PR number alone answers LEAVE on the one run this rule exists
to cancel -- the zombie and its replacement share the number (row O5). Matching
on the base alone reads one group as licence to judge its neighbour: #3266's
base oid IS #3270's head oid.

NOT REDUNDANT WITH THE DEAD-REF PASS. That pass asks whether the git ref
resolves; this asks whether the GROUP is still an entry of the live queue. They
disagree in both directions -- a discarded group's ref can linger, a live
group's ref can 404 mid-rebuild.

ABSENCE OF EVIDENCE IS NOT ABSENCE OF THE GROUP. Unreadable, wrong-shape, and
EMPTY entries all REFUSE. Empty is the dangerous one: read as "the queue is
empty" it licenses cancelling every merge_group run in flight -- the whole
queue, in one sweep (rows O3/O3b/O3c).

Verified live, against a freshly fetched payload rather than the fixture:
  pr-3270-eb262f8e -> LEAVE  (entry at position 1)
  pr-3266-f5d02deb -> LEAVE  (entry at position 2)
  pr-3266-2261757f -> CANCEL (the measured zombie)
  pr-9999-eb262f8e -> CANCEL (a live base, a PR not in the queue at all)

Mutation proof: disabling the absence check turns O2 red (self-test exit 1,
"got LEAVE, wanted CANCEL"); restored, exit 0. 53 rows, 0 red.

Pmat-Ticket: PMAT-3292
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ad (#3292)

#3278 was armed on 2026-09-16 while its guard-tree was RED. The merge queue took
it, put it at POSITION 1, and when it failed there it ejected the batch queued
behind it. An arming decision is not local: a PR armed on a red guard costs every
PR behind it a rebuild, and the queue is deepest exactly when that hurts most.

THE STALE-CHECK TRAP IS THE WHOLE POINT. "guard-tree is green on this PR" is a
claim about a SHA, not about a PR, and every name-keyed surface (`gh pr checks`,
`statusCheckRollup`) will show a success from two pushes ago next to the newer
head. So the head sha is read FIRST, the checks come from
repos/{owner}/{repo}/commits/<that sha>/check-runs -- an endpoint that cannot
answer about another commit -- and every row is filtered on `head_sha == <that
sha>` again anyway. A success that exists only on an older sha is the named state
`stale`, not a silent `absent`: the two want different fixes.

ABSENT IS A REFUSAL. No row, no failure, arm -- that is the fail-open shape this
repo keeps shipping. Here it exits 3, as do in_progress, queued, skipped, and a
re-run in flight beside an earlier success on the same sha.

WHY NOT pr_review_quorum_arm.sh. It was searched for first and it IS an arming
path, but it evaluates S13's review-quorum predicate and refuses with Q1 when
there is no signed review receipt, so it cannot be the general arming entry
point; and it reads statusCheckRollup, the surface this trap lives in. The two
compose -- a quorum PERMIT still has to clear this precondition.

12 rows, 0 red, fixtures only, with A1 as the positive control (without it a
function that always refuses satisfies every other row). Verified live in all
three polarities against open PRs, --dry-run:
  #3366 refuse: guard-tree is failure on 179c69b...      exit 3
  #3361 refuse: guard-tree is in_progress on 062e471...  exit 3
  #3363 WOULD-ARM on e5cf234...                          exit 0
  #99999999 (no such PR)                                  exit 2
bashrs: 0 errors.

Pmat-Ticket: PMAT-3292
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@noahgift noahgift added this to the 0.68.0 milestone Sep 16, 2026
@github-actions

Copy link
Copy Markdown

§13.11 rung 1 — quorum shadow verdict

S13-SHADOW pr=3403 head=b5883ae7442111cebcfc30b5fb989e969c1ade49 verdict=REFUSE class=Q1 arm_rc=1

Shadow mode: this records a verdict and merges nothing. A refusal
to arm is not a block (§13 adds zero rows to §7) — the pull request is
exactly as green as it was.

@noahgift
noahgift enabled auto-merge September 16, 2026 19:07
@noahgift
noahgift added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit 11a1118 Sep 16, 2026
19 of 20 checks passed
@noahgift
noahgift deleted the PMAT-3292-rule3-arming branch September 16, 2026 19:28
@noahgift noahgift mentioned this pull request Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant