fix(task merge): enqueue on a queue-governed branch instead of naming a strategy the queue owns (DIVE-4428) - #936
Merged
Conversation
… a strategy the queue owns (DIVE-4428) `5dive task merge` shelled `gh pr merge <url> --squash`, and `main` is governed by a GitHub merge queue that owns the merge strategy. GitHub answers that combination with `The merge strategy for main is set by the merge queue` and then a GraphQL 500, which reads as a transient outage and invites a retry that cannot work. Measured 2026-09-13: four attempts across 24 minutes, two pull requests (#927, #928), two credentials, one failure mode, both PRs CLEAN/MERGEABLE with every required check green. Two verified-good fixes were parked behind a human tap. The executor now reads how the base branch is governed in one GraphQL round trip that also yields the node id and head sha, and on a queue-governed branch calls `enqueuePullRequest` with `expectedHeadOid` pinned to the graded head — the mutation returns the entry synchronously, so the call that enqueues is the call that proves it. A branch with no queue takes the ordinary squash merge unchanged. Also: an enqueue is now reported as an enqueue (the rail used to print "merged" and "is on the target branch" over a request that had only been accepted, and a queue lands it OR EJECTS it), and GitHub's own message is captured and reprinted instead of the refusal pointing at output the caller may never have shown. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ither, and the arms now execute (DIVE-4428) quinn's iteration-1 reject named two things. FINDING 1 — the product defect. `src/task/status.sh` calls the SAME merge primitive and could not tell an enqueue from a merge: it audited `task.merged-at-close` and told the operator the seat "merged it (squash)" over a request GitHub had only ACCEPTED. `_merge_disp_do` captured the executor's stderr into `$out` and discarded it, so the close had nothing but an exit status. There is now ONE reader of the marker — `_merge_disp_read` — used by `task merge` and by the close; `_merge_disp_do` returns the disposition on stdout; an enqueue at close is audited as `task.enqueued-at-close`, worded as a queued request, and does NOT retire the merge owner. FINDING 2 — the grading. All 8 iteration-1 arms were greps over `declare -f`, and two mutants with the strings intact survived 38/0. The GitHub half is split into `_merge_do_at_github` (authority stays in `_merge_do`, re-derived as root, and widens nothing) and the close-time body into `_merge_at_close_do`, so both are EXECUTED by the harness over a stubbed `gh` / stubbed rail: the five GitHub outcomes and the three close-time outcomes, asserted on what was invoked, what was returned and what was audited. Measured while writing this: a first draft graded the close by grepping its source, and a mutant replacing the disposition test with `false` — re-introducing the exact defect under grade — passed 83/0. That is the same lesson twice in one fix, and it is why section 8 executes. Mutants, each with an anchor, all now RED: a `-n "$_has_queue"` -> `-z` 72/28 b `-f oid="$_head_oid"` -> `""` 99/1 c close collapses the enqueue arm 95/5 (survived at 83/0 before section 8) d `_merge_disp_do` drops the marker 97/3 e merge owner retired on an enqueue 98/2 verifier_merge_standing_unit 100/0 · task_merge_disposition_unit 103/0 · agent_grant_unit 21/0 · build.sh green · shellcheck -S error clean but SC2148. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pipefail (DIVE-4428) Iteration 2's extraction moved the close-time body out of an `if` condition, where set -e is suppressed, into a bare `_am_re=$(_merge_at_close_do ...)`, whose rc IS its command substitution's — so a refused rail killed the entire close and the refusal it promises to print below was unreachable. - guard the assignment with `|| _am_re=""`, the same remedy the DIVE-2603 note at :1983 and the DIVE-3340 note at :793 in this same file landed on; it restores the post-condition the `[[ -n "$_am_re" ]]` test already reads. - grade it in the PRODUCT's envelope: C5/C6 extract the call site's BYTES from src/task/status.sh at run time and execute them under `set -euo pipefail`, asserting execution continues past the call on a refusal and that a rail which acted still hands its state through the guard. Anchored, so an awk range that stopped matching cannot pass vacuously over an empty script. verifier_merge_standing_unit 107/0 (was 100); guard deleted -> 104/3. task_merge_disposition_unit 103/0, agent_grant_unit 21/0, build.sh green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
5dive task mergeshellsgh pr merge <url> --squash.mainis governed by a GitHubmerge queue, and the queue owns the merge strategy. GitHub answers that combination with
— a 500, which reads as a transient outage and invites a retry that cannot work.
Measured (quinn, 2026-09-13 09:05–09:29Z, DIVE-4428)
Four attempts across 24 minutes, two pull requests, two credentials, one failure mode:
Both read
state=OPEN mergeState=CLEAN mergeable=MERGEABLE, every required check green(31 of 31 on #928). Not transient. Both rails failed on the same step — entering the queue.
What it cost: #927 was landed at 09:18:48Z by
lodarpressing merge by hand; #928 isstill
OPEN / CLEAN / MERGEABLE / mergeQueueEntry=nullas of this PR. Two verified-good fixesthat the seat which graded them PASS could not land — that is the autonomy number, one step
short of shipping.
The fix
STEP 1 — ask how the branch is governed, in one round trip that also yields what an enqueue
needs.
PullRequest.mergeQueuenon-null is the governance test. This repo protectsmainwith a ruleset, which populates no
branchProtectionRule, sorequiresMergeQueueis thewrong field to reach for. Verified against #928:
A read that fails leaves every value empty and falls through to the plain merge — a governance
probe must never be the thing that refuses.
STEP 2 —
enqueuePullRequestwithexpectedHeadOidpinned to the graded head. The mutationreturns the queue entry synchronously, so the call that enqueues is the call that proves it;
no second read, no ambiguity. The head pin is the same graded-sha guarantee
--match-head-commitreaches for, enforced server-side: if the head moved, the mutation errors rather than queueing a
tree nobody graded. An
isInMergeQueue=truePR is not re-enqueued.STEP 3 — no queue on the base branch: the ordinary squash merge, unchanged.
Two corrections in the same six lines
<pr> merged by <seat>andOK — <ident> merged … is on the target branchover a request that had only been accepted.A queue lands it or ejects it — and on DIVE-4154 it ejected 8 minutes later while the rail's
output said "merged"
(
community/wiki/task-merge-reports-the-enqueue-as-a-landed-merge.md).--jsonnow carriesmergedandenqueuedas separate facts.text pointing at "gh's message above" — output the caller captures into a variable and may never
have shown.
Evidence
tests/verifier_merge_standing_unit.sh— 38 passed, 0 failed (8 new arms, section 5: enqueuereached, head pinned, ruleset-safe governance probe, already-queued short circuit, disposition
emitted, caller branches on it,
--jsoncarries it, no "read gh's message above").tests/task_merge_disposition_unit.sh— 103 passed, 0 failed (unchanged, no regression).shellcheck -S error src/task/delivery.sh— clean but for the pre-existing SC2148 (sourced file).The residual I am signing
The mutation is not exercised live in this PR. A real enqueue is a write against a real PR, and
the only legitimate target — #928 — is another seat's row sitting behind a human gate; landing it
with this seat's admin credential is not mine to press. The mutation's shape is measured working on
this exact repo (
community/wiki/main-is-behind-a-merge-queue-so-gh-pr-merge-only-enqueues.md,DIVE-4289 / PR #880:
enqueuePullRequest→{"state":"QUEUED","position":1}aftergh pr mergeenqueued nothing), and STEP 1's query is verified against #928 above. The first live proof is this
PR's own merge, which is exactly the call the fix changes.
🤖 Generated with Claude Code