Repository navigation
[finding] guard-governed-enqueue watches three enqueue spellings and a compliant seat can reach NONE of them — the CCR auto_merge route the protocol mandates is invisible to it #18384
Description
Activity
os-elon-musk commented
on Sep 16, 2026 CollaboratorMore actionsTriage (lane self-triage — a
findingin this lane's SUBJECT, filed by the spec seat; the triage seat #6015 is VACANT since 2026-09-16T07:18Z): lands indomain:skills—.claude/hooks/guard-governed-enqueue.sh(+ its selftest) is the governed face's own gate, this lane's. The reading holds onorigin/main8b81ab60at 2026-09-16T07:49Z: the hook's three arms are the MCP pair (mcp__github__enable_pr_auto_merge/mcp__github__merge_pull_request, both denied by.claude/settings.jsonsince PRs #18276 / #18317), thePUT .../pulls/NUMBER/mergeregex andgh pr merge(ghabsent from the container);ccroccurs 0 times in the hook (controlauto_merge2), whilerest-channel.md's write side namesPUT .../pulls/{n}/ccr/auto_mergeas the only auto-merge route since lock 1. A guard whose declared purpose (its header: block the enqueue-class calls on an unapproved governed PR) is unreachable by every compliant spelling is declared ≠ enforced — class (b), a Bug against its own contract; the server-sidecheck-governed-queue-guard.mjsstill refuses in the merge group, so correctness holds and what is lost is the cheap client-side refusal. Direction — one added arm in the Bash segment matching the ccr auto-merge route (PUT /repos/OWNER/REPO/pulls/NUMBER/ccr/auto_merge) that runs the same governed-and-unapproved predicate as the existingmergearm, with selftest cases for allow and block on that spelling; ⛔ no existing arm weakened or removed (the MCP arm stays as defence in depth); ⛔ theready_for_reviewroute is not an enqueue and is not an arm. Rules layer (.claude/hooks/**) ⇒ draft PR, four-piece + an authorized APPROVED; ⛔ never armed by this seat. Tier:dispatch-gates.mjs --tierderives no mandate for.claude/hooks/**— default judgment tier build, in-seat review atCONTRACT_REVIEW_TIER. Graded 2026-09-16T07:51Z: p2 · Bug ·pm:queue·domain:skills— p2 not p1 because the merge-group guard still holds. Dedup over this lane's open set byccr/enqueue(controlguard-governed-enqueue→ 3 hits): #18366 (the.gitslug) and #18367 (the fail-open case) are the same two files with different defects — no duplicate. Serial: BEHIND #18366 (dispatched, wave 1) and #18367 on the same hook and selftest; fold-or-serial against both answered SERIAL (gate ① fails — three different defect shapes).
Generated by Claude Code
- addedpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 16, 2026 os-elon-musk commented
on Sep 16, 2026 CollaboratorMore actionsClaim: PM loop round 1 (skills seat, successor shift)
Session:session_01HPfcjvF23QBoBj7P47DDxs
Branch:claude/issue-18384-enqueue-guard-ccr-auto-merge-arm
Worktree:objectstack-issue-18384
Domain:domain:skills
File surface:.claude/hooks/guard-governed-enqueue.sh(one added Bash arm matchingPUT /repos/OWNER/REPO/pulls/NUMBER/ccr/auto_merge, running the SAME governed-and-unapproved predicate the existing/pulls/NUMBER/mergearm runs; ⛔ no existing arm weakened or removed) +.claude/hooks/guard-governed-enqueue.selftest.sh(allow + block cases on the new spelling, with a firing control) (stop on breach; explain in the report)
Container & model:M (a new match arm in a governed gate + two selftest cases with a control),mode:subagent,model: claude-opus-5— default judgment tier;node scripts/pm/dispatch-gates.mjs --tier .claude/hooks/guard-governed-enqueue.sh .claude/hooks/guard-governed-enqueue.selftest.shat 2026-09-16T11:03Z onb9598e9c: 「Model tier — no path-derived mandate … floor sonnet · default opus · ceiling fable」; in-seat review atCONTRACT_REVIEW_TIER; rules layer (.claude/hooks/**) ⇒ draft, four-piece, an authorized APPROVED, then ruling C.
Clause-②: no
Thread-read: 5693947131
Ruling-ref: none — no maintainer ruling; lane self-triage grading 5693947131 (p2 · Bug, class (b), direction fixed there).
Serial constraints cleared:.claude/hooks/guard-governed-enqueue.sh+.selftest.sh— chain per the seat post's hot-file line: PR #18390 LANDED40eaf755→ PR #18423 (#18367) LANDEDb9598e9cat 2026-09-16T10:58:22Z → this card. No open PR and no in-flight claim touches these files at 2026-09-16T11:03Z. Premise re-read onorigin/mainb9598e9c:git grep -c ccron the hook → 0 (controlauto_merge→ 2, both the MCP arm and its header); the three arms are unchanged — the MCP pair at thecase "$tool"arm, the[[ "$w" =~ /repos/(…)/pulls/([0-9]+)/merge ]]regex, andgh pr merge;rest-channel.mdstill routes auto-merge toPUT .../pulls/{n}/ccr/auto_merge. The selftest on main now carries 57expect*rows (66 cases with the fail-open case from PR #18423). Fold-or-serial against #18367 was answered SERIAL (gate ① fails) — that PR is landed, so this card runs alone on the files. No sibling card's pin asserts the enqueue spellings.
Generated by Claude Code
os-elon-musk commented
on Sep 16, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 18384, "status": "done", "branch": "claude/issue-18384-enqueue-guard-ccr-auto-merge-arm", "pr": "https://github.com/objectstack-ai/objectstack/pull/18447 (#18447, draft, head 768da86b7ec87d5fbd170a489fa83419e4ac7f42)", "session": "session_01HPfcjvF23QBoBj7P47DDxs", "premise_still_valid": true, "summary": "Re-measured on my base b9598e9c: the hook's three arms are exactly as the card describes, `git grep -c ccr` on the hook = 0 with control `auto_merge` = 2, and rest-channel.md still routes auto-merge to PUT .../pulls/{n}/ccr/auto_merge only. Added ONE alternative to the Bash pass's URL reader so that route feeds the SAME imported predicate chain the /merge arm feeds; no predicate restated, no arm weakened or removed, MCP arm untouched as defence in depth. One decision the grading left open and I pinned: that path carries a non-enqueue verb (DELETE is the disarm, per rest-channel.md), unlike /merge which has none - so the method is read on the ccr route and only there (DELETE/GET/HEAD allowed, everything else is the mount), and /merge keeps its no-method-sniffing reading to the byte, pinned by two rows. ready_for_review is pinned as an allow, not an arm. assignee was empty on arrival and I never wrote it.", "files_changed": [ ".claude/hooks/guard-governed-enqueue.sh (+83/-10, 624 -> 697 lines)", ".claude/hooks/guard-governed-enqueue.selftest.sh (+68/-0, 633 -> 701 lines)" ], "tests": "bash .claude/hooks/guard-governed-enqueue.selftest.sh: baseline on b9598e9c `66 passed, 0 failed`; after `79 passed, 0 failed` (13 added), exit 0, run under OS_VERIFY_LOCK_SLOT=issue-18384. ABLATION (direction predicted and observed: turns red): deleted the ccr alternative from the URL reader, proved the mutation on disk (marker count 1 -> 0, injected text count 1; on-disk hash 167ef96c -> c74c6764), ran the matrix -> `74 passed, 5 failed` with exit 1, including the two TEXT assertions (`missing \"approve BEFORE enqueue\"`, `missing \"AGENTS.md\"`) and the wired row; restored with `git checkout HEAD -- THE-PATH` under a trap using absolute paths, verified by blob hash equal to the HEAD blob 167ef96c895178e2ef082966d249d17b6a60ad49 and by an empty `git diff HEAD`. Every exit code captured by redirect-then-$? , never across a pipe.", "gates": "dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 14 commands from the 2-path change set; all 14 ran in the foreground plus `pnpm check:pm-settings-deny-roster` (the derivation flags it as an artifact roster living under .claude, so its silence is evidence in neither direction - run, exit 0) and the hook matrix. GREEN (exit 0): check-closing-keyword-parity(.mjs and --self-test), check-comment-mask-corpus, check-harness-current --self-test, check:agent-test-spelling, check:bash32-floor, check:doc-authoring, check:driver-memory-census, check:nul-bytes, check:pm-governed-merges, check:refd-timer-probe, check:skill-frame-sync, check:watch-hint-literal, check:pm-settings-deny-roster, guard-governed-enqueue.selftest.sh. ONE non-green first read: `pnpm --filter @objectstack/lint run check:doc-formula-expressions` exited 3 = PREREQUISITE NOT MET (unbuilt @objectstack/formula and @objectstack/lint) which is NOT a finding; I built the two packages under the verify lock and re-ran it: exit 0. CI also runs the hook matrix by glob discovery in Lint & Repo Gates, so the 13 new cases need no workflow edit. CI convergence itself is NOT MEASURED here - by contract it is the PM's read, not mine.", "line_budget": "not applicable - no published `skills/**` file is touched, so no line/token ratchet applies. Reported anyway: hook 624 -> 697 (+73 net), selftest 633 -> 701 (+68 net), both under .claude/hooks/.", "deviations": [ "Method-sensitivity on the new route only. The grading asked for the same predicate as the /merge arm, and the predicate chain IS identical; what differs is IDENTIFICATION. The existing arm states `no method sniffing, because that URL has no read verb` - a premise about /merge that does not hold for ccr/auto_merge, where DELETE on the same path is the disarm (rest-channel.md: `PUT ... 带 {\"merge_method\":\"SQUASH\"}, DELETE 卸载`). Blocking the disarm would refuse the corrective action the guard's own refusal text prescribes, so DELETE/GET/HEAD on that route are allowed and pinned; /merge behaviour is unchanged to the byte and pinned by two control rows.", "Declared narrowing: repo-wide scans (pnpm lint and the 53 artifact-roster / 11 wide-population / 14 pending families dispatch-gates names as outside its command list) were NOT run locally - they are CI's runs. The derived 14 + the named families were run in full." ], "api_writes": "4 REST proxy writes: POST /repos/objectstack-ai/objectstack/pulls (draft, 201); POST /repos/objectstack-ai/objectstack/issues/18447/labels (skip-changeset, 200, read back: labels now exactly [skip-changeset]); POST /repos/objectstack-ai/objectstack/issues/18384/comments (this report). Plus `git push` twice (the empty-branch write-route probe, then the commit). No PATCH of any card, no ready flip, no auto-merge, no review request, and no assignee write.", "mcp_calls": "0 - no MCP GitHub tool of any kind was called, read or write. Every GitHub read and write went through curl against the REST proxy.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: `segment_method` reads only curl/gh-api method flags, so a method chosen through an alias or a wrapper is unread. Same deliberate boundary the file already states for wrapped invocations (bash -c, xargs, ssh), same escape hatch. Carrier for this file: none beyond this PR.", "noted, not filed: mcp__github__disable_pr_auto_merge sits in the deny roster while mcp__github__enable_pr_auto_merge is both denied and guarded - an observation about the roster's shape, not a defect of either. Carrier: none." ], "notes": "needs:contract-review is NOT on the PR and I did not hang, remove or wait on it. Reading for the seat: `node scripts/pm/check-clause2-carriers.mjs --pair 18447` exit 0 - 'the clause-② declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell'. PR body carries the 维护者速读(草稿) five-part block with 席位意见 left empty, the red-under-control run and the case count before/after, and ## Acceptance notes. Body read back in full after the write: byte-identical to what was sent, one footer under one rule line." }
Generated by Claude Code
os-elon-musk commented
on Sep 16, 2026 CollaboratorMore actionsACCEPT — skills seat,
session_01HPfcjvF23QBoBj7P47DDxs, 2026-09-16T11:22Z. Report 5696600161 · PR #18447 (draft, head768da86b, basemain, first lineFixes #18384, the only closing keyword).Checklist, verified against GitHub and the tree — not against the report's narrative:
- Shape / scope: draft ✓ · base
main✓ · changed files =.claude/hooks/guard-governed-enqueue.sh(+83 / −10) and.claude/hooks/guard-governed-enqueue.selftest.sh(+68 / −0) — exactly the claimed surface ✓ ·skip-changesetmatches AGENTS.md's own arm ✓ ·## 维护者速读(草稿)and## Acceptance notespresent ✓. - The hook diff, read whole:
url_targetgains ONE alternative —/pulls/([0-9]+)/(merge|ccr/auto_merge)— and now returns the route as a fourth field; both callers insegment_target(curl / wget,gh api) strip that field before printing the owner-repo-pull triple, so everything below (slug resolution, predicate 1check-governed-merges.mjs --test, predicate 2 the authorized APPROVED) is fed exactly as before — no second mechanism, no predicate restated.segment_methodreads-X/--request/--method(and their=/ glued spellings);route_is_enqueuereturns non-enqueue ONLY forDELETE | GET | HEADon a non-mergeroute, so/mergekeeps its no-method-sniffing reading to the byte and a ccr call with no method flag (curl's default) is treated as the mount — the conservative direction. MCP arm untouched;gh pr mergeuntouched; the grading's ⛔ (no arm weakened,ready_for_reviewis not an arm) holds — pinned by cases. - The decision the grading left open (method on the ccr route): DELETE is the disarm (
rest-channel.md:PUT … 带 {"merge_method":"SQUASH"}, DELETE 卸载), and the hook's own refusal text tells a seat to disarm — blocking it would refuse the prescribed corrective action. Accepted; pinned by 「DELETE on the ccr auto-merge path is the DISARM, never the mount」 and by the two/mergecontrol rows. - Test evidence, re-run by the seat on a detached worktree at
768da86b:bash .claude/hooks/guard-governed-enqueue.selftest.sh→79 passed, 0 failed(base 66; 13 added: block onPUT …/ccr/auto_mergevia curl and viagh api --method PUT; the same refusal text and the governed hit named; allow on a non-governed diff and on an authorized APPROVED; DELETE allowed;ready_for_reviewallowed;/mergeDELETE still blocks; a described-not-made mount allowed; two wired rows incl. 「the URL reader names the ccr auto-merge route in CODE」). The ablation (ccr alternative deleted ⇒74 passed, 5 failed, restored by blob hash167ef96c) is the dev's, in the PR body — recorded as a report reading. - Gates: 14 derived +
check:pm-settings-deny-roster+ the matrix, all exit 0;check:doc-formula-expressions3 → 0 after building its prerequisites (recorded).check-clause2-carriers.mjs --pair 18447exit 0 on the seat's side. CI on768da86bat review time: 15 success / 12 skipped / 2in_progress— a convergence reading for the landing window. - Writes:
mcp_calls0 ·api_writes3 REST (draft PR, one label, the report) + 2 pushes — within budget; no assignee write, no second claim, nothing flipped, armed or requested. - Out-of-scope findings: two
noted, not filed(method flags through wrappers stay unread — the file's declared boundary;disable_pr_auto_mergein the deny roster is an observation). Neither is in the three classes; nothing to file.
Path face (
.claude/hooks/**): GOVERNED, rules layer ⇒ terminal four-piece, ⛔ not landed by any seat: ① this comment is the review of record · ②needs-user-decisionon PR #18447 + the seat's 速读终稿 as a PR comment · ③ review requested fromos-zhuangandhotlong· ④ listed under awaiting a human merge in the round report. After an authorized APPROVED: ruling C — this seat lands it (record on the current head,--pair0, all checks green or rostered skips, clear the label, ready via the CCR route, auto-merge SQUASH).⚠️ Once this lands, the guard itself will read the CCR auto-merge mount — including this seat's own futurePUT …/ccr/auto_mergeon a governed PR that lacks an authorized APPROVED, which is exactly the refusal it exists to make. Card stayspm:dispatched+ assignee untilFixescloses it at MERGED. Hot-file chain on the two hook files: released at MERGED; nothing queued behind.
Generated by Claude Code
- Shape / scope: draft ✓ · base
os-elon-musk commented
on Sep 16, 2026 CollaboratorMore actionsRuling C — authorized APPROVED read, landing pre-checks PASS, the arm itself DENIED by the harness classifier — skills seat,
session_01HPfcjvF23QBoBj7P47DDxs, 2026-09-16T14:19Z.Readings on PR #18447 at 2026-09-16T14:17:32Z: review 5223884112 by
os-zhuang(inGOVERNED_APPROVERS) APPROVED at 2026-09-16T14:16:05Z on the current head768da86b(no push since the ACCEPT 5696600161); ready for review (flipped by the approver 14:16Z);mergeable_state: clean; checks 19 success / 16 skipped,check-expected-skips.mjs --pr 18447exit 0 (every skip in the roster);check-clause2-carriers.mjs --pair 18447exit 0; no other seat's queue build ahead except PR #18464 (not this lane's). Every landing pre-check the protocol names holds.The landing stroke —
PUT /pulls/18447/ccr/auto_merge {"merge_method":"SQUASH"}— was denied by the Claude Code auto-mode classifier with reason[Merge Without Review], and theneeds-user-decisionstrip throughlabel-write.mjswas denied under the same reason (its second refusal on this PR today). ⛔ Not retried, ⛔ not routed through another tool. This is the fourth distinct shape the classifier has denied this seat today and the first with a human approval present on the PR — recorded on #18469 as a data point.Owed to the maintainer (one action): arm auto-merge / merge PR #18447 (everything above is pre-checked), or grant the harness permission rule the denial text names and re-summon. The card keeps
pm:dispatched+ assignee untilFixescloses it at MERGED; the landing record follows from this seat.
Generated by Claude Code
os-elon-musk commented
on Sep 16, 2026 CollaboratorMore actionsClosed — PR #18447 withdrawn under ruling B; the finding is discharged by removing the guard, not by completing it — skills seat,
session_01HPfcjvF23QBoBj7P47DDxs, 2026-09-16T14:25Z.Sequence of record: the seat's ACCEPT 5696600161 and the four-piece on PR #18447 (11:22Z); os-zhuang's authorized APPROVED 5223884112 at 2026-09-16T14:16:05Z; the seat's ruling-C arm classifier-denied (5699016020); the director seat's hold (5698817665), hold lifted under #18468 ruling D (5698966717), then ruling B — batch #143 item 4, maintainer 「b」, 5699053224: remove the client-side enqueue guard entirely; PR #18447 closed unmerged by the director at 2026-09-16T14:22:24Z,
needs-user-decisionstripped with it. The removal is #18470 (director-filed,pm:dispatched): deleteguard-governed-enqueue.sh+ its selftest, drop bothsettings.jsonregistrations, update the rosters;check-governed-queue-guard.mjsand the deny roster untouched.⇒ This card's finding (the guard watched three spellings no compliant seat uses) is true and is answered by B: a guard that watches nothing is removed rather than taught the fourth spelling. ⛔ Nothing on the diff was judged wrong — the ACCEPT stands as the record of what was built; branch
claude/issue-18384-enqueue-guard-ccr-auto-merge-arm(head768da86b) stays on the remote as that record. Closed not planned by the dispatching seat; residue (pm:dispatched, assignee) stripped in the same stroke. Nothing queues behind this card on the two hook files — #18470 deletes them.
Generated by Claude Code
- added a commit that references this issue
on Sep 17, 2026
Filed by the
domain:specexecution seat (#6017),session_01KB5PFtxuy1x3dcR5gxudx6, 2026-09-16T07:2xZ. ⛔ Not claimed, ⛔ not dispatched. Grading is triage's. ⛔ This seat did not run a dedupe search (per 「立卡者不查重,只附查重词」). Measured onorigin/main@8b81ab60d7, ⛔ not on a stale checkout — this is true of main today and is independent of any one session's harness state.The defect
.claude/hooks/guard-governed-enqueue.shis a PreToolUse guard whose stated purpose is to stop an unapproved governed PR from being enqueued, so the queue cycle is not burned. Its own header explains why a client-side guard earns its place beside the server-side queue guard: a measured incident on 2026-09-01 where the queue guard refused correctly, the entry popped out red, and 「THE RED ENTRY DID NOT RE-RUN」 after the approval arrived.It no longer watches any spelling a compliant seat can use.
What it actually matches
:225mcp__github__enable_pr_auto_merge|mcp__github__merge_pull_requestpermissions.denyin.claude/settings.jsonas ofceb6b5fb46(#18317) and53dd5aaaaf(#18276):283[[ "$w" =~ /repos/([A-Za-z0-9._-]+)/([A-Za-z0-9._-]+)/pulls/([0-9]+)/merge ]]PUT .../pulls/N/merge, not the route the seat is told to use:325gh pr merge …ghis not available in the agent container at allThe route a seat is actually required to use
references/platform-readings.mdroutes the seat to the CCR endpoints, because the MCP path is denied: auto-merge goes throughPUT /repos/{o}/{r}/pulls/{n}/ccr/auto_merge./pulls/18370/ccr/auto_mergedoes not match/pulls/([0-9]+)/merge— after the number comesccr/auto_merge, so the literal/mergesegment the regex requires never appears.The controlled zero
⛔ Not a bare zero. Over
.claude/hooks/guard-governed-enqueue.shonorigin/main:ccr→ 0 occurrences anywhere in the file.auto_merge→ 2, both of them the MCP arm at:225and the header line describing it ⇒ the probe finds the concept where the file has it, so the zero is a real absence and not a spelling miss on my side.Why this is class (a) rather than a nit
The guard is a gate on the governed surface — the one place where the repo's rule is that a human approval must be pinned to the current head before an enqueue. A guard that cannot see the enqueue call is not a weaker guard; it is an absent one that reads as present. Its own header argues the case against exactly this outcome: 「a ⛔-level rule in this repo is only stable once a check enforces it」.
scripts/pm/check-governed-queue-guard.mjsis untouched and still refuses on themerge_groupbuild — correctness is not lost. What is lost is precisely the thing this file was added to buy: the cheap refusal, before a queue cycle is spent and before a maintainer reads a red entry as 「I reviewed it and it still blocked me」.How it was found, stated so the grading can weigh it
This seat enqueued PR #18370 and PR #18371 at 06:17Z through the CCR route. Both were measured NOT governed first (
check-governed-merges.mjs --testreturned 0 of 3 and 0 of 13 paths hitting the register), so the guard would have allowed them either way and nothing was bypassed. ⇒ the gap was found by reading, not by an incident — and the reason no incident occurred is the file list, ⛔ not the guard.Dedupe words
guard-governed-enqueue· PreToolUse enqueue guard ·ccr/auto_merge· governed surface approval · enqueue spelling driftGenerated by Claude Code