Repository navigation
finding(pm-protocol): six open PRs were sitting UNARMED — one fully green for three hours — and nothing in the normal read path can see it #8671
Description
Filed by the domain:ui PM seat (session_01YBWFb5YgMU5dw8p2VKj16S), found while doing the periodic reverse-check that objectui#7014's closing note prescribes. ⛔ Not claimed.
The measurement
Called enable_pr_auto_merge against every open non-draft PR in the lane. The response distinguishes two states, and that distinction is the whole finding:
| PR | response | ⇒ prior state |
|---|---|---|
| #8644 | method: MERGE, enabled at 2026-09-08T19:33:19Z |
already armed |
| #8656 | method: MERGE, enabled at 2026-09-08T20:07:05Z |
already armed |
| #8553 | method: MERGE, enabled at 2026-09-08T16:49:58Z |
already armed |
| #8622 | method: , enabled at |
NOT armed |
| #8658 | method: , enabled at |
NOT armed |
| #8660 | method: , enabled at |
NOT armed |
| #8661 | method: , enabled at |
NOT armed |
| #8662 | method: , enabled at |
NOT armed |
| #8664 | method: , enabled at |
NOT armed |
When the PR was already armed the call echoes the existing arm's method and timestamp; when it was not, both fields come back empty because there was no prior state to echo. Six of nine were unarmed.
Why this is not a nuisance
#8622 had been fully green since 17:54Z — all 34 check runs success or skipped — and was still open and unmerged at 21:10Z. Three hours and sixteen minutes of a finished PR sitting in the queue's blind spot, not because anything was wrong with it, but because nothing had told the queue to take it.
⭐ And #8664 is the sharper case: I recorded it in my own session notes as "flipped to ready and armed." It was flipped. It was not armed. #8664 is the PR that unblocks #8553's red — so the belief that it was armed is also why #8553 looked like it was merely waiting.
The mechanism, and it is a trap with a natural entry
enable_pr_auto_merge refuses a draft:
GitHub GraphQL error on enable_pr_auto_merge: Pull request Pull request is a draft
⇒ the PM flow is necessarily flip first, then arm. But the flip is the satisfying step — it is the one that changes what the PR looks like — and an arm attempted before it fails with an error that reads like a transient. Do them in the wrong order once, or lose the second call to a context boundary, and the PR is silently left unarmed in exactly the state that looks finished.
⭐ Why nothing catches it
pull_request_read with method: "get" does not return the auto-merge field. Not as auto_merge, not as anything. So:
- a PM reading the PR back after arming it cannot verify the arm;
- a PM reviewing an open PR later cannot tell an armed one from an unarmed one;
list_pull_requestsdoes not carry it either, at anyfieldsselection.
⇒ the only available instrument is calling enable_pr_auto_merge again and reading whether the echo is populated — which is a write used as a read, discovered by accident, and depends on response-formatting behaviour nothing documents.
What this seat is doing about it now
All six are armed. But the remedy cannot be "remember to arm, then remember to check" — that is precisely the class objectui#7014 closed on:
「派发即写状态、交付即收状态、并定期全量反查」才是解法。靠记得去做的事情,都会漏。
The reverse-check found this. Nothing else would have, and it only ran because I went looking for a different stale flag.
Directions, sketched without recommending — this is a triage call
- A — make the state readable. Add
auto_mergeto whatpull_request_read getreturns. Cheapest, and it converts an unverifiable write into a checkable fact.⚠️ Does not stop the drop-on-conflict case, only makes it visible. - B — a scheduled sweep. Something that lists open non-draft PRs whose head is green and reports the ones the queue is not holding. Catches both this and the conflict-drop, and needs no protocol change.
- C — protocol only. Write "arm AFTER the flip, then verify by re-calling and reading the echo" into the PM guidance.
⚠️ Weakest: it is a remembered step, which is the thing that failed.
Reproduction
Any draft PR: call enable_pr_auto_merge → Pull request is a draft. Flip with update_pull_request draft:false, do not re-arm, then call pull_request_read get — nothing in the response distinguishes it from an armed PR. Call enable_pr_auto_merge and read the empty method: / enabled at fields.
Dedup
search_issues returns false zeros — measured repeatedly today, including total_count: 0 for a token carried in a matching issue's own title. ⇒ a zero from that instrument is not evidence of absence. Manual check performed instead: every open issue created since 2026-09-07T10:56Z was listed and read by title, and no card covers auto-merge arming state. Adjacent but different: objectui#8587 (74 cards on pm:on-hold and nothing re-measures the gates) is the same shape — state that only a periodic full reverse-check can find — on a different object.
Related
objectui#7014 (the reverse-check discipline, and its closing note) · objectui#8587 (stale pm:on-hold state, same shape) · objectui#8553 / #8664 (the pair where the unread arm state mattered)
Activity
os-justin commentedon Sep 8, 2026
Outcome, 30 minutes later — all six merged. The arm was the only thing missing.
Re-listed the open PRs after arming. Every one of the six is gone from the open set:
⇒ none of them was blocked on anything. No red check, no conflict, no missing approval, no queue backlog. Six finished PRs sat in the open set solely because a single idempotent call had not been made, and they merged within half an hour of it being made.
⭐ That converts this from "a state you cannot read" into "a state you cannot read, and it was silently costing throughput the whole time." The three-hour figure on #8622 was not the worst case — it was just the one I happened to measure; the others had been open for varying periods with nothing wrong with them either.
And the knock-on it was hiding
#8664 is the ObjectTree flake repair. PR #8553 had been red for hours on
ObjectTree.contractEnvelope-6839.test.tsx:124— a failure that is not its own (its diff ispackages/typesand cannot reachplugin-tree). The standing read was "waiting for #8664 to land." #8664 was not going to land, because it was not armed.⇒ an unread arm state does not just delay its own PR; it silently converts every PR waiting on that one into an indefinite wait that looks like patience. #8553's branch is updated now that #8664 is in
main.What this adds to the directions above
It sharpens the case for B (a scheduled sweep) over C (protocol only): the protocol here was not merely un-followed, it was believed to have been followed — I had recorded #8664 as armed. A remembered step cannot catch a step you remember doing. A (make
auto_mergereadable) remains the cheap prerequisite that would let any sweep, or any person, check it at all.Generated by Claude Code
os-justin commentedon Sep 9, 2026
This card told readers that
enable_pr_auto_mergereturning an emptymethod:/enabled atmeans "the PR was NOT armed." ⛔ That is not what it means, and I have just proved it the expensive way.What happened
mainaccepted no merge between 22:07:50Z and now — over two hours, while ten PRs sat green. I probed eight of them with the instrument above. All eight returned the empty echo. I concluded they had been silently disarmed, said so, and re-armed all eight.Then I looked at the actual queue:
Twelve entries, stacked — each branch's name carries the previous entry's head sha, so this is one speculative chain, and every
merge_groupCI run I checked came back success (pr-864423:38→23:54,pr-868923:39→23:56,pr-869223:39→23:56,pr-869823:40→23:56,pr-869723:55→00:11,pr-869923:56→00:11;pr-8707still in progress).⇒ Nothing was disarmed. Nothing was stalled. The whole batch is queued and building.
mainwill jump forward by about a dozen commits when the chain completes, which is exactly what a two-hour flatgit loglooks like from the outside while a deep speculative chain runs.⭐ The actual semantics, and why the instrument cannot tell
GitHub clears the PR-level auto-merge flag when the PR is taken into the merge queue. So the empty echo has (at least) two causes:
method: MERGE, enabled at <ts>method: , enabled at⇒ the reading this card published collapses "nobody armed it" with "it is queued and building right now" — opposite states, one needing action and one needing to be left alone.
What still stands from the original finding
⭐ The 21:10 measurement is unaffected and was real. Six PRs were genuinely unarmed — #8622 had been fully green for 3h16m — and all six merged within thirty minutes of being armed. At that moment the queue was empty, so the empty echo had only one possible cause. ⇒ the finding was sound; the instrument I generalised from it was not.
⭐ And the core defect is now WORSE, not better:
pull_request_readdoes not expose auto-merge state, and the one workaround this card offered turns out to be ambiguous and side-effecting. There is currently no safe read of a PR's auto-merge state from this toolset.⇒ direction A (make⚠️ Any sweep built on the current instrument would periodically kick healthy PRs out of the merge queue.
auto_mergereadable) is no longer the cheap nice-to-have I framed it as — it is the only correct fix. B (a scheduled sweep) is actively dangerous while the only available probe is a write that can disturb the queue.The reliable read, for anyone doing this before A lands
⛔ Do not call
enable_pr_auto_mergeto find out whether a PR is armed. Instead:A
gh-readonly-queue/main/pr-<N>-<sha>branch means PR N is in the queue — armed, accepted, and building. It costs nothing, touches nothing, and answers the question the write only guesses at. Absence from that list plus a green head is the case that actually warrants arming.The lesson, and it is mine
I spent tonight cataloguing instruments whose readings are not what they appear to be — a census keyed by a matcher, a sweep of wait expressions read as behaviour, a pin whose
\s*swallowed the distinction it existed to make. ⇒ I then published one of my own, on this card, and acted on it without a control. The control was available and cheap: the queue branches were onegit ls-remoteaway the whole time.A zero-hit reading is evidence only if a lit control fires. I wrote that into a dozen dispatches today and did not apply it to my own instrument.
Generated by Claude Code
os-justin commentedon Sep 9, 2026
⭐ The mechanism, confirmed — a queue EJECTION silently disarms the PR, and nothing says so
The correction above established that an empty echo can mean "queued and building." With the queue now draining I have the other half, measured on a PR I can trace end to end.
PR #8707:
gh-readonly-queue/main/pr-8707-7d6632ff…, itsmerge_groupCI runningmain, still open, and the arm echo comes back empty⇒ it entered the queue, left the queue, did not merge, and was disarmed. That is an ejection, and nothing anywhere reports it: the PR shows
state: open, its own check runs are green, andmergeable_statereadsunknown. The only evidence it was ever queued is a branch that no longer exists.Same reading for #8706 and #8711. #8702 by contrast still echoes its original
23:27:32Z— armed, never yet taken.⇒ this is worse than the card's original finding. It is not "someone forgot to arm six PRs" — it is the queue disarming PRs as a side effect of ejecting them, so a PR that was correctly armed, correctly green, and correctly queued ends up parked with no arm and no signal.⚠️ Left alone it never merges, and the only way anyone finds out is a periodic full reverse-check — the same discipline that found the original six.
⭐ The complete instrument, which needs BOTH reads
Neither the queue-branch listing nor the arm echo is sufficient alone. Together they disambiguate:
main?What this does to the directions
auto_mergereadable) — still the only correct fix, and now clearly insufficient on its own: a readable flag would have shownfalseon test(gantt): anchor both contractEnvelope-6839 refusal arms on completion, and diagnose the six siblings #8707 without saying why, and "never armed" versus "ejected two minutes ago" call for different responses.Tonight's tally, for whoever grades this
Between 22:07 and 00:45
mainaccepted 5 merges while I had ~10 PRs armed and green. Of those ten: five landed, five did not — and of the five that did not, three were disarmed without merging and one was still waiting to be taken. ⇒ roughly a third of a night's finished work parked silently, recoverable only by a manual reverse-check that nothing schedules.Generated by Claude Code