Skip to content

fix(source-control): confirm a merge-queue PR entered the queue after gh pr merge - #7075

Merged
kyle-sexton merged 6 commits into
mainfrom
fix/6630-merge-confirm-queue-entry
Oct 13, 2026
Merged

kyle-sexton merged 6 commits into
mainfrom
fix/6630-merge-confirm-queue-entry

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Kyle · project thread

Closes #6630

Summary

Before: the merge phase (plugins/source-control/skills/pull-request/reference/merge.md) said gh pr merge on a merge-queue base "enqueues and returns". When it returned with a CLEAN PR left out of the queue (seen on #6481 and #6426), or ran while GitHub still read mergeStateStatus: UNKNOWN and armed auto-merge instead (#6953), the 4.2.1 poll loop read OPEN false and reported "PR left the merge queue unmerged", and the 4.2 path printed "not merged yet" with nothing confirming the PR was queued at all.

After: 4.2 and 4.2.1 poll the merge state while it reads UNKNOWN and merge or enqueue only on CLEAN. A new §4.2.2 "Confirm queue entry" runs after every merge call on a merge-queue base. A run ends in one of three reported states: merged, queued, or held with the blocker GitHub returned.

Fix

  • Layout follows refactor(source-control): move pull-request and worktree spoke GitHub commands to provider files #7170 (ADR 0060): the phase file (skills/pull-request/reference/merge.md) states conditions in provider-neutral prose, and the gh commands live in reference/providers/github/pull-request/merge.md.
  • 4.1 step 2 captures HEAD_SHA from the PR's headRefOid. Every merge call in 4.2 (regular checkout, linked worktree, async API) and 4.2.1 pins to it with --match-head-commit / sha, so a push after approval fails the merge instead of landing unreviewed.
  • New provider section "Wait for a known merge state": polls mergeStateStatus at the pinned head while it reads UNKNOWN (15 s, 20 tries) and goes on only on CLEAN; any other state or a moved head is a hold. 4.2 runs it before any merge call; the 4.2.1 script runs the same loop in place of its single CLEAN read. REST fallback: .mergeable_state while unknown, go on only on clean.
  • §4.2.2 is a read that polls past UNKNOWN and prints merged, queued, or enqueue <node id>, or exits 1 with the hold (head moved, not CLEAN, with auto-merge-armed advice). On enqueue, the enqueuePullRequest call runs as its own command with the node id and head written as literal values, not a variable or $(jq ...), so the fail-closed guardrails merge guard in feat(guardrails): block a PR merge while review threads, change requests or failing checks remain #7056 can resolve the PR from the literal PR_... id. Then the read is re-run: queued is the result; enqueue again is a hold.
  • An enqueue refusal is reported with GitHub's error text unedited. Never jump. An armed auto-merge is reported, and on a hold the user is offered gh pr merge <n> --disable-auto; the phase does not run it. This changes what the merge path reports, not who may merge.
  • The SKILL.md merge row mentions the §4.2.2 step. Changelog fragment for source-control (patch).

Verification

  • Extracted the provider-file blocks (wait, merge after AI review, confirm read, enqueue call); shellcheck -s bash -e SC2016: clean.
  • Stub-gh run of the confirm read and the wait loop, 10 cases, all as written: UNKNOWN then CLEAN prints enqueue PR_x; queued; merged; BLOCKED holds; head moved holds; auto-merge armed hold prints the --disable-auto advice; stuck UNKNOWN holds after the tries; wait loop UNKNOWN then CLEAN passes, BLOCKED holds, moved head holds.
  • MergeStateStatus values checked by GraphQL introspection (DIRTY UNKNOWN BLOCKED BEHIND UNSTABLE HAS_HOOKS CLEAN); REST mergeable_state read live on this PR (unknown just after a push).
  • scripts/affected-tests.sh --base origin/main --run: 4 shell suites passed; the 2 Python suites (test_guards.py, test_check_contract_clause_coverage.py) with uvx pytest: 48 passed.
  • markdownlint-cli2 on the 4 changed files: 0 issues. scripts/check-changelog-fragments.sh --check-required origin/main: pass. scripts/check-changed-skills.sh origin/main: pull-request PASS (3 pre-existing warnings).

Left out: no live run against a real merge queue, and #7056's hook was not run against the enqueue command; the literal-id shape follows its documented rule.

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_01WVW9hAwnSBrnFsR3kzXcB4

… gh pr merge

`gh pr merge` on a merge-queue base can return with a CLEAN PR left out
of the queue. The merge phase now reads isInMergeQueue after every merge
on a queue base, enqueues a CLEAN PR left out once at the pinned head
with enqueuePullRequest, and reports GitHub's refusal verbatim, so a run
ends merged, queued, or held with a named blocker.

Closes #6630

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR

Copy link
Copy Markdown
Contributor Author

ci-status is red because this PR is a draft, not because of the change. On a draft every lint and test lane skips (lint-repo, test-bash, test-node, test-python, check-skills, check-plugins), and the aggregate counts a skip as not passing. See .github/workflows/pr-require-checks.yml:507-513.

The lanes run once the PR is marked ready. That flip waits for Kyle, because it also starts the review lanes. Until then, the local runs listed under Verification in the PR body are the signal.


Generated by Claude Code

…on armed auto-merge

4.1 step 2 now captures HEAD_SHA from the PR's headRefOid, and the 4.2,
async and 4.2.1 merge calls pin to it instead of re-reading the live head,
so 4.2.2's head-moved check compares against the verified head. A 4.2.2
hold with auto-merge armed now says a later push could merge on
ci-status alone and offers gh pr merge --disable-auto without running it.

Refs: #6630

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR
Resolves the pull-request SKILL.md merge row: keeps #7042's neutral
wording (pinned to the reviewed head commit, never arm auto-merge) and
adds the 4.2.2 queue-entry confirmation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR
kyle-sexton pushed a commit that referenced this pull request Oct 11, 2026
… and prefix gaps

From review of #7056:
- A GraphQL merge, auto-merge or merge-queue mutation whose PR cannot be
  resolved (id from $ID, "$PR_ID", $(jq ...), an unreadable node, no gh)
  now fails closed, with the literal-id fix in the message. Tests cover
  the source-control 4.2.2 enqueue shape from #7075.
- gh pr merge --help and -h are not merges; stay-quiet tests assert no
  gh call.
- All reads share one 35 s budget, each call capped at what is left and
  at 15 s, enforced by timeout or, where it is missing, a watcher process,
  so the shared row stays under its 60 s timeout.
- Leading reserved words (then, do, {, ...) and the command, time, exec
  and nohup prefixes are stepped over, and gh pr -R o/r merge is read.
- The fail-open notice says the merge will run unchecked.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR
kyle-sexton and others added 2 commits October 13, 2026 09:14
Resolve merge.md against #7170, which moved the phase's gh commands into
the GitHub provider file: the phase prose stays provider-neutral and the
HEAD_SHA pin, --match-head-commit and the confirm-queue-entry script move
to reference/providers/github/pull-request/merge.md.

Also, per the owner's direction on #6953: poll mergeStateStatus while it
reads UNKNOWN before any merge call in 4.2 and 4.2.1, merging or
enqueueing only on CLEAN; and split the enqueue into a read that prints
the PR node id and a separate enqueuePullRequest call with the node id and
head written as literals, so the guardrails merge guard can resolve the PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WVW9hAwnSBrnFsR3kzXcB4
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 13, 2026 13:15
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-13T13:18:40.285010Z 7de07c6 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 4m 34s —— View job


Code review complete

  • Gather context (PR body, diff, custom instructions)
  • Read changed files
  • Resolve project standards / REVIEW.md criteria
  • Review for correctness, error handling, maintainability
  • Post findings (inline + summary)

2 findings posted inline:

  1. TOCTOU gap in HEAD_SHA capture — reference/providers/github/pull-request/merge.md L22-24. The new HEAD_SHA=$(gh pr view ... ) line is a second, independent read of headRefOid, separate from the read the stale-base compare on the line above just checked. A push landing between the two calls pins every later merge/enqueue call to a commit the stale-base guard never verified — undermining the exact hazard 4.1 step 2 calls out ("a stale-base squash can silently revert recently-landed fixes"). Fix: capture headRefOid once and reuse it for both the compare and HEAD_SHA.
  2. Restates volatile upstream GitHub behavior instead of pointing at it — skills/pull-request/reference/merge.md L54-56, with the same pattern repeated at L113-115. Both paragraphs paraphrase the GitHub mergeStateStatus/auto-merge-arming mechanism already recorded with its own pointer in the GitHub reference file, which the same paragraphs already link — a restatement .claude/rules/skill-bodies-state-current-rules.md flags.

Everything else in the diff (the new §4.2.2 "Confirm queue entry" read/enqueue script, the --match-head-commit additions, the SKILL.md row update, and the changelog fragment) checked out: the state-machine case in the confirm-queue script covers merged/queued/CLEAN-enqueue/hold correctly, the enqueue mutation shape matches enqueuePullRequest's documented input, and the "wait for a known merge state" poller (unlike the stale-base guard above) correctly folds the head-moved check into the same read via jq rather than a second call. Author-claimed verification (shellcheck, stubbed-gh runs, affected-tests, markdownlint, changelog checks) is noted in the PR body but not independently re-run here.

claude-lane-summary: lane=claude-review sha=7de07c6fd97d01bc433d38fdc559b5ca069b554a run=38403543511

@claude

claude Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 7m 23s —— View job


Security review complete

  • Gather context (PR diff, changed files)
  • Skip-gate check (not a skip: touches merge-queue automation instructions)
  • Review changed files for security issues
  • Adversarial validation of candidate findings
  • Post findings / summary

Scope: This PR is documentation-only — it changes skill-reference markdown that instructs an agent how to run gh/gh api graphql commands when merging behind GitHub's merge queue, plus a changelog fragment:

What I checked:

  • Shell/GraphQL injection in the new example snippets (the "Wait for a known merge state" and new "Confirm queue entry" ## sections). HEAD_SHA, node ids, and the GraphQL mutation inputs are bound via gh api graphql -F/-f variables or quoted shell variables, never spliced into query/command text; the doc explicitly tells the operator to write the enqueue mutation's id/head as literals rather than a variable or substitution, which is itself a sound anti-injection instruction.
  • Whether GitHub-returned error/state text is ever treated as instructions instead of data: it is only ever echoed/reported, never evaluated.
  • Whether this diff removes or weakens any prior guardrail (compared old vs. new text for every hunk): it doesn't — the old single-shot CLEAN check is replaced by a poll-then-check loop preserving the same head-pinning gate, and the new §4.2.2 step adds verification rather than removing any.
  • Instruction-surface deletions: none found; nothing here deletes, narrows, or softens a standing rule, so the protected-class register didn't surface a match.

One candidate finding (the new "Confirm queue entry" case statement reports merged/already-queued without re-checking headRefOid against the pinned HEAD_SHA on those two branches, only on the about-to-enqueue branch) was raised by a hunter pass and rejected on adversarial verification: the scenario it describes — auto-merge armed outside this flow merging a later, unreviewed push on ci-status alone — is a pre-existing, already-documented architectural tradeoff (ci-status is the only required check; nearby unchanged text already warns about it), not a new gap this diff introduces. The new auto-merge-armed warning this PR adds is a net improvement, not a regression.

No security findings to report.

claude-lane-summary: lane=claude-security-review sha=7de07c6fd97d01bc433d38fdc559b5ca069b554a run=38403543473

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7de07c6fd9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/source-control/reference/providers/github/pull-request/merge.md Outdated
Comment thread plugins/source-control/reference/providers/github/pull-request/merge.md Outdated
Comment thread plugins/source-control/skills/pull-request/reference/merge.md
Comment thread plugins/source-control/skills/pull-request/reference/merge.md Outdated
Comment thread plugins/source-control/reference/providers/github/pull-request/merge.md Outdated
Comment thread plugins/source-control/skills/pull-request/reference/merge.md Outdated
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

claude-review has reviewed this pull request through 501539e; a later push is reviewed from there.

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review has reviewed this pull request through 501539e; a later push is reviewed from there.

…ueued moved head

- Stale-base guard reads mergeStateStatus, baseRefName and headRefOid once
  and uses that head for both the compare and HEAD_SHA, so the pinned head
  is the one the compare checked.
- The confirm read holds when the PR is queued at a head other than
  HEAD_SHA instead of reporting it queued.
- Phase body keeps the hold policy and the provider pointer; the GitHub
  timing and enqueue behavior stays in the provider record.
- readiness.md notes that a PR left UNSTABLE by a redundant stuck status
  goes to the user, since the merge phase merges only on CLEAN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WVW9hAwnSBrnFsR3kzXcB4
@claude

claude Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 54s —— View job


Code review complete (incremental, since 7de07c6f)

  • Gather context (PR body, diff, prior review, custom instructions)
  • Read incremental diff (.claude-lane/incremental.diff) for the 3 scoped files
  • Resolve project standards / REVIEW.md criteria
  • Review for correctness, error handling, maintainability
  • Adversarial validation of candidate findings
  • Post findings (inline + summary)

0 findings. This increment cleanly resolves every issue raised in the prior claude-review pass and the Codex P1s on the same lines:

  • TOCTOU in HEAD_SHA capture (reference/providers/github/pull-request/merge.md L24-29) — now a single gh pr view read feeds mergeStateStatus, baseRefName, and HEAD_SHA via one read, so the compare and the pinned head are guaranteed to be the same read. No second independent call remains.
  • queued branch skipped the pinned-head check (L191-194) — the 'OPEN true '* case now compares headRefOid to HEAD_SHA and holds on mismatch, matching the check already applied to the CLEAN/enqueue branch.
  • Volatile upstream behavior restated in the skill body (skills/pull-request/reference/merge.md, both the §4.2 and §4.2.1 paragraphs) — both restated paragraphs about UNKNOWN/auto-merge timing are removed; the body now keeps only the decision plus the existing pointer to the provider file's "Wait for a known merge state" section, which already owns that fact with its own as-of/recheck-trigger.

The readiness.md edit (L77) documents, rather than silently resolves, the tension Codex's P2 flagged (a non-blocking stuck-status UNSTABLE is still held by 4.2's CLEAN-only gate) — that's a deliberate, stated hold-and-report tradeoff, not a bug.

No new issues found in the changed lines. Shell logic in the updated blocks (read word-splitting, function-before-use ordering for hold(), case-pattern matching) checked out.

claude-lane-summary: lane=claude-review sha=501539e4b9e369a1f6ce207032afac5addd131af run=38404845309

@claude

claude Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 45s —— View job


Security review complete

  • Gather context (incremental diff, prior review scope)
  • Read changed files
  • Skip-gate check (not a skip: touches merge-queue automation instructions)
  • Review for security issues
  • Adversarial validation
  • Post findings / summary

Scope: incremental review of what changed since 7de07c6 (this lane's last review), limited to the 3 files the diff touches:

What changed and what I checked:

  1. Stale-base guard (merge.md#L19-L34) — the two prior TOCTOU findings (mine and Codex's, both P1) flagged that HEAD_SHA was captured via a second, independent gh pr view call after the stale-base compare had already read headRefOid separately, so a push landing between the two calls could pin later merge/enqueue calls to a commit the stale-base guard never checked. This commit fixes it: read -r MSS BASE HEAD_SHA <<<"$(gh pr view ... --jq '"\(.mergeStateStatus) \(.baseRefName) \(.headRefOid)"')" is now a single atomic read, and both the compare call and the downstream HEAD_SHA pin derive from that one snapshot. The REST fallback was likewise collapsed to one call. Verified: mergeStateStatus/ref-name/SHA values can't contain whitespace, so there's no field-alignment hazard in the read split.

  2. Confirm queue entry — queued branch (merge.md#L189-L198) — the second prior P1 (Codex) flagged that the 'OPEN true '*) (already-queued) branch printed queued with no check that the queued head matched the pinned HEAD_SHA, so a later push that got queued via auto-merge could be reported back as the approved merge. Now closed: that branch checks jq -r .headRefOid <<<"$PR" against HEAD_SHA and holds on mismatch, using the same atomic $PR read already in scope (not a re-fetch) — same pattern as the sibling CLEAN branch.

  3. readiness.md / skill merge.md — prose-only changes (trims a restated upstream-behavior paragraph per the style finding from the last round; readiness.md clarifies that the strict-CLEAN gate still governs even when UNSTABLE is classified non-blocking). No logic change, no security implication.

Dispatched an independent adversarial verifier (review:lane-verifier) against both fixes specifically hunting for a residual race, a word-splitting/injection hazard in the new read line, or a weakened check — it found both gaps genuinely closed and no new issue.

No security findings to report.

claude-lane-summary: lane=claude-security-review sha=501539e4b9e369a1f6ce207032afac5addd131af run=38404845165

@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit 8feb4b8 Oct 13, 2026
19 checks passed
@kyle-sexton
kyle-sexton deleted the fix/6630-merge-confirm-queue-entry branch October 13, 2026 13:35
github-merge-queue Bot pushed a commit that referenced this pull request Oct 13, 2026
…sts or failing checks remain (#7056)

<!-- claude-projects-attribution: {"github_login":"kyle-sexton"} -->
_Requested by **Kyle** · [project
thread](https://claude.ai/code/project/chan_01FAwvkDrP9dbkyZDmMQiMav?thread=cmsg_01FAwvkDrP9dbkyZDmMQiMav3VhqGrPAKf4CfZbLHxPHqU)_

Closes #6463

## Summary
Before: nothing checked a direct `gh pr merge`, REST or GraphQL merge,
or MCP merge call. An agent could merge (or ask for `--admin`) without
reading an unresolved review thread.

After: a new guardrails guard, `block-unresolved-pr-merge`, blocks a
merge while the PR has an unresolved review thread, a reviewer whose
latest decisive review requests changes, or a failed check run or commit
status on its head. The block message lists each thread (path:line,
first line of the comment, link), each changes-requested reviewer and
each failing check, and names the fix. `--admin` and `--auto` are held
to the same rule.

## Fix
- `plugins/guardrails/hooks/block-unresolved-pr-merge.sh`, on the
existing Bash|PowerShell `run-guards` row plus a new MCP row
(`^mcp__.+__(merge_pull_request|enable_pr_auto_merge)$`). Matches the
scope update's table: `gh pr merge` (all flags), `PUT
repos/{o}/{r}/pulls/{n}/merge` via `gh api` or `curl`, the
`mergePullRequest` / `enablePullRequestAutoMerge` / `enqueuePullRequest`
mutations via `gh api graphql` or `curl`, and the MCP merge tools.
Matching runs on parsed argv at a command position, so a quoted mention
(commit message, echo, heredoc) stays quiet. Commands that name neither
`merge` nor `enqueue` leave after one string test.
- **GraphQL is refused in Claude Code cloud sessions (HTTP 403)**, so PR
state is read over REST: `pulls/{n}`, `pulls/{n}/reviews`,
`commits/{sha}/check-runs`, `commits/{sha}/status`. Thread resolution
has no plain REST route: GraphQL `reviewThreads` where served, else the
cloud route `GET pulls/{n}/ccr/review_threads` named in that 403 body (a
`CLAUDE_CODE_REMOTE` session tries the cloud route first). The same 403
body names `PUT pulls/{n}/ccr/auto_merge` as the cloud auto-merge path,
so that route is matched too.
- Fails open with a notice on both channels (systemMessage and
additionalContext) saying the merge will run unchecked when `gh` is
missing or unauthenticated, a read fails, the read budget runs out, or
the PR cannot be identified.
- **Fails closed for a GraphQL merge, auto-merge or merge-queue mutation
whose PR cannot be resolved.** That covers an id from `$ID`, `"$PR_ID"`,
`$(jq ...)` (the source-control merge reference 4.2.2 enqueue shape from
#7075), a node that can't be read, or no gh. The block message says how
to fix it: pass a literal `-f id=PR_...` or use `gh pr merge <n>`.
- **One time budget**: all reads share 35 s, and each call is capped at
the time left and at 15 s, so the shared Bash row stays under its 60 s
timeout. A row timeout would drop every guard's block (run-guards.sh,
AGGREGATION). The cap is enforced by `timeout` where it exists, and by a
watcher process where it doesn't (stock macOS). No environment variable
changes the budget: `settings.json` `env` reaches a hook's environment,
so a knob could make every merge run unchecked. The budget tests run a
copy of the hook with its one budget line rewritten.
- **Command position**: the guard steps over leading assignments,
reserved words the tokenizer leaves at a segment head (`then`, `do`,
`{`, ...), and the `command`/`time [-p]`/`exec`/`nohup` prefixes. It
reads `gh pr -R o/r merge`. `gh pr merge --help`/`-h` and
`--disable-auto` are not merges. The guard header lists the remaining
gaps: wrappers like `sudo`/`env`/`xargs`, child shells, `$(...)`, and
`--input` mutation files.
- Registered in `guard-requires.sh` (MCP inputs declared unprimed),
`hooks.json`, `plugin.json` userConfig
`block_unresolved_pr_merge_enabled` (default on), README guard/option
tables, changelog fragment (minor).

Defaults I picked where the issue left a detail open (please confirm):
1. **`gh pr ready` is not matched.** The 2026-10-06 scope update dropped
the advisory and its table lists merge paths only; the issue title still
says "and gh pr ready". Adding it is one line if wanted.
2. **Pending checks block an immediate merge but not an auto-merge or
merge-queue request** (`--auto`, `enablePullRequestAutoMerge`,
`enqueuePullRequest`, `enable_pr_auto_merge`, `ccr/auto_merge`), since
those wait for checks by design. Failed checks, threads and change
requests block all paths.
3. **Every unreadable state fails open** (not only
missing/unauthenticated gh), with the org ruleset as the server-side
backstop, as the issue's residual note says. The one exception is the
unresolvable GraphQL mutation above, which fails closed.
4. Outdated unresolved threads count, matching the babysit merge gate.

Open for Kyle (found by the overnight verifier, not changed here):
5. **The fail-closed GraphQL rule also covers gh missing or
unauthenticated.** A mutation with a literal `PR_...` id whose node
can't be read is blocked. That covers no gh, a 401, and the cloud
GraphQL 403. The issue's acceptance says the guard fails open when gh is
missing or unauthenticated. Option A: keep it, since the mutation can't
be checked. Option B: fail closed only for a non-literal id and fail
open, with the notice, for a literal one.
6. **Interaction with #7075.** Its source-control merge reference 4.2.2
passes the enqueue id through `$(jq ...)`. With this guard on, that step
is always blocked, even on a clean PR (verified against branch
`fix/6630-merge-confirm-queue-entry`). One side must change before both
merge: #7075 passes a literal id or uses `gh pr merge`, or this guard
relaxes the rule.

## Verification
- `bash plugins/guardrails/hooks/block-unresolved-pr-merge.test.sh` ->
162 passed, 0 failed (at 96f4c12). Since the first push, the suite also
covers:
- the fail-closed GraphQL cases (substitution id, `$ID`, `"$PR_ID"`,
`--input`, the full #7075 shape, an unreadable literal node)
  - `--help`/`-h` stay-quiet cases that assert no gh call
  - every command-position prefix, plus `gh pr -R o/r merge`
- the budget with a hung fake gh, both with and without a `timeout`
binary on PATH (returns in under 8 s), run on a copy with a 2 s budget
- a case where an env var can't shorten the budget
(`GUARDRAILS_BUPM_BUDGET_S=1` with a 0.7 s gh still blocks). It failed
before the knob was removed.

A fake `gh` on PATH serves fixtures and logs calls. Covers every merge
path, both shells, MCP, GraphQL-refused fallback to the cloud route,
current-branch lookup, the fail-open cases (gh missing, 401, no thread
source, no node id), and MUST-stay-quiet cases that also assert no `gh`
call (commit message, echo, `gh pr view`, `--disable-auto`, GET of the
merge route, a GraphQL query, grep, `git merge`, heredoc, PowerShell
quoted mention, an unrelated MCP tool). Also runs through
`run-guards.sh`.
- `scripts/affected-tests.sh --base origin/main --run`:
- First push: 35 shell suites passed, including `run-guards.test.sh`'s
declaration checks for the new guard. The 2 selected `.mjs` suites, run
separately with `node --test`, also passed.
- Fix push: on a loaded host the run hit the 30-minute background limit.
Every suite that finished passed: all 26 guardrails hook suites, plus
apply-plan, splice-assets, both routing suites and the lib suites. The
four script suites it had not reached all passed when run directly:
`check-killswitch-hoist` 28/28, `check-script-contract` 51/51,
`node-notice-rows` 6/6, `check-guardrails-ps-differential` 21/21.
- Knob-removal push (96f4c12): the run was stopped early at wrap-up. The
suites that finished passed, apart from `block-hook-bypass`, which fails
identically on `origin/main` when the checkout sits under `/tmp`. A full
affected run on this head is still to do. `shellcheck -x`,
`check-killswitch-hoist`, `check-changelog-fragments --check-required
origin/main` and `check-changelog-parity --check` pass.
- Live, read-only, in a Claude Code cloud session: the guard against
open PRs #7036 (blocked: failing `ci-status`, `ci-lanes`), #6910
(blocked: pending checks), #7009 (allowed); 3.0 to 3.6 s per merge
command (five sequential calls), recorded in the guard header and
README.
- `shellcheck -x` (repo `.shellcheckrc`) clean;
`scripts/validate-plugins.sh`, `scripts/check-changelog-fragments.sh
--check-required origin/main`, `scripts/check-changelog-parity.sh
--check`, `scripts/check-killswitch-hoist.sh`, markdownlint on the
README and fragment: all pass.

This changes what agents may do unattended (it blocks merges), so it
needs a human merge decision.

## Related
- #5947 (opt-in ask guard for high-stakes gh calls; independent)
- `/source-control:babysit-prs` merge gate counts threads the same way
(outdated included)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR

---
_Generated by [Claude
Code](https://claude.ai/code/session_01L8KdG7fWg2YrErVYWtohmR)_


---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: melodic-automation-lanes[bot] <337595936+melodic-automation-lanes[bot]@users.noreply.github.com>
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.

fix(source-control): pull-request merge must confirm the PR entered the merge queue

2 participants