Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@
"mcp__github__sub_issue_write",
"mcp__github__merge_pull_request",
"mcp__github__create_repository",
"mcp__github__fork_repository"
"mcp__github__fork_repository",
"mcp__github__enable_pr_auto_merge",
"mcp__github__disable_pr_auto_merge"
]
},
"hooks": {
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/pm-dispatch/references/rest-channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
- ✓ auto-merge 挂载 `PUT .../pulls/{n}/ccr/auto_merge` 带 `{"merge_method":"SQUASH"}`,`DELETE` 卸载。
- ⛔ `PUT .../ccr/auto_merge` 在 draft 上 422 零存储;`DELETE` 无挂载回 422 = 本就没挂,非失败。
- 入队读 timeline `added_to_merge_queue`,落地读 `git rev-list --parents`;⛔ `auto_merge` 与回显都不作数。
- ⛔ 永不 MCP `update_pull_request`(锁 1 已拒);ready/draft 翻转只走 ccr 路;auto-merge 备用 MCP 未拒
- ⛔ 永不 MCP `update_pull_request`(锁 1 已拒);ready/draft 翻转只走 ccr 路;auto-merge MCP 锁 1 同拒
- 直合仓 `PUT .../pulls/{n}/merge`;actor 记通道令牌:REST 按会话为 `claude[bot]` 或用户,MCP 恒用户。

## 不可迁移 —— 只有这三件,围着它们排计划;红窗守候规则住 `platform-readings.md` 配额段
Expand Down
55 changes: 40 additions & 15 deletions scripts/pm/check-settings-deny-roster.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,23 @@
* tells a seat which tools are closed POINTS at it; a second enumeration is the
* hand reconciliation this gate exists to end.
*
* ## What is deliberately NOT asserted
* ## The ENQUEUE class is in the roster
*
* `enable_pr_auto_merge` and `disable_pr_auto_merge` are declared closed
* (#18282, ruled A). The REST route `PUT .../pulls/{n}/ccr/auto_merge` in
* `.claude/skills/pm-dispatch/references/rest-channel.md` already serves every
* real enqueue, so denying the MCP pair costs a seat nothing, and a tool the
* runtime refuses cannot be mis-called by a seat that mis-reads the scope of
* the PreToolUse hook `.claude/hooks/guard-governed-enqueue.sh`. That hook
* stays as defence in depth: it judges a governed PR's pinned approval, this
* gate judges membership, and neither reads the other.
*
* The ENQUEUE class -- `enable_pr_auto_merge` and `disable_pr_auto_merge` -- is
* not in the roster. Both are a live fallback channel in
* `.claude/skills/pm-dispatch/references/rest-channel.md`, so whether they are
* closed is the maintainer's call and is carded separately (#18282). If they
* are ruled closed, this gate needs exactly one edit -- two more names in the
* constant -- and that is the point of a single constant.
* ## What is deliberately NOT asserted
*
* Also not asserted: that `allow` and `deny` agree, that the tools exist on the
* MCP server, or that a session honours either list. This gate holds the one
* property neither prose surface can hold about itself -- that the enforced
* list still equals the declared one.
* That `allow` and `deny` agree, that the tools exist on the MCP server, or
* that a session honours either list. This gate holds the one property neither
* prose surface can hold about itself -- that the enforced list still equals
* the declared one.
*
* ## Exit contract
*
Expand Down Expand Up @@ -99,9 +103,9 @@ import { isEntrypoint } from '../invoked-as.mjs';
*
* Membership rule, so the next editor does not have to infer it: a tool is here
* when invoking it PUBLISHES or MUTATES repository content -- an issue, a
* comment, a review body, a pull request, a branch, a file, a repository. The
* enqueue pair is deliberately absent (see the header). Read-only tools are not
* in this namespace question at all.
* comment, a review body, a pull request, a branch, a file, a repository -- or
* arms or disarms a pull request's landing, which is the enqueue pair (see the
* header). Read-only tools are not in this namespace question at all.
*/
export const CONTENT_WRITE_TOOLS = Object.freeze([
'mcp__github__issue_write',
Expand All @@ -119,6 +123,8 @@ export const CONTENT_WRITE_TOOLS = Object.freeze([
'mcp__github__merge_pull_request',
'mcp__github__create_repository',
'mcp__github__fork_repository',
'mcp__github__enable_pr_auto_merge',
'mcp__github__disable_pr_auto_merge',
]);

/** The namespace this gate judges. Entries outside it are another subsystem's. */
Expand Down Expand Up @@ -337,12 +343,13 @@ const SELF_TEST_BATTERIES = Object.freeze({
'settingsPathFromArgv': 3,
'the exit contract': 4,
'the shipped .claude/settings.json': 4,
'the enqueue class — declared closed': 4,
'the dispatch-gates declaration': 3,
});

// DELETING an entry silences that battery's floor exactly as effectively as
// zeroing it, so the roster's own size is pinned too.
const SELF_TEST_BATTERY_FLOOR = 7;
const SELF_TEST_BATTERY_FLOOR = 8;

// The key an assertion is filed under when no battery is open. It is not a
// declared battery, so it reds by the same set difference rather than silently
Expand Down Expand Up @@ -436,6 +443,24 @@ function selfTest() {
assert('the shipped deny list declares no tool this roster lacks', verdict(shipped.deny, CONTENT_WRITE_TOOLS).drift, []);
assert('every shipped deny entry is a string', shipped.deny.every((entry) => typeof entry === 'string'), true);

// --- the enqueue class -------------------------------------------------
//
// Ruled closed (see the header). Pinned on FIXTURES, not on the shipped
// file: the shipped-file battery above deliberately leaves containment to
// the live run, and this battery keeps that line. What it holds is that the
// roster names both tools and that the gate's verdict moves with them.
battery('the enqueue class — declared closed');
assert('the roster declares the auto-merge arming tool', CONTENT_WRITE_TOOLS.includes('mcp__github__enable_pr_auto_merge'), true);
assert('the roster declares the auto-merge disarming tool', CONTENT_WRITE_TOOLS.includes('mcp__github__disable_pr_auto_merge'), true);
// A settings document denying exactly the roster -- every entry, the pair
// included -- reads declared = enforced in both directions.
const exactlyTheRoster = denyList(settingsWith([...CONTENT_WRITE_TOOLS]));
assert('a document denying exactly the roster reads declared = enforced', verdict(exactlyTheRoster.deny, CONTENT_WRITE_TOOLS), { missing: [], drift: [] });
// The pre-ruling shape, reproduced: the pair absent from the file is the
// containment finding, and it names the tool.
const withoutDisarm = CONTENT_WRITE_TOOLS.filter((tool) => tool !== 'mcp__github__disable_pr_auto_merge');
assert('a document missing one of the pair names it as left open', verdict(withoutDisarm, CONTENT_WRITE_TOOLS).missing, ['mcp__github__disable_pr_auto_merge']);

// --- the dispatch-gates declaration ------------------------------------
//
// Enforcement cannot hold any of these from here: the declaration is read by
Expand Down
Loading