diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index 8217afbb95..e95ccbfa54 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -331,7 +331,7 @@ export const CEILINGS = new Map([ ['skills/objectstack-platform/SKILL.md', 12984], // 14239 -> 14391: the pull-directed split-resolution order joined the decision // frame (maintainer ruling 2026-08-27, verbatim and untranslated: 「tong y 4」 — - // accepting the four-rule set), and this file carries TWO enforced frame copies + // accepting the four-rule set), and this file carries one enforced frame copy // (check:skill-frame-sync COPIES), so the rule ships to third-party installers // with the frame it amends — the #5130 drift is exactly a frame-semantics change // that skipped this mirror. +152 tokens across both copies, compressed to the @@ -341,8 +341,8 @@ export const CEILINGS = new Map([ // 2026-09-01, verbatim and untranslated (kept on ONE line, #11106: a governed // quotation that soft-breaks stops being findable by the things that grep it): // 「四维分析中,长期合理应该权重最高,至少50%」 - // Same shape and same reason as the +152 row above it: this file carries TWO - // enforced frame copies (check:skill-frame-sync COPIES), and a rule that + // Same shape and same reason as the +152 row above it: this file carries one + // enforced frame copy (check:skill-frame-sync COPIES), and a rule that // changes WHICH RECOMMENDATION the frame yields is exactly the #5130 drift // class if it ships to third-party installers with only the old tie-break — // the customer's agent would weigh the axes co-equally while this repo weighs @@ -356,7 +356,7 @@ export const CEILINGS = new Map([ // existing sentence redundant, and a re-wrap moves no tokens and pays nothing. // 14549 -> 9708: re-locked at the landed count after the #14296 item-4 split — // the developer-agent operating template moved to `rules/dev-template.md` - // (its own row below); the two gate-pinned copies of the decision frame stay + // (its own row below); the one gate-pinned copy of the decision frame stays // in this file (check:skill-frame-sync reads its copies by path). Lowered, // not raised: shrink-only, no ruling needed for this direction. ['skills/objectstack-pm-dispatch/SKILL.md', 9708], diff --git a/scripts/pm/check-skill-line-ratchet.mjs b/scripts/pm/check-skill-line-ratchet.mjs index f7c2519c8c..bd730a1bca 100644 --- a/scripts/pm/check-skill-line-ratchet.mjs +++ b/scripts/pm/check-skill-line-ratchet.mjs @@ -568,9 +568,11 @@ export const CEILINGS = new Map([ // 4 lines are re-flow slack, never a cut. Headroom 0 again, same convention. // Raised 466 → 469 by the axis-weighting ruling (maintainer 2026-09-01, PM // chat, verbatim and untranslated): 「四维分析中,长期合理应该权重最高,至少50%」. - // This file is one of the four `check:skill-frame-sync` COPIES, so a rule that - // changes which recommendation the frame yields has to reach it — the #5130 - // drift is exactly a frame-semantics change that skipped a mirror. +3 lines, + // This file is not a `check:skill-frame-sync` COPIES entry, but it names the + // decision frame's mechanism (the dev reads it from the PM's pasted copy at + // dispatch time), and a rule that changes which recommendation the frame + // yields still has to reach that description — the #5130 drift is exactly a + // frame-semantics change that skipped a mirror. +3 lines, // folded into the existing binding sentence rather than added as a new // paragraph. ⚠️ Re-wrap funding was AVAILABLE here and was REFUSED: three // paragraphs nearby carry wrap artifacts (two orphan lines of 6 and 8 bytes) diff --git a/scripts/pm/dispatch-gates.mjs b/scripts/pm/dispatch-gates.mjs index 0805e00611..6772fd7f76 100644 --- a/scripts/pm/dispatch-gates.mjs +++ b/scripts/pm/dispatch-gates.mjs @@ -8564,12 +8564,12 @@ export const MANDATORY_TIER_GLOBS = [ { glob: '.claude/agents/os-dev.md', tier: CONTRACT_REVIEW_TIER, - why: 'clause ① (2026-08-20 narrowing): the dev-agent definition is protocol semantics — every dispatched dev runs under it, and it carries an enforced copy of the decision frame', + why: 'clause ① (2026-08-20 narrowing): the dev-agent definition is protocol semantics — every dispatched dev runs under it, and receives the decision frame the PM pastes into its prompt at dispatch time rather than carrying a copy of its own', }, { glob: 'skills/objectstack-pm-dispatch/SKILL.md', tier: CONTRACT_REVIEW_TIER, - why: 'clause ① (2026-08-20 narrowing): the published PM skill carries two enforced copies of the decision frame (check:skill-frame-sync COPIES) and ships verbatim to third-party projects', + why: 'clause ① (2026-08-20 narrowing): the published PM skill carries one enforced copy of the decision frame (check:skill-frame-sync COPIES) and ships verbatim to third-party projects', }, ]; @@ -16397,7 +16397,7 @@ function selfTest() { const fableOf = (paths) => deriveTier(paths); t('the pm-dispatch SKILL.md MAIN file is fable-mandatory', fableOf(['.claude/skills/pm-dispatch/SKILL.md']).tier === CONTRACT_REVIEW_TIER); t('the dev-agent definition is fable-mandatory', fableOf(['.claude/agents/os-dev.md']).tier === CONTRACT_REVIEW_TIER); - t('the published PM skill (two enforced frame copies) is fable-mandatory', fableOf(['skills/objectstack-pm-dispatch/SKILL.md']).tier === CONTRACT_REVIEW_TIER); + t('the published PM skill (one enforced frame copy) is fable-mandatory', fableOf(['skills/objectstack-pm-dispatch/SKILL.md']).tier === CONTRACT_REVIEW_TIER); t('a pm-dispatch REFERENCES path carries NO path mandate — the 2026-08-20 narrowing, inverted from the pre-narrowing pin', fableOf(['.claude/skills/pm-dispatch/references/review-checklist.md']).mandatory === false); const mixed = fableOf(['packages/spec/src/data/filter.zod.ts', '.claude/agents/os-dev.md']); t('a MIXED surface is mandatory — one mandatory path decides, ordinary paths do not dilute it', mixed.mandatory && mixed.tier === CONTRACT_REVIEW_TIER); @@ -17935,6 +17935,27 @@ function selfTest() { ['--tier', '--residue', '--commands', '--json', '--ran', '--repo', '--changed', '--self-test'] .every((flag) => USAGE_LINE.includes(flag)), ); + // ⭐ #14870 — the mirror-image fix: `--changed` sat OUTSIDE the alternation + // as a whole-invocation alternative, which reads as excluding every mode + // beside it, though `--changed --commands` is legal and answers (CONTROL + // below). Moved to the position ` ...` occupies, the other path + // source it stands in for. + t( + '⭐ the usage line no longer presents --changed as a whole-invocation alternative that takes no other flag (#14870)', + !USAGE_LINE.includes('] | --changed | --self-test'), + ); + t( + '…and it still offers --changed where ... sits, combining with the modes before it', + USAGE_LINE.includes('[ ... | --changed]'), + ); + // CONTROL: --changed really does combine with a stdout-shape flag — the + // usage-line fix above would otherwise be cosmetic on a refusal that does + // not exist. + const changedCommandsRun = runCli(['--changed', '--commands']); + t( + 'CONTROL: --changed --commands is legal and answers, so the moved usage line describes a real combination', + changedCommandsRun.status === 0 && (changedCommandsRun.stdout ?? '').length > 0, + ); } // ── END TO END: the CI-measured family, on the card it was measured on (#14004) @@ -18304,6 +18325,14 @@ const invokedDirectly = isEntrypoint(import.meta.url); * ⛔ Deleting `[--residue]` instead would understate it — the flag really is * legal with the other three, and with the plain human rendering. * + * `--changed` had the mirror-image problem: it sat OUTSIDE the alternation as + * a whole-invocation alternative, which reads as excluding every member next + * to it — `--commands`/`--json` included, though `--changed --commands` is + * legal and answers (it derives the path list ` ...` would otherwise + * supply, and nothing more). Moved to the position ` ...` occupies, the + * other path source it stands in for, so the line no longer implies a refusal + * the argv chain does not make (#14870). + * * A CONSTANT rather than a literal at the print site, because the pin belongs * beside the refusals it mirrors: reaching the print site needs a checkout * where `changedPathsFromGit()` refuses, and a pin that cannot be run in the @@ -18312,7 +18341,7 @@ const invokedDirectly = isEntrypoint(import.meta.url); const USAGE_LINE = 'usage: node scripts/pm/dispatch-gates.mjs' + ' [--tier | [--residue] [--commands | --json | --ran ]]' - + ' [--repo owner/name] [ ...] | --changed | --self-test'; + + ' [--repo owner/name] [ ... | --changed] | --self-test'; /** * Executed only as a CLI. Importing this module must have NO side effect.