@event4u/agent-config 12.1.0 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +151 -0
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/cost/report.md +23 -0
  5. package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
  6. package/dist/agent-src/contexts/subagent-configuration.md +69 -7
  7. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  8. package/dist/agent-src/rules/design-fidelity.md +1 -1
  9. package/dist/agent-src/rules/design-review-after-ui-write.md +10 -5
  10. package/dist/agent-src/rules/evaluator-independence.md +27 -5
  11. package/dist/agent-src/rules/git-history-discipline.md +5 -1
  12. package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
  13. package/dist/agent-src/rules/ui-audit-gate.md +9 -4
  14. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  15. package/dist/agent-src/scripts/update_roadmap_progress.ts +17 -2
  16. package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
  17. package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
  18. package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
  19. package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
  20. package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
  21. package/dist/agent-src/skills/roadmap-writing/SKILL.md +4 -4
  22. package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
  23. package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
  24. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  25. package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-3B72LSZL.js} +1 -1
  26. package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
  27. package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-N7FEST6D.js} +17 -17
  28. package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
  29. package/dist/cli-delegate/cmd_conformance.js +2 -2
  30. package/dist/cli-delegate/cmd_doctor.js +2 -2
  31. package/dist/cli-delegate/cmd_export.js +2 -2
  32. package/dist/cli-delegate/cmd_handoff.js +1 -1
  33. package/dist/cli-delegate/cmd_prune.js +2 -2
  34. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  35. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  36. package/dist/cli-delegate/cmd_sync.js +2 -2
  37. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  38. package/dist/cli-delegate/cmd_validate.js +2 -2
  39. package/dist/discovery/deprecation-report.md +1 -1
  40. package/dist/discovery/discovery-manifest.json +43 -22
  41. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  42. package/dist/discovery/discovery-manifest.summary.md +3 -3
  43. package/dist/discovery/dormancy-report.md +1 -1
  44. package/dist/discovery/orphan-report.md +1 -1
  45. package/dist/discovery/packs.json +6 -5
  46. package/dist/discovery/trust-report.md +2 -2
  47. package/dist/discovery/workspaces.json +5 -4
  48. package/dist/hooks/dispatch.js +936 -346
  49. package/dist/install/install.mjs +12 -4
  50. package/dist/mcp/registry-manifest.json +3 -3
  51. package/dist/mcp/server.json +2 -2
  52. package/dist/mcp/server.mjs +1192 -160
  53. package/dist/router.json +1 -1
  54. package/dist/server/schemas/settings.js +2 -2
  55. package/dist/server/schemas/settings.js.map +1 -1
  56. package/docs/guidelines/design-fidelity-mechanics.md +21 -0
  57. package/docs/guidelines/design-handover-extraction.md +87 -0
  58. package/docs/guidelines/prompt-templates.md +24 -1
  59. package/package.json +1 -1
  60. package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  61. package/src/agent-src/scripts/update_roadmap_progress.ts +17 -2
  62. package/src/config/evaluator-budgets.json +5 -4
  63. package/src/config/gate-coverage.yml +33 -11
  64. package/src/config/gate-violation-baselines.json +9 -3
  65. package/src/config/hook-token-budget.json +8 -1
  66. package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
  67. package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
  68. package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
  69. package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
  70. package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
  71. package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
  72. package/src/scripts/_lib/skill_catalogue.ts +258 -5
  73. package/src/scripts/_lib/tier_budget_routing.ts +31 -331
  74. package/src/scripts/ai_council/clients.ts +21 -21
  75. package/src/scripts/bench_ab_v2_complexity.ts +252 -0
  76. package/src/scripts/bench_ab_v2_run.ts +3 -42
  77. package/src/scripts/bench_ab_v2_safety.ts +296 -0
  78. package/src/scripts/bench_ab_v2_search.ts +343 -0
  79. package/src/scripts/bench_ab_v2_stats.ts +191 -0
  80. package/src/scripts/build_archive_index.ts +383 -0
  81. package/src/scripts/capture_skill_catalogue.ts +107 -2
  82. package/src/scripts/check_source_size_budget.ts +428 -0
  83. package/src/scripts/check_suppression_hygiene.ts +5 -1
  84. package/src/scripts/cost/backfill_rates.mjs +298 -0
  85. package/src/scripts/cost/budget.mjs +12 -32
  86. package/src/scripts/cost_summary.ts +25 -0
  87. package/src/scripts/external_sources_denylist.json +1 -0
  88. package/src/scripts/hook_manifest.yaml +28 -2
  89. package/src/scripts/hooks/concern_registry.ts +2 -0
  90. package/src/scripts/hooks/skill_route_hook.ts +226 -0
  91. package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
  92. package/src/scripts/lint_design_antipattern_parity.ts +40 -6
  93. package/src/scripts/lint_harvest_provenance.ts +35 -1
  94. package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
  95. package/src/scripts/lint_skill_router_head.ts +21 -0
  96. package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
  97. package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
  98. package/src/scripts/mcp_server/tools.ts +87 -0
  99. package/src/scripts/release.ts +5 -5
  100. package/src/scripts/routing_doctor.ts +50 -13
  101. package/src/scripts/schemas/command.schema.json +1 -1
  102. package/src/scripts/schemas/skill.schema.json +18 -1
  103. package/src/scripts/trigger_coverage.ts +224 -3
  104. package/src/server/schemas/settings.ts +2 -2
  105. package/src/config/budget-routing.json +0 -7
@@ -0,0 +1,428 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * check_source_size_budget — a growth ratchet over oversized TypeScript sources.
4
+ *
5
+ * ## The gap
6
+ *
7
+ * Every other size budget in this suite guards prose. Rules carry a char cap
8
+ * (`lint_load_context.cap_for`). Skills carry a token budget class
9
+ * (`lint_token_budget_discipline`). The always-loaded set carries two budgets
10
+ * (`check_always_budget`). The on-demand depth layer carries a per-file char
11
+ * ceiling (`check_depth_budget`). The 1,063 hand-written `.ts` files this
12
+ * package ships carry **none** — which is where the largest artefacts sit:
13
+ * `install.ts` at 5,461 lines, `skill_linter.ts` at 4,742, `council_cli.ts` at
14
+ * 4,058.
15
+ *
16
+ * Fifteen files are over 1,500 lines. Nothing measured that, so nothing stopped
17
+ * it, and nothing would notice a sixteenth.
18
+ *
19
+ * ## Why a ratchet BEFORE a split — the ordering is the decision
20
+ *
21
+ * The obvious move is to split the god-files. Doing that first is how a refactor
22
+ * becomes unreviewable: a 5,461-line file broken into six is a diff nobody can
23
+ * read, with nothing asserting it improved anything. A ratchet freezes the
24
+ * number where it is, which makes every later split a **lowering commit** whose
25
+ * gain is visible in one integer. That ordering is the deliberate call carried
26
+ * from the roadmap this gate closes, not an implementation detail.
27
+ *
28
+ * ## What the ratchet counts, and why it is NOT a file count
29
+ *
30
+ * The metric is **total lines above the ceiling**, summed across the tree:
31
+ *
32
+ * Σ max(0, lines(f) − CEILING) for every .ts under src/
33
+ *
34
+ * A file *count* was the obvious shape and it is the weak one: `install.ts`
35
+ * could grow 5,461 → 9,000 and the count of over-ceiling files would not move,
36
+ * so the gate would stay green while the exact defect it exists for got worse.
37
+ * That objection was raised against the count shape and it is correct. Summing
38
+ * the excess costs the same one integer and closes it:
39
+ *
40
+ * - a listed file growing by one line raises the total → **regressed**, fails;
41
+ * - a new file over the ceiling raises the total → **regressed**, fails;
42
+ * - splitting any god-file lowers the total → **improved**, tighten the ratchet;
43
+ * - the 56-day expiry in `_lib/gate_baseline` fails a total that never drops,
44
+ * so "freeze it and forget it" is not a stable state.
45
+ *
46
+ * That last clause is the one that separates a control from a number that feels
47
+ * like progress. A ceiling with no forcing function would leave these fifteen
48
+ * files at these fifteen sizes indefinitely with the gate green the whole time.
49
+ *
50
+ * ## What the ceiling is — and what it is NOT
51
+ *
52
+ * 1,500 lines is a **growth ratchet, not a quality threshold**, and the same
53
+ * honesty its sibling `check_depth_budget` states applies here: nothing claims a
54
+ * 1,499-line module is comprehensible and a 1,501-line one is not. The number
55
+ * was chosen by measurement of this tree and of the ledger it produces — 15
56
+ * files over 1,500, versus 44 over 1,000 and 75 over 800. A ceiling that lists
57
+ * 75 files is an allowlist, and this repository's own rules call an allowlist
58
+ * past 20 entries evidence that the linter is wrong. 1,500 is the tightest
59
+ * ceiling whose ledger stays signal.
60
+ *
61
+ * Exit codes: 0 = at or under baseline, 1 = the excess grew (or the baseline
62
+ * went stale), 2 = usage.
63
+ */
64
+ import * as fs from 'node:fs';
65
+ import * as os from 'node:os';
66
+ import * as path from 'node:path';
67
+ import { fileURLToPath, pathToFileURL } from 'node:url';
68
+
69
+ import { checkRatchet } from './_lib/gate_baseline.js';
70
+ import { GateLedger } from './_lib/gate_ledger.js';
71
+ import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
72
+ import { reportScanned } from './_lib/scan_scope.js';
73
+
74
+ const SCRIPTS_DIR = path.dirname(fileURLToPath(import.meta.url));
75
+ const REAL_REPO_ROOT = path.dirname(path.dirname(SCRIPTS_DIR));
76
+
77
+ /**
78
+ * The ceiling, in lines.
79
+ *
80
+ * Lines rather than chars — unlike the prose layers, a source file's reviewable
81
+ * unit is the line, and a line count is what every reader, editor and diff
82
+ * already reports.
83
+ */
84
+ export const SOURCE_CEILING_LINES = 1_500;
85
+
86
+ /** The root this gate walks. */
87
+ export const SOURCE_ROOTS = ['src'] as const;
88
+
89
+ export interface SourceFile {
90
+ readonly file: string;
91
+ readonly lines: number;
92
+ }
93
+
94
+ function* walk(dir: string): Generator<string> {
95
+ let entries: fs.Dirent[];
96
+ try {
97
+ entries = fs.readdirSync(dir, { withFileTypes: true });
98
+ } catch {
99
+ return;
100
+ }
101
+ for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
102
+ const full = path.join(dir, e.name);
103
+ if (e.isDirectory()) {
104
+ if (e.name === 'node_modules') continue;
105
+ yield* walk(full);
106
+ } else if (e.isFile() && e.name.endsWith('.ts')) {
107
+ // Same two exclusions this repository's own gate-population
108
+ // classifier makes (`_lib/gate_population.ts`), for the same stated
109
+ // reasons: a type declaration has no runtime behaviour, and a test
110
+ // file is not the reviewable code this ceiling is aimed at. Latent
111
+ // rather than cosmetic — a GENERATED `.d.ts` over the ceiling would
112
+ // otherwise red a ratchet that exists to bound hand-written code.
113
+ if (e.name.endsWith('.d.ts') || e.name.endsWith('.test.ts')) continue;
114
+ yield full;
115
+ }
116
+ }
117
+ }
118
+
119
+ export function collectSourceFiles(repoRoot: string): string[] {
120
+ const out: string[] = [];
121
+ for (const root of SOURCE_ROOTS) {
122
+ for (const f of walk(path.join(repoRoot, root))) {
123
+ out.push(f);
124
+ }
125
+ }
126
+ return [...new Set(out)].sort();
127
+ }
128
+
129
+ /**
130
+ * Count lines the way `wc -l` does — newline characters.
131
+ *
132
+ * Stated rather than left implicit: a file with no trailing newline reads one
133
+ * lower than an editor would show. Every file in this tree ends with one, and
134
+ * pinning the convention to `wc -l` means a contributor can reproduce the
135
+ * gate's number with a command they already know.
136
+ */
137
+ export function countLines(text: string): number {
138
+ let n = 0;
139
+ for (let i = 0; i < text.length; i += 1) {
140
+ if (text[i] === '\n') n += 1;
141
+ }
142
+ return n;
143
+ }
144
+
145
+ export function measure(repoRoot: string, files: readonly string[]): SourceFile[] {
146
+ const out: SourceFile[] = [];
147
+ for (const f of files) {
148
+ // Deliberately NOT wrapped in a try/catch that `continue`s. A file this
149
+ // walker just enumerated and cannot read is a real problem, and a bare
150
+ // `continue` is the silent skip `_lib/gate_ledger` exists to catch: the
151
+ // target is already planned, so swallowing the error only defers the
152
+ // failure into an `UnaccountedTargetsError` naming a file whose read
153
+ // nobody can connect to it. Letting the read throw names the file and
154
+ // the reason. The ledger's skip vocabulary carries no code for
155
+ // "unreadable", and widening that shared union is not this change.
156
+ const text = fs.readFileSync(f, 'utf-8');
157
+ out.push({ file: path.relative(repoRoot, f).split(path.sep).join('/'), lines: countLines(text) });
158
+ }
159
+ return out;
160
+ }
161
+
162
+ /** Lines a single file carries above the ceiling; 0 when it is within budget. */
163
+ export function excessOf(file: SourceFile, ceiling: number = SOURCE_CEILING_LINES): number {
164
+ return Math.max(0, file.lines - ceiling);
165
+ }
166
+
167
+ /** The ratchet value: total lines above the ceiling across the whole tree. */
168
+ export function totalExcess(
169
+ files: readonly SourceFile[],
170
+ ceiling: number = SOURCE_CEILING_LINES,
171
+ ): number {
172
+ let sum = 0;
173
+ for (const f of files) sum += excessOf(f, ceiling);
174
+ return sum;
175
+ }
176
+
177
+ interface Args {
178
+ readonly json: boolean;
179
+ readonly quiet: boolean;
180
+ }
181
+
182
+ function parseArgs(argv: readonly string[]): Args {
183
+ let json = false;
184
+ let quiet = false;
185
+ for (const arg of argv) {
186
+ if (arg === '--json') { json = true; }
187
+ else if (arg === '--quiet') { quiet = true; }
188
+ else if (arg === '-h' || arg === '--help') {
189
+ process.stdout.write('usage: check_source_size_budget [--json] [--quiet]\n');
190
+ process.exit(0);
191
+ } else {
192
+ process.stderr.write(`check_source_size_budget: unrecognized argument: ${arg}\n`);
193
+ process.exit(2);
194
+ }
195
+ }
196
+ return { json, quiet };
197
+ }
198
+
199
+ export function selfTest(): number {
200
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cssb-selftest-'));
201
+ const mkRepo = (bodies: Record<string, number>, baseline?: number): string => {
202
+ const root = fs.mkdtempSync(path.join(tmp, 'repo-'));
203
+ for (const [rel, lines] of Object.entries(bodies)) {
204
+ const p = path.join(root, rel);
205
+ fs.mkdirSync(path.dirname(p), { recursive: true });
206
+ fs.writeFileSync(p, '//\n'.repeat(lines), 'utf-8');
207
+ }
208
+ const cfg = path.join(root, 'src', 'config');
209
+ fs.mkdirSync(cfg, { recursive: true });
210
+ // Absent baseline ⇒ any excess is a violation, which is the
211
+ // discrimination most cases need. A supplied baseline is how the
212
+ // per-file-growth case proves the metric is not a file count.
213
+ const gates =
214
+ baseline === undefined
215
+ ? {}
216
+ : {
217
+ check_source_size_budget: {
218
+ count: baseline,
219
+ landed: new Date().toISOString().slice(0, 10),
220
+ note: 'self-test fixture',
221
+ },
222
+ };
223
+ fs.writeFileSync(
224
+ path.join(cfg, 'gate-violation-baselines.json'),
225
+ JSON.stringify({ gates }),
226
+ 'utf-8',
227
+ );
228
+ return root;
229
+ };
230
+ const run = (root: string): number => {
231
+ process.env['CHECK_SOURCE_SIZE_BUDGET_ROOT'] = root;
232
+ try {
233
+ return runGateCli(
234
+ REAL_REPO_ROOT,
235
+ 'src/scripts/check_source_size_budget.ts',
236
+ ['--quiet'],
237
+ root,
238
+ );
239
+ } finally {
240
+ delete process.env['CHECK_SOURCE_SIZE_BUDGET_ROOT'];
241
+ }
242
+ };
243
+
244
+ try {
245
+ const over = mkRepo({ 'src/big.ts': SOURCE_CEILING_LINES + 1 });
246
+ const under = mkRepo({ 'src/ok.ts': SOURCE_CEILING_LINES - 1 });
247
+ const exact = mkRepo({ 'src/edge.ts': SOURCE_CEILING_LINES });
248
+ const empty = mkRepo({ 'README.md': 10 });
249
+ // The discriminator against a file-count ratchet: ONE over-ceiling file
250
+ // in both trees, so a count would read 1 = 1 and pass. The excess grew
251
+ // by 100 lines, so this metric fails.
252
+ const grown = mkRepo({ 'src/big.ts': SOURCE_CEILING_LINES + 200 }, 100);
253
+ const shrunk = mkRepo({ 'src/big.ts': SOURCE_CEILING_LINES + 50 }, 100);
254
+ return runSelfTest({
255
+ gate: 'check_source_size_budget',
256
+ minCases: 4,
257
+ minRejectCases: 3,
258
+ cases: [
259
+ {
260
+ name: 'a file one line over the ceiling is rejected',
261
+ expect: 'reject',
262
+ run: () => run(over),
263
+ },
264
+ {
265
+ name: 'a file one line under the ceiling passes',
266
+ expect: 'accept',
267
+ run: () => run(under),
268
+ },
269
+ {
270
+ name: 'the ceiling itself is inclusive — exactly at the limit passes',
271
+ expect: 'accept',
272
+ run: () => run(exact),
273
+ },
274
+ {
275
+ name: 'a dead scan root is rejected, not reported as clean',
276
+ expect: 'reject',
277
+ run: () => run(empty),
278
+ },
279
+ {
280
+ name: 'an existing over-ceiling file that GREW is rejected — the metric is excess lines, not a file count',
281
+ expect: 'reject',
282
+ run: () => run(grown),
283
+ },
284
+ {
285
+ name: 'an over-ceiling file that shrank passes and asks for a tighter ratchet',
286
+ expect: 'accept',
287
+ run: () => run(shrunk),
288
+ },
289
+ ],
290
+ });
291
+ } finally {
292
+ fs.rmSync(tmp, { recursive: true, force: true });
293
+ }
294
+ }
295
+
296
+ export function main(argv?: readonly string[]): number {
297
+ const raw = argv ?? process.argv.slice(2);
298
+ if (raw.includes('--self-test')) {
299
+ return selfTest();
300
+ }
301
+ const args = parseArgs(raw);
302
+ // Root derived from this file's own location, so the gate is robust to the
303
+ // cwd a wrapper or hook invokes it from. The env override exists so the
304
+ // self-test can drive the REAL binary against a synthetic tree; deliberately
305
+ // NOT a cwd fallback, because a "fall back to the real repo when the given
306
+ // root looks empty" rule would make the dead-scan-root case silently pass.
307
+ const root = process.env['CHECK_SOURCE_SIZE_BUDGET_ROOT'] ?? REAL_REPO_ROOT;
308
+
309
+ const files = collectSourceFiles(root);
310
+ const ledger = new GateLedger('check_source_size_budget');
311
+ ledger.plan(files.map((f) => path.relative(root, f).split(path.sep).join('/')));
312
+
313
+ const measured = measure(root, files);
314
+ const over = measured
315
+ .filter((m) => excessOf(m) > 0)
316
+ .sort((a, b) => b.lines - a.lines);
317
+ const overSet = new Set(over.map((o) => o.file));
318
+ for (const m of measured) {
319
+ if (overSet.has(m.file)) {
320
+ ledger.fail(
321
+ m.file,
322
+ `${String(m.lines)} lines, ${String(excessOf(m))} over ceiling ${String(SOURCE_CEILING_LINES)}`,
323
+ );
324
+ } else {
325
+ ledger.complete(m.file);
326
+ }
327
+ }
328
+ const tally = ledger.finalize();
329
+ const excess = totalExcess(measured);
330
+
331
+ // `--json` must emit JSON and nothing else, so a caller can pipe it to a
332
+ // JSON reader. Both of the calls below normally append prose to the same
333
+ // stdout, which would make the document unparseable — so under `--json` the
334
+ // scan report is routed to a no-op writer (the `assertScanned` dead-root
335
+ // check inside it still runs, it just does not print) and the ratchet
336
+ // verdict is carried as a FIELD rather than as a trailing paragraph.
337
+ if (args.json) {
338
+ reportScanned(
339
+ {
340
+ gate: 'check_source_size_budget',
341
+ scanned: measured.length,
342
+ units: 'source file(s)',
343
+ roots: [...SOURCE_ROOTS],
344
+ },
345
+ () => true,
346
+ );
347
+ const verdict = checkRatchet({
348
+ gate: 'check_source_size_budget',
349
+ actual: excess,
350
+ repoRoot: root,
351
+ });
352
+ process.stdout.write(
353
+ `${JSON.stringify(
354
+ {
355
+ version: 1,
356
+ ceiling: SOURCE_CEILING_LINES,
357
+ excess_lines: excess,
358
+ over: over.map((o) => ({ ...o, excess: excessOf(o) })),
359
+ scanned: measured.length,
360
+ ledger: tally,
361
+ verdict: {
362
+ status: verdict.status,
363
+ ok: verdict.ok,
364
+ baseline: verdict.baseline,
365
+ message: verdict.message,
366
+ },
367
+ },
368
+ null,
369
+ 2,
370
+ )}\n`,
371
+ );
372
+ return verdict.ok ? 0 : 1;
373
+ }
374
+
375
+ if (!args.quiet && over.length > 0) {
376
+ process.stdout.write(
377
+ `\nOver the ${String(SOURCE_CEILING_LINES)}-line source ceiling ` +
378
+ '(a growth ratchet, NOT a measured quality threshold):\n',
379
+ );
380
+ for (const o of over) {
381
+ process.stdout.write(
382
+ ` ${String(o.lines).padStart(6, ' ')} (+${String(excessOf(o)).padStart(5, ' ')}) ${o.file}\n`,
383
+ );
384
+ }
385
+ process.stdout.write(` ${'—'.repeat(6)}\n total excess: ${String(excess)} line(s)\n`);
386
+ }
387
+
388
+ reportScanned({
389
+ gate: 'check_source_size_budget',
390
+ scanned: measured.length,
391
+ units: 'source file(s)',
392
+ roots: [...SOURCE_ROOTS],
393
+ });
394
+
395
+ const verdict = checkRatchet({
396
+ gate: 'check_source_size_budget',
397
+ actual: excess,
398
+ repoRoot: root,
399
+ });
400
+ if (verdict.ok) {
401
+ process.stdout.write(`\n⚠️ ${verdict.message}\n`);
402
+ return 0;
403
+ }
404
+ process.stderr.write(`\n❌ ${verdict.message}\n`);
405
+ return 1;
406
+ }
407
+
408
+ const _HERE = fileURLToPath(import.meta.url);
409
+ function _isCliEntry(): boolean {
410
+ if (process.argv[1] === undefined) {
411
+ return false;
412
+ }
413
+ if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
414
+ return true;
415
+ }
416
+ try {
417
+ return (
418
+ fs.realpathSync(fileURLToPath(import.meta.url)) ===
419
+ fs.realpathSync(path.resolve(process.argv[1]))
420
+ );
421
+ } catch {
422
+ return false;
423
+ }
424
+ }
425
+
426
+ if (_isCliEntry() || process.argv[1] === _HERE) {
427
+ process.exit(main());
428
+ }
@@ -238,7 +238,11 @@ export const SUPPRESSION_INVENTORY: readonly SuppressionSpec[] = [
238
238
  listKey: 'entries',
239
239
  tier: 'object',
240
240
  keyFields: ['slug'],
241
- newInThisChange: true,
241
+ // `newInThisChange` removed: the baseline now resolves at the base ref,
242
+ // so the bootstrap window this flag exists for has closed. That is the
243
+ // self-closing contract below working as designed — the gate reported
244
+ // the stale flag rather than leaving it to silently accept a future
245
+ // mistyped path as a fresh baseline.
242
246
  what: 'review prompts whose recorded prompt_hash does not re-derive',
243
247
  },
244
248
  ];