vigiles 37.0.0 → 38.1.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.
@@ -21,6 +21,7 @@ import type { PluginScore } from "./score-core.js";
21
21
  import type { RuleInventoryItem } from "./rule-inventory.js";
22
22
  import type { RuleRouting } from "./rule-routing.js";
23
23
  import type { EvidenceCounts } from "./coverage-evidence.js";
24
+ import type { DeprecatedKeyFilesFinding } from "./deprecated-key-files.js";
24
25
  /**
25
26
  * The current schema version. Bump only on a BREAKING change to the shape.
26
27
  * v2 (2026-07-15): the rule-map `mechanism` enum value for an unrouted rule
@@ -131,6 +132,13 @@ export interface AuditReport {
131
132
  * version unchanged.
132
133
  */
133
134
  readonly brokenReferences?: readonly string[];
135
+ /**
136
+ * Instruction specs still using the deprecated `keyFiles` (the
137
+ * `deprecated-key-files` rule's findings: path, entry count, compiled
138
+ * characters). Advisory — never scored. Present only when at least one spec
139
+ * uses the field. Additive/optional — schema version unchanged.
140
+ */
141
+ readonly deprecatedKeyFiles?: readonly DeprecatedKeyFilesFinding[];
134
142
  /**
135
143
  * The adoption preview — "what would vigiles catch in your repo?" Present only
136
144
  * when the model-gated tier ran (behind consent); a deterministic read omits it.
@@ -74,6 +74,17 @@ function inventoryOf(report) {
74
74
  function outputStylesField(n) {
75
75
  return n === undefined || n === 0 ? {} : { outputStyles: n };
76
76
  }
77
+ /** The scan's concrete finding lists, each present only when non-empty. */
78
+ function scanFindingLists(report) {
79
+ return {
80
+ ...(report.danglingRefs.length
81
+ ? { brokenReferences: report.danglingRefs }
82
+ : {}),
83
+ ...(report.deprecatedKeyFiles
84
+ ? { deprecatedKeyFiles: report.deprecatedKeyFiles }
85
+ : {}),
86
+ };
87
+ }
77
88
  /**
78
89
  * Assemble the versioned {@link AuditReport} from a scan report — pure, no clock.
79
90
  * The CLI attaches `meta.generatedAt` when it writes the JSON artifact; the
@@ -97,9 +108,7 @@ function buildAuditReport(report, opts) {
97
108
  verdict,
98
109
  recommendations,
99
110
  inventory: inventoryOf(report),
100
- ...(report.danglingRefs.length
101
- ? { brokenReferences: report.danglingRefs }
102
- : {}),
111
+ ...scanFindingLists(report),
103
112
  ...(adoptable ? { adoptable } : {}),
104
113
  ...(opts.observations ? { observations: opts.observations } : {}),
105
114
  ...(opts.rulesInventory && opts.rulesInventory.length
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * vigiles CLI — verify your agent harness is real, and prove it works.
3
3
  *
4
+ * This is the VERB barrel: what each verb does is defined here and nowhere
5
+ * else. `src/cli.ts` loads it lazily for everything that is not a hook
6
+ * decision (those go to `src/hook-runtime.ts`), so a top-level import added
7
+ * here is paid by every verb but never by a gated tool call.
8
+ *
4
9
  * The verbs and their one-liners live in ONE place, `COMMAND_HELP` + `HELP_GROUPS`
5
10
  * near the bottom of this file, and `--help` prints from that table. A second list
6
11
  * here would be a copy that rots — this docblock WAS that copy: it named four
@@ -24,6 +29,8 @@ interface LintReport {
24
29
  frontmatterRules: number;
25
30
  specRefIssues: number;
26
31
  specRefErrors: number;
32
+ /** `compile-budget` findings at "error" (reported by `lint` only then). */
33
+ compileBudgetErrors: number;
27
34
  duplicatePairs: number;
28
35
  /** Severity of `duplicate-instructions` — carried so the exit code can tier it. */
29
36
  duplicateSeverity: RuleSeverity;
@@ -79,6 +86,8 @@ interface LintReport {
79
86
  hookMatcherErrors: number;
80
87
  instructionWeightIssues: number;
81
88
  instructionWeightErrors: number;
89
+ deprecatedKeyFilesIssues: number;
90
+ deprecatedKeyFilesErrors: number;
82
91
  docRefErrors: number;
83
92
  symbolRefErrors: number;
84
93
  mcpRefErrors: number;
package/dist/cli-main.js CHANGED
@@ -2,6 +2,11 @@
2
2
  /**
3
3
  * vigiles CLI — verify your agent harness is real, and prove it works.
4
4
  *
5
+ * This is the VERB barrel: what each verb does is defined here and nowhere
6
+ * else. `src/cli.ts` loads it lazily for everything that is not a hook
7
+ * decision (those go to `src/hook-runtime.ts`), so a top-level import added
8
+ * here is paid by every verb but never by a gated tool call.
9
+ *
5
10
  * The verbs and their one-liners live in ONE place, `COMMAND_HELP` + `HELP_GROUPS`
6
11
  * near the bottom of this file, and `--help` prints from that table. A second list
7
12
  * here would be a copy that rots — this docblock WAS that copy: it named four
@@ -22,6 +27,7 @@ const repo_path_js_1 = require("./core/repo-path.js");
22
27
  const node_child_process_1 = require("node:child_process");
23
28
  const glob_1 = require("glob");
24
29
  const exclude_js_1 = require("./exclude.js");
30
+ const glob_ignore_js_1 = require("./core/glob-ignore.js");
25
31
  const generate_types_js_1 = require("./core/generate-types.js");
26
32
  const generate_harness_js_1 = require("./core/generate-harness.js");
27
33
  const capability_diff_js_1 = require("./core/capability-diff.js");
@@ -96,6 +102,7 @@ const adopt_js_1 = require("./core/adopt.js");
96
102
  const coverage_js_1 = require("./core/coverage.js");
97
103
  const orphans_js_1 = require("./core/orphans.js");
98
104
  const doc_refs_js_1 = require("./core/doc-refs.js");
105
+ const deprecated_key_files_js_1 = require("./deprecated-key-files.js");
99
106
  const hook_runtime_js_1 = require("./hook-runtime.js");
100
107
  // ---------------------------------------------------------------------------
101
108
  // Constants
@@ -126,6 +133,32 @@ function findSpecs(excludes, pattern) {
126
133
  cwd: process.cwd(),
127
134
  });
128
135
  }
136
+ /**
137
+ * The `deprecated-key-files` detector's I/O: every instruction spec under
138
+ * `root` (the glob `compile` uses), read from disk. Lint and audit both call
139
+ * this, so the two cannot disagree. `node_modules` is never read — someone
140
+ * else's spec is not this repository's to migrate.
141
+ */
142
+ function specsUsingKeyFiles(root, ignore) {
143
+ return (0, deprecated_key_files_js_1.findDeprecatedKeyFiles)({
144
+ specPaths: (0, glob_1.globSync)("**/*.md.spec.ts", {
145
+ cwd: root,
146
+ dot: true,
147
+ nodir: true,
148
+ posix: true,
149
+ ignore: (0, glob_ignore_js_1.withIgnored)(["**/node_modules/**"], ignore),
150
+ }),
151
+ read: (path) => (0, node_fs_1.readFileSync)((0, node_path_1.join)(root, path), "utf-8"),
152
+ });
153
+ }
154
+ /** `audit --single` names ONE harness: more than one directory is a contradiction (exit 2). */
155
+ function refuseSingleOverMany(single, dirs) {
156
+ if (!single || dirs.length <= 1)
157
+ return;
158
+ console.error(`--single audits ONE directory as one harness, but ${String(dirs.length)} were given. ` +
159
+ `Drop --single for a leaderboard, or pass a single directory.`);
160
+ process.exit(2);
161
+ }
129
162
  /**
130
163
  * Why the last `loadSpec()` returned null.
131
164
  *
@@ -353,6 +386,7 @@ async function compileClaudeToFile(spec, specPath, config, dialect) {
353
386
  maxSectionLines: config.maxSectionLines,
354
387
  catalogOnly: config.catalogOnly,
355
388
  linters: config.linters,
389
+ ...compileBudgetOptions(config),
356
390
  });
357
391
  const primaryOutput = specPath.replace(/\.spec\.ts$/, "");
358
392
  // Budget findings print BEFORE the pass/fail line and never change the exit
@@ -985,6 +1019,7 @@ function lintExitCode(report) {
985
1019
  report.hookBlockErrors > 0 ||
986
1020
  report.hookMatcherErrors > 0 ||
987
1021
  report.instructionWeightErrors > 0 ||
1022
+ report.deprecatedKeyFilesErrors > 0 ||
988
1023
  report.symbolRefErrors > 0 ||
989
1024
  report.mcpRefErrors > 0 ||
990
1025
  // `doc-refs` is opt-in and this counter is only non-zero when the user set
@@ -992,7 +1027,8 @@ function lintExitCode(report) {
992
1027
  // error — it used to sit at exit 1 because it fired unasked and could not be
993
1028
  // turned off.
994
1029
  report.docRefErrors > 0 ||
995
- report.specRefErrors > 0)
1030
+ report.specRefErrors > 0 ||
1031
+ report.compileBudgetErrors > 0)
996
1032
  return 2;
997
1033
  // Tierable now: a `warn` orphan/duplicate finding is reported and does NOT
998
1034
  // change the exit code. Both used to feed the exit directly, which is what
@@ -1363,9 +1399,13 @@ function overBundles(fn, run, bundles) {
1363
1399
  */
1364
1400
  async function checkSpecRefs(excludes, config, silent, dialect) {
1365
1401
  const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["spec-refs"]) ?? "error";
1366
- if (!sev)
1367
- return { issues: 0, errors: 0 };
1368
- const found = [];
1402
+ // `compile-budget` is reported here only at "error": at its default it is a
1403
+ // compile-time warning, and `lint` printed nothing about size before the rule
1404
+ // had a name — the default output does not change.
1405
+ const budgetGate = (0, types_js_1.ruleSeverity)(config?.rules?.["compile-budget"]) === "error";
1406
+ if (!sev && !budgetGate)
1407
+ return { issues: 0, errors: 0, budgetErrors: 0 };
1408
+ const derived = [];
1369
1409
  let knownAgents;
1370
1410
  for (const specPath of findSpecs(excludes)) {
1371
1411
  const target = specPath.replace(/\.spec\.ts$/, "");
@@ -1379,9 +1419,11 @@ async function checkSpecRefs(excludes, config, silent, dialect) {
1379
1419
  // collected lazily so a repo with no railway spec never pays for the walk.
1380
1420
  if (spec._specType === "railway")
1381
1421
  knownAgents ??= await collectAgentNames(excludes);
1382
- const errors = await specCompileErrors(spec, specPath, dialect, config, knownAgents ?? []);
1383
- for (const e of errors)
1384
- found.push(`${target}: ${e.message} (from ${specPath})`);
1422
+ derived.push({
1423
+ target,
1424
+ specPath,
1425
+ findings: await specCompileErrors(spec, specPath, dialect, config, knownAgents ?? []),
1426
+ });
1385
1427
  }
1386
1428
  catch {
1387
1429
  // A spec that will not load is `compile`'s finding, not this one's —
@@ -1389,6 +1431,9 @@ async function checkSpecRefs(excludes, config, silent, dialect) {
1389
1431
  continue;
1390
1432
  }
1391
1433
  }
1434
+ const messages = (pick) => derived.flatMap((d) => pick(d.findings).map((e) => `${d.target}: ${e.message} (from ${d.specPath})`));
1435
+ const found = sev ? messages((f) => f.errors) : [];
1436
+ const overBudget = messages((f) => f.budget);
1392
1437
  if (found.length > 0 && !silent) {
1393
1438
  console.log("\nSpec reference check:\n");
1394
1439
  for (const msg of found) {
@@ -1396,7 +1441,18 @@ async function checkSpecRefs(excludes, config, silent, dialect) {
1396
1441
  ghAnnotate(sev === "error" ? "error" : "warning", msg);
1397
1442
  }
1398
1443
  }
1399
- return { issues: found.length, errors: sev === "error" ? found.length : 0 };
1444
+ if (overBudget.length > 0 && !silent) {
1445
+ console.log("\nCompile budget check (compile-budget):\n");
1446
+ overBudget.forEach((msg) => {
1447
+ console.log(` ✗ ${msg}`);
1448
+ ghAnnotate("error", `compile-budget: ${msg}`);
1449
+ });
1450
+ }
1451
+ return {
1452
+ issues: found.length,
1453
+ errors: sev === "error" ? found.length : 0,
1454
+ budgetErrors: overBudget.length,
1455
+ };
1400
1456
  }
1401
1457
  /**
1402
1458
  * The ONE spec-type dispatcher, in the only shape both directions can share:
@@ -1420,8 +1476,8 @@ async function checkSpecRefs(excludes, config, silent, dialect) {
1420
1476
  async function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
1421
1477
  const basePath = process.cwd();
1422
1478
  switch (spec._specType) {
1423
- case "claude":
1424
- return (await (0, compile_js_1.compileClaude)(spec, {
1479
+ case "claude": {
1480
+ const r = await (0, compile_js_1.compileClaude)(spec, {
1425
1481
  basePath,
1426
1482
  specFile: specPath,
1427
1483
  dialect,
@@ -1430,19 +1486,52 @@ async function specCompileErrors(spec, specPath, dialect, config, knownAgents) {
1430
1486
  maxSectionLines: config?.maxSectionLines,
1431
1487
  catalogOnly: config?.catalogOnly,
1432
1488
  linters: config?.linters,
1433
- })).errors;
1489
+ ...compileBudgetOptions(config),
1490
+ });
1491
+ // A size finding is `compile-budget`'s, not a dead reference: split by
1492
+ // IDENTITY against the compiler's own list, never by the type's name.
1493
+ return {
1494
+ errors: r.errors.filter((e) => !r.budgetFindings.includes(e)),
1495
+ budget: r.errors.filter((e) => r.budgetFindings.includes(e)),
1496
+ };
1497
+ }
1434
1498
  case "skill":
1435
- return (await (0, compile_js_1.compileSkill)(spec, { basePath, specFile: specPath, dialect })).errors;
1499
+ return {
1500
+ errors: (await (0, compile_js_1.compileSkill)(spec, { basePath, specFile: specPath, dialect })).errors,
1501
+ budget: [],
1502
+ };
1436
1503
  case "agent":
1437
- return (await (0, compile_js_1.compileAgent)(spec, { basePath, specFile: specPath, dialect })).errors;
1504
+ return {
1505
+ errors: (await (0, compile_js_1.compileAgent)(spec, { basePath, specFile: specPath, dialect })).errors,
1506
+ budget: [],
1507
+ };
1438
1508
  case "railway":
1439
- return (0, compile_js_1.compileRailway)(spec, { specFile: specPath, knownAgents }).errors;
1509
+ return {
1510
+ errors: (0, compile_js_1.compileRailway)(spec, { specFile: specPath, knownAgents })
1511
+ .errors,
1512
+ budget: [],
1513
+ };
1440
1514
  default:
1441
1515
  // A `pipeline` spec compiles through its underlying railway, so it has no
1442
1516
  // artifact of its own to re-derive refs for.
1443
- return [];
1517
+ return { errors: [], budget: [] };
1444
1518
  }
1445
1519
  }
1520
+ /**
1521
+ * The `compile-budget` rule, as `compileClaude` options: its severity and any
1522
+ * thresholds the config set. The ONE translation, so `compile` (which writes)
1523
+ * and `lint` (which re-derives) can never measure against different numbers.
1524
+ */
1525
+ function compileBudgetOptions(config) {
1526
+ const rule = config?.rules?.["compile-budget"];
1527
+ // No config at all (a programmatic caller, a missing file) is the default.
1528
+ if (rule === undefined)
1529
+ return { budgetSeverity: "warn" };
1530
+ return {
1531
+ budgetSeverity: (0, types_js_1.ruleSeverity)(rule),
1532
+ ...(0, types_js_1.ruleOptions)(rule),
1533
+ };
1534
+ }
1446
1535
  /**
1447
1536
  * Write a compiled artifact. Accepts ONLY a {@link StampedMarkdown}, so a body
1448
1537
  * that failed to compile cannot reach the disk — there is no stamp to pass.
@@ -1698,6 +1787,17 @@ async function runLint(restArgs, flags, excludes, config) {
1698
1787
  ghAnnotate(level, message);
1699
1788
  },
1700
1789
  });
1790
+ // 7w. Deprecated-key-files — an instruction spec still using `keyFiles`. Once
1791
+ // over the repository, like the ratchet: a nested bundle's spec would
1792
+ // otherwise be reported again by every bundle that encloses it.
1793
+ const deprecatedKeyFiles = (0, deprecated_key_files_js_1.checkDeprecatedKeyFiles)({
1794
+ severity: (0, types_js_1.ruleSeverity)(config?.rules?.["deprecated-key-files"]),
1795
+ find: () => specsUsingKeyFiles(process.cwd(), excludes.globIgnore),
1796
+ silent,
1797
+ annotate: (level, message, path) => {
1798
+ ghAnnotate(level, message, frame.repo((0, node_path_1.resolve)(process.cwd(), path)));
1799
+ },
1800
+ });
1701
1801
  // 8. Validate vigiles builder calls inside markdown code blocks — the
1702
1802
  // `doc-refs` rule, DEFAULT OFF. Illustrative blocks opt out via
1703
1803
  // `<!-- vigiles:ignore -->` (single block) or `<!-- vigiles:ignore-file -->`
@@ -1775,6 +1875,7 @@ async function runLint(restArgs, flags, excludes, config) {
1775
1875
  frontmatterRules,
1776
1876
  specRefIssues: specRefs.issues,
1777
1877
  specRefErrors: specRefs.errors,
1878
+ compileBudgetErrors: specRefs.budgetErrors,
1778
1879
  duplicatePairs: dups.pairCount,
1779
1880
  duplicateSeverity: (0, types_js_1.ruleSeverity)(config?.rules?.["duplicate-rules"]) ?? "warn",
1780
1881
  coverageEnabled: coverage.enabled,
@@ -1828,6 +1929,8 @@ async function runLint(restArgs, flags, excludes, config) {
1828
1929
  hookMatcherErrors: hookMatcher.errors,
1829
1930
  instructionWeightIssues: instructionWeight.issues,
1830
1931
  instructionWeightErrors: instructionWeight.errors,
1932
+ deprecatedKeyFilesIssues: deprecatedKeyFiles.issues,
1933
+ deprecatedKeyFilesErrors: deprecatedKeyFiles.errors,
1831
1934
  // Only the "error" tier gates. At "warn" the findings are printed and
1832
1935
  // annotated, and the exit code is untouched — same contract as every other
1833
1936
  // opt-in rule here.
@@ -2460,6 +2563,9 @@ export default instructionFile({${targetLine}
2460
2563
  // Prose sections become ## headings in the compiled output.
2461
2564
  // Do not add # or ## headers inside sections.
2462
2565
  // positioning: "What this project does and why.",
2566
+ // A file a decision depends on: name it where the decision is, with
2567
+ // file() inside a prose section — verified to exist at compile time. No
2568
+ // file map: the agent finds files itself, and a map loads on every request.
2463
2569
 
2464
2570
  // This section is included in the compiled output to help agents
2465
2571
  // understand how to work with specs. Remove it once your team is familiar.
@@ -2472,11 +2578,6 @@ export default instructionFile({${targetLine}
2472
2578
  // "npm test": "Run all tests",
2473
2579
  },
2474
2580
 
2475
- keyFiles: {
2476
- // File paths are verified to exist at compile time.
2477
- // "src/index.ts": "Main entry point",
2478
- },
2479
-
2480
2581
  rules: {
2481
2582
  // enforce() — backed by a linter rule, verified to exist AND be enabled:
2482
2583
  // "no-console": enforce("eslint/no-console", "Use structured logger."),
@@ -7242,11 +7343,7 @@ async function main() {
7242
7343
  // contradiction. Refuse it (exit 2 = "could not do what you asked") rather
7243
7344
  // than auditing the first and dropping the rest — silently honouring half
7244
7345
  // an argument list is the same defect class as the ignored `--out` above.
7245
- if (single && dirs.length > 1) {
7246
- console.error(`--single audits ONE directory as one harness, but ${String(dirs.length)} were given. ` +
7247
- `Drop --single for a leaderboard, or pass a single directory.`);
7248
- process.exit(2);
7249
- }
7346
+ refuseSingleOverMany(single, dirs);
7250
7347
  const targets = !single && market && market.onDisk.length > 0
7251
7348
  ? [...market.onDisk]
7252
7349
  : dirs;
@@ -7336,6 +7433,7 @@ async function main() {
7336
7433
  // (#281): cwd, unless the audited dir is somebody else's repository.
7337
7434
  const frame = (0, frame_js_1.frameFor)(process.cwd(), root);
7338
7435
  const report = (0, scan_js_1.scanPlugin)(targets[0], adapter.layout, adapter.dialect, {
7436
+ deprecatedKeyFiles: specsUsingKeyFiles(root, excludes.globIgnore),
7339
7437
  sharedDirs: config.sharedDirs,
7340
7438
  sharedDirsRoot: frame.root,
7341
7439
  // `.vigilesrc.json#exclude` reaches surface DISCOVERY, not just the
@@ -15,10 +15,6 @@ exports.default = (0, spec_js_1.instructionFile)({
15
15
  sections: {
16
16
  scope: `Working in \`src/core/\`? This is the harness-AGNOSTIC domain (spec, compile, linters, the lint/audit detectors). The root \`CLAUDE.md\` holds the full positioning + rule set — read it first. Two invariants live closest to this code: the core must not import an adapter (\`core ⊄ adapter\`, eslint-enforced) and must not hard-code a Claude Code literal (read it from the injected layout/dialect). This file adds the rule for ADDING or CHANGING a detector.`,
17
17
  },
18
- keyFiles: {
19
- "src/core/rule-meta.ts": "The RuleMeta registry — every rule's decidability bucket + severity + detector, the single source the detector-meta rule enforces.",
20
- "src/core/types.ts": "RulesConfig — the rule-name keys the registry is keyed on.",
21
- },
22
18
  rules: {
23
19
  "detector-meta": (0, spec_js_1.guidance)("A deterministic DETECTOR here is one half of a RULE — and a rule is not done until it is DECLARED. Three things move together (sibling of one-detector-no-drift + rules-docs-in-sync): (1) the pure detector function (shared by `lint` AND `audit`, never reimplemented per surface; read the layout/dialect, never a CC literal); (2) its entry in `src/core/rule-meta.ts` — the `Record<RuleName, RuleMeta>` won't typecheck without it — declaring its DECIDABILITY BUCKET (structural-closed = a type could prevent it / external-decidable = needs the world, error-capable / heuristic-behavioral = warn-or-measure-only), surface, defaultSeverity, the detector name, and any upstreamPrevention; (3) its `docs/rules/<name>.md` (the coverage test binds the registry to the docs by an EXACT set match, so a missing meta or doc fails CI). The bucket is the CEILING, not a preference — a heuristic proxy may NEVER default to `error` (it cries wolf); a structural/external fact MAY, once proven FP-safe. Before writing a new detector, CLASSIFY the defect into a bucket — that decides whether it can ever gate. The full model + the prose behind the buckets is the root `lint-rule-calibration` rule and `research/enforcement-model.md`."),
24
20
  },
@@ -56,7 +56,7 @@ export declare function verifyHash(content: string): {
56
56
  */
57
57
  /** @internal */ export declare function estimateTokens(text: string): number;
58
58
  export interface CompileError {
59
- type: "stale-file" | "stale-command" | "stale-ref" | "invalid-rule" | "budget-exceeded" | "section-too-long" | "section-has-header" | "reserved-section-key" | "spec-name-mismatch" | "unknown-tool" | "invalid-railway" | "purity-violation" | "output-without-fork" | "effect-in-skill" | "inline-code-too-long" | "entry-too-long" | "section-too-large";
59
+ type: "stale-file" | "stale-command" | "stale-ref" | "invalid-rule" | "budget-exceeded" | "section-too-long" | "section-has-header" | "reserved-section-key" | "spec-name-mismatch" | "unknown-tool" | "invalid-railway" | "purity-violation" | "output-without-fork" | "effect-in-skill" | "inline-code-too-long" | "entry-too-long" | "section-too-large" | "rules-too-large";
60
60
  message: string;
61
61
  path?: string;
62
62
  }
@@ -70,12 +70,21 @@ export interface CompileClaudeResult {
70
70
  markdown: string;
71
71
  errors: CompileError[];
72
72
  /**
73
- * Budget findings — never errors. An over-long `keyFiles` description or a
74
- * bloated section does not break the harness, it makes every request more
75
- * expensive, so it must not stop a faithful adoption from compiling. Same
76
- * channel discipline as `checkInlineCode`.
73
+ * Budget findings at the rule's DEFAULT severity (`compile-budget: "warn"`).
74
+ * An over-long `keyFiles` description or a bloated section does not break
75
+ * the harness, it makes every request more expensive, so by default it must
76
+ * not stop a faithful adoption from compiling. Same channel discipline as
77
+ * `checkInlineCode`. Empty when the rule is at `"error"` (the findings are in
78
+ * `errors` then) or off.
77
79
  */
78
80
  warnings: CompileError[];
81
+ /**
82
+ * Every budget finding, whatever the severity — the SAME objects that were
83
+ * placed in `warnings` or `errors`. `lint` re-derives a spec's references
84
+ * through this compiler and must not report a size finding as a dead
85
+ * reference, so it tells the two apart by identity here, never by type name.
86
+ */
87
+ budgetFindings: CompileError[];
79
88
  linterResults: LinterCheckResult[];
80
89
  /** Estimated token count of compiled output (~4 chars/token). */
81
90
  tokens: number;
@@ -83,10 +92,21 @@ export interface CompileClaudeResult {
83
92
  targets: string[];
84
93
  }
85
94
  export interface CompileClaudeOptions {
86
- /** Per-entry `keyFiles`/`commands` character budget (0 disables). */
95
+ /**
96
+ * The `compile-budget` rule's severity. `"warn"` (the default) reports the
97
+ * per-entry and per-section budgets as warnings and evaluates the Rules total
98
+ * only when a threshold for it is passed; `"error"` (what
99
+ * `vigiles:recommended` sets) evaluates every budget, with defaults, as an
100
+ * error; `false` reports none (an explicit `maxTokens` still fails, as it
101
+ * always has — a separate check).
102
+ */
103
+ budgetSeverity?: "warn" | "error" | false;
104
+ /** Per-entry `commands` character budget (0 disables). */
87
105
  maxEntryChars?: number;
88
106
  /** Per-section character budget, beside the line budget (0 disables). */
89
107
  maxSectionChars?: number;
108
+ /** Character budget for the whole rendered Rules section (0 disables). */
109
+ maxRulesChars?: number;
90
110
  basePath?: string;
91
111
  specFile?: string;
92
112
  /** Injected harness dialect; its instructionTargets[0] is the default target. */
@@ -136,6 +156,16 @@ export declare const DEFAULT_MAX_ENTRY_CHARS = 200;
136
156
  * cost — characters do, because that is what the harness loads.
137
157
  */
138
158
  export declare const DEFAULT_MAX_SECTION_CHARS = 15000;
159
+ /**
160
+ * The Rules section, as one budget. Under `compile-budget: "error"` only.
161
+ *
162
+ * WHY THE SECTION AND NOT EACH RULE. A per-rule length cap was built and
163
+ * removed within an hour: it opened on 40 of 53 rules, because a rule's length
164
+ * is not where the cost is — the SUM is, and it is loaded on every request.
165
+ * 20,000 characters is half of Claude Code's 40,000-character instruction
166
+ * budget, which leaves the other half for everything else the file carries.
167
+ */
168
+ export declare const DEFAULT_MAX_RULES_CHARS = 20000;
139
169
  /**
140
170
  * Compile a ClaudeSpec into markdown.
141
171
  *