Found while wiring scripts/pm/ci-failure.mjs's --self-test into Lint & Repo Gates for #9898 (PR #10195). Not fixed there — that card's declared file surface is .github/workflows/lint.yml and this is a different tool. Filed unassigned.
The observation
scripts/pm/git-history.mjs ships a --self-test (dispatch at :306, reporter at :503, documented in its own usage block at :13) and nothing runs it: no workflow names it, and root package.json carries no alias for it.
Derived at origin/main by resolving pnpm check:* aliases rather than grepping workflows for filenames — the naive filename scan is wrong here, because most PM tools reach CI through an alias and their filename never appears in any workflow:
| tool | self-test in CI |
|---|
check-governed-merges.mjs | yes (pnpm check:pm-governed-merges) |
check-governed-prose.mjs | yes (pnpm check:pm-governed-prose) |
check-half-states.mjs | yes (pnpm check:pm-half-states) |
check-label-desc-cap.mjs | yes (pnpm check:pm-label-desc-cap) |
check-skill-id-lint.mjs | yes (pnpm check:pm-skill-id-lint) |
check-skill-line-ratchet.mjs | yes (pnpm check:pm-skill-ratchet) |
os-verify-lock.sh | yes (direct) |
release-rehearsal-clone.mjs | yes (direct) |
ci-failure.mjs | no → PR #10195 |
git-history.mjs | no ← this card |
(dispatch-gates.mjs is covered indirectly: pnpm check:pm-dispatch-gates runs the dedicated check-dispatch-gates.mjs gate, whose header argues why it is a separate file.)
Why this is worth a card rather than a shrug
This exact class has now been closed five separate times, one instance at a time: #8110, #8170, #8162, #8528, #9348 — and #9898 is the sixth. Every one was found by a human or an agent noticing, never by a gate. The rot is quiet by construction: a self-test nobody runs stays green in the only sense that matters (nobody looks), and the tool it guards is typically reached for mid-round, under time pressure, by someone who needs it to work right then.
git-history.mjs also sits in the blast radius of #9902 (seat tools asking windowed history questions with no shallow guard), which makes an unrun self-test on it worth more than average.
Suggested shape — close the class, not the instance
Rather than a seventh manual wiring, a gate that asserts every scripts/pm/** tool shipping a --self-test is reachable from some workflow (directly or through a package.json alias), with an explicit, reasoned opt-out list for anything deliberately excluded. The alias-resolution above is the whole detector; it is ~20 lines and it already exists in prose in several step comments in lint.yml.
That converts "somebody notices the seventh instance" into "the seventh instance cannot merge unwired", which is the same trade every step in the Lint & Repo Gates self-test family already accepts.
⚠️ Worth measuring before building: whether the runtime of git-history.mjs --self-test is CI-affordable, and whether it needs network. Neither was measured here — only its wiring was.
Generated by Claude Code
Found while wiring
scripts/pm/ci-failure.mjs's--self-testintoLint & Repo Gatesfor #9898 (PR #10195). Not fixed there — that card's declared file surface is.github/workflows/lint.ymland this is a different tool. Filed unassigned.The observation
scripts/pm/git-history.mjsships a--self-test(dispatch at:306, reporter at:503, documented in its own usage block at:13) and nothing runs it: no workflow names it, and rootpackage.jsoncarries no alias for it.Derived at
origin/mainby resolvingpnpm check:*aliases rather than grepping workflows for filenames — the naive filename scan is wrong here, because most PM tools reach CI through an alias and their filename never appears in any workflow:check-governed-merges.mjspnpm check:pm-governed-merges)check-governed-prose.mjspnpm check:pm-governed-prose)check-half-states.mjspnpm check:pm-half-states)check-label-desc-cap.mjspnpm check:pm-label-desc-cap)check-skill-id-lint.mjspnpm check:pm-skill-id-lint)check-skill-line-ratchet.mjspnpm check:pm-skill-ratchet)os-verify-lock.shrelease-rehearsal-clone.mjsci-failure.mjsgit-history.mjs(
dispatch-gates.mjsis covered indirectly:pnpm check:pm-dispatch-gatesruns the dedicatedcheck-dispatch-gates.mjsgate, whose header argues why it is a separate file.)Why this is worth a card rather than a shrug
This exact class has now been closed five separate times, one instance at a time: #8110, #8170, #8162, #8528, #9348 — and #9898 is the sixth. Every one was found by a human or an agent noticing, never by a gate. The rot is quiet by construction: a self-test nobody runs stays green in the only sense that matters (nobody looks), and the tool it guards is typically reached for mid-round, under time pressure, by someone who needs it to work right then.
git-history.mjsalso sits in the blast radius of #9902 (seat tools asking windowed history questions with no shallow guard), which makes an unrun self-test on it worth more than average.Suggested shape — close the class, not the instance
Rather than a seventh manual wiring, a gate that asserts every
scripts/pm/**tool shipping a--self-testis reachable from some workflow (directly or through apackage.jsonalias), with an explicit, reasoned opt-out list for anything deliberately excluded. The alias-resolution above is the whole detector; it is ~20 lines and it already exists in prose in several step comments inlint.yml.That converts "somebody notices the seventh instance" into "the seventh instance cannot merge unwired", which is the same trade every step in the
Lint & Repo Gatesself-test family already accepts.git-history.mjs --self-testis CI-affordable, and whether it needs network. Neither was measured here — only its wiring was.Generated by Claude Code