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
8 changes: 4 additions & 4 deletions .claude/skills/pm-dispatch/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,10 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
- PR 上的标签 = 待维护者审阅,不入六态;与请审同为等人批清单,随获批或撤回判决离开。
- 批准判定单源 = 队列守卫常量 `GOVERNED_APPROVERS`:授权账户 APPROVED 即算,⛔ 不卡 `commit_id`。
- 批准后再推亦不过期;席位落地 = 过落地前检、清标、ready、auto-merge,踢出/变基同法。
- ③ 在 draft PR 上向两个授权批准账户 `os-zhuang` 与 `hotlong` 都 request review,主动推。
- PR 作者身份即两账户之一的席位时,对该账户请审必失败(author-identity 422)。
- 该账户改为把 PR assign 给它替代通知,另一账户照常请审;轮次报告点名说明走了兜底。
- 请审走免碰 draft 位的 REST 专用路,ready/draft 走中继 op;MCP 兜底已拒;端点见 rest-channel。
- ③ 请审是中继 op:`pr_create`/`pr_ready` 命中 Tier H 即向 `GOVERNED_APPROVERS` 全员请审。
- 已请审/已指派/已评审者跳过(零写);PR 作者即其一时请审必 422,op 改为 assign 该账户替代。
- 席位只核对 run 摘要 governed review 行,缺口才手补(免碰 draft 位 REST 路)并在轮次报告点名。
- ready/draft 走中继 op;MCP 兜底已拒;端点见 rest-channel。
- ④ 轮次报告单列 awaiting a human merge。
- skills 车道自有 PR:纯代码面如 `scripts/pm/` 由本席按达档自审(清单不减)后落地。
- 受管面两层:Tier S = `.claude/**` 全树,余皆 Tier H 等人批;S 经达档复核 PASS 后 ready → 入队。
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 @@ -44,7 +44,7 @@
- ✓ 建卡带标签 `POST .../issues` · 改正文 `PATCH .../issues/{n}` · 认领 `POST .../issues/{n}/assignees`。
- ✓ 该 `PATCH` 带 `state` 关卡/重开,`state_reason` 交付 `completed`、撤单 `not_planned`,走裸 REST。
- 请求体走文件(`-d @file`)或引号定界 heredoc(`<<'EOF'`),⛔ 永不内联双引号串。
- ✓ 请求复审 `POST .../pulls/{n}/requested_reviewers` · 开 PR `POST .../pulls` 带 `draft=true`。
- ✓ 请审 `POST .../pulls/{n}/requested_reviewers`(Tier H 归中继 op)· 开 PR `POST .../pulls` 带 `draft=true`。
- ✓ `origin/main` 合进 PR head:中继 op `pr_update_branch`(`pull` + 40 位 `expected_head_sha`),真合并提交。
- ✓ 落地经中继 `node scripts/pm/fleet-write/dispatch.mjs --repo O/N --actions-file F`;`--via dispatch` 同载。
- ✓ 启动 run 走 op `workflow_dispatch`:解锁物是白名单 run 才启;入表只经 workflow 自己的 PR。
Expand Down
67 changes: 44 additions & 23 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,31 +1,52 @@
# .github/CODEOWNERS — governance routing (#6785; ruling #6741).
# .github/CODEOWNERS — governance routing: every Tier H governed surface to
# every authorized approver.
#
# The ruling this file helps enforce (maintainer, verbatim):
# 「adr 只能由维护者自己确认,人工合并,ai 不得擅自合并。」
#
# Routing docs/adr/ to the maintainer does two things: every PR touching an
# ADR requests his review automatically, and a ruleset / branch-protection
# rule "require review from Code Owners" has an entry to bind to (Half 2 of
# #6785 — a repo-settings change only the maintainer can apply; the
# button-by-button instructions are in the PR that added this file).
# The owner list on EVERY row mirrors `GOVERNED_APPROVERS` in
# scripts/pm/check-governed-queue-guard.mjs — the one list of accounts whose
# APPROVED review lands a Tier H surface (「os-zhuang hotlong 批准算数」). The
# rows mirror the Tier H rows of `GOVERNED_SURFACES` in
# scripts/pm/check-governed-merges.mjs. This file is static and can import
# neither, so the two stay in step the only way a static file can:
# `node scripts/pm/check-governed-queue-guard.mjs --self-test` reads this file
# (`codeownersMirrorVerdict`) and reds when a Tier H row has no line here, when
# any line names fewer or other owners than that constant, or when a Tier S row
# (`.claude/**`, which lands on a seat's review of record) is routed here.
# Change the constant or the register first; this file follows in the same PR.
#
# The per-PR CI half that used to sit beside this file (`ADR maintainer
# approval`) retired under the 2026-08-18 ruling (#9495): a human merge IS
# the review record for a governed surface, the pre-merge line of defense is
# seat discipline (no AI seat flips ready / enqueues / arms auto-merge on a
# governed PR — AGENTS.md Prime Directive #14), and detection moved to the
# report-only post-merge audit, scripts/pm/check-governed-merges.mjs, whose
# header carries the incident history that used to live in the gate script.
# What routing buys, and what it does not: GitHub requests a code owner's
# review when a pull is opened READY or marked ready for review. A DRAFT open
# requests nobody — measured on this repository's own Tier H pulls, where the
# request arrived at `ready_for_review` and never at the draft create. Every
# seat-driven Tier H pull is opened draft and stays draft until the approval,
# so the fleet relay requests the same accounts itself at `pr_create` and
# `pr_ready` (scripts/pm/fleet-write/ops.mjs, "The governed-review follow-up");
# this file is the platform half — for a pull opened ready or readied by hand —
# and the entry a ruleset's "require review from Code Owners" can bind to. A
# code owner without write access is ignored by GitHub without a word:
# `GET /repos/{owner}/{repo}/codeowners/errors` reads this file's own verdict.
#
# `hotlong` is verified against the repo history, not assumed: 2,153 commits
# on main are authored as 50353452+hotlong@users.noreply.github.com — GitHub's
# {id}+{login} noreply address form ties the login to the account.
# Both logins are verified, not assumed: `hotlong` authors the bulk of main's
# history as 50353452+hotlong@users.noreply.github.com (GitHub's {id}+{login}
# noreply form ties the login to the account); `os-zhuang` is named by the
# ruling above and has readied and reviewed this repository's pulls.
#
# The enforcement chain itself is routed the same way, deliberately: a PR
# that edits the audit script or this file is a governance change — without
# these entries the docs/adr/ rule could be lifted by editing the enforcers
# instead of the ADRs.
# The enforcement chain is routed the same way, deliberately: a PR that edits
# the audit script, the approver constant or this file is a governance change —
# without these entries the rule could be lifted by editing the enforcers
# instead of the surfaces.

/docs/adr/ @hotlong
/.github/CODEOWNERS @hotlong
/scripts/pm/check-governed-merges.mjs @hotlong
# Tier H governed surfaces — the register's rows with tier H
/docs/adr/ @os-zhuang @hotlong
/skills/ @os-zhuang @hotlong
/AGENTS.md @os-zhuang @hotlong
/CLAUDE.md @os-zhuang @hotlong
/docs/NORTH-STAR.md @os-zhuang @hotlong
/.github/workflows/ @os-zhuang @hotlong

# The enforcement chain
/.github/CODEOWNERS @os-zhuang @hotlong
/scripts/pm/check-governed-merges.mjs @os-zhuang @hotlong
/scripts/pm/check-governed-queue-guard.mjs @os-zhuang @hotlong
114 changes: 113 additions & 1 deletion scripts/pm/check-governed-queue-guard.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
'⭐ the 2026-09-20 ruling: a certified PURE REGENERATION carries the record to the queued head': 13,
'the per-row repository scope (ruling B2, 2026-10-13)': 14,
'⭐ B′ with C: the record stands at the tier its FACE maps to, read off the committed paths': 8,
'the CODEOWNERS mirror: every Tier H register row routes to every GOVERNED_APPROVERS account, nothing else is routed, and the file says so': 12,
});

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

// 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 @@ -618,6 +620,84 @@ export const EVENT_PULL_REQUEST = 'pull_request';
*/
export const GOVERNED_APPROVERS = Object.freeze(['os-zhuang', 'hotlong']);

// ── the CODEOWNERS mirror — the platform half of "both approvers are asked" ─

/**
* `.github/CODEOWNERS` is the platform's own review router: GitHub requests a
* code owner's review when a pull is opened READY or marked ready for review.
* It is a static file — it can import neither `GOVERNED_SURFACES` nor
* `GOVERNED_APPROVERS` — so the two stay in step the only way a static file
* can: this predicate reads it and the self-test reds when it drifts. ⛔ It is
* a pin inside this existing self-test, not a gate, a check-run or a patrol
* row: the fleet's own request on the draft open is the relay's
* governed-review follow-up (`fleet-write/ops.mjs`), and the gap both close is
* what the H43 patrol row keeps reading.
*
* `codeownersRows` parses the file's effective lines — a `#` comment to end of
* line stripped, blank lines dropped — into `{ pattern, owners, line }`.
* `codeownersPatternFor` spells the one CODEOWNERS pattern a register row maps
* to: its prefix or exact path, root-anchored. Both pure.
*/
export function codeownersRows(text) {
const rows = [];
String(text ?? '')
.split('\n')
.forEach((raw, i) => {
const line = raw.replace(/#.*$/, '').trim();
if (line === '') return;
const [pattern, ...owners] = line.split(/\s+/);
rows.push({ pattern, owners, line: i + 1 });
});
return rows;
}

export function codeownersPatternFor(surface) {
return `/${surface.prefix ?? surface.exact}`;
}

/**
* The verdict: `{ ok, problems, tierHPatterns }`. `ok` holds only when
* - every Tier H row of the register that governs `repo` has a CODEOWNERS
* line at exactly its pattern, naming exactly `@` + every account in
* `approvers` (order-free; nobody else, nobody fewer);
* - every other line — the enforcement chain's rows — names exactly that
* same owner set, so an enforcer cannot be routed to a subset;
* - no line routes a Tier S row's pattern: Tier S lands on a seat's review of
* record, and a code-owner request there would summon the maintainer for a
* landing that is not his;
* - the file's prose names `GOVERNED_APPROVERS`, so a reader finds the
* constant the owner list mirrors.
* Every problem is a sentence naming the row or line; the list is whole, never
* the first hit. Pure.
*/
export function codeownersMirrorVerdict({ text, approvers = GOVERNED_APPROVERS, repo = SELF_REPO_ID } = {}) {
const rows = codeownersRows(text);
const wanted = [...approvers].map((a) => `@${a}`.toLowerCase()).sort();
const sameOwners = (owners) => JSON.stringify([...owners].map((o) => o.toLowerCase()).sort()) === JSON.stringify(wanted);
const problems = [];
// The rows that govern THIS repository, read from the register (ruling B2: the question has a repository in it).
const governing = governedSurfacesFor(repo);
const tierH = governing.filter((s) => s.tier === GOVERNED_TIER_H);
const tierHPatterns = tierH.map(codeownersPatternFor);
for (const surface of tierH) {
const pattern = codeownersPatternFor(surface);
const row = rows.find((r) => r.pattern === pattern);
if (!row) {
problems.push(`Tier H row \`${surface.id}\` (${surface.glob}) has no CODEOWNERS line at \`${pattern}\` — a pull touching it summons nobody when opened ready`);
continue;
}
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`);
}
for (const row of rows) {
if (tierHPatterns.includes(row.pattern)) continue;
const tierS = governing.find((s) => s.tier === GOVERNED_TIER_S && codeownersPatternFor(s) === row.pattern);
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`);
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(' ')})`);
}
if (!/GOVERNED_APPROVERS/.test(String(text ?? ''))) problems.push('the file never names `GOVERNED_APPROVERS` — its header must say which constant the owner list mirrors');
return { ok: problems.length === 0, problems, tierHPatterns, rows };
}

// ── the landing TIER: Tier S lands on a review of record, Tier H on an approval ─

/**
Expand Down Expand Up @@ -4529,6 +4609,38 @@ export async function selfTest() {
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));
const proseCode = await tierRun({ comments: onOldHead, runGit: qgit(qProseCode), baseRef: QBASE });
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));
// ── the CODEOWNERS mirror ─────────────────────────────────────────────────
//
// The platform half of "both approvers are asked": `.github/CODEOWNERS` is a
// static file, so this is the ONE place that proves it mirrors the register's
// Tier H rows and `GOVERNED_APPROVERS`. The file on disk is read here; the
// negative controls drive the predicate with fixtures.
battery('the CODEOWNERS mirror: every Tier H register row routes to every GOVERNED_APPROVERS account, nothing else is routed, and the file says so');
{
const onDisk = readFileSync(join(repoRoot, '.github', 'CODEOWNERS'), 'utf8');
const live = codeownersMirrorVerdict({ text: onDisk });
assert('⭐ the-file-on-disk-mirrors-the-register-and-the-approver-list', live.ok, live.problems.join(' | '));
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));
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());
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));
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 }]));
const owners = GOVERNED_APPROVERS.map((a) => `@${a}`).join(' ');
const full = (extra = '') => `# mirrors GOVERNED_APPROVERS\n${live.tierHPatterns.map((p) => `${p} ${owners}`).join('\n')}\n${extra}`;
assert('a-fixture-carrying-exactly-the-Tier-H-rows-with-every-owner-passes', codeownersMirrorVerdict({ text: full() }).ok);
const one = codeownersMirrorVerdict({ text: full().replace(`/docs/adr/ ${owners}`, `/docs/adr/ @${GOVERNED_APPROVERS[1]}`) });
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(' | '));
const missing = codeownersMirrorVerdict({ text: full().split('\n').filter((l) => !l.startsWith('/skills/')).join('\n') });
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(' | '));
const tierS = codeownersMirrorVerdict({ text: full(`/.claude/ ${owners}\n`) });
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(' | '));
const enforcer = codeownersMirrorVerdict({ text: full(`/.github/CODEOWNERS @${GOVERNED_APPROVERS[1]}\n`) });
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(' | '));
const silent = codeownersMirrorVerdict({ text: full().replace('# mirrors GOVERNED_APPROVERS\n', '') });
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(' | '));
const other = codeownersMirrorVerdict({ text: onDisk, approvers: ['someone-else'] });
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));
}

// ── The floor: every declared battery RAN, and ran its cases (#13489) ────
//
// Evaluated after every battery has had its chance and BEFORE the verdict, so
Expand Down
Loading
Loading