Skip to content

Commit 994cd68

Browse files
claude[bot]claude
andauthored
pm: add a line-ratchet ceiling for root CLAUDE.md (#11274)
Root CLAUDE.md is the other repo-root instruction file — same read path (every seat session), same governance — and structurally growth-prone: it exists to inline the rules that must never be missed, so every new such rule argues for appending to it. Maintainer ruling 2026-08-20 on the card: add a CEILINGS row. Set at its measured count, 86, headroom 0. Coupled in the same commit, all inside the ratchet: - the header's boundary sentence no longer names CLAUDE.md as uncovered - ROOT_FILE_WATCH_HINTS gains 'CLAUDE.md/**' (the existing self-test requires a hint for every separator-less ceiling), matching the spelling check-governed-merges.mjs already uses for the two root files - a self-test case pinning the new row, and the AGENTS.md-exclusivity case replaced with the two-root-file form Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 050d8d8 commit 994cd68

1 file changed

Lines changed: 29 additions & 16 deletions

File tree

scripts/pm/check-skill-line-ratchet.mjs

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@
3131
* ENUMERATION, never a root glob: the pm-dispatch surface (SKILL.md, its
3232
* references/, the per-lane job descriptions), the four other
3333
* `.claude/skills/` playbook SKILL.md files, the dev-agent definition
34-
* `.claude/agents/os-dev.md`, and the root `AGENTS.md`. Read a file's absence
35-
* from the map as a fact to check, not an oversight to infer — `.claude/hooks/`,
36-
* `.claude/settings.json` and `CLAUDE.md` carry no ceiling either.
34+
* `.claude/agents/os-dev.md`, and both root instruction files — `AGENTS.md`
35+
* and `CLAUDE.md`. Read a file's absence from the map as a fact to check, not
36+
* an oversight to infer — `.claude/hooks/` and `.claude/settings.json` carry no
37+
* ceiling.
3738
*
3839
* The **published** `skills/` catalog — the one that ships to customer projects
3940
* — is deliberately OUTSIDE the ceiling. It is the omission worth stating
@@ -141,6 +142,16 @@ export const CEILINGS = new Map([
141142
// (issue #10126, comment 5353111732). Headroom is 0 again by construction, and
142143
// the next author needing a line is back to compressing.
143144
['AGENTS.md', 961],
145+
// #9965: root CLAUDE.md is the other repo-root instruction file — same read
146+
// path (every seat session), same governance (Prime Directive #14). It is
147+
// structurally growth-prone in the way the ratchet is built for: it exists to
148+
// inline the rules that must never be missed, so every new must-never-miss
149+
// rule is an argument for appending to it. Maintainer ruling 2026-08-20,
150+
// verbatim and untranslated: 「其他接受你的建议。」 (issue #9965, comment
151+
// 5353931707) — rationale on record: a one-line ceiling now prevents compound
152+
// growth cheaply. Set at its line count on `origin/main` (headroom 0, same
153+
// convention as the entries above).
154+
['CLAUDE.md', 86],
144155
]);
145156

146157
/**
@@ -151,21 +162,22 @@ export const CEILINGS = new Map([
151162
*
152163
* That tool reads a gate's population out of the path literals in the gate's own
153164
* source, and "looks like a path" there means "carries a separator" (plus a short
154-
* allowlist of dotted top-level dirs). Every key above satisfies that except
155-
* `AGENTS.md` — a repo-root FILE has no separator to be found by. So the map's
156-
* eighteen entries yielded seventeen watch hints, an AGENTS.md card derived ZERO
157-
* gates, and the dev met this ratchet as red CI instead of as a local command.
158-
* That lands on the largest ceiling in the map at headroom 0, where one added
159-
* paragraph crosses it. CI still enforces either way (lint.yml carries no path
160-
* filter) — what was missing was discoverability, and this restores it.
165+
* allowlist of dotted top-level dirs). Every key above satisfies that except the
166+
* repo-root files — a repo-root FILE has no separator to be found by. So before
167+
* this list existed an AGENTS.md card derived ZERO gates, and the dev met this
168+
* ratchet as red CI instead of as a local command. That lands on the largest
169+
* ceiling in the map at headroom 0, where one added paragraph crosses it. CI
170+
* still enforces either way (lint.yml carries no path filter) — what was missing
171+
* was discoverability, and this restores it.
161172
*
162-
* ## Why the subtree spelling, and why it covers exactly one file
173+
* ## Why the subtree spelling, and why it covers exactly the root files
163174
*
164175
* `<file>/**` is the only form that reaches a repo-root file: the extractor
165176
* requires the separator, and dispatch-gates collapses a hint's globs before
166-
* comparing, which reduces this back to `AGENTS.md` and matches that path alone.
167-
* Nothing in the tree lives under `AGENTS.md/`, so it claims no directory —
168-
* measured at exactly one (gate, file) pair added, one family gaining coverage.
177+
* comparing, which reduces each back to its bare filename and matches that path
178+
* alone. Nothing in the tree lives under `AGENTS.md/` or `CLAUDE.md/`, so neither
179+
* claims a directory — measured at exactly one (gate, file) pair added per root
180+
* file, one family gaining coverage.
169181
*
170182
* The alternative was widening the extractor to accept bare top-level `*.md`
171183
* literals. Measured over 114 families x 6326 tracked files it is cheap by
@@ -185,7 +197,7 @@ export const CEILINGS = new Map([
185197
* self-test pins both halves — every separator-less ceiling is declared here,
186198
* and nothing declared here is a CEILINGS key.
187199
*/
188-
export const ROOT_FILE_WATCH_HINTS = ['AGENTS.md/**'];
200+
export const ROOT_FILE_WATCH_HINTS = ['AGENTS.md/**', 'CLAUDE.md/**'];
189201

190202
export function verdict(rel, lineCount, maxLines) {
191203
if (lineCount === 0) return { ok: false, msg: `${rel} read as empty — refusing to treat a missing/empty input as a pass (#4690).` };
@@ -248,13 +260,14 @@ function selfTest() {
248260
['all seven lane job descriptions are covered', ['engine', 'services', 'cli', 'devx', 'skills', 'spec', 'hotcrm'].every((n) => CEILINGS.has(`.claude/skills/pm-dispatch/references/lanes/${n}.md`)), true],
249261
['the other four skills are covered (#9473)', ['checklist-test', 'checklist-author', 'dogfood-verification', 'spec-property-retirement'].every((n) => CEILINGS.has(`.claude/skills/${n}/SKILL.md`)), true],
250262
['root AGENTS.md is covered (#9792)', CEILINGS.has('AGENTS.md'), true],
263+
['root CLAUDE.md is covered (#9965)', CEILINGS.has('CLAUDE.md'), true],
251264
// The dispatch-gates declaration (#9964). Enforcement cannot hold any of
252265
// these: the declaration is read by another tool entirely, so a wrong or
253266
// missing entry runs perfectly green here and only shows up as a dev
254267
// dispatched on a root-file card with an empty gate brief.
255268
['every separator-less ceiling declares a root-file watch hint', [...CEILINGS.keys()].filter((k) => !k.includes('/')).every((k) => ROOT_FILE_WATCH_HINTS.includes(`${k}/**`)), true],
256269
['and the declaration names no file the map does not cover', ROOT_FILE_WATCH_HINTS.every((h) => CEILINGS.has(h.replace(/\/\*+$/, ''))), true],
257-
['AGENTS.md is the root file it declares', ROOT_FILE_WATCH_HINTS.includes('AGENTS.md/**'), true],
270+
['both root instruction files are declared', ROOT_FILE_WATCH_HINTS.join(',') === 'AGENTS.md/**,CLAUDE.md/**', true],
258271
// Provenance, never a lookup key: `run` opens every CEILINGS key, so the
259272
// glob form appearing there would make the ratchet read a path that does
260273
// not exist — red under #4690's cannot-read rule, for a file that is fine.

0 commit comments

Comments
 (0)