Skip to content

fix(task merge): enqueue on a queue-governed branch instead of naming a strategy the queue owns (DIVE-4428) - #936

Merged
5dive-bot merged 4 commits into
mainfrom
dive-4428-enqueue-on-queue-governed-main
Sep 13, 2026
Merged

fix(task merge): enqueue on a queue-governed branch instead of naming a strategy the queue owns (DIVE-4428)#936
5dive-bot merged 4 commits into
mainfrom
dive-4428-enqueue-on-queue-governed-main

Conversation

@lodar

@lodar lodar commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

The defect

5dive task merge shells gh pr merge <url> --squash. main is governed by a GitHub
merge queue, and the queue owns the merge strategy. GitHub answers that combination with

! The merge strategy for main is set by the merge queue
GraphQL: Something went wrong while executing your query

— 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:

PR row attempts result
#927 DIVE-4412 09:05:13 / 09:05:21 / 09:06:03Z identical 500 each time
#928 DIVE-4420 09:29:18Z byte-identical

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 lodar pressing merge by hand; #928 is
still OPEN / CLEAN / MERGEABLE / mergeQueueEntry=null as of this PR. Two verified-good fixes
that 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.mergeQueue non-null is the governance test. This repo protects main
with a ruleset, which populates no branchProtectionRule, so requiresMergeQueue is the
wrong field to reach for. Verified against #928:

{"id":"PR_kwDOSeJZAM8AAAABDUlPyw","headRefOid":"4924ed95…","isInMergeQueue":false,
 "mergeQueue":{"id":"MQ_kwDOSeJZAM4ABATG","configuration":{"mergeMethod":"SQUASH"}}}

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 — enqueuePullRequest with expectedHeadOid pinned to the graded head. The mutation
returns 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-commit
reaches for, enforced server-side: if the head moved, the mutation errors rather than queueing a
tree nobody graded. An isInMergeQueue=true PR is not re-enqueued.

STEP 3 — no queue on the base branch: the ordinary squash merge, unchanged.

Two corrections in the same six lines

  • An enqueue is reported as an enqueue. The rail printed <pr> merged by <seat> and
    OK — <ident> merged … is on the target branch over 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). --json now carries
    merged and enqueued as separate facts.
  • GitHub's own message is captured and reprinted, verbatim and first, instead of the refusal
    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.sh38 passed, 0 failed (8 new arms, section 5: enqueue
    reached, head pinned, ruleset-safe governance probe, already-queued short circuit, disposition
    emitted, caller branches on it, --json carries it, no "read gh's message above").
  • tests/task_merge_disposition_unit.sh103 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} after gh pr merge
enqueued 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

lodar and others added 4 commits September 13, 2026 09:59
… 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>
@5dive-bot
5dive-bot added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit 0c9d93b Sep 13, 2026
31 checks passed
@5dive-bot
5dive-bot deleted the dive-4428-enqueue-on-queue-governed-main branch September 13, 2026 15:54
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.

2 participants