Skip to content

Commit 5d4d55a

Browse files
claude[bot]claude
andauthored
tooling(pm): govern hotcrm in check-governed-merges (#14867) (#14987)
* tooling(pm): govern `hotcrm` in check-governed-merges (#14867) `GOVERNED_REPOS` gains `objectstack-ai/hotcrm`, per the maintainer ruling recorded on #14867 (2026-09-03, verbatim 「其他同意」 adopting 纳入). The governed-surface definition already read "agent instruction files, judged the same across repos"; the 2026-08-18 ruling's list (objectui, cloud, objectos) was an enumeration of the repos in view that day, not an exclusion. The gap this closes is a silence, not a wrong row: a CONFIGURED repo that cannot be read prints a loud `⚠️ UNAUDITED` row, while a repo that was never configured prints nothing at all — indistinguishable in the output from a repo that swept clean. Meanwhile the `repo:hotcrm` seat hand-merged that repo's `AGENTS.md` chain as governed with no post-merge audit behind it. - the header's derived statement of the set names `hotcrm` and cites the 2026-09-03 ruling beside it; the 2026-08-18 quotation is untouched; - the four/five repo counts in the header, the usage line, the `--since-ref` rationale, the constant's JSDoc and the `--test` note follow the register; - the `:288` dead-mirror measurement keeps its recorded batch verbatim and now says what that batch was (a hand probe of a then-unconfigured control); - `--self-test`: the ordered-ids pin moves to the new set (the pin doing its job), plus two new assertions — `hotcrm` pinned by id AND slug so a later removal reds a check that names it, and a fixture pinning that a configured repo with no checkout renders UNAUDITED rather than nothing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox * tooling(pm): make the dropped-hotcrm ablation print named reds, not a stack `assert` in `--self-test` COLLECTS failures and prints them at the end, so a throw inside a fixture aborts the run before any collected failure is shown. Measured on the #14867 reverse verification: deleting the `hotcrm` register entry made `byId.hotcrm` undefined, and the new no-checkout fixture threw a TypeError — exit 1, loud, but the two pins that NAME hotcrm never printed. Read the row defensively so a removal reds as three named lines instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
1 parent 95464ed commit 5d4d55a

1 file changed

Lines changed: 59 additions & 15 deletions

File tree

scripts/pm/check-governed-merges.mjs

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33

44
/**
55
* check-governed-merges — report-only post-merge audit of the governed
6-
* surfaces (#9495), across the four governed repos (#9619), plus the
6+
* surfaces (#9495), across the five governed repos (#9619, #14867), plus the
77
* pre-arm `--test` predicate every seat runs before flipping ready (#9550).
88
* Enumerates the PRs that MERGED into `main` since a given date/ref whose diff
99
* touched a governed surface, with merge attribution, for the PM round report
1010
* and the report-only patrol family (the `check-half-states.mjs` precedent: a
1111
* completed sweep exits 0 whether it found 0 or 40 entries; non-zero exits
1212
* classify the ENVIRONMENT, not the tree).
1313
*
14-
* node scripts/pm/check-governed-merges.mjs # sweep, last 24h, all four repos
14+
* node scripts/pm/check-governed-merges.mjs # sweep, last 24h, all five repos
1515
* node scripts/pm/check-governed-merges.mjs --since 7d # or 36h, or ISO date
1616
* node scripts/pm/check-governed-merges.mjs --since-ref v5.0.0-rc.3 # topological, exact
1717
* node scripts/pm/check-governed-merges.mjs --since-ref objectstack=<tip> --since-ref objectui=<tip>
@@ -216,14 +216,32 @@
216216
* giving the guard job dependencies, which is a CI-cost decision no ruling
217217
* covers; it is filed rather than taken.
218218
*
219-
* ## The governed REPOS (maintainer 「同意」 2026-08-18, wired here by #9619)
219+
* ## The governed REPOS (maintainer 「同意」 2026-08-18, wired here by #9619;
220+
* ## `hotcrm` added by the 2026-09-03 ruling on #14867)
220221
*
221222
* The same day, asked whether the rule reaches the sibling repos — 「任何对
222223
* agents.md 等文件的修改…包括 objectui cloud仓库」 — the maintainer answered
223224
* 「同意」. So the surface register above is REPO-AGNOSTIC: the same five
224-
* globs are governed in `objectstack`, `objectui`, `cloud` and `objectos`,
225-
* and this sweep covers all four in one invocation regardless of the working
226-
* directory it is run from.
225+
* surface globs are governed in `objectstack`, `objectui`, `cloud`,
226+
* `objectos` and `hotcrm`, and this sweep covers all five repos in one
227+
* invocation regardless of the working directory it is run from.
228+
*
229+
* ⚠️ The 2026-08-18 quotation above names two sibling repos and the register
230+
* below holds five, so read that quote as the repos IN VIEW that day — an
231+
* enumeration, never an exclusion, and never the register. The definition it
232+
* states (agent instruction files, judged the same across repos) already
233+
* covered `hotcrm`, a repo agents edit daily; only the CONFIGURATION lagged,
234+
* and it lagged in the direction that reads as safety. Measured cost of the
235+
* lag, #14867: the `repo:hotcrm` seat treated that repo's `AGENTS.md` chain
236+
* as governed and paid for hand-merges on it, while this sweep printed no row
237+
* for hotcrm at all — and a repo that produces NO ROW is indistinguishable in
238+
* this output from a repo that swept clean. That is the #4690 rule failing one
239+
* level up: a configured repo that cannot be read is loudly UNAUDITED, but an
240+
* unconfigured repo is silent, so absence of coverage can only be made loud by
241+
* configuring the repo. Ruled 2026-09-03 (#14867, sibling #14881 ruled with
242+
* it), maintainer verbatim 「其他同意」 adopting 纳入 — `GOVERNED_REPOS` gains
243+
* `hotcrm`, and every "CLEAN WINDOW" this sweep published before that date is
244+
* re-read as "clean over the FOUR repos then configured".
227245
*
228246
* ⚠️ Until #9619 that was not true, and the gap was not theoretical: run from
229247
* the objectui checkout the sweep still enumerated objectstack PRs, so a
@@ -266,7 +284,7 @@
266284
* asks whether the floor predates the window rather than whether the clone is
267285
* shallow, so a shallow container with enough depth — the common case, since
268286
* the default window is 24h — still sweeps exactly as before, with no fetch.
269-
* Deepening is never done here: this sweep reads four checkouts it does not
287+
* Deepening is never done here: this sweep reads five checkouts it does not
270288
* own, so the remedy is printed as a command for the operator to run.
271289
*
272290
* ## And a DEAD MIRROR is the same fact again, in the direction that reads as
@@ -287,12 +305,17 @@
287305
* `git fetch origin main` in the surviving local clone answered
288306
* `fatal: repository ... not found`; `objectos`, `hotcrm`, `objectui` and
289307
* `objectstack` all resolved in the same batch, so this was a scope change for
290-
* one repo and not a broken channel. Wherever that clone survives, this sweep
308+
* one repo and not a broken channel. (⚠️ Read that batch with its date: on
309+
* 2026-08-30 `hotcrm` was probed BY HAND, as a control this sweep had no row
310+
* for — it was not configured here until the 2026-09-03 ruling above. The
311+
* reading stands as measured; what changed is that those four names are now
312+
* four of the five repos this sweep audits itself.) Wherever that clone
313+
* survives, this sweep
291314
* printed `✓ audited objectstack-ai/cloud — tip 15f55df2d ... 0 mainline
292315
* commit(s) in window` and exited 0 — and would have printed it FOREVER, since
293316
* the row's own prescribed remedy (`git fetch origin main`) is exactly the
294317
* command that 404s. The control had not degraded; it had been silently
295-
* switched off for one of the four repos it covers while continuing to print a
318+
* switched off for one of the repos it covers while continuing to print a
296319
* tick for it.
297320
*
298321
* So reachability is a MEASURED PRECONDITION, asked of every governed repo in
@@ -418,7 +441,8 @@
418441
* resolves, and `--since-ref <repoId>=<ref>` pins one repo (repeatable).
419442
* A repo no ref resolves in falls back to the date window below, and its
420443
* report line SAYS which repos got which window — one repo's ref silently
421-
* dating four repos' windows is the same class of bug as the one above.
444+
* dating every other repo's window is the same class of bug as the one
445+
* above.
422446
* Recording the previous round's tip is the CALLER's obligation (this
423447
* script keeps no state), so every sweep prints the `--since-ref` line to
424448
* use next round. Bonus property: for a topological window the #9902
@@ -824,7 +848,9 @@ export function applyGeneratedExceptions(verdict, provenanceByPath = new Map())
824848
}
825849

826850
/**
827-
* The four repos the 2026-08-18 cross-repo extension governs. `id` doubles as
851+
* The five governed repos: the four the 2026-08-18 cross-repo extension named,
852+
* plus `hotcrm`, added by the 2026-09-03 ruling on #14867 (「其他同意」 → 纳入)
853+
* because the definition's own terms already covered it. `id` doubles as
828854
* the sibling directory name beside this checkout — the layout every session
829855
* container uses — and `--repo-root <id>=<path>` overrides it for any other
830856
* layout. A checkout whose `origin` remote is not `slug` is treated as ABSENT
@@ -835,6 +861,7 @@ export const GOVERNED_REPOS = Object.freeze([
835861
Object.freeze({ id: 'objectui', slug: 'objectstack-ai/objectui', what: 'the UI repo (live skills/** tree)' }),
836862
Object.freeze({ id: 'cloud', slug: 'objectstack-ai/cloud', what: 'the cloud repo' }),
837863
Object.freeze({ id: 'objectos', slug: 'objectstack-ai/objectos', what: 'the objectos repo' }),
864+
Object.freeze({ id: 'hotcrm', slug: 'objectstack-ai/hotcrm', what: 'the hotcrm exemplar app repo (#14867, ruled 2026-09-03)' }),
838865
]);
839866

840867
export const SELF_REPO_ID = 'objectstack';
@@ -974,7 +1001,7 @@ export function classifyCommit({ sha, date, subject }, changedPaths, repo = null
9741001

9751002
/**
9761003
* The pre-arm answer, as data. Pure and repo-agnostic — the register is the
977-
* same in all four governed repos, so a seat can run this from anywhere with
1004+
* same in all five governed repos, so a seat can run this from anywhere with
9781005
* the file list of any PR in any of them.
9791006
*/
9801007
export function testVerdict(paths) {
@@ -2542,7 +2569,15 @@ async function selfTest() {
25422569
assert('exit-test-not-governed-is-0', EXIT_TEST_NOT_GOVERNED === 0);
25432570

25442571
// ── multi-repo scope (#9619) ──────────────────────────────────────────────
2545-
assert('four-governed-repos-declared', GOVERNED_REPOS.map((r) => r.id).join(',') === 'objectstack,objectui,cloud,objectos', GOVERNED_REPOS.map((r) => r.id).join(','));
2572+
assert('five-governed-repos-declared', GOVERNED_REPOS.map((r) => r.id).join(',') === 'objectstack,objectui,cloud,objectos,hotcrm', GOVERNED_REPOS.map((r) => r.id).join(','));
2573+
// #14867: hotcrm pinned by SLUG as well as id. The defect this closes is not
2574+
// a wrong row — it is NO row: an unconfigured repo prints nothing at all, so
2575+
// a later removal would restore a silence indistinguishable from a clean
2576+
// sweep. Pinned separately from the ordered-ids assertion above so a removal
2577+
// reds a check that NAMES hotcrm rather than one that reads as a reordering.
2578+
assert('hotcrm-is-a-governed-repo-with-its-slug (#14867)',
2579+
GOVERNED_REPOS.some((r) => r.id === 'hotcrm' && r.slug === 'objectstack-ai/hotcrm'),
2580+
JSON.stringify(GOVERNED_REPOS.map((r) => `${r.id}=${r.slug}`)));
25462581
assert('slug-from-https-remote', slugFromRemote('https://github.com/objectstack-ai/objectui') === 'objectstack-ai/objectui');
25472582
assert('slug-from-ssh-remote-with-suffix', slugFromRemote('git@github.com:objectstack-ai/cloud.git') === 'objectstack-ai/cloud');
25482583
assert('slug-from-nonsense-is-null', slugFromRemote('/some/local/path') === null);
@@ -2556,7 +2591,16 @@ async function selfTest() {
25562591
};
25572592
const resolved = resolveRepoCheckouts({ selfRoot: '/w/objectstack', siblingDir: '/w', probe: (p) => layout[p] ?? { exists: false, slug: null } });
25582593
const byId = Object.fromEntries(resolved.map((r) => [r.id, r]));
2559-
assert('all-four-repos-are-resolved-not-just-the-self-repo', resolved.length === 4);
2594+
assert('all-five-repos-are-resolved-not-just-the-self-repo', resolved.length === 5);
2595+
// /w/hotcrm is absent from the layout too, so this fixture also pins the
2596+
// property #14867 bought: a governed repo with no checkout is UNAUDITED and
2597+
// says so, where an unconfigured repo said nothing.
2598+
// ⚠️ Read defensively. `assert` COLLECTS failures and prints them at the
2599+
// end, so a throw here aborts the run before the two pins that NAME hotcrm
2600+
// are ever printed — measured on the #14867 ablation: dropping the register
2601+
// entry turned three named red lines into one TypeError stack.
2602+
assert('a-configured-repo-with-no-checkout-is-UNAUDITED-never-silent',
2603+
byId.hotcrm?.status === 'unaudited' && /no git checkout at \/w\/hotcrm/.test(String(byId.hotcrm?.reason)), JSON.stringify(byId.hotcrm ?? null));
25602604
assert('the-self-repo-is-audited-from-the-scripts-own-root-not-cwd', byId.objectstack.status === 'audited' && byId.objectstack.path === '/w/objectstack');
25612605
assert('a-sibling-checkout-beside-it-is-audited', byId.objectui.status === 'audited' && byId.objectos.status === 'audited');
25622606
assert('an-absent-checkout-is-UNAUDITED-never-clean', byId.cloud.status === 'unaudited' && /no git checkout at \/w\/cloud/.test(byId.cloud.reason), JSON.stringify(byId.cloud));
@@ -3419,7 +3463,7 @@ async function selfTest() {
34193463
for (const failure of failures) console.error(` • ${failure}`);
34203464
process.exit(1);
34213465
}
3422-
console.log(`✓ check-governed-merges --self-test: ${checked} assertions (the unified governed predicate + near misses, subject→PR spellings, window parsing, the #12633 landing window — the QS-7 regression pin in both directions, the topological close beyond the budget, the unproven-boundary EDGE, the listed-or-INCOMPLETE invariant over every fixture, the escalating floors, per-repo --since-ref resolution and its named fallback, and the window words — the replay fixtures, the four-repo resolution incl. absent/wrong-origin/relocated checkouts, the attribution channel chain + its proxy-transport re-arm plan and its one named fallback line, the three-way attribution column (resolved · every-channel-failed · NOT LOOKED UP, and the note pointer that belongs to the middle one alone), the --test pre-arm predicate, the generated-artifact provenance exception — the register's invariants incl. the RETIRED #9866 row staying retired (no row lifts anything under .claude/**, and the audit workflow is plainly governed again), a row with no recompute failing closed, lift/reject/absent-provenance semantics, the untouched mixed-diff rule, named-rows-not-a-class, the #11084 generator co-edit fence in both directions incl. a row with no instrument tree, and its render words — the #11705 generator-owned rows inside skills/** (a genuine generated file passes, the same path hand-edited does not, a path no generator declares is hand-authored content, per-row fences, and the enumeration read from the real generator), the exit table, the report wording pins, and the #13307 remote-reachability leg — the pure freshness verdicts in every branch (unreachable · a remote naming no commit · an unreadable local tip · a mirror behind its remote · the two-unreadable-shas degenerate case that must never read as a match), the report words in both directions (an unreachable repo never renders the tick, a reachable one still says a MEASURED zero, and a row with no remote reading never claims one), and the REAL prober on local bare-repo fixtures over the file transport — a live remote, a deleted one, the --exit-code branch, and a mirror the remote moved past — the #13423 identity leg (an origin no slug parses from refuses, pure and end-to-end, with audited reachable only through a parsed matching slug), the #13424 per-repo window resolution (a sibling-only pin resolves in its own repo, the self-only control still errors, and the end-to-end sibling-pin sweep reports instead of exiting 1), the #13307 sweep-code provenance line in all three branches, and the #13836 attribution set — every refusal carries its precondition category on the row, in the footer, and in --json; the shallow-clone path in both directions; and the run-1-vs-run-2 flip reproduced on real fixtures with zero local writes).\n ${liveNote}`);
3466+
console.log(`✓ check-governed-merges --self-test: ${checked} assertions (the unified governed predicate + near misses, subject→PR spellings, window parsing, the #12633 landing window — the QS-7 regression pin in both directions, the topological close beyond the budget, the unproven-boundary EDGE, the listed-or-INCOMPLETE invariant over every fixture, the escalating floors, per-repo --since-ref resolution and its named fallback, and the window words — the replay fixtures, the five-repo resolution incl. absent/wrong-origin/relocated checkouts, the attribution channel chain + its proxy-transport re-arm plan and its one named fallback line, the three-way attribution column (resolved · every-channel-failed · NOT LOOKED UP, and the note pointer that belongs to the middle one alone), the --test pre-arm predicate, the generated-artifact provenance exception — the register's invariants incl. the RETIRED #9866 row staying retired (no row lifts anything under .claude/**, and the audit workflow is plainly governed again), a row with no recompute failing closed, lift/reject/absent-provenance semantics, the untouched mixed-diff rule, named-rows-not-a-class, the #11084 generator co-edit fence in both directions incl. a row with no instrument tree, and its render words — the #11705 generator-owned rows inside skills/** (a genuine generated file passes, the same path hand-edited does not, a path no generator declares is hand-authored content, per-row fences, and the enumeration read from the real generator), the exit table, the report wording pins, and the #13307 remote-reachability leg — the pure freshness verdicts in every branch (unreachable · a remote naming no commit · an unreadable local tip · a mirror behind its remote · the two-unreadable-shas degenerate case that must never read as a match), the report words in both directions (an unreachable repo never renders the tick, a reachable one still says a MEASURED zero, and a row with no remote reading never claims one), and the REAL prober on local bare-repo fixtures over the file transport — a live remote, a deleted one, the --exit-code branch, and a mirror the remote moved past — the #13423 identity leg (an origin no slug parses from refuses, pure and end-to-end, with audited reachable only through a parsed matching slug), the #13424 per-repo window resolution (a sibling-only pin resolves in its own repo, the self-only control still errors, and the end-to-end sibling-pin sweep reports instead of exiting 1), the #13307 sweep-code provenance line in all three branches, and the #13836 attribution set — every refusal carries its precondition category on the row, in the footer, and in --json; the shallow-clone path in both directions; and the run-1-vs-run-2 flip reproduced on real fixtures with zero local writes).\n ${liveNote}`);
34233467

34243468
return SELF_TEST_VERDICT;
34253469
}

0 commit comments

Comments
 (0)