Skip to content

Commit 91669b1

Browse files
committed
feat(pm): the relay requests review from every GOVERNED_APPROVERS account on a Tier H pull, and CODEOWNERS routes every Tier H surface to both
A seat-driven Tier H pull is opened draft and stays draft until the approval, and GitHub requests a code owner only at ready_for_review (measured on this repository's own Tier H pulls: the CODEOWNERS request arrived one second after the ready flip, never at the draft create). CODEOWNERS listed docs/adr/ -> @hotlong alone, and SKILL.md's "request review from both approvers" was prose no op performed. So an approver waited on a pull nobody told him about. - fleet-write/ops.mjs: pr_create and pr_ready carry governedReview; governedReviewRequests spells the two conditional writes (requested_reviewers, issue-assignees); pr_ready spends issues for the assignee fallback. - fleet-write/execute.mjs: after a governedReview row lands, read the pull's whole file list (every page, a cap that reds as a truncated subset), classify through the register (governedPathsIn + landingTierOf, scoped to payload.repo), and on a Tier H hit request every GOVERNED_APPROVERS account not already requested, assigned or holding a submitted review; the pull's own author is assigned instead (a request naming it is a 422). Tier S, ungoverned and covered pulls send nothing and say so; an unreadable list, pull or review list, a truncated walk or a refused write is a FAILED action, never a quiet skip. Self-test: a new battery of 29 cases; fixtures of the pr ops now declare the files read. - fleet-write/validate.mjs: pins the two rows, the two endpoints against the refused families, and the three alsoSpends rows. - .github/CODEOWNERS: every Tier H register row and the enforcement chain route to @os-zhuang @hotlong; the header names GOVERNED_APPROVERS as the list it mirrors. - check-governed-queue-guard.mjs: codeownersMirrorVerdict reads the file and the self-test reds when a Tier H row is missing, an owner set drifts from the constant, or a Tier S row is routed (a pin inside an existing self-test, no new gate). - pm-dispatch SKILL.md step 3 and rest-channel.md point at the op (net 0 lines each). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M82feuetLzrCiBz41BJb7L
1 parent b289090 commit 91669b1

7 files changed

Lines changed: 525 additions & 44 deletions

File tree

‎.claude/skills/pm-dispatch/SKILL.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,10 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
195195
- PR 上的标签 = 待维护者审阅,不入六态;与请审同为等人批清单,随获批或撤回判决离开。
196196
- 批准判定单源 = 队列守卫常量 `GOVERNED_APPROVERS`:授权账户 APPROVED 即算,⛔ 不卡 `commit_id`。
197197
- 批准后再推亦不过期;席位落地 = 过落地前检、清标、ready、auto-merge,踢出/变基同法。
198-
- ③ 在 draft PR 上向两个授权批准账户 `os-zhuang` 与 `hotlong` 都 request review,主动推。
199-
- PR 作者身份即两账户之一的席位时,对该账户请审必失败(author-identity 422)。
200-
- 该账户改为把 PR assign 给它替代通知,另一账户照常请审;轮次报告点名说明走了兜底。
201-
- 请审走免碰 draft 位的 REST 专用路,ready/draft 走中继 op;MCP 兜底已拒;端点见 rest-channel。
198+
- ③ 请审是中继 op:`pr_create`/`pr_ready` 命中 Tier H 即向 `GOVERNED_APPROVERS` 全员请审。
199+
- 已请审/已指派/已评审者跳过(零写);PR 作者即其一时请审必 422,op 改为 assign 该账户替代。
200+
- 席位只核对 run 摘要 governed review 行,缺口才手补(免碰 draft 位 REST 路)并在轮次报告点名。
201+
- ready/draft 走中继 op;MCP 兜底已拒;端点见 rest-channel。
202202
- ④ 轮次报告单列 awaiting a human merge。
203203
- skills 车道自有 PR:纯代码面如 `scripts/pm/` 由本席按达档自审(清单不减)后落地。
204204
- 受管面两层:Tier S = `.claude/**` 全树,余皆 Tier H 等人批;S 经达档复核 PASS 后 ready → 入队。

‎.claude/skills/pm-dispatch/references/rest-channel.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
- ✓ 建卡带标签 `POST .../issues` · 改正文 `PATCH .../issues/{n}` · 认领 `POST .../issues/{n}/assignees`。
4545
- ✓ 该 `PATCH` 带 `state` 关卡/重开,`state_reason` 交付 `completed`、撤单 `not_planned`,走裸 REST。
4646
- 请求体走文件(`-d @file`)或引号定界 heredoc(`<<'EOF'`),⛔ 永不内联双引号串。
47-
- ✓ 请求复审 `POST .../pulls/{n}/requested_reviewers` · 开 PR `POST .../pulls` 带 `draft=true`。
47+
- ✓ 请审 `POST .../pulls/{n}/requested_reviewers`(Tier H 归中继 op)· 开 PR `POST .../pulls` 带 `draft=true`。
4848
- ✓ `origin/main` 合进 PR head:中继 op `pr_update_branch`(`pull` + 40 位 `expected_head_sha`),真合并提交。
4949
- ✓ 落地经中继 `node scripts/pm/fleet-write/dispatch.mjs --repo O/N --actions-file F`;`--via dispatch` 同载。
5050
- ✓ 启动 run 走 op `workflow_dispatch`:解锁物是白名单 run 才启;入表只经 workflow 自己的 PR。

‎.github/CODEOWNERS‎

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,52 @@
1-
# .github/CODEOWNERS — governance routing (#6785; ruling #6741).
1+
# .github/CODEOWNERS — governance routing: every Tier H governed surface to
2+
# every authorized approver.
23
#
34
# The ruling this file helps enforce (maintainer, verbatim):
45
# 「adr 只能由维护者自己确认,人工合并,ai 不得擅自合并。」
56
#
6-
# Routing docs/adr/ to the maintainer does two things: every PR touching an
7-
# ADR requests his review automatically, and a ruleset / branch-protection
8-
# rule "require review from Code Owners" has an entry to bind to (Half 2 of
9-
# #6785 — a repo-settings change only the maintainer can apply; the
10-
# button-by-button instructions are in the PR that added this file).
7+
# The owner list on EVERY row mirrors `GOVERNED_APPROVERS` in
8+
# scripts/pm/check-governed-queue-guard.mjs — the one list of accounts whose
9+
# APPROVED review lands a Tier H surface (「os-zhuang hotlong 批准算数」). The
10+
# rows mirror the Tier H rows of `GOVERNED_SURFACES` in
11+
# scripts/pm/check-governed-merges.mjs. This file is static and can import
12+
# neither, so the two stay in step the only way a static file can:
13+
# `node scripts/pm/check-governed-queue-guard.mjs --self-test` reads this file
14+
# (`codeownersMirrorVerdict`) and reds when a Tier H row has no line here, when
15+
# any line names fewer or other owners than that constant, or when a Tier S row
16+
# (`.claude/**`, which lands on a seat's review of record) is routed here.
17+
# Change the constant or the register first; this file follows in the same PR.
1118
#
12-
# The per-PR CI half that used to sit beside this file (`ADR maintainer
13-
# approval`) retired under the 2026-08-18 ruling (#9495): a human merge IS
14-
# the review record for a governed surface, the pre-merge line of defense is
15-
# seat discipline (no AI seat flips ready / enqueues / arms auto-merge on a
16-
# governed PR — AGENTS.md Prime Directive #14), and detection moved to the
17-
# report-only post-merge audit, scripts/pm/check-governed-merges.mjs, whose
18-
# header carries the incident history that used to live in the gate script.
19+
# What routing buys, and what it does not: GitHub requests a code owner's
20+
# review when a pull is opened READY or marked ready for review. A DRAFT open
21+
# requests nobody — measured on this repository's own Tier H pulls, where the
22+
# request arrived at `ready_for_review` and never at the draft create. Every
23+
# seat-driven Tier H pull is opened draft and stays draft until the approval,
24+
# so the fleet relay requests the same accounts itself at `pr_create` and
25+
# `pr_ready` (scripts/pm/fleet-write/ops.mjs, "The governed-review follow-up");
26+
# this file is the platform half — for a pull opened ready or readied by hand —
27+
# and the entry a ruleset's "require review from Code Owners" can bind to. A
28+
# code owner without write access is ignored by GitHub without a word:
29+
# `GET /repos/{owner}/{repo}/codeowners/errors` reads this file's own verdict.
1930
#
20-
# `hotlong` is verified against the repo history, not assumed: 2,153 commits
21-
# on main are authored as 50353452+hotlong@users.noreply.github.com — GitHub's
22-
# {id}+{login} noreply address form ties the login to the account.
31+
# Both logins are verified, not assumed: `hotlong` authors the bulk of main's
32+
# history as 50353452+hotlong@users.noreply.github.com (GitHub's {id}+{login}
33+
# noreply form ties the login to the account); `os-zhuang` is named by the
34+
# ruling above and has readied and reviewed this repository's pulls.
2335
#
24-
# The enforcement chain itself is routed the same way, deliberately: a PR
25-
# that edits the audit script or this file is a governance change — without
26-
# these entries the docs/adr/ rule could be lifted by editing the enforcers
27-
# instead of the ADRs.
36+
# The enforcement chain is routed the same way, deliberately: a PR that edits
37+
# the audit script, the approver constant or this file is a governance change —
38+
# without these entries the rule could be lifted by editing the enforcers
39+
# instead of the surfaces.
2840

29-
/docs/adr/ @hotlong
30-
/.github/CODEOWNERS @hotlong
31-
/scripts/pm/check-governed-merges.mjs @hotlong
41+
# Tier H governed surfaces — the register's rows with tier H
42+
/docs/adr/ @os-zhuang @hotlong
43+
/skills/ @os-zhuang @hotlong
44+
/AGENTS.md @os-zhuang @hotlong
45+
/CLAUDE.md @os-zhuang @hotlong
46+
/docs/NORTH-STAR.md @os-zhuang @hotlong
47+
/.github/workflows/ @os-zhuang @hotlong
48+
49+
# The enforcement chain
50+
/.github/CODEOWNERS @os-zhuang @hotlong
51+
/scripts/pm/check-governed-merges.mjs @os-zhuang @hotlong
52+
/scripts/pm/check-governed-queue-guard.mjs @os-zhuang @hotlong

‎scripts/pm/check-governed-queue-guard.mjs‎

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
559559
'⭐ the 2026-09-20 ruling: a certified PURE REGENERATION carries the record to the queued head': 13,
560560
'the per-row repository scope (ruling B2, 2026-10-13)': 14,
561561
'⭐ B′ with C: the record stands at the tier its FACE maps to, read off the committed paths': 8,
562+
'the CODEOWNERS mirror: every Tier H register row routes to every GOVERNED_APPROVERS account, nothing else is routed, and the file says so': 12,
562563
});
563564

564565
// DELETING an entry silences that battery's floor exactly as effectively as
@@ -567,7 +568,8 @@ const SELF_TEST_BATTERIES = Object.freeze({
567568
// 5770886272 on #19061, letter B): the ordinary direction, one battery, one row.
568569
// Raised 23 → 24 with the queued pull-back battery: one battery, one row.
569570
// Raised 25 → 26 with the B′-with-C face battery (ruling record 6159870311): one battery, one row.
570-
const SELF_TEST_BATTERY_FLOOR = 26;
571+
// Raised 26 → 27 with the CODEOWNERS mirror battery: one battery, one row.
572+
const SELF_TEST_BATTERY_FLOOR = 27;
571573

572574
// The key an assertion is filed under when no battery is open. It is not a
573575
// declared battery, so it reds by the same set difference rather than silently
@@ -618,6 +620,84 @@ export const EVENT_PULL_REQUEST = 'pull_request';
618620
*/
619621
export const GOVERNED_APPROVERS = Object.freeze(['os-zhuang', 'hotlong']);
620622

623+
// ── the CODEOWNERS mirror — the platform half of "both approvers are asked" ─
624+
625+
/**
626+
* `.github/CODEOWNERS` is the platform's own review router: GitHub requests a
627+
* code owner's review when a pull is opened READY or marked ready for review.
628+
* It is a static file — it can import neither `GOVERNED_SURFACES` nor
629+
* `GOVERNED_APPROVERS` — so the two stay in step the only way a static file
630+
* can: this predicate reads it and the self-test reds when it drifts. ⛔ It is
631+
* a pin inside this existing self-test, not a gate, a check-run or a patrol
632+
* row: the fleet's own request on the draft open is the relay's
633+
* governed-review follow-up (`fleet-write/ops.mjs`), and the gap both close is
634+
* what the H43 patrol row keeps reading.
635+
*
636+
* `codeownersRows` parses the file's effective lines — a `#` comment to end of
637+
* line stripped, blank lines dropped — into `{ pattern, owners, line }`.
638+
* `codeownersPatternFor` spells the one CODEOWNERS pattern a register row maps
639+
* to: its prefix or exact path, root-anchored. Both pure.
640+
*/
641+
export function codeownersRows(text) {
642+
const rows = [];
643+
String(text ?? '')
644+
.split('\n')
645+
.forEach((raw, i) => {
646+
const line = raw.replace(/#.*$/, '').trim();
647+
if (line === '') return;
648+
const [pattern, ...owners] = line.split(/\s+/);
649+
rows.push({ pattern, owners, line: i + 1 });
650+
});
651+
return rows;
652+
}
653+
654+
export function codeownersPatternFor(surface) {
655+
return `/${surface.prefix ?? surface.exact}`;
656+
}
657+
658+
/**
659+
* The verdict: `{ ok, problems, tierHPatterns }`. `ok` holds only when
660+
* - every Tier H row of the register that governs `repo` has a CODEOWNERS
661+
* line at exactly its pattern, naming exactly `@` + every account in
662+
* `approvers` (order-free; nobody else, nobody fewer);
663+
* - every other line — the enforcement chain's rows — names exactly that
664+
* same owner set, so an enforcer cannot be routed to a subset;
665+
* - no line routes a Tier S row's pattern: Tier S lands on a seat's review of
666+
* record, and a code-owner request there would summon the maintainer for a
667+
* landing that is not his;
668+
* - the file's prose names `GOVERNED_APPROVERS`, so a reader finds the
669+
* constant the owner list mirrors.
670+
* Every problem is a sentence naming the row or line; the list is whole, never
671+
* the first hit. Pure.
672+
*/
673+
export function codeownersMirrorVerdict({ text, approvers = GOVERNED_APPROVERS, repo = SELF_REPO_ID } = {}) {
674+
const rows = codeownersRows(text);
675+
const wanted = [...approvers].map((a) => `@${a}`.toLowerCase()).sort();
676+
const sameOwners = (owners) => JSON.stringify([...owners].map((o) => o.toLowerCase()).sort()) === JSON.stringify(wanted);
677+
const problems = [];
678+
// The rows that govern THIS repository, read from the register (ruling B2: the question has a repository in it).
679+
const governing = governedSurfacesFor(repo);
680+
const tierH = governing.filter((s) => s.tier === GOVERNED_TIER_H);
681+
const tierHPatterns = tierH.map(codeownersPatternFor);
682+
for (const surface of tierH) {
683+
const pattern = codeownersPatternFor(surface);
684+
const row = rows.find((r) => r.pattern === pattern);
685+
if (!row) {
686+
problems.push(`Tier H row \`${surface.id}\` (${surface.glob}) has no CODEOWNERS line at \`${pattern}\` — a pull touching it summons nobody when opened ready`);
687+
continue;
688+
}
689+
if (!sameOwners(row.owners)) problems.push(`CODEOWNERS line ${row.line} \`${pattern}\` names ${row.owners.join(' ') || 'nobody'}, not every GOVERNED_APPROVERS account (${wanted.join(' ')}) — a Tier H pull then waits on an approver who is never asked`);
690+
}
691+
for (const row of rows) {
692+
if (tierHPatterns.includes(row.pattern)) continue;
693+
const tierS = governing.find((s) => s.tier === GOVERNED_TIER_S && codeownersPatternFor(s) === row.pattern);
694+
if (tierS) problems.push(`CODEOWNERS line ${row.line} routes \`${row.pattern}\`, a Tier S row (\`${tierS.id}\`) that lands on a seat's review of record — a code-owner request there asks the maintainer for a landing that is not his`);
695+
else if (!sameOwners(row.owners)) problems.push(`CODEOWNERS line ${row.line} \`${row.pattern}\` (an enforcement-chain row) names ${row.owners.join(' ') || 'nobody'}, not every GOVERNED_APPROVERS account (${wanted.join(' ')})`);
696+
}
697+
if (!/GOVERNED_APPROVERS/.test(String(text ?? ''))) problems.push('the file never names `GOVERNED_APPROVERS` — its header must say which constant the owner list mirrors');
698+
return { ok: problems.length === 0, problems, tierHPatterns, rows };
699+
}
700+
621701
// ── the landing TIER: Tier S lands on a review of record, Tier H on an approval ─
622702

623703
/**
@@ -4529,6 +4609,38 @@ export async function selfTest() {
45294609
assert('⭐ a-record-on-an-OLDER-head-CARRIES-when-the-head-moved-ONLY-on-prose-faces-outside-the-review-surface', proseCarried.exitCode === EXIT_CLEAR && proseCarried.entries[0].record.state === 'stands' && proseCarried.entries[0].record.carriedHops === 1, JSON.stringify(proseCarried.entries[0].record));
45304610
const proseCode = await tierRun({ comments: onOldHead, runGit: qgit(qProseCode), baseRef: QBASE });
45314611
assert('⛔ a-CODE-path-moved-beside-the-prose-REFUSES-exactly-as-an-uncarried-old-head-does', proseCode.exitCode === EXIT_REFUSED_UNAPPROVED && proseCode.entries[0].record.state === 'absent', JSON.stringify(proseCode.entries[0].record));
4612+
// ── the CODEOWNERS mirror ─────────────────────────────────────────────────
4613+
//
4614+
// The platform half of "both approvers are asked": `.github/CODEOWNERS` is a
4615+
// static file, so this is the ONE place that proves it mirrors the register's
4616+
// Tier H rows and `GOVERNED_APPROVERS`. The file on disk is read here; the
4617+
// negative controls drive the predicate with fixtures.
4618+
battery('the CODEOWNERS mirror: every Tier H register row routes to every GOVERNED_APPROVERS account, nothing else is routed, and the file says so');
4619+
{
4620+
const onDisk = readFileSync(join(repoRoot, '.github', 'CODEOWNERS'), 'utf8');
4621+
const live = codeownersMirrorVerdict({ text: onDisk });
4622+
assert('⭐ the-file-on-disk-mirrors-the-register-and-the-approver-list', live.ok, live.problems.join(' | '));
4623+
assert('every-Tier-H-row-that-governs-this-repository-is-a-pattern-the-file-carries', live.tierHPatterns.length === governedSurfacesFor(REPO).filter((s) => s.tier === GOVERNED_TIER_H).length && live.tierHPatterns.every((p) => live.rows.some((r) => r.pattern === p)), JSON.stringify(live.tierHPatterns));
4624+
assert('the-patterns-are-the-register-rows-root-anchored-prefix-or-exact', live.tierHPatterns.join() === '/docs/adr/,/skills/,/AGENTS.md,/CLAUDE.md,/docs/NORTH-STAR.md,/.github/workflows/', live.tierHPatterns.join());
4625+
assert('every-line-on-disk-names-every-approver-spelled-as-a-login', live.rows.every((r) => GOVERNED_APPROVERS.every((a) => r.owners.includes(`@${a}`))) && live.rows.length >= live.tierHPatterns.length, JSON.stringify(live.rows));
4626+
assert('the-parser-strips-comments-and-blank-lines-and-splits-owners', JSON.stringify(codeownersRows('# c\n\n/docs/adr/ @a @b # trailing\n /x.md @c\n')) === JSON.stringify([{ pattern: '/docs/adr/', owners: ['@a', '@b'], line: 3 }, { pattern: '/x.md', owners: ['@c'], line: 4 }]));
4627+
const owners = GOVERNED_APPROVERS.map((a) => `@${a}`).join(' ');
4628+
const full = (extra = '') => `# mirrors GOVERNED_APPROVERS\n${live.tierHPatterns.map((p) => `${p} ${owners}`).join('\n')}\n${extra}`;
4629+
assert('a-fixture-carrying-exactly-the-Tier-H-rows-with-every-owner-passes', codeownersMirrorVerdict({ text: full() }).ok);
4630+
const one = codeownersMirrorVerdict({ text: full().replace(`/docs/adr/ ${owners}`, `/docs/adr/ @${GOVERNED_APPROVERS[1]}`) });
4631+
assert('⛔ one-approver-on-the-adr-row-is-a-problem-naming-the-row-and-the-missing-account', !one.ok && one.problems.length === 1 && one.problems[0].includes('`/docs/adr/`') && one.problems[0].includes(`@${GOVERNED_APPROVERS[0]}`), one.problems.join(' | '));
4632+
const missing = codeownersMirrorVerdict({ text: full().split('\n').filter((l) => !l.startsWith('/skills/')).join('\n') });
4633+
assert('⛔ a-Tier-H-row-with-no-line-is-a-problem-naming-the-row', !missing.ok && missing.problems.length === 1 && missing.problems[0].includes('`skills-catalog`'), missing.problems.join(' | '));
4634+
const tierS = codeownersMirrorVerdict({ text: full(`/.claude/ ${owners}\n`) });
4635+
assert('⛔ routing-the-Tier-S-row-is-a-problem-a-seat-record-landing-summons-nobody', !tierS.ok && tierS.problems.length === 1 && tierS.problems[0].includes('`claude-tree`'), tierS.problems.join(' | '));
4636+
const enforcer = codeownersMirrorVerdict({ text: full(`/.github/CODEOWNERS @${GOVERNED_APPROVERS[1]}\n`) });
4637+
assert('⛔ an-enforcement-chain-row-routed-to-a-subset-is-a-problem', !enforcer.ok && enforcer.problems.length === 1 && enforcer.problems[0].includes('enforcement-chain'), enforcer.problems.join(' | '));
4638+
const silent = codeownersMirrorVerdict({ text: full().replace('# mirrors GOVERNED_APPROVERS\n', '') });
4639+
assert('⛔ a-file-that-never-names-the-constant-is-a-problem', !silent.ok && silent.problems.length === 1 && silent.problems[0].includes('GOVERNED_APPROVERS'), silent.problems.join(' | '));
4640+
const other = codeownersMirrorVerdict({ text: onDisk, approvers: ['someone-else'] });
4641+
assert('⭐ the-approver-list-is-READ-not-restated-another-list-reds-the-file-on-disk', !other.ok && other.problems.length >= live.tierHPatterns.length, String(other.problems.length));
4642+
}
4643+
45324644
// ── The floor: every declared battery RAN, and ran its cases (#13489) ────
45334645
//
45344646
// Evaluated after every battery has had its chance and BEFORE the verdict, so

0 commit comments

Comments
 (0)