Skip to content

fix(pm): the line ratchet declares its repo-root population, so an AGENTS.md card derives it - #9978

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-9964-agents-md-gate-discovery
Aug 19, 2026
Merged

fix(pm): the line ratchet declares its repo-root population, so an AGENTS.md card derives it#9978
os-warren merged 1 commit into
mainfrom
claude/issue-9964-agents-md-gate-discovery

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#9964

node scripts/pm/dispatch-gates.mjs AGENTS.md derived zero gates. extractWatchHints treats "looks like a path" as "carries a separator" (plus a short allowlist of dotted top-level dirs), so the line ratchet's eighteen CEILINGS keys yielded seventeen watch hints — the repo-root AGENTS.md has no separator to be found by. A dev dispatched on an AGENTS.md card got an empty local gate brief and first met check:pm-skill-ratchet as red CI, on the largest ceiling in that map (958 lines, headroom 0), where one added paragraph crosses it. lint.yml carries no path filter, so CI always enforced: this was discoverability, never enforcement.

The measurement gate — both options priced on the original refusal's corpus

The card required option 2 and option 3 to be measured against the same corpus the original genericity refusal used, before either could be implemented. The methodology is stated in hintCovers' docblock ("Measured over 107 discovered families against all 6181 tracked files: watch-hint (gate, file) pairs"); no measurement script was ever committed, so it was reconstructed offline: discover the check families exactly as derive() does, extract each family's hints, count the (family, tracked file) pairs hintCovers accepts. Corpus today: 114 families x 6326 tracked files.

The control validates the harness. Re-running the generalisation the original refusal rejected (accept any bare literal naming a real top-level directory) reproduces its published shape on today's larger tree:

pairsdeltaone packages/spec/src/index.ts card
refusal's published figures (107 x 6181)19024 to 158108+1390847 to 34 families
reproduced here (114 x 6326)26060 to 175192+1491328 to 37 families

Option 2 — extractor allowance for bare top-level literals ending in an instruction-file extension and naming a real tracked file:

  • 26060 to 26077 pairs (+17), 14 families gain, 0 lose. Volume is trivial — 0.065%.
  • It fails anyway, on provenance. 8 of the 17 new pairs are fabricated: gates spell README.md and CHANGELOG.md as basenames they join with a directory, not as files they read — a manifest files entry (CANONICAL in check-published-files), a per-package markdown exclusion (MARKDOWN_EXCLUDED in check-published-readme-exports), a .changeset/ basename filter (check-changeset-no-major), a remote directory listing filter (check-objectui-pin-fresh), and join(target.dir, 'CHANGELOG.md') in check-release-body.
  • Concretely: a README.md card would come back with six leads of which five name a gate that never reads that file. That is the exact mechanism the original refusal named ("every package.json / turbo.json / tsconfig.json basename a gate joins with a package directory"), one extension over.
  • It also needs a two-place change — extractWatchHintsandhintCovers' genericity refusal — since the latter refuses separator-less, non-dotted hints.

Option 3 — the ratchet declares its root-file population in a form the extractor already accepts:

  • 26060 to 26061 pairs (+1), exactly one family gaining coverage, on exactly one file. Zero families lose. Extractor and comparison both untouched.

Criterion used, and it is the refusal's own, in two parts. (i) Volume — no order-of-magnitude blowup (the refused generalisation was x6.7; the two accepted changes were +4.3% and +22.6%). (ii) Provenance — "ZERO lost" and every new pair a genuine population declaration, because this file's stated error direction prices a fabricated lead far above a missing one ("pasted into EVERY dispatch prompt whose surface brushes it"). Option 2 passes (i) with room to spare and fails (ii). Option 3 passes both. Volume alone was never the whole threshold, and judging option 2 on its +17 would have inverted the refusal's own reasoning.

Chosen: option 3. Option 1's documentation half was therefore not implemented (the card scopes it to "both fail"); the existing residue sentence naming the top-level-FILE class with README.md stays accurate, since the class is still refused.

What landed

scripts/pm/check-skill-line-ratchet.mjs gains ROOT_FILE_WATCH_HINTS = ['AGENTS.md/**']. The subtree spelling is the only form that reaches a repo-root file: the extractor requires the separator, and collapseHint reduces it back to AGENTS.md, matching that path alone (nothing in the tree lives under it). It is provenance, not a lookup key — it stays out of CEILINGS, which is the map run opens files through; a key rewritten into the glob form would send the ratchet looking for a file that does not exist.

scripts/pm/dispatch-gates.mjs gains the four self-test cases and a docblock paragraph recording the re-measurement, so the next reader finds the AGENTS.md/** hint as a decision rather than deleting it as a typo.

Result:

Local gates for this card (paste into the dispatch prompt):
- pnpm check:pm-skill-ratchet [lint.yml] matched via AGENTS.md ⇢ gate source 'AGENTS.md/**'

Residue accounting for an AGENTS.md card moves by exactly one family: 0 matched · 35 undetermined · 79 silent becomes 1 matched · 35 undetermined · 78 silent.

Reverse verification — direction predicted in writing first

Predicted: emptying ROOT_FILE_WATCH_HINTS to [] while leaving the docblock's prose copy of the glob in place returns the derivation to zero (proving the hint comes from the module body, not from prose — the accident the #9639 narrowing exists to stop relying on); exactly 1 of 4 new dispatch-gates cases goes red, because the other three assert absence and cannot distinguish; exactly 2 of 4 new ratchet cases go red, the other two being vacuously true over an empty list.

Observed, all three exactly as predicted:

legresult
ablated: dispatch-gates AGENTS.mdNo check family names the given paths..., 0 matched · 35 undetermined · 79 silent
ablated: dispatch-gates self-test✗ dispatch-gates self-test: 1 of 314 case(s) failed. — the positive case only
ablated: ratchet self-test✗ check-skill-line-ratchet self-test: 2 of 19 case(s) failed.
restoredboth green again; file byte-identical to the commit (git hash-object equals the HEAD blob)

No rebuild applies to this ablation and none was needed: both files are plain .mjs executed from source, and dispatch-gates reads the ratchet's source text off disk — nothing here resolves through a package exports field or a dist/, so there is no stale-artifact leg to defeat.

Control paths gain no phantom hints (each still derives zero families, and residue totals are unchanged at 35 undetermined · 79 silent): README.md, CLAUDE.md, and examples/AGENTS.md — the last being the segment-boundary control, a same-named file one directory down.

Gates

Union derived from the real diff by node scripts/pm/dispatch-gates.mjs with no path arguments (it reads its own change set from the merge base), run after the final commit, at 63fce4a24. Exit codes captured before any pipe; each gate's own verdict line quoted:

gateexitverdict line
pnpm check:cross-package-test-inputs0OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
pnpm check:pm-dispatch-gates0✓ dispatch-gates self-test: 314 cases pass.
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: AGENTS.md is 958 lines (ceiling 958; headroom 0).
pnpm check:nul-bytes0check-nul-bytes: OK (scanned 6321 text file(s) -- 6321 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).

node scripts/check-cross-package-test-inputs.mjs also appears in the derived union; it is the same script the first row runs.

Self-test counts: dispatch-gates 310 to 314, line ratchet 15 to 19.

No changeset: internal PM tooling under scripts/pm/, nothing published changes. skip-changeset applied at PR creation.

Not a governed path (scripts/pm/), so this is a draft for the PM to flip ready.


Generated by Claude Code

…ENTS.md card derives it
`extractWatchHints` requires a path separator, so the ratchet's eighteen
CEILINGS keys yielded seventeen hints: the repo-root `AGENTS.md` has none.
`node scripts/pm/dispatch-gates.mjs AGENTS.md` therefore derived ZERO gates,
and a dev dispatched on the largest ceiling in that map (958 lines, headroom 0)
first met `check:pm-skill-ratchet` as red CI. lint.yml has no path filter, so
CI always enforced — what was missing was discoverability.
The gate now declares its root-file population as `AGENTS.md/**`, the one form
the extractor already accepts for a repo-root file: `collapseHint` reduces it
back to `AGENTS.md` and it matches that path alone. Provenance only — it stays
out of CEILINGS, which is the map `run` opens files through.
Measured over 114 families x 6326 tracked files, reusing the methodology of the
original genericity refusal (the control reproduces it: 26060 -> 175192 pairs,
one packages/spec card 8 -> 37 families). This declaration: 26060 -> 26061
pairs, one family gaining coverage, one file. The extractor-widening
alternative was measured and REFUSED: 26060 -> 26077 is cheap by volume but 8
of its 17 new pairs are fabricated, because gates spell README.md and
CHANGELOG.md as basenames they join with a package directory — a README.md card
would gain six leads of which five name a gate that never reads it.
Self-tests: dispatch-gates 310 -> 314, line ratchet 15 -> 19.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeA3nU1B5Q2pgxqxgUrexd
@os-warrenos-warren added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 19, 2026 — with Claude
@os-warren
os-warren marked this pull request as ready for review August 19, 2026 11:48
@os-warren
os-warren enabled auto-merge August 19, 2026 11:49
@os-warren
os-warren added this pull request to the merge queueAug 19, 2026
Merged via the queue into main with commit f48be89Aug 19, 2026
24 checks passed
@os-warren
os-warren deleted the claude/issue-9964-agents-md-gate-discovery branch August 19, 2026 12:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: an AGENTS.md-only card derives ZERO gates — check:pm-skill-ratchet is locally undiscoverable for the file carrying its largest ceiling

2 participants

@os-warren@claude