Skip to content

fix(hooks): watch the ccr auto-merge route in guard-governed-enqueue - #18447

Closed
os-elon-musk wants to merge 1 commit into
mainfrom
claude/issue-18384-enqueue-guard-ccr-auto-merge-arm
Closed

os-elon-musk wants to merge 1 commit into
mainfrom
claude/issue-18384-enqueue-guard-ccr-auto-merge-arm

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

Fixes #18384

The guard's declared job is to refuse an enqueue-class call on a governed PR that carries no authorized approval. It watched three spellings and a compliant seat on this box could reach none of them:

arm spelling reachable?
MCP mcp__github__enable_pr_auto_merge / mcp__github__merge_pull_request no — both in permissions.deny
Bash REST a URL carrying the literal /merge segment after the pull number no — not the route the protocol mandates
Bash gh gh pr merge no — gh is absent from the agent container

The protocol's only auto-merge route is PUT /repos/OWNER/REPO/pulls/NUMBER/ccr/auto_merge (references/rest-channel.md, write side). After the pull number comes ccr/, so the literal /merge segment the old regex required never appears. Declared, not enforced.

What changed

.claude/hooks/guard-governed-enqueue.sh — one added alternative in the Bash pass's URL reader, so the ccr auto-merge route feeds the same imported predicate chain (check-governed-merges.mjs --test + authorizedApprovalVerdict / GOVERNED_APPROVERS) the /merge arm already feeds. No predicate is restated, no path list is added, no arm is weakened or removed — the MCP arm stays as defence in depth, because a deny roster is policy and policy can be edited.

The one asymmetry, pinned: unlike /merge, that path carries a non-enqueue verb. DELETE on it is the disarm — the corrective call that takes a mounted PR back out of the queue, and rest-channel.md names it as such. Refusing the disarm on an unapproved governed PR would block the one action this guard's own refusal text asks for, so the method is read on that route and only on that route: DELETE / GET / HEAD are allowed there, everything else on that path is the mount (a segment naming no method included — the protocol spells the mount as a PUT, and an unnamed method there is a call this guard would rather see than miss). /merge keeps its "no method sniffing" reading to the byte, pinned by two rows below.

POST .../pulls/NUMBER/ccr/ready_for_review is not an arm: it queues nothing, and it is the step that produces the review this guard is waiting for. Pinned as an allow.

Self-test: 66 cases before, 79 after (13 added)

Block / allow / approved on the new spelling, the gh api --method PUT spelling, the disarm, the ready flip, writing about the route, the merge arm unchanged under both a named DELETE and no method at all, and two wired rows (the Bash matcher door in .claude/settings.json, and the route named in the hook's code with comments excluded) so the arm cannot be dropped silently — the MCP wired rows were green the whole time the guard watched nothing a seat could write.

79 passed, 0 failed

Red under a control — the new rows are not vacuous

The ccr alternative was deleted from the URL reader, the mutation was proved on disk, the matrix was run, and the file was restored to its HEAD blob. Direction predicted and observed: turns red.

HEAD blob            : 167ef96c895178e2ef082966d249d17b6a60ad49
on-disk before       : 167ef96c895178e2ef082966d249d17b6a60ad49
marker count before  : 1
marker count after   : 0   (injected text count: 1)
on-disk after        : c74c676458d9b35ff5845d55d0f2f650f25dcb3e
selftest exit (mutated): 1
  FAIL want=block got=allow  a REST PUT .../pulls/NUMBER/ccr/auto_merge through curl
  FAIL missing "approve BEFORE enqueue"  the ccr mount earns the SAME refusal, not a second one
  FAIL missing "AGENTS.md"  the ccr refusal names the governed hit, so the register really ran
  FAIL want=block got=allow  gh api --method PUT .../pulls/NUMBER/ccr/auto_merge
  FAIL the URL reader no longer names the ccr auto-merge route: the route a seat can reach is unwatched again

74 passed, 5 failed
on-disk restored     : 167ef96c895178e2ef082966d249d17b6a60ad49
git diff HEAD (must be empty):
RESTORE OK — byte-identical to HEAD

Two of the five are the text assertions, which is the reading a says row owes: they discriminate, rather than passing in both worlds.

Gates run locally (all green, foreground, exit codes captured before any pipe)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 14 commands from this change set; all 14 ran, plus pnpm check:pm-settings-deny-roster (which that derivation flags as an artifact roster living under .claude, i.e. its silence is evidence in neither direction — so it was run, and it is green) and the hook matrix itself.

  • bash .claude/hooks/guard-governed-enqueue.selftest.sh79 passed, 0 failed, exit 0
  • pnpm check:pm-settings-deny-roster — exit 0 (unaffected by a Bash arm, as expected)
  • pnpm check:bash32-floor — exit 0. The new segment_method upper-cases with tr, never the bash-4 case-modification operator.
  • pnpm check:nul-bytes, check:pm-governed-merges, check:doc-authoring, check:agent-test-spelling, check:driver-memory-census, check:refd-timer-probe, check:skill-frame-sync, check:watch-hint-literal, check:closing-keyword-parity (+ its self-test), check-comment-mask-corpus, check-harness-current --self-test — all exit 0
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions — first run exit 3 = PREREQUISITE NOT MET (unbuilt packages), which is not a finding; the two packages were built and it was re-run: exit 0.

CI runs this matrix by glob discovery in Lint & Repo Gates, so the new cases are picked up with no workflow edit.

Changeset

skip-changeset. AGENTS.md Post-Task Checklist step 3, verbatim: 「that label is for a diff that publishes nothing from any released package」. This diff is two files under .claude/hooks/, which no package's files[] ships.

维护者速读(草稿)

改了什么 — 仓库有个客户端守卫,作用是在「把一个受管 PR 送进合并队列」之前先拦一道:没有人工批准就不让排队。它原本盯着三种写法,而今天任何一个守规矩的 agent 席位一种都碰不到——两个 MCP 写工具早已进了拒绝名单,容器里没有 gh,剩下的 REST 写法它要求 URL 里出现字面的 /merge,而协议规定的唯一 auto-merge 路由在编号后面接的是 ccr/。这次给它加上那条真正在用的路由,让它跟原有那条走同一套判定(是否受管、是否有授权批准),一条旧的都没动、没削弱。

为什么改 — 守卫的价值不是「最终正确性」,那一道在服务端,没丢。丢的是便宜:没有它,一个未批准的受管 PR 会真的排进队列,烧掉一个队列周期,然后弹出一条红色条目——而 GitHub 不会因为事后补了批准就重跑那条队列条目。维护者看到的是一条「我明明审过了它还是红」的记录。一个读起来存在、实际上一种写法都盯不到的守卫,比没有更糟。

风险与代价(含回滚) — 风险面很窄:只多认一条 URL 形状,判定链一个字没改,所以它只可能拒绝服务端本来也会拒绝的东西。唯一需要拍板的判断是:同一条路径上的 DELETE 是「卸载 auto-merge」,属于纠正动作,本 PR 明确放行并用测试钉死;ready_for_review 同样不拦。回滚代价为零:两个文件,git revert 即可,不影响任何已发布产物,不涉及数据或线上行为。

席位意见 — (留空,待席位定稿)

你要做的 — 读一眼上面那条 DELETE 放行的判断是否合你意,然后批准这个 PR(受管面,按规矩由你说了算再落地)。

Acceptance notes

  • Scope held to this card: one added arm + its matrix rows. No existing arm touched; the MCP pair and gh pr merge walk are byte-identical.
  • The dispatch's suggested route (a single alternation in the existing regex) was taken, with one departure it named as a decision to make: the route is captured as a fourth field because the two routes do not read the method alike, per the paragraph above.
  • noted, not filed: the hook's segment_method reads only curl / gh api method flags; a method chosen by an alias or a wrapper is unread. This is the same deliberate boundary the file already states for wrapped invocations (bash -c, xargs, ssh) and the same escape hatch answers it. Carrier for this file: none beyond this PR.
  • noted, not filed: mcp__github__disable_pr_auto_merge is 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.

Generated by Claude Code

The guard's declared job is to refuse an enqueue-class call on a governed PR
that carries no authorized approval. It watched three spellings and a compliant
seat could reach none of them: both MCP tool names sit in `permissions.deny`,
there is no `gh` in the agent container, and the REST arm required a literal
`/merge` segment that the protocol's only auto-merge route never carries.

Adds the `PUT .../pulls/<n>/ccr/auto_merge` route to the same URL reader, feeding
the SAME imported predicate chain the `/merge` arm feeds — no second mechanism,
no arm weakened, no arm removed. The MCP arm stays as defence in depth.

That route, unlike `/merge`, carries a non-enqueue verb on the same path:
`DELETE` is the disarm. So the method is read there, and only there — the disarm
and the read verbs are allowed, everything else on that path is the mount.
`ccr/ready_for_review` is not an enqueue and is not an arm.

Self-test: 66 -> 79 cases, covering block / allow / approved on the new spelling,
the disarm, the ready flip, the merge arm unchanged under both a named and an
unnamed method, and two wired rows (the Bash matcher door, the route named in
code) so the arm cannot be dropped silently.

Claude-Session: https://claude.ai/code/session_01HPfcjvF23QBoBj7P47DDxs
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

维护者速读(终稿)

skills 席,session_01HPfcjvF23QBoBj7P47DDxs,2026-09-16T11:22Z。席内复核 ACCEPT 记录在卡 #18384。受管规则层(.claude/hooks/**)⇒ 等一位授权批准账号 APPROVED 后由本席按裁决 C 落地;在此之前保持 draft。

改了什么
入队守卫 guard-governed-enqueue.sh 的 URL 读取器多认一条路由:PUT /repos/OWNER/REPO/pulls/N/ccr/auto_merge(协议规定的挂 auto-merge 唯一路径),命中后走与 /merge 完全相同的两道谓词(受管面判定 + 授权批准)。在这条路由上读方法:DELETE(卸载)、GET、HEAD 不算入队;/merge 一字未动,MCP 臂保留作纵深。自检从 66 条加到 79 条。

为什么改
守卫原来只看三种入队拼法——两个已被 deny 的 MCP 工具、PUT …/merge、容器里没有的 gh pr merge——合规席位真正用的 ccr auto-merge 路由它一条也看不见,等于守卫在场却不起作用。服务端合并队列守卫仍在,正确性没丢,丢的是「批准前就拦住、别烧一个队列周期」这层便宜的客户端防线。

风险与代价(含回滚)
风险低:只增一条匹配、不删不弱任何现有臂;席位在 PR head 上亲跑自测 79/79,dev 在正文展示删掉该匹配后 5 条用例变红。代价:落地后本席自己在无授权批准的受管 PR 上挂 auto-merge 也会被它拦——这正是它该拦的。回滚 = revert 一个 squash 提交。

席位意见
建议批准。方向与本卡定级评论一致,方法判定(DELETE 是卸载不是挂载)有据、已钉用例。

你要做的
一个动作:在本 PR 上 Approve。


Generated by Claude Code

hotlong commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Held — this is today's fourth edit of guard-governed-enqueue.sh, and each landing on a harness-loaded path re-seats every live PM session · 2026-09-16T14:06Z

Director seat, summon #24, session_01Wj1HUjzyeiBQ8atRf1ZhaL. Nothing here is a judgment on the diff — the skills seat's in-seat ACCEPT (#18384) and its 终稿 stand, and this seat read the two-file scope and agrees with the direction. What changed is sequencing: the maintainer raised on the direct channel that seven PM seats have had to re-seat repeatedly today, and the measured cause is three landings of this same hook in nine hours (8b81ab60 06:41 · 40eaf755 09:14 · b9598e9c 10:37) under check-harness-current's all-or-nothing prescription. A fourth landing now is a fourth re-seat wave.

The maintainer's approval is asked to wait until objectstack#18468 (the gate's per-path prescription) is ruled; this PR then lands in the same window as that change — one re-seat instead of two. Recorded as a director sequencing call under 「技术任务间顺序与依赖」; the four-piece set on this PR is untouched, and it stays draft.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 16, 2026 14:16

hotlong commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Hold lifted — ruling D on objectstack#18468 (maintainer 「D:整个去掉「重开」处方」, 2026-09-16T14:13Z) removes the re-seat prescription; the rule PR is dispatched · 2026-09-16T14:16Z

Director seat, summon #24, session_01Wj1HUjzyeiBQ8atRf1ZhaL. Sequencing stays: the #18468 rule PR lands first (governed, maintainer's approval), this PR lands in the same approval window right after it, so the rule change's own landing is the last re-seat wave under the old text. The four-piece set on this PR is untouched; nothing else changes for its reviewers.


Generated by Claude Code

hotlong commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Ruling: batch #143 item 4 · letter B (remove the client-side enqueue guard; this PR is withdrawn) · maintainer 「b」 2026-09-16T14:21Z

Director seat, summon #24, session_01Wj1HUjzyeiBQ8atRf1ZhaL. Presented on the direct channel as a binary — A: land this PR so the guard watches the route seats actually use; B: remove the guard entirely; C (an inert guard) refused — with B recommended. The maintainer's reply, verbatim: 「b」.

What was read before recommending B

  • The hard line is scripts/pm/check-governed-queue-guard.mjs on the merge_group build — it reads reviews live and refuses an unapproved governed PR (measured 2026-09-01, held). Main's ruleset itself requires no review (required_approving_review_count: 0, require_code_owner_review: false, read via GET /rules/branches/main), so that CI check plus the approver's click are the whole server-side wall, and neither moves under B.
  • guard-governed-enqueue.sh only ever bought the cheapness — one queue cycle and one red entry saved per violation — and as shipped it watches three spellings no compliant seat uses (two denied MCP tools, gh pr merge, a literal /merge), so today it guards nothing. This PR would have made it real; B removes it instead.
  • Why removal over completion: it reads shell text (its own header lists the wrappers that walk past it), every fix to it is a governed PR (three today), and the cost it saves is paid only when a seat breaks protocol — the exception, not the path. remove 优于 declare-and-maintain.
  • ⚠️ B reverses the maintainer's own 2026-09-01 ask that produced the file (quoted in its header: 「你写skills 有用吗?他经常会忘记。」). That concern is now answered by the queue guard's red (mechanical, one step later) and by the settings.json deny roster for the MCP enqueue class, which stays.

Execution

  • This PR is closed unmerged; needs-user-decision stripped in the same act (the four-piece leaves with the verdict). ⛔ Nothing here is a judgment on the diff's correctness — the skills seat's ACCEPT stands as a record of what was built.
  • The removal is filed as its own card on the skills lane (linked below): delete .claude/hooks/guard-governed-enqueue.sh and guard-governed-enqueue.selftest.sh, drop both settings.json registrations, and update every roster that names them so the gates go green by design, not by exemption. check-governed-queue-guard.mjs and the deny roster are ⛔ not touched.

Generated by Claude Code

@hotlong hotlong closed this Sep 16, 2026
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…ILL.md:92 become a seat-post reading (objectstack-ai#18476)

Fixes objectstack-ai#18468

Maintainer ruling D on the card, verbatim: 「D:整个去掉「重开」处方」

## What changed

- `.claude/skills/pm-dispatch/SKILL.md:92` re-keyed in place: 812/812
lines, the edited line is 119 bytes, `:91` is byte-identical to base.
The prescription 「否 ⇒ 收班、换新会话再派,⛔ 不推进共享检出」 is gone; the line now reads
「否 ⇒ 记座位贴,自然收班时换会话,⛔ 不中断批次;读数走 `scripts/pm/check-harness-current.mjs`。」
— the reading stays a reading and the pointer stays.
- No twin in `references/core-rules.md`. `grep -n -e
check-harness-current -e 收班 -e 换新会话 -e 不推进共享检出 -e harness
.claude/skills/pm-dispatch/references/core-rules.md` hits only :19, :20
and :76 — the shift-handover readings, none of them a twin of :92.
Repo-wide, the prescription strings lived only in SKILL.md:92 and this
script (its header, verdict and self-test).
- `scripts/pm/check-harness-current.mjs`:
- (a) STALE verdict re-keyed from prescription to report: `…: note it on
the seat post and pick it up at the seat's next natural shift boundary;
⛔ never interrupt a batch for it`. Exit codes 0/1/2 unchanged; the
per-path `-- STALE` / `-- UNDECIDED` line-terminal markers and their
positions unchanged (the file's own provenance contract); the CURRENT
and UNDECIDED strings byte-identical.
- (b) Header docblock: the load-moment claim is narrowed to what was
measured, `.claude/settings.json` (the deny-roster measurement); the
load moment of `.claude/hooks/*` and `.claude/agents/*.md` is stated as
UNMEASURED and the tool asserts neither; the sentence naming re-seating
as the remedy is gone.
- (c) `--self-test`: the byte-for-byte STALE summary expectation is
re-keyed to the new wording; one case is added pinning that the summary
names the seat post and the shift boundary and names neither a re-seat
nor advancing the shared checkout; `SELF_TEST_CASE_FLOOR` rises 14 → 15
so the added case cannot silently stop running. The ✗-line regex is
unchanged because the ✗ line is unchanged. No assertion deleted or
weakened.
- Untouched, by ruling: the `settings.json` deny roster,
`.claude/hooks/*`, `.claude/agents/*.md`, every other SKILL.md line.

## Readings

Self-test, three exit codes (the PM's mechanism hypothesis, confirmed):

- baseline at `8fe5cb8e` (origin/main at branch time): exit 0, 14 cases
- after (a)+(b), before (c): exit 1 — exactly one failure, `and its
summary line is the STALE one, byte for byte`
- after (c): exit 0, 15 cases

Ablation, one-shot from the committed state (taken at `c67c0df1`; the
two files are byte-identical at the rebased head `6f008921`): the
verdict line was mutated back to the old prescription (on-disk grep:
injected clause 1 hit; the new wording left in the self-test expectation
only, 1 hit) → `--self-test` exit 1 with exactly the two STALE pins
failing (`byte for byte` and `a report, not a prescription`); restore
via `git checkout HEAD -- PATH` → `git hash-object` equals the HEAD blob
`616f53f6…`, `git diff HEAD` empty, `git status --porcelain` empty;
control run on the restored tree exit 0, 15/15. Trap-guarded, absolute
path.

Gates: `node scripts/pm/dispatch-gates.mjs --commands` with no paths
(changeset from merge-base) derives 40 families; all 40 were run with
each exit code redirected to a file before any pipe, and `--ran` with
per-command exit codes reconciles `40 derived, 40 run, 0 NOT-MEASURED, 0
UNRUN` (a DERIVED zero). One first-run reading was not a measurement:
`pnpm --filter @objectstack/lint run check:doc-formula-expressions`
exited 3 `PREREQUISITE NOT MET` (unbuilt `@objectstack/formula` and
`@objectstack/lint`); those two were built under
`scripts/pm/os-verify-lock.sh` (VERDICT command-exit 0, held 194s) and
the gate rerun: exit 0. `pnpm check:skill-frame-freshness` was also run,
exit 0 (not derived; the PM asked for it).

The 40-run was taken at `c67c0df1`; `origin/main` then moved by one
comment-only commit (`0fe38a92`, `scripts/check-bash32-floor.mjs`, a
derived family), so the branch was rebased onto it (no PR existed; the
branch was unshared), the derivation re-run at `6f008921` (identical 40
families, no stale-tree warning) and these families rerun at `6f008921`,
all exit 0: `check:bash32-floor`, `check:pm-skill-ratchet` (SKILL.md 812
lines, ceiling 812, headroom 0), `check:pm-skill-id-lint`,
`check:skill-frame-sync`, `check:nul-bytes`, and the self-test.

Changeset: `skip-changeset` — `.claude/**` and `scripts/pm/**` publish
nothing; no released package's `files[]` content moves.

Writes: REST only — `git push` ×2 (initial, then the lease-pinned rebase
push), `POST /pulls` ×1, `POST /issues/N/labels` ×1 (`skip-changeset`),
`POST objectstack-ai/issues/18468/comments` ×1 (the os-dev-report). MCP GitHub write
tools: 0.

## Acceptance notes

- noted, not filed: the self-test keeps one total floor
(`SELF_TEST_CASE_FLOOR`) rather than the per-battery roster AGENTS.md's
self-test section prescribes; the floor was raised to cover the added
case, and the roster shape is not this card's surface. 承接者:无。
- `needs:contract-review` is the seat's to hang or strip; this PR
neither hangs nor strips it.

## 维护者速读(草稿)

**改了什么**:把「共享检出上的 harness 文件落后于
origin/main」这条读数,从「必须收班、换新会话再派」的处方,改成「记在座位贴、到自然收班时再换会话、不打断当前批次」的报告。落在三处:SKILL.md
第 92 行原地改(812 行不变),`check-harness-current`
脚本的判决文案与头部说明,以及它的自测期望。退出码不变,调用它的席位无感。

**为什么改**:裁决 D,原文「D:整个去掉「重开」处方」。今天同一个钩子返工三次,每次都让在岗的七个 PM
会话同时重开;而重开所防的缺口(旧 deny 清单少三个 MCP 写工具)整班一次都没被踩到,并且 ACCEPT 核验仍旧拒收任何点名 MCP
写工具的报告。

**风险与代价(含回滚)**:接受的残余是维护者原话:容器早于 deny
清单变更的席位,到下次自然收班前仍持有那几个被拒的工具;若席位违规,可用绑定账号改一次 PR 状态——可回退,受管 PR
仍由队列规则集与你的点击把守。行数代价为零(812/812)。回滚 = revert 这两个
commit,脚本自测随之回到旧文案,无数据迁移、无配置变更。

**席位意见**:

**你要做的**:本 PR 触受管面(`.claude/**`),保持 draft,等你一句批准后由归属席位落地;随后 PR objectstack-ai#18447
按裁决 D 在同一批准窗口跟进(先规则、后钩子),一次收班顶两次。

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…roup queue guard is the line (objectstack-ai#18478)

Fixes objectstack-ai#18470

Executes maintainer ruling **B** on PR objectstack-ai#18447 — reply verbatim 「b」, then
「这个卡你也直接派发」: remove the client-side enqueue guard hook entirely. **The
server-side line stays**: `scripts/pm/check-governed-queue-guard.mjs` on
the `merge_group` build (reads reviews live and refuses an unapproved
governed PR) plus the `.claude/settings.json` `deny` roster for the MCP
enqueue class, plus the approver's click.

## What changed

| file | change |
|---|---|
| `.claude/hooks/guard-governed-enqueue.sh` | deleted (624 lines) |
| `.claude/hooks/guard-governed-enqueue.selftest.sh` | deleted (633
lines) |
| `.claude/settings.json` | the standalone MCP-enqueue matcher entry
(`enable_pr_auto_merge` / `merge_pull_request`) removed — this hook was
its only command; the one `guard-governed-enqueue.sh` command block
removed from the `Bash` matcher. 13 lines, all removals. `permissions`
(incl. the 17-entry `deny`) is byte-identical to `origin/main`; the Edit
matcher and the other four Bash hooks are unchanged and in their
original order. |
| `scripts/pm/check-governed-merges.mjs` | two comments re-keyed: the
`--test` stderr note no longer names the hook as the stdout consumer
(stdout stays a machine-read `--json` surface for any caller). No code
change. |
| `scripts/pm/check-governed-queue-guard.mjs` | the
`authorizedApprovalVerdict` docblock re-keyed: the importer note now
records that the seat-side hook was removed by ruling and that the queue
leg is this function's only consumer; the rename fence stays as "grep
for importers in the same diff". Refusal logic untouched — 238 self-test
cases pass unchanged. |
| `scripts/pm/check-settings-deny-roster.mjs` | two header paragraphs
re-keyed: the deny roster is now the only seat-side refusal of the MCP
enqueue class; the hook is no longer described as defence in depth.
`CONTENT_WRITE_TOOLS` untouched — 36 self-test cases pass, 17 declared =
17 enforced. |

Not touched, by ruling: the `deny` roster, CODEOWNERS,
`check-governed-queue-guard.mjs`'s refusal logic; and objectstack-ai#18468's surface
(`.claude/skills/pm-dispatch/SKILL.md:92`,
`scripts/pm/check-harness-current.mjs`).

## Premise check against origin/main (BASE `8fe5cb8e`)

`git grep -l guard-governed-enqueue origin/main` → exactly six files:
the two hook files, `.claude/settings.json`,
`check-governed-merges.mjs`, `check-governed-queue-guard.mjs`,
`check-settings-deny-roster.mjs`. Neither
`scripts/pm/dispatch-gates.mjs` nor `package.json` names the hook or its
self-test: no family runs it, and `lint.yml`'s hook self-test step
discovers `*.selftest.sh` with `find` at run time, so the deletion needs
no edit there (it now discovers 5).

Prose grep (deliverable step 4), all zero lines describing the hook as a
live control: `grep -rn -iE 'enqueue[ -]guard|governed[
-]enqueue|guard.governed.enqueue|GOVERNED_ENQUEUE|enqueue hook|seat-side
hook|PreToolUse|client-side|客户端|钩子' .claude/skills .claude/agents
AGENTS.md CLAUDE.md docs/adr`. The PreToolUse mentions in `AGENTS.md`
(lines 221, 323) and `.claude/agents/os-dev.md` (line 27) are the
worktree and stash guards; `platform-readings.md:395` names the CI
context `Governed Surface Queue Guard` (server-side, stays);
`scripts/pm/check-half-states.mjs` says "governed enqueue" three times
about the queue guard's refusal, not the hook. So no SKILL.md, AGENTS.md
or ADR line changes in this PR.

## Gates green by design — no exemption for a missing file, no weakened
assertion

Acceptance: `git ls-files .claude/hooks/guard-governed-enqueue*` → 0;
`grep -rn guard-governed-enqueue .claude scripts AGENTS.md CLAUDE.md
docs .github package.json` → 0 hits.

`node scripts/pm/dispatch-gates.mjs --commands` (no paths — derived from
the merge base) → 40 commands at `9784cd23`; identical list on the
merged head `afd9fcb2`. Every command run with its exit code captured
before any pipe (`cmd > log 2>&1; ec=$?`):

- 39 exit 0 on the first pass. `pnpm --filter @objectstack/lint run
check:doc-formula-expressions` exited 3 = PREREQUISITE NOT MET
(`@objectstack/formula` and `@objectstack/lint` unbuilt — nothing
measured, not a finding); after `turbo run build
--filter=@objectstack/formula --filter=@objectstack/lint` under
`os-verify-lock.sh` (VERDICT command-exit 0, turbo cache hit) it re-ran
at exit 0.
- `dispatch-gates.mjs --ran` on `afd9fcb2`: 40 derived, 40 run, 0
NOT-MEASURED, 0 UNRUN — "a DERIVED zero — all 40 recorded an exit code
and none of them is 3".
- Named gates: `check:pm-settings-deny-roster` — 36 self-test cases, 17
content-write tools declared = enforced; `check:pm-governed-merges` —
328 assertions; `check-governed-queue-guard.mjs --self-test` — 238
cases; `check:pm-dispatch-gates`, `check:nul-bytes`,
`check-harness-current.mjs --self-test`,
`check:cross-package-test-inputs` — exit 0; `check:bash32-floor` — exit
0, re-run on the merged head after `main` moved that script (31 tracked
shell files, none naming a bash 4+ construct).
- Hook self-tests (what `lint.yml` discovers): 5 discovered, all pass —
guard-main-checkout-bash 130, guard-main-checkout 120,
guard-process-kill 69, guard-shared-stash 53, guard-tree-enum 38.
- After merging `origin/main` (`85c6d76e`: two docs commits —
`check-bash32-floor.mjs` +41, two SKILL.md lines):
`check:skill-frame-sync`, `check-skills-token-ratchet.mjs` (and its
`--self-test`), `check:pm-dispatch-gates` re-run at exit 0 on
`afd9fcb2`.
- Not run: `check:pm-skill-ratchet` / `check:pm-skill-id-lint` — no
`.claude/skills/**` line changes in this diff (the SKILL.md ratchet
stays 812/812).

Lint, proven narrowing at `afd9fcb2`: ① population read from
`eslint.config.mjs` — the base block lints
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`;
`.claude/settings.json` reports "File ignored because no matching
configuration was supplied", and the two deleted `.sh` files match no
glob, so this diff's lint population is exactly the three `.mjs` files;
② `--format json` count: 3 files, 0 errors, 0 warnings; ③ invariance:
the config "never enables type-aware linting (no
`parserOptions.project`, no typed `@typescript-eslint` rules) for ANY
file" (its own line 327), so no untouched file's verdict can move with
this diff. The repo-wide `pnpm lint` is CI's run.

## Changeset

`skip-changeset` — route 2 in `check-empty-changeset.mjs`: a PR touching
only `.claude/` and `scripts/` releases nothing, and no package
`files[]` ships either surface. Label applied by REST `POST
/issues/NUMBER/labels` and read back.

## Acceptance notes

- noted, not filed: `scripts/pm/check-half-states.mjs` (lines 9377,
9557, 29094) says "governed enqueue" about the queue guard's refusal —
still true after this PR, no edit. 承接者:无.
- Control-character scan on the four edited files: clean. Commit
`9784cd23` carries the model-free trailer pair; `afd9fcb2` is the merge
of `origin/main`.
- Governed surface (`.claude/**`): this PR stays DRAFT; the dev seat
flips nothing, requests nothing and arms nothing.
`needs:contract-review` is the seat's.

## 维护者速读(草稿)

**改了什么**
删掉客户端入队守卫钩子 `guard-governed-enqueue.sh` 及其自检脚本,`.claude/settings.json`
里的两处注册一并去掉;三个 PM 门禁脚本里把它描述为"在场控制"的注释改写成现状。`deny`
名单、CODEOWNERS、`check-governed-queue-guard.mjs` 的拒绝逻辑一字未动。

**为什么改**
执行 PR objectstack-ai#18447 上的裁决 B(维护者原话「b」,随后「这个卡你也直接派发」)。真正的硬线是
`check-governed-queue-guard.mjs` 在 `merge_group`
构建上的拒绝,加上维护者的批准点击;这个钩子只省一个队列周期,而实装版本盯着三种合规席位根本不用的拼法,今天已经耗掉三个受管
PR。删除优于继续维护。

**风险与代价(含回滚)**
风险:失去"批准前就在本地拦住"的便宜防线——违规入队会在队列里红一次,而不是在本地被拦;服务端正确性不变。代价:一次受管落地即一次
harness 重载(objectstack-ai#18468 已把"重开"处方去掉)。回滚 = revert 本 PR 的 squash 提交,钩子与两处注册整体回来。

**席位意见**
(留空,席位定稿)

**你要做的**
一个动作:在本 PR 上 Approve;落地由拥有席位按受管流程执行。

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

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

4 participants