Found while landing the triage lane reference (PR #15958, which adds .claude/skills/pm-dispatch/references/lanes/triage.md and its two map rows). Not fixed there: that card's ruling scopes the ratchet edit to the two map rows, and the fix below also moves a pinned self-test label.
What the roster does today
scripts/pm/check-skill-line-ratchet.mjs's self-test carries one case for the lane job descriptions:
['all eight lane/seat job descriptions are covered', ['engine', 'services', 'cli', 'devx', 'skills', 'spec', 'hotcrm', 'director'].every((n) => CEILINGS.has(`.claude/skills/pm-dispatch/references/lanes/${n}.md`)), true],
It is an .every over a hand-written list of eight names, so a ninth lane file added to CEILINGS keeps it green and is not itself named by it. After the triage file lands there are nine lane files and the roster still names eight.
Why it matters, and how much
The case exists because enforcement cannot hold coverage: a lane file with no ceiling row simply is not read by the gate, and nothing turns red. That is exactly the defect two earlier findings closed for references/ files. For the eight named lanes, deleting a CEILINGS row fails this case by name. For a lane file that is not in the roster, deleting its row is caught only by the map-wide cases (every covered file has a positive ceiling, every ceilinged file carries a pin) — which say nothing about a row that is simply gone.
So the gap is bounded: the new file is metered normally on every run; what is missing is the pin against silent removal.
Why it is not a one-line fix
The case label is itself pinned in the self-test's expectation table ('all eight lane/seat job descriptions are covered': 1), which is half of the duplicate-label refusal. Extending the roster means editing three places in step: the name list, the label (eight → nine), and that table entry.
The shape worth considering
A roster that cannot go stale would be better than a longer literal: derive the lane names by reading the lanes/ directory (the same idiom the compile-surfaces finding used to find the last uncovered references file — enumerate the directory against the map), and assert every file found there has a CEILINGS row. That turns "the author remembered to extend a list" into "the tree and the map agree", and it removes the label arithmetic entirely.
Surface: scripts/pm/check-skill-line-ratchet.mjs. Repo tooling only; no runtime or published surface is involved.
Found while landing the triage lane reference (PR #15958, which adds
.claude/skills/pm-dispatch/references/lanes/triage.mdand its two map rows). Not fixed there: that card's ruling scopes the ratchet edit to the two map rows, and the fix below also moves a pinned self-test label.What the roster does today
scripts/pm/check-skill-line-ratchet.mjs's self-test carries one case for the lane job descriptions:It is an
.everyover a hand-written list of eight names, so a ninth lane file added toCEILINGSkeeps it green and is not itself named by it. After the triage file lands there are nine lane files and the roster still names eight.Why it matters, and how much
The case exists because enforcement cannot hold coverage: a lane file with no ceiling row simply is not read by the gate, and nothing turns red. That is exactly the defect two earlier findings closed for
references/files. For the eight named lanes, deleting aCEILINGSrow fails this case by name. For a lane file that is not in the roster, deleting its row is caught only by the map-wide cases (every covered file has a positive ceiling,every ceilinged file carries a pin) — which say nothing about a row that is simply gone.So the gap is bounded: the new file is metered normally on every run; what is missing is the pin against silent removal.
Why it is not a one-line fix
The case label is itself pinned in the self-test's expectation table (
'all eight lane/seat job descriptions are covered': 1), which is half of the duplicate-label refusal. Extending the roster means editing three places in step: the name list, the label (eight → nine), and that table entry.The shape worth considering
A roster that cannot go stale would be better than a longer literal: derive the lane names by reading the
lanes/directory (the same idiom the compile-surfaces finding used to find the last uncovered references file — enumerate the directory against the map), and assert every file found there has aCEILINGSrow. That turns "the author remembered to extend a list" into "the tree and the map agree", and it removes the label arithmetic entirely.Surface:
scripts/pm/check-skill-line-ratchet.mjs. Repo tooling only; no runtime or published surface is involved.