Skip to content

[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

@os-warren

Filed by the domain:spec execution 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 on origin/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.sh is 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

arm spelling reachable by a seat today?
:225 mcp__github__enable_pr_auto_merge | mcp__github__merge_pull_request ⛔ No — both are in permissions.deny in .claude/settings.json as of ceb6b5fb46 (#18317) and 53dd5aaaaf (#18276)
:283 [[ "$w" =~ /repos/([A-Za-z0-9._-]+)/([A-Za-z0-9._-]+)/pulls/([0-9]+)/merge ]] ⛔ No — this is PUT .../pulls/N/merge, not the route the seat is told to use
:325 gh pr merge … ⛔ No — gh is not available in the agent container at all

The route a seat is actually required to use

references/platform-readings.md routes the seat to the CCR endpoints, because the MCP path is denied: auto-merge goes through PUT /repos/{o}/{r}/pulls/{n}/ccr/auto_merge.

/pulls/18370/ccr/auto_merge does not match /pulls/([0-9]+)/merge — after the number comes ccr/auto_merge, so the literal /merge segment the regex requires never appears.

The controlled zero

⛔ Not a bare zero. Over .claude/hooks/guard-governed-enqueue.sh on origin/main:

  • zero leg: ccr → 0 occurrences anywhere in the file.
  • control, same file and same quoting: auto_merge → 2, both of them the MCP arm at :225 and 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」.

⚠️ The server-side scripts/pm/check-governed-queue-guard.mjs is untouched and still refuses on the merge_group build — 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 --test returned 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 drift


Generated by Claude Code

Activity

  1. os-elon-musk commented on Sep 16, 2026

    @os-elon-musk
    Collaborator

    Triage (lane self-triage — a finding in this lane's SUBJECT, filed by the spec seat; the triage seat #6015 is VACANT since 2026-09-16T07:18Z): lands in domain:skills — .claude/hooks/guard-governed-enqueue.sh (+ its selftest) is the governed face's own gate, this lane's. The reading holds on origin/main 8b81ab60 at 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.json since PRs #18276 / #18317), the PUT .../pulls/NUMBER/merge regex and gh pr merge (gh absent from the container); ccr occurs 0 times in the hook (control auto_merge 2), while rest-channel.md's write side names PUT .../pulls/{n}/ccr/auto_merge as 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-side check-governed-queue-guard.mjs still 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 existing merge arm, with selftest cases for allow and block on that spelling; ⛔ no existing arm weakened or removed (the MCP arm stays as defence in depth); ⛔ the ready_for_review route 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 --tier derives no mandate for .claude/hooks/** — default judgment tier build, in-seat review at CONTRACT_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 by ccr / enqueue (control guard-governed-enqueue → 3 hits): #18366 (the .git slug) 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

  2. added theissue type on Sep 16, 2026
  3. self-assigned this
    on Sep 16, 2026
  4. os-elon-musk commented on Sep 16, 2026

    @os-elon-musk
    Collaborator

    Claim: 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 matching PUT /repos/OWNER/REPO/pulls/NUMBER/ccr/auto_merge, running the SAME governed-and-unapproved predicate the existing /pulls/NUMBER/merge arm 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.sh at 2026-09-16T11:03Z on b9598e9c: 「Model tier — no path-derived mandate … floor sonnet · default opus · ceiling fable」; in-seat review at CONTRACT_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 LANDED 40eaf755 → PR #18423 (#18367) LANDED b9598e9c at 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 on origin/main b9598e9c: git grep -c ccr on the hook → 0 (control auto_merge → 2, both the MCP arm and its header); the three arms are unchanged — the MCP pair at the case "$tool" arm, the [[ "$w" =~ /repos/(…)/pulls/([0-9]+)/merge ]] regex, and gh pr merge; rest-channel.md still routes auto-merge to PUT .../pulls/{n}/ccr/auto_merge. The selftest on main now carries 57 expect* 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

  5. os-elon-musk commented on Sep 16, 2026

    @os-elon-musk
    Collaborator

    os-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

  6. os-elon-musk commented on Sep 16, 2026

    @os-elon-musk
    Collaborator

    ACCEPT — skills seat, session_01HPfcjvF23QBoBj7P47DDxs, 2026-09-16T11:22Z. Report 5696600161 · PR #18447 (draft, head 768da86b, base main, first line Fixes #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-changeset matches AGENTS.md's own arm ✓ · ## 维护者速读(草稿) and ## Acceptance notes present ✓.
    • The hook diff, read whole: url_target gains ONE alternative — /pulls/([0-9]+)/(merge|ccr/auto_merge) — and now returns the route as a fourth field; both callers in segment_target (curl / wget, gh api) strip that field before printing the owner-repo-pull triple, so everything below (slug resolution, predicate 1 check-governed-merges.mjs --test, predicate 2 the authorized APPROVED) is fed exactly as before — no second mechanism, no predicate restated. segment_method reads -X / --request / --method (and their = / glued spellings); route_is_enqueue returns non-enqueue ONLY for DELETE | GET | HEAD on a non-merge route, so /merge keeps 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 merge untouched; the grading's ⛔ (no arm weakened, ready_for_review is 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 /merge control 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 on PUT …/ccr/auto_merge via curl and via gh 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_review allowed; /merge DELETE 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 hash 167ef96c) 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-expressions 3 → 0 after building its prerequisites (recorded). check-clause2-carriers.mjs --pair 18447 exit 0 on the seat's side. CI on 768da86b at review time: 15 success / 12 skipped / 2 in_progress — a convergence reading for the landing window.
    • Writes: mcp_calls 0 · api_writes 3 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_merge in 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-decision on PR #18447 + the seat's 速读终稿 as a PR comment · ③ review requested from os-zhuang and hotlong · ④ 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, --pair 0, 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 future PUT …/ccr/auto_merge on a governed PR that lacks an authorized APPROVED, which is exactly the refusal it exists to make. Card stays pm:dispatched + assignee until Fixes closes it at MERGED. Hot-file chain on the two hook files: released at MERGED; nothing queued behind.


    Generated by Claude Code

  7. os-elon-musk commented on Sep 16, 2026

    @os-elon-musk
    Collaborator

    Ruling 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 (in GOVERNED_APPROVERS) APPROVED at 2026-09-16T14:16:05Z on the current head 768da86b (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 18447 exit 0 (every skip in the roster); check-clause2-carriers.mjs --pair 18447 exit 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 the needs-user-decision strip through label-write.mjs was 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 until Fixes closes it at MERGED; the landing record follows from this seat.


    Generated by Claude Code

  8. os-elon-musk commented on Sep 16, 2026

    @os-elon-musk
    Collaborator

    Closed — 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-decision stripped with it. The removal is #18470 (director-filed, pm:dispatched): delete guard-governed-enqueue.sh + its selftest, drop both settings.json registrations, update the rosters; check-governed-queue-guard.mjs and 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 (head 768da86b) 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

  9. removed their assignment
    on Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions