Skip to content

Commit f2ade4a

Browse files
fix(pm): the governed register reads .claude/** Tier S in objectstack alone (#25221)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgSZnZg1LiHJmcHVXsZXKpyR)_ Fixes #25199 Clause-②: no Metadata-protocol: none ## What changed The governed register's `claude-tree` row read Tier S in every governed repository, but only objectstack's queue guard has a Tier S arm. objectui's guard (`scripts/check-governed-queue-guard.mjs` at objectui `1aa8851`) carries no tier at all: its `merge_group` leg refuses every governed diff, `.claude/**` included, without an authorized approval. So `check-governed-merges --pr` promised objectui a seat landing its own queue refuses, and the relay's governed-review follow-up (which sends on Tier H alone) never summoned the approvers. `scripts/pm/check-governed-merges.mjs` only: - A row's `tier` can be scoped per repository by a `tierRepos` key, read by one pure function, `surfaceTierIn(surface, repo)`: the row's `tier` where the key is absent or names `repo`, else Tier H (fail closed, so a repository added to `GOVERNED_REPOS` later never inherits a cheaper landing than its own guard enforces). - `governedSurfacesFor(repo)` hands every matcher the tier for its repository (the table's own frozen row where the tier holds, a frozen copy carrying H otherwise), so `governedPathsIn`, `governedTierFor`, `testVerdict`, `--pr`/`--branch`/`--test` and the relay's `governedReviewPlan` all follow with no change of their own. - The `claude-tree` row gains `tierRepos: ['objectstack']`. Membership is untouched (the row still governs `.claude/**` in all five repositories, no `repos` key), and the raw row still reads `tier: 'S'`, which is what the queue guard's `tierSGlobs()` words read. - The header's "two landing tiers" section and the table's docblock now say where Tier S holds; a new self-test battery (12 cases) pins it, and the roster floor moves 33 to 34. Route per triage 6337908442. No gate is loosened. A Tier S arm for objectui's own guard is the maintainer's open question from triage's round report; this PR makes the register follow the guard that is enforced today and does not answer that question. Cloud, objectos and hotcrm: their guards are not readable from this session (private, HTTP 403 on every channel tried), so their `.claude/**` reads Tier H, the stricter tier, **unverified**. Reading Tier S as H costs one authorized approval; the reverse lands a maintainer-owned file on a seat's own review. ## Evidence - Live replay of objectui#13320 (head `8a0c369`, six `.claude/hooks/` paths), through `--root` on an objectui checkout and `--branch` (the `--pr` read answers HTTP 403 for objectui on every channel here): - before (`15764cd`): `GOVERNED — Tier S(席内达档复核落地)`, exit 3; - after: `GOVERNED — a human merge is the review record` with `⚖️ landing tier: H(人合)`, exit 3. - The relay follow-up, `governedReviewPlan` from `fleet-write/execute.mjs` on the same six paths: `objectstack-ai/objectui` answers tier `H`, requesting `os-zhuang`, `hotlong`; `objectstack-ai/objectstack` answers tier `S`, nothing sent. `ops.mjs` and `execute.mjs` are unchanged. - objectstack is byte-identical: `tierSGlobs()` reads `.claude/**` before and after; the per-repo tier table differs only in the four non-objectstack rows of `claude-tree`; `check-governed-queue-guard.mjs --self-test` prints the same output before and after (334 cases). - Ablation (fix committed first; the `tierRepos` key deleted through `scripts/ablation-replace.mjs`, mutation and restore verified on disk, blob back to HEAD `2dc2457c`): the self-test reds with 8 failures, among them the objectui and cloud/objectos/hotcrm Tier H cases and the #13320 replay; the objectstack Tier S case, the objectstack #13320 case and the membership case stay green. A first attempt with an empty `--replacement` was a no-op (the tool refused to run the command) and is not counted. ## Gates (head `0f43df5`) - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 35 derived families, all run, all exit 0; `--ran` reconciliation: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. - Beyond the derivation, exit 0 each: `check-governed-merges.mjs --self-test` (512 assertions, was 500), `check-governed-queue-guard.mjs --self-test` (334), `fleet-write/execute.mjs --self-test` (188), `fleet-write/validate.mjs --self-test` (193), `pnpm check:pm-governed-prose`, `check-half-states.mjs --self-test` (4916). - `check-governed-merges.mjs --test scripts/pm/check-governed-merges.mjs`: NOT governed. The file is a CODEOWNERS enforcement-chain row, so GitHub requests the approvers when this PR is marked ready. - `skip-changeset`: the diff is `scripts/pm/**` only; the root package is private and nothing here ships in any package's `files[]`. ## Acceptance notes - `scripts/pm/fleet-write/ops.mjs`'s header lists the Tier H surfaces as the rows "with tier `H`" (`docs/adr/**`, `skills/**`, ...). That list is objectstack's view; a non-objectstack `.claude/**` is now Tier H through the per-repo tier without being in it. Not edited here (outside this card's file surface); carrier: none. - `AGENTS.md` Prime Directive 14 says Tier S is "all of `.claude/**`". That is this repository's own instruction file, and in objectstack it stays exactly true; no edit needed. --- _Generated by [Claude Code](https://claude.ai/code/session_01T1ABkBhrMzga6Kq81uZ8ro)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8c73398 commit f2ade4a

1 file changed

Lines changed: 67 additions & 7 deletions

File tree

‎scripts/pm/check-governed-merges.mjs‎

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -414,14 +414,19 @@
414414
* them. What moved is the LANDING each row waits for, carried as `tier` on the
415415
* row and derived per PR by `governedTierFor`:
416416
*
417-
* H 人合 — every register row except `.claude/**` (today: `docs/adr/**`,
417+
* H 人合 — every register row except objectstack's `.claude/**` (today: `docs/adr/**`,
418418
* `docs/NORTH-STAR.md`, `skills/**`, `AGENTS.md`, `CLAUDE.md`, and since
419419
* the B2 ruling of 2026-10-13 the `workflows` row, `.github/workflows/**`,
420420
* scoped to objectstack, objectui and cloud): the maintainer's hand, or
421421
* an authorized APPROVED review by `GOVERNED_APPROVERS` and then the
422422
* owning seat lands it. The tier ruling left this landing as it was; the
423-
* list itself grew by one row on 2026-10-13.
424-
* S 席内达档复核落地 — the whole `.claude/**` tree: the owning seat lands it
423+
* list itself grew by one row on 2026-10-13. The `.claude/**` row of every
424+
* repository but objectstack lands here too (#25199): its `tierRepos` names
425+
* objectstack alone, because no other repository's own queue guard has a
426+
* Tier S arm (objectui's, measured, refuses `.claude/**` without an
427+
* authorized approval; cloud's, objectos' and hotcrm's are unread and fail
428+
* closed to H).
429+
* S 席内达档复核落地 — objectstack's whole `.claude/**` tree: the owning seat lands it
425430
* through the queue once the PR thread (or its card) carries a
426431
* `## Contract review` record for the PR's CURRENT head with
427432
* `Served-tier: CONTRACT_REVIEW_TIER` and `**VERDICT: PASS**` and every
@@ -1000,6 +1005,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
10001005
'the governed predicate: the 2026-08-18 unified list, exactly': 9,
10011006
'the dispatch-gates declaration (#9979)': 9,
10021007
'⚖️ the two landing tiers (#19133, ruled 2026-09-18)': 19,
1008+
"a row's tier is scoped per repository (#25199)": 12,
10031009
'since parsing': 4,
10041010
'the window: landing order, not committer dates (#12633)': 15,
10051011
'since-ref is topological (#12633 route B)': 7,
@@ -1034,7 +1040,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
10341040

10351041
// DELETING an entry silences that battery's floor exactly as effectively as
10361042
// zeroing it, so the roster's own size is pinned too.
1037-
const SELF_TEST_BATTERY_FLOOR = 33;
1043+
const SELF_TEST_BATTERY_FLOOR = 34;
10381044

10391045
// The key an assertion is filed under when no battery is open. It is not a
10401046
// declared battery, so it reds by the same set difference rather than silently
@@ -1172,10 +1178,18 @@ export const HUMAN_MERGE_LINE_THRESHOLD = 3000;
11721178
* it — no ruling names them, and a new gate defaults to no. Every reader of
11731179
* this table therefore asks for ONE repository (`governedPathsIn(paths,
11741180
* repo)`), and a call that names none is refused, never answered for "all".
1181+
*
1182+
* A row's `tier` may be scoped the same way, by a `tierRepos` key naming the
1183+
* ids where it holds; in every other repository the row governs, it lands
1184+
* Tier H, the fail-closed tier. `surfaceTierIn` is the one reader of that key,
1185+
* and `governedSurfacesFor` hands every matcher the tier for its repository.
1186+
* The `claude-tree` row's Tier S holds in objectstack alone: the tier is the
1187+
* landing the repository's own queue guard enforces, and only objectstack's
1188+
* has a Tier S arm (#25199).
11751189
*/
11761190
export const GOVERNED_SURFACES = Object.freeze([
11771191
Object.freeze({ id: 'adr', prefix: 'docs/adr/', glob: 'docs/adr/**', tier: GOVERNED_TIER_H, what: 'architecture decision records' }),
1178-
Object.freeze({ id: 'claude-tree', prefix: '.claude/', glob: '.claude/**', tier: GOVERNED_TIER_S, what: 'the agent instruction tree (skills, agents, hooks, settings)' }),
1192+
Object.freeze({ id: 'claude-tree', prefix: '.claude/', glob: '.claude/**', tier: GOVERNED_TIER_S, tierRepos: Object.freeze(['objectstack']), what: 'the agent instruction tree (skills, agents, hooks, settings)' }),
11791193
Object.freeze({ id: 'skills-catalog', prefix: 'skills/', glob: 'skills/**', tier: GOVERNED_TIER_H, what: 'the published skills catalog' }),
11801194
Object.freeze({ id: 'agents-md', exact: 'AGENTS.md', glob: 'AGENTS.md', tier: GOVERNED_TIER_H, what: 'the repo-root agent instruction file' }),
11811195
Object.freeze({ id: 'claude-md', exact: 'CLAUDE.md', glob: 'CLAUDE.md', tier: GOVERNED_TIER_H, what: 'the repo-root Claude instruction file' }),
@@ -1404,6 +1418,18 @@ export function surfaceRepos(surface) {
14041418
return Array.isArray(surface?.repos) ? [...surface.repos] : GOVERNED_REPOS.map((r) => r.id);
14051419
}
14061420

1421+
/**
1422+
* The landing tier ONE register row carries in `repo`: its `tier` when it
1423+
* declares no `tierRepos` or names `repo` there, else Tier H — the fail-closed
1424+
* tier, so a repository added to `GOVERNED_REPOS` later never inherits a
1425+
* cheaper landing than its own guard enforces. Pure, and the only reader of
1426+
* `tierRepos`.
1427+
*/
1428+
export function surfaceTierIn(surface, repo) {
1429+
if (!Array.isArray(surface?.tierRepos) || surface.tierRepos.includes(repo)) return surface?.tier;
1430+
return GOVERNED_TIER_H;
1431+
}
1432+
14071433
/**
14081434
* The register's question has a repository in it, or it has no answer. Throws
14091435
* on a missing or unknown id — loudly, naming the ids this register knows —
@@ -1432,11 +1458,17 @@ export function assertGovernedRepo(repo, caller = 'governedPathsIn') {
14321458

14331459
/**
14341460
* The register's rows that govern `repo`, in report order — the per-repository
1435-
* view every matcher reads. Refuses a missing or unknown repository.
1461+
* view every matcher reads, each carrying the tier it lands in `repo`
1462+
* (`surfaceTierIn`): a row whose tier holds there is handed over as the table's
1463+
* own frozen row, any other as a frozen copy carrying Tier H. Refuses a missing
1464+
* or unknown repository.
14361465
*/
14371466
export function governedSurfacesFor(repo) {
14381467
assertGovernedRepo(repo, 'governedSurfacesFor');
1439-
return GOVERNED_SURFACES.filter((surface) => surfaceRepos(surface).includes(repo));
1468+
return GOVERNED_SURFACES.filter((surface) => surfaceRepos(surface).includes(repo)).map((surface) => {
1469+
const tier = surfaceTierIn(surface, repo);
1470+
return tier === surface.tier ? surface : Object.freeze({ ...surface, tier });
1471+
});
14401472
}
14411473

14421474
/**
@@ -4493,6 +4525,34 @@ async function selfTest() {
44934525
assert('and-null-once-every-hit-is-lifted', allLifted.governed === false && allLifted.tier === null, JSON.stringify(allLifted.tier));
44944526
assert('the-tier-words-name-the-landing-each-waits-for', /GOVERNED_APPROVERS/.test(GOVERNED_TIERS.H.landing) && /Contract review/.test(GOVERNED_TIERS.S.landing) && !/--pair/.test(GOVERNED_TIERS.S.landing));
44954527

4528+
// ── a row's tier is scoped per repository (#25199) ───────────────────────
4529+
//
4530+
// The tier a row lands on is the one the repository's OWN queue guard
4531+
// enforces, and only objectstack's guard has a Tier S arm: objectui's refuses
4532+
// a `.claude/**` diff without an authorized approval. So the `claude-tree`
4533+
// row's Tier S holds in objectstack alone and every other repository reads
4534+
// it Tier H — membership untouched, the raw row untouched for the readers of
4535+
// its glob words (the queue guard's `tierSGlobs`).
4536+
battery('a row\'s tier is scoped per repository (#25199)');
4537+
const claudeRow = GOVERNED_SURFACES.find((s) => s.id === 'claude-tree');
4538+
const HOOK = '.claude/hooks/x.sh';
4539+
const PR_13320 = ['guard-main-checkout-bash', 'guard-shared-stash', 'guard-tree-enum'].flatMap((h) => [`.claude/hooks/${h}.selftest.sh`, `.claude/hooks/${h}.sh`]);
4540+
const elsewhere = everyRepo.filter((r) => r !== 'objectstack');
4541+
assert('the-claude-tree-row-holds-Tier-S-in-objectstack-alone', claudeRow.tier === GOVERNED_TIER_S && claudeRow.tierRepos?.join() === 'objectstack' && claudeRow.repos === undefined, JSON.stringify(claudeRow));
4542+
assert('it-is-the-only-row-that-scopes-its-tier', GOVERNED_SURFACES.filter((s) => s.tierRepos !== undefined).map((s) => s.id).join() === 'claude-tree');
4543+
assert('every-tier-scope-is-a-non-empty-duplicate-free-subset-of-the-rows-own-repositories-on-a-Tier-S-row', GOVERNED_SURFACES.every((s) => s.tierRepos === undefined || (s.tier === GOVERNED_TIER_S && Array.isArray(s.tierRepos) && s.tierRepos.length > 0 && new Set(s.tierRepos).size === s.tierRepos.length && s.tierRepos.every((id) => surfaceRepos(s).includes(id)))));
4544+
assert('an-objectstack-claude-path-is-Tier-S-unchanged', governedTierFor([HOOK], 'objectstack') === GOVERNED_TIER_S && testVerdict([HOOK], { repo: 'objectstack' }).tier === GOVERNED_TIER_S);
4545+
assert('an-objectui-claude-path-is-governed-by-the-same-row-and-lands-Tier-H', governedPathsIn([HOOK], 'objectui').map((s) => `${s.id}:${s.tier}`).join() === 'claude-tree:H' && governedTierFor([HOOK], 'objectui') === GOVERNED_TIER_H && testVerdict([HOOK], { repo: 'objectui' }).governed === true);
4546+
assert('cloud-objectos-and-hotcrm-read-Tier-H-their-guards-unread-so-fail-closed', elsewhere.every((r) => governedTierFor([HOOK], r) === GOVERNED_TIER_H && governedPathsIn([HOOK], r).map((s) => s.id).join() === 'claude-tree'));
4547+
assert('a-repository-the-scope-does-not-name-answers-H-including-one-added-later', surfaceTierIn(claudeRow, 'objectstack') === GOVERNED_TIER_S && surfaceTierIn(claudeRow, 'a-future-repo') === GOVERNED_TIER_H && GOVERNED_SURFACES.filter((s) => s.tierRepos === undefined).every((s) => everyRepo.every((r) => surfaceTierIn(s, r) === s.tier)));
4548+
assert('the-scope-moves-no-membership-every-repository-keeps-its-rows-in-order', everyRepo.every((r) => governedSurfacesFor(r).map((s) => s.id).join() === GOVERNED_SURFACES.filter((s) => surfaceRepos(s).includes(r)).map((s) => s.id).join()));
4549+
assert('objectstack-is-handed-the-tables-own-frozen-rows-and-the-raw-row-keeps-S-for-the-glob-words', governedSurfacesFor('objectstack').every((s) => GOVERNED_SURFACES.includes(s)) && governedSurfacesFor('objectui').find((s) => s.id === 'claude-tree') !== claudeRow && Object.isFrozen(governedSurfacesFor('objectui').find((s) => s.id === 'claude-tree')) && claudeRow.tier === GOVERNED_TIER_S);
4550+
const ui13320 = testVerdict(PR_13320, { repo: 'objectui' });
4551+
const ui13320Text = renderTestVerdict(ui13320);
4552+
assert('the-objectui-13320-replay-reads-GOVERNED-Tier-H-and-says-so', ui13320.governed === true && ui13320.tier === GOVERNED_TIER_H && ui13320.hitPaths.length === 6 && /landing tier: H/.test(ui13320Text) && !/Tier S/.test(ui13320Text), ui13320Text);
4553+
assert('and-the-same-six-paths-in-objectstack-still-read-Tier-S', testVerdict(PR_13320, { repo: 'objectstack' }).tier === GOVERNED_TIER_S && /Tier S/.test(renderTestVerdict(testVerdict(PR_13320, { repo: 'objectstack' }))));
4554+
assert('an-objectui-mixed-diff-stays-H-and-the-exit-stays-the-shared-3', governedTierFor([HOOK, 'README.md'], 'objectui') === GOVERNED_TIER_H && governedTierFor([HOOK, 'AGENTS.md'], 'objectui') === GOVERNED_TIER_H && runTestModeExitFor([HOOK], 'objectui') === EXIT_TEST_GOVERNED);
4555+
44964556
// ── --since parsing ───────────────────────────────────────────────────────
44974557
battery('since parsing');
44984558
const now = new Date('2026-08-18T12:00:00Z');

0 commit comments

Comments
 (0)