Commit ceb6b5f
fix(settings,pm): deny the MCP enqueue class — enable/disable_pr_auto_merge join lock 1 and the roster gate (#18317)
Fixes #18282
**Draft, rules layer — stays a draft until an authorized APPROVED
review.** The diff touches `.claude/settings.json` (a governed surface
under Prime Directive 14), so no seat readies, queues, or arms
auto-merge on it. Landing follows ruling C on card #17971 (maintainer
direct ruling, 2026-09-13, comment 5652306063: 「C. approve
后不管后续改动都由席位落地:」) — after the authorized approval the owning seat lands
it; no reviewer request comes from the dev.
## The ruling this implements — director seat, comment 5682626575 on
#18282, verbatim and untranslated
```text
A — mcp__github__enable_pr_auto_merge and mcp__github__disable_pr_auto_merge join permissions.deny in .claude/settings.json (lock 1, 15 → 17 entries); guard-governed-enqueue.sh stays as defence in depth; references/rest-channel.md :53 drops the 「auto-merge 备用 MCP 未拒」 clause. The REST route (PUT …/pulls/N/ccr/auto_merge, rest-channel :50) already serves every real enqueue, so nothing a seat does today is lost.
Rules layer: the change lands as a four-piece governed draft on an authorized APPROVED, ⛔ never armed or merged by a seat; the objectui mirror of lock 1 (PR objectui#9448) gets the same two entries in its own card once this lands.
```
Class-1 self-adjudication, batch #135 追认, the maintainer's 「135 同意」.
## What changes — three files, nothing else
1. **`.claude/settings.json`** — `mcp__github__enable_pr_auto_merge` and
`mcp__github__disable_pr_auto_merge` appended to `permissions.deny`
after `mcp__github__fork_repository`, in the file's own write-class
order (issue → PR → comments → review → files → branch → sub-issue →
merge → repo → enqueue). 15 → 17 `mcp__github__` entries; every other
byte of the file unchanged. The `guard-governed-enqueue.sh` matcher on
line 82 still names `enable_pr_auto_merge` — that is the hook's
registration, not a permission, and it stays.
2. **`scripts/pm/check-settings-deny-roster.mjs`** (the gate PR #18285
landed) — `CONTENT_WRITE_TOOLS` gains the same two names in the same
order (15 → 17; still the ONE declaration). The header paragraph that
said the enqueue class is NOT in the roster is re-keyed to say it IS,
ruled closed, and why; the membership rule above the constant now covers
"arms or disarms a pull request's landing" so the criterion and the
constant agree. A new self-test battery `the enqueue class — declared
closed` (4 fixture cases: both names in the roster; a document denying
exactly the roster reads declared = enforced; a document missing one of
the pair names it); `SELF_TEST_BATTERY_FLOOR` 7 → 8; self-test 32 → 36
cases.
3. **`.claude/skills/pm-dispatch/references/rest-channel.md` line 53** —
the 「auto-merge 备用 MCP 未拒」 clause is dropped in place; the row now ends
「auto-merge MCP 锁 1 同拒。」. 118 → 117 bytes, one line stays one line, the
file stays 82/82 on the ratchet, no `ruledRaises` owed.
Untouched by design: `.claude/hooks/guard-governed-enqueue.sh` and its
self-test (defence in depth, per the ruling), every other row of
`rest-channel.md`, `platform-readings.md`, objectui.
### Why the header paragraph is re-keyed rather than deleted — the one
design choice, on the four axes
- **实际业务需求** — the next editor of `CONTENT_WRITE_TOOLS` reads that
header to learn the membership rule. Arming auto-merge is not obviously
"publishing content", so without a sentence saying the pair is in and
why, the pair reads as drift to someone applying the old rule. A reader
today: the seat that mirrors lock 1 into objectui.
- **项目长远合理性** — AGENTS.md makes a gate script's own header the authority
on its detail; one short paragraph with the card number is the file's
existing convention (it already carries #18281, #18218, #4690) and
carries no narrative.
- **防 AI 写代码犯错** — an AI "tidying" the roster later would otherwise drop
the pair as not-content-writes; the gate would go red on roster drift
(the safety net held both ways in the ablation below), but the header
should not invite that edit in the first place.
- **创业阶段不扩散需求** — re-keying replaces the forecast paragraph one-for-one
(net +4 header lines); no new mechanism, no staged window.
## Verification (all readings on this branch; final union on `ad7a3da5`)
**Reverse verification — the count is the control.**
- Base `6ef33128` (origin/main at cut): `pnpm
check:pm-settings-deny-roster` → `✓ … 15 content-write tool(s) declared
= enforced in .claude/settings.json (15 mcp__github__ deny entr(ies)
…)`; `--self-test` → `32 cases pass`.
- Base roster vs a 17-entry fixture (the two new deny entries added to
main's file): exit 1 — `denies mcp__github__enable_pr_auto_merge,
mcp__github__disable_pr_auto_merge, which this gate's roster does not
declare — roster drift`. This is the direction the change removes.
- Head: `✓ check-settings-deny-roster: 17 content-write tool(s) declared
= enforced in .claude/settings.json (17 mcp__github__ deny entr(ies), 0
outside this gate's population and ignored).`; `--self-test` → `36 cases
pass`.
- Head roster vs a fixture missing `disable_pr_auto_merge`: exit 1 —
`does not deny mcp__github__disable_pr_auto_merge — the charter declares
it closed while the enforced list leaves it open`.
**Ablation from the committed state, hash proofs per leg** (`git restore
--source=BASE -- PATH` for the mutation, `git checkout HEAD -- PATH` for
the restore, a trap on EXIT/INT/TERM with absolute paths, every blob
hash compared and non-empty):
- Leg A — `.claude/settings.json` at the base blob `0acd039e` (15
entries), gate at HEAD: exit 1, `does not deny
mcp__github__enable_pr_auto_merge, mcp__github__disable_pr_auto_merge …
leaves them open`. Restored to HEAD blob `428a0300`.
- Leg B — gate at the base blob `391f20ce` (15-name roster), settings at
HEAD: exit 1, `denies mcp__github__enable_pr_auto_merge,
mcp__github__disable_pr_auto_merge, which this gate's roster does not
declare — roster drift`. Restored to HEAD blob `5736aad3`.
- Restored tree: 17 = 17 green; `git diff HEAD` empty; `git status
--porcelain` empty.
**The hook is untouched and its self-test reads the same before and
after.** `git diff --stat 6ef3312 ad7a3da -- .claude/hooks/` is empty.
`bash .claude/hooks/guard-governed-enqueue.selftest.sh` reads `54
passed, 1 failed` on the untouched base and byte-identically on the head
(`diff` of the two logs empty). The one red is pre-existing and
environmental, not this PR's: the case `an exception-row path in a repo
this container cannot resolve` expects allow on the premise that no
sibling `cloud` checkout exists, and this container has
`/home/user/cloud`, which the hook resolves at its lines 453–454 and
recomputes against, so it blocks. See Acceptance notes.
**Derived gate union on the merge commit `ad7a3da5`** (`node
scripts/pm/dispatch-gates.mjs --commands`, 40 commands, identical list
before and after merging origin/main; each exit captured by redirect,
then `--ran`):
- `✓ dispatch-gates --ran: 40 derived famil(ies) accounted for — 39 run,
1 NOT-MEASURED (1 DERIVED from a recorded exit 3).`
- 38 commands exit 0, including `pnpm check:pm-settings-deny-roster`,
`pnpm check:pm-skill-ratchet` (`rest-channel.md is 82 lines (ceiling 82;
headroom 0)`), `pnpm check:pm-skill-id-lint`, `pnpm
check:pm-half-states` (on the #18311 copy, `4196 cases pass`), `pnpm
check:nul-bytes`, `pnpm check:watch-hint-literal`, `pnpm
check:skill-frame-sync`, `pnpm check:pm-governed-merges`, `node
scripts/check-self-test-wired.mjs` (+ `--self-test`).
- `pnpm check:pm-dispatch-gates` (the battery) run detached: exit 0,
`dispatch-gates self-test: 1730 cases pass`, 454 s on this box (462 s on
the pre-merge run).
- NOT MEASURED: `pnpm --filter @objectstack/lint run
check:doc-formula-expressions` — exit 3, `PREREQUISITE NOT MET — the
workspace package @objectstack/formula is not built`. The diff touches
no package; building `@objectstack/formula` means building
`@objectstack/spec` first, so this gate is left to CI, where it runs on
a built tree. Declared narrowing, not a skip.
- ESLint on the gate file: exit 0 before and after. Control-character
self-scan over the three files: no hits.
**`skip-changeset`** — every path is in the never-published lane
(`.claude/**`, `scripts/pm/**`); no package `files[]` moves.
## Acceptance notes
- Out of scope, for the seat to card (as #18310 did for the
`update_pull_request` rows): `references/platform-readings.md` still
describes `enable_pr_auto_merge` / `disable_pr_auto_merge` BEHAVIOUR as
if the MCP tools were callable — after PR #18307's renumbering these are
lines 41 (转 draft 与 `disable_pr_auto_merge` 都做), 49
(`enable_pr_auto_merge` 恒显式传 `mergeMethod`), 58 (照样成功 on
`mergeable_state: clean`), 60 (配额枯竭时回成功). Once lock 1 denies the pair
those rows describe a channel no seat can reach; the REST
`ccr/auto_merge` readings on rest-channel 50–52 are the live ones. Not
touched here — this card is three files.
- Finding, for the seat (`.claude/hooks/**` is outside this card):
`guard-governed-enqueue.selftest.sh` is not hermetic on a box with a
sibling `cloud` checkout — the case above reads red there and green in
CI. Repro: with `/home/user/cloud` present, `bash
.claude/hooks/guard-governed-enqueue.selftest.sh` → `54 passed, 1
failed`, `FAIL want=allow got=block an exception-row path in a repo this
container cannot resolve`. Dedupe words: guard-governed-enqueue selftest
sibling cloud checkout hermetic.
- The objectui mirror of lock 1 (PR objectui#9448) gets the same two
entries in its own card once this lands — per the ruling, not this PR.
## 维护者速读(草稿)
**改了什么** — 把 MCP 的 `enable_pr_auto_merge` / `disable_pr_auto_merge`
两个工具加进 `.claude/settings.json` 的 `permissions.deny`(锁 1,15 → 17 条);门禁
`check:pm-settings-deny-roster` 的名单同步加这两条、它自己的说明改成「入队类已裁定关闭」、自测 32 → 36
例;`rest-channel.md` 第 53 行删掉「auto-merge 备用 MCP 未拒」,改为「auto-merge MCP 锁 1
同拒」。钩子 `guard-governed-enqueue.sh` 一字未动,继续作纵深防御。
**为什么改** — 董事席裁决 A(#135 批追认,你的「135 同意」):真实入队全部走 REST 的 `PUT
…/pulls/N/ccr/auto_merge`,MCP 那条备用路没人用却留着口子,靠钩子挡只覆盖受管
PR;运行时直接拒掉,席位就不可能误用,声明 = 强制的原则延伸到入队类。
**风险与代价(含回滚)** — 席位现有流程零损失(所有真实入队都走 REST)。风险只有一种:某个会话若真去调这两个 MCP
工具会被运行时拒绝——这正是目的。回滚 = revert 本 PR 的一个 commit;门禁把 deny
与名单钉成相等,任何半回滚都会立刻变红,不可能静默存在。
**席位意见** — (留空,席位定稿)
**你要做的** — 审阅并 APPROVE(`.claude/settings.json` 属 rules 层,需要授权账号的
APPROVED);之后由席位落地,不需要你合并。objectui 镜像(PR objectui#9448 的锁 1)另开一张卡跟进,不在本
PR。
---
_Generated by [Claude
Code](https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0cc5ce8 commit ceb6b5f
3 files changed
Lines changed: 44 additions & 17 deletions
File tree
- .claude
- skills/pm-dispatch/references
- scripts/pm
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
67 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
68 | 70 | | |
69 | 71 | | |
70 | 72 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
53 | | - | |
| 53 | + | |
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
57 | | - | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
58 | 67 | | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | | - | |
64 | | - | |
| 68 | + | |
65 | 69 | | |
66 | | - | |
67 | | - | |
68 | | - | |
69 | | - | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
70 | 74 | | |
71 | 75 | | |
72 | 76 | | |
| |||
99 | 103 | | |
100 | 104 | | |
101 | 105 | | |
102 | | - | |
103 | | - | |
104 | | - | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
105 | 109 | | |
106 | 110 | | |
107 | 111 | | |
| |||
119 | 123 | | |
120 | 124 | | |
121 | 125 | | |
| 126 | + | |
| 127 | + | |
122 | 128 | | |
123 | 129 | | |
124 | 130 | | |
| |||
337 | 343 | | |
338 | 344 | | |
339 | 345 | | |
| 346 | + | |
340 | 347 | | |
341 | 348 | | |
342 | 349 | | |
343 | 350 | | |
344 | 351 | | |
345 | | - | |
| 352 | + | |
346 | 353 | | |
347 | 354 | | |
348 | 355 | | |
| |||
436 | 443 | | |
437 | 444 | | |
438 | 445 | | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
439 | 464 | | |
440 | 465 | | |
441 | 466 | | |
| |||
0 commit comments