vigiles 38.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
@@ -81,6 +86,8 @@ interface LintReport {
81
86
  hookMatcherErrors: number;
82
87
  instructionWeightIssues: number;
83
88
  instructionWeightErrors: number;
89
+ deprecatedKeyFilesIssues: number;
90
+ deprecatedKeyFilesErrors: number;
84
91
  docRefErrors: number;
85
92
  symbolRefErrors: number;
86
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
  *
@@ -986,6 +1019,7 @@ function lintExitCode(report) {
986
1019
  report.hookBlockErrors > 0 ||
987
1020
  report.hookMatcherErrors > 0 ||
988
1021
  report.instructionWeightErrors > 0 ||
1022
+ report.deprecatedKeyFilesErrors > 0 ||
989
1023
  report.symbolRefErrors > 0 ||
990
1024
  report.mcpRefErrors > 0 ||
991
1025
  // `doc-refs` is opt-in and this counter is only non-zero when the user set
@@ -1753,6 +1787,17 @@ async function runLint(restArgs, flags, excludes, config) {
1753
1787
  ghAnnotate(level, message);
1754
1788
  },
1755
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
+ });
1756
1801
  // 8. Validate vigiles builder calls inside markdown code blocks — the
1757
1802
  // `doc-refs` rule, DEFAULT OFF. Illustrative blocks opt out via
1758
1803
  // `<!-- vigiles:ignore -->` (single block) or `<!-- vigiles:ignore-file -->`
@@ -1884,6 +1929,8 @@ async function runLint(restArgs, flags, excludes, config) {
1884
1929
  hookMatcherErrors: hookMatcher.errors,
1885
1930
  instructionWeightIssues: instructionWeight.issues,
1886
1931
  instructionWeightErrors: instructionWeight.errors,
1932
+ deprecatedKeyFilesIssues: deprecatedKeyFiles.issues,
1933
+ deprecatedKeyFilesErrors: deprecatedKeyFiles.errors,
1887
1934
  // Only the "error" tier gates. At "warn" the findings are printed and
1888
1935
  // annotated, and the exit code is untouched — same contract as every other
1889
1936
  // opt-in rule here.
@@ -2516,6 +2563,9 @@ export default instructionFile({${targetLine}
2516
2563
  // Prose sections become ## headings in the compiled output.
2517
2564
  // Do not add # or ## headers inside sections.
2518
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.
2519
2569
 
2520
2570
  // This section is included in the compiled output to help agents
2521
2571
  // understand how to work with specs. Remove it once your team is familiar.
@@ -2528,11 +2578,6 @@ export default instructionFile({${targetLine}
2528
2578
  // "npm test": "Run all tests",
2529
2579
  },
2530
2580
 
2531
- keyFiles: {
2532
- // File paths are verified to exist at compile time.
2533
- // "src/index.ts": "Main entry point",
2534
- },
2535
-
2536
2581
  rules: {
2537
2582
  // enforce() — backed by a linter rule, verified to exist AND be enabled:
2538
2583
  // "no-console": enforce("eslint/no-console", "Use structured logger."),
@@ -7298,11 +7343,7 @@ async function main() {
7298
7343
  // contradiction. Refuse it (exit 2 = "could not do what you asked") rather
7299
7344
  // than auditing the first and dropping the rest — silently honouring half
7300
7345
  // an argument list is the same defect class as the ignored `--out` above.
7301
- if (single && dirs.length > 1) {
7302
- console.error(`--single audits ONE directory as one harness, but ${String(dirs.length)} were given. ` +
7303
- `Drop --single for a leaderboard, or pass a single directory.`);
7304
- process.exit(2);
7305
- }
7346
+ refuseSingleOverMany(single, dirs);
7306
7347
  const targets = !single && market && market.onDisk.length > 0
7307
7348
  ? [...market.onDisk]
7308
7349
  : dirs;
@@ -7392,6 +7433,7 @@ async function main() {
7392
7433
  // (#281): cwd, unless the audited dir is somebody else's repository.
7393
7434
  const frame = (0, frame_js_1.frameFor)(process.cwd(), root);
7394
7435
  const report = (0, scan_js_1.scanPlugin)(targets[0], adapter.layout, adapter.dialect, {
7436
+ deprecatedKeyFiles: specsUsingKeyFiles(root, excludes.globIgnore),
7395
7437
  sharedDirs: config.sharedDirs,
7396
7438
  sharedDirsRoot: frame.root,
7397
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
  },
@@ -35,6 +35,7 @@ const integrity_js_1 = require("./integrity.js");
35
35
  const markdown_js_1 = require("./markdown.js");
36
36
  const symbols_js_1 = require("./symbols.js");
37
37
  const skill_normalize_js_1 = require("./skill-normalize.js");
38
+ const keyfiles_js_1 = require("./keyfiles.js");
38
39
  const linters_js_1 = require("./linters.js");
39
40
  const tool_contract_js_1 = require("./tool-contract.js");
40
41
  const effects_js_1 = require("./effects.js");
@@ -564,12 +565,12 @@ async function compileSectionsSection(spec, basePath, maxSectionLines) {
564
565
  return { lines, errors };
565
566
  }
566
567
  function compileKeyFilesSection(spec, basePath) {
567
- if (!spec.keyFiles)
568
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- keyFiles is deprecated, not removed: it still compiles until the next major
569
+ const keyFiles = spec.keyFiles;
570
+ if (!keyFiles)
568
571
  return { lines: [], errors: [] };
569
- const lines = ["## Key Files", ""];
570
572
  const errors = [];
571
- for (const [filePath, desc] of Object.entries(spec.keyFiles)) {
572
- lines.push(`- \`${filePath}\` — ${desc}`);
573
+ for (const [filePath, desc] of Object.entries(keyFiles)) {
573
574
  const err = validateFileRef(filePath, basePath);
574
575
  if (err)
575
576
  errors.push(err);
@@ -580,7 +581,9 @@ function compileKeyFilesSection(spec, basePath) {
580
581
  });
581
582
  }
582
583
  }
583
- return { lines: [lines.join("\n")], errors };
584
+ // The one renderer the `deprecated-key-files` rule measures with, so the
585
+ // size a finding reports is the size of what this emits.
586
+ return { lines: [(0, keyfiles_js_1.renderKeyFiles)(keyFiles)], errors };
584
587
  }
585
588
  function compileCommandsSection(spec, basePath) {
586
589
  if (!spec.commands)
@@ -150,6 +150,7 @@ export declare const vigilesConfigSchema: z.ZodObject<{
150
150
  "hook-matcher": z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodLiteral<"warn">, z.ZodLiteral<"error">, z.ZodLiteral<false>, z.ZodLiteral<"off">, z.ZodLiteral<0>, z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<true>]>, z.ZodTransform<false | "error" | "warn", boolean | 0 | 1 | 2 | "error" | "warn" | "off">>>;
151
151
  "doc-refs": z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodLiteral<"warn">, z.ZodLiteral<"error">, z.ZodLiteral<false>, z.ZodLiteral<"off">, z.ZodLiteral<0>, z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<true>]>, z.ZodTransform<false | "error" | "warn", boolean | 0 | 1 | 2 | "error" | "warn" | "off">>>;
152
152
  "instruction-weight": z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodLiteral<"warn">, z.ZodLiteral<"error">, z.ZodLiteral<false>, z.ZodLiteral<"off">, z.ZodLiteral<0>, z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<true>]>, z.ZodTransform<false | "error" | "warn", boolean | 0 | 1 | 2 | "error" | "warn" | "off">>>;
153
+ "deprecated-key-files": z.ZodDefault<z.ZodPipe<z.ZodUnion<readonly [z.ZodLiteral<"warn">, z.ZodLiteral<"error">, z.ZodLiteral<false>, z.ZodLiteral<"off">, z.ZodLiteral<0>, z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<true>]>, z.ZodTransform<false | "error" | "warn", boolean | 0 | 1 | 2 | "error" | "warn" | "off">>>;
153
154
  }, z.core.$strict>>;
154
155
  files: z.ZodDefault<z.ZodArray<z.ZodString>>;
155
156
  maxRules: z.ZodOptional<z.ZodNumber>;
@@ -182,6 +182,7 @@ const rulesSchema = zod_1.z
182
182
  "hook-matcher": severitySchema.default("warn"),
183
183
  "doc-refs": severitySchema.default(false),
184
184
  "instruction-weight": severitySchema.default("error"),
185
+ "deprecated-key-files": severitySchema.default("warn"),
185
186
  })
186
187
  .strict();
187
188
  /** The rule names, for the "did you mean" on an unknown one. */
@@ -0,0 +1,9 @@
1
+ import { type KeyFilesUsage } from "./keyfiles.js";
2
+ /**
3
+ * The `keyFiles` usage in one spec's source, or `null` when the spec does not
4
+ * use the field. The first `keyFiles:` property in any object literal counts —
5
+ * an instruction spec has one builder call, and nothing else in a spec is
6
+ * called that.
7
+ */
8
+ export declare function findKeyFilesInSpecSource(source: string): KeyFilesUsage | null;
9
+ //# sourceMappingURL=keyfiles-source.d.ts.map
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.findKeyFilesInSpecSource = findKeyFilesInSpecSource;
7
+ /**
8
+ * Reads `keyFiles` out of an instruction spec's SOURCE, without running it.
9
+ *
10
+ * `lint` and `audit` both report the deprecated field (`deprecated-key-files`),
11
+ * and neither may import a spec: `audit` is a side-effect-free read, and a spec
12
+ * is the user's program. So the source is PARSED (TypeScript syntax tree) and
13
+ * the field is read off the tree — a `keyFiles` inside a comment or a string is
14
+ * not usage, and a text search could not tell the difference.
15
+ *
16
+ * Pure: the file walk that feeds it is `src/deprecated-key-files.ts`.
17
+ */
18
+ const typescript_1 = __importDefault(require("typescript"));
19
+ const keyfiles_js_1 = require("./keyfiles.js");
20
+ /** Field used, size not knowable from the syntax alone. */
21
+ const UNMEASURED = (entries) => ({
22
+ entries,
23
+ chars: 0,
24
+ measured: false,
25
+ });
26
+ /** A literal key or value: `"a"`, `'a'`, `` `a` `` (and, for a key only, `a`). */
27
+ function literalText(node) {
28
+ return typescript_1.default.isStringLiteral(node) || typescript_1.default.isNoSubstitutionTemplateLiteral(node)
29
+ ? node.text
30
+ : undefined;
31
+ }
32
+ /** One `path: "description"` pair, or `undefined` when either side is not literal. */
33
+ function literalEntry(prop) {
34
+ if (!typescript_1.default.isPropertyAssignment(prop))
35
+ return undefined;
36
+ const key = typescript_1.default.isIdentifier(prop.name)
37
+ ? prop.name.text
38
+ : literalText(prop.name);
39
+ const value = literalText(prop.initializer);
40
+ return key === undefined || value === undefined ? undefined : [key, value];
41
+ }
42
+ /**
43
+ * Read a `{ path: "description" }` object literal. A computed key or value, or a
44
+ * spread, makes the map unmeasurable: its pairs are still counted (each one
45
+ * compiles to a line), its size is not guessed.
46
+ */
47
+ function measureLiteral(obj) {
48
+ const pairs = obj.properties.filter(typescript_1.default.isPropertyAssignment);
49
+ const literal = obj.properties
50
+ .map(literalEntry)
51
+ .filter((e) => e !== undefined);
52
+ return literal.length === obj.properties.length
53
+ ? (0, keyfiles_js_1.measureKeyFiles)(Object.fromEntries(literal))
54
+ : UNMEASURED(pairs.length);
55
+ }
56
+ /** The usage a node declares, if it is a `keyFiles` property. */
57
+ function usageAt(node) {
58
+ if (typescript_1.default.isShorthandPropertyAssignment(node))
59
+ return node.name.text === "keyFiles" ? UNMEASURED(0) : undefined;
60
+ if (!typescript_1.default.isPropertyAssignment(node))
61
+ return undefined;
62
+ const name = typescript_1.default.isIdentifier(node.name)
63
+ ? node.name.text
64
+ : literalText(node.name);
65
+ if (name !== "keyFiles")
66
+ return undefined;
67
+ return typescript_1.default.isObjectLiteralExpression(node.initializer)
68
+ ? measureLiteral(node.initializer)
69
+ : UNMEASURED(0);
70
+ }
71
+ /** Depth-first: the first `keyFiles` property under `node`. */
72
+ function firstUsage(node) {
73
+ return (usageAt(node) ??
74
+ typescript_1.default.forEachChild(node, (child) => firstUsage(child)) ??
75
+ undefined);
76
+ }
77
+ /**
78
+ * The `keyFiles` usage in one spec's source, or `null` when the spec does not
79
+ * use the field. The first `keyFiles:` property in any object literal counts —
80
+ * an instruction spec has one builder call, and nothing else in a spec is
81
+ * called that.
82
+ */
83
+ function findKeyFilesInSpecSource(source) {
84
+ const sf = typescript_1.default.createSourceFile("spec.ts", source, typescript_1.default.ScriptTarget.Latest,
85
+ /* setParentNodes */ false, typescript_1.default.ScriptKind.TS);
86
+ return firstUsage(sf) ?? null;
87
+ }
88
+ //# sourceMappingURL=keyfiles-source.js.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `keyFiles` — DEPRECATED, removed in the major after the one that ships this
3
+ * notice (the one-major alias window in `core/spec.ts`).
4
+ *
5
+ * WHY. The field compiles a flat "## Key Files" path → description list into
6
+ * the instruction file the harness loads on EVERY request. Measured on this
7
+ * repository's own root spec (2026-10-11): 230 entries, 45,467 characters —
8
+ * 29% of a 157,872-character file, paid on every request, to tell an agent where files are
9
+ * when it finds them itself with Glob/Grep. Anthropic's own guidance lists
10
+ * "File-by-file descriptions of the codebase" under what to EXCLUDE from
11
+ * `CLAUDE.md` (https://code.claude.com/docs/en/best-practices).
12
+ *
13
+ * THE REPLACEMENT, decided per entry, in this order:
14
+ * - the file's own header, the docs or the path already say it → delete it;
15
+ * - a fact about that one file, written nowhere else → that file's header
16
+ * comment, where a reader meets it on opening the file;
17
+ * - a rule an agent must follow while working in one folder → that folder's
18
+ * nested instruction file (`<folder>/CLAUDE.md` from its own spec), which
19
+ * loads only when the agent works there;
20
+ * - only when nothing else would lead an agent to the file → a one-line
21
+ * `file()` pointer where the decision that needs it is written (verified
22
+ * exactly like a `keyFiles` path, by `validateFileRef`).
23
+ *
24
+ * This module is pure: the renderer `compile` uses, the measurement the
25
+ * `deprecated-key-files` rule reports, and its message — one wording for lint
26
+ * and audit.
27
+ */
28
+ /** Where the migration is written up. Printed in every finding. */
29
+ export declare const KEYFILES_DOCS_URL = "https://github.com/zernie/vigiles/blob/main/docs/rules/deprecated-key-files.md";
30
+ /**
31
+ * The compiled "## Key Files" section — the ONE renderer, so the size a
32
+ * finding reports is the size of what actually loads.
33
+ */
34
+ export declare function renderKeyFiles(keyFiles: Readonly<Record<string, string>>): string;
35
+ /**
36
+ * What a spec's `keyFiles` costs. `measured: false` when the map is not a
37
+ * literal (a variable, a computed key or value, a spread) — then the field is
38
+ * known to be USED and its size is not guessed.
39
+ */
40
+ export interface KeyFilesUsage {
41
+ readonly entries: number;
42
+ readonly chars: number;
43
+ readonly measured: boolean;
44
+ }
45
+ /** Measure a map the way it compiles. */
46
+ export declare function measureKeyFiles(keyFiles: Readonly<Record<string, string>>): KeyFilesUsage;
47
+ /** The cost half of a finding, e.g. `230 entries, 45,467 characters compiled into …`. */
48
+ export declare function keyFilesCost(u: KeyFilesUsage): string;
49
+ /**
50
+ * The instruction file a spec compiles to — `pkg/AGENTS.md.spec.ts` →
51
+ * `AGENTS.md` — so the fix names the nested file in the reader's own harness
52
+ * instead of assuming one.
53
+ */
54
+ export declare function instructionFileOfSpec(specPath: string): string;
55
+ /**
56
+ * The fix, one sentence — shared by the lint message and the audit line. The
57
+ * four places an entry can go, in the order to try them; the full recipe is
58
+ * the migration guide. `targets` are the instruction files the reported specs
59
+ * compile to (`CLAUDE.md`, `AGENTS.md`), so the nested file is named for real.
60
+ */
61
+ export declare function keyFilesFix(targets: readonly string[]): string;
62
+ /** The whole finding: what it costs, then where each entry goes and where it is written up. */
63
+ export declare function keyFilesDeprecationMessage(u: KeyFilesUsage, target: string): string;
64
+ //# sourceMappingURL=keyfiles.d.ts.map
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ /**
3
+ * `keyFiles` — DEPRECATED, removed in the major after the one that ships this
4
+ * notice (the one-major alias window in `core/spec.ts`).
5
+ *
6
+ * WHY. The field compiles a flat "## Key Files" path → description list into
7
+ * the instruction file the harness loads on EVERY request. Measured on this
8
+ * repository's own root spec (2026-10-11): 230 entries, 45,467 characters —
9
+ * 29% of a 157,872-character file, paid on every request, to tell an agent where files are
10
+ * when it finds them itself with Glob/Grep. Anthropic's own guidance lists
11
+ * "File-by-file descriptions of the codebase" under what to EXCLUDE from
12
+ * `CLAUDE.md` (https://code.claude.com/docs/en/best-practices).
13
+ *
14
+ * THE REPLACEMENT, decided per entry, in this order:
15
+ * - the file's own header, the docs or the path already say it → delete it;
16
+ * - a fact about that one file, written nowhere else → that file's header
17
+ * comment, where a reader meets it on opening the file;
18
+ * - a rule an agent must follow while working in one folder → that folder's
19
+ * nested instruction file (`<folder>/CLAUDE.md` from its own spec), which
20
+ * loads only when the agent works there;
21
+ * - only when nothing else would lead an agent to the file → a one-line
22
+ * `file()` pointer where the decision that needs it is written (verified
23
+ * exactly like a `keyFiles` path, by `validateFileRef`).
24
+ *
25
+ * This module is pure: the renderer `compile` uses, the measurement the
26
+ * `deprecated-key-files` rule reports, and its message — one wording for lint
27
+ * and audit.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.KEYFILES_DOCS_URL = void 0;
31
+ exports.renderKeyFiles = renderKeyFiles;
32
+ exports.measureKeyFiles = measureKeyFiles;
33
+ exports.keyFilesCost = keyFilesCost;
34
+ exports.instructionFileOfSpec = instructionFileOfSpec;
35
+ exports.keyFilesFix = keyFilesFix;
36
+ exports.keyFilesDeprecationMessage = keyFilesDeprecationMessage;
37
+ /** Where the migration is written up. Printed in every finding. */
38
+ exports.KEYFILES_DOCS_URL = "https://github.com/zernie/vigiles/blob/main/docs/rules/deprecated-key-files.md";
39
+ /**
40
+ * The compiled "## Key Files" section — the ONE renderer, so the size a
41
+ * finding reports is the size of what actually loads.
42
+ */
43
+ function renderKeyFiles(keyFiles) {
44
+ return [
45
+ "## Key Files",
46
+ "",
47
+ ...Object.entries(keyFiles).map(([path, desc]) => `- \`${path}\` — ${desc}`),
48
+ ].join("\n");
49
+ }
50
+ /** Measure a map the way it compiles. */
51
+ function measureKeyFiles(keyFiles) {
52
+ return {
53
+ entries: Object.keys(keyFiles).length,
54
+ chars: renderKeyFiles(keyFiles).length,
55
+ measured: true,
56
+ };
57
+ }
58
+ const grouped = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
59
+ /** The cost half of a finding, e.g. `230 entries, 45,467 characters compiled into …`. */
60
+ function keyFilesCost(u) {
61
+ if (!u.measured)
62
+ return "not a literal map — size not measured";
63
+ const noun = u.entries === 1 ? "entry" : "entries";
64
+ return `${String(u.entries)} ${noun}, ${grouped(u.chars)} characters compiled into the instruction file`;
65
+ }
66
+ /**
67
+ * The instruction file a spec compiles to — `pkg/AGENTS.md.spec.ts` →
68
+ * `AGENTS.md` — so the fix names the nested file in the reader's own harness
69
+ * instead of assuming one.
70
+ */
71
+ function instructionFileOfSpec(specPath) {
72
+ return specPath.replace(/^.*[\\/]/, "").replace(/\.spec\.ts$/, "");
73
+ }
74
+ /**
75
+ * The fix, one sentence — shared by the lint message and the audit line. The
76
+ * four places an entry can go, in the order to try them; the full recipe is
77
+ * the migration guide. `targets` are the instruction files the reported specs
78
+ * compile to (`CLAUDE.md`, `AGENTS.md`), so the nested file is named for real.
79
+ */
80
+ function keyFilesFix(targets) {
81
+ // In the order given — the detector sorts findings by path — once each.
82
+ const nested = Array.from(new Set(targets), (t) => `<folder>/${t}`).join(" or ");
83
+ return ("For each entry: delete it if the file or the docs already say it · " +
84
+ "move a fact about one file into that file's header comment · " +
85
+ `move a rule for one folder into ${nested} · ` +
86
+ "keep a one-line file() pointer only if nothing else leads there");
87
+ }
88
+ /** The whole finding: what it costs, then where each entry goes and where it is written up. */
89
+ function keyFilesDeprecationMessage(u, target) {
90
+ return (`\`keyFiles\` is deprecated and goes away in the next major (${keyFilesCost(u)}). ` +
91
+ `${keyFilesFix([target])} (guide: ${exports.KEYFILES_DOCS_URL})`);
92
+ }
93
+ //# sourceMappingURL=keyfiles.js.map
@@ -217,7 +217,7 @@ function formatOrphanReport(report) {
217
217
  ];
218
218
  for (const o of report.orphans)
219
219
  lines.push(` ${o}`);
220
- lines.push(" Fix: link each from another .md (README, a spec's Key Files, or a doc).", " To silence: add `<!-- vigiles-disable orphan-docs -->` to the doc, or", " exclude it via .vigilesrc.json → `orphans.exclude` (or narrow `orphans.include`).");
220
+ lines.push(" Fix: link each from another .md (README, an instruction file, or a doc).", " To silence: add `<!-- vigiles-disable orphan-docs -->` to the doc, or", " exclude it via .vigilesrc.json → `orphans.exclude` (or narrow `orphans.include`).");
221
221
  return lines.join("\n");
222
222
  }
223
223
  //# sourceMappingURL=orphans.js.map
@@ -238,6 +238,15 @@ exports.RULE_META = {
238
238
  detector: "checkContentBudgets",
239
239
  upstreamPrevention: "compile measures the spec before writing; a render policy (enforce render, guidance details) meets a budget without deleting a rule",
240
240
  },
241
+ "deprecated-key-files": {
242
+ id: "deprecated-key-files",
243
+ bucket: "structural-closed",
244
+ surface: ["instruction"],
245
+ defaultSeverity: "warn",
246
+ summary: "An instruction spec doesn't use the deprecated keyFiles (always-loaded file map).",
247
+ detector: "findDeprecatedKeyFiles",
248
+ upstreamPrevention: "the @deprecated keyFiles field is removed next major",
249
+ },
241
250
  "skill-description-budget": {
242
251
  id: "skill-description-budget",
243
252
  bucket: "heuristic-behavioral",
@@ -57,6 +57,13 @@ export declare function loadSpecSurface(basePath: string): SpecSurface;
57
57
  /**
58
58
  * Load keyFiles from compiled markdown files. Reads each target's
59
59
  * "## Key Files" section and extracts the file paths.
60
+ *
61
+ * @deprecated Goes away together with the deprecated `keyFiles` spec field (see
62
+ * the `deprecated-key-files` rule), in the major after the one that deprecates
63
+ * it. It reads the field back out of COMPILED markdown with a regex rather than
64
+ * from the spec, and it is deliberately not ported in the release that
65
+ * deprecates the field: with the field gone, the session audit's "files the
66
+ * specs know about" is `surface.trackedInputs` (the sidecar manifests) alone.
60
67
  */
61
68
  export declare function loadKeyFilesFromSpecs(basePath: string): Set<string>;
62
69
  /** A finding about a file not tracked by any spec. */
@@ -143,6 +143,13 @@ function loadSpecSurface(basePath) {
143
143
  /**
144
144
  * Load keyFiles from compiled markdown files. Reads each target's
145
145
  * "## Key Files" section and extracts the file paths.
146
+ *
147
+ * @deprecated Goes away together with the deprecated `keyFiles` spec field (see
148
+ * the `deprecated-key-files` rule), in the major after the one that deprecates
149
+ * it. It reads the field back out of COMPILED markdown with a regex rather than
150
+ * from the spec, and it is deliberately not ported in the release that
151
+ * deprecates the field: with the field gone, the session audit's "files the
152
+ * specs know about" is `surface.trackedInputs` (the sidecar manifests) alone.
146
153
  */
147
154
  function loadKeyFilesFromSpecs(basePath) {
148
155
  const keyFiles = new Set();
@@ -175,6 +182,7 @@ function loadKeyFilesFromSpecs(basePath) {
175
182
  function analyzeSession(basePath, baseRef) {
176
183
  const changedFiles = gitChangedFiles(basePath, baseRef);
177
184
  const surface = loadSpecSurface(basePath);
185
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- keyFiles is deprecated, not removed: it still compiles until the next major, and this reads it back until then
178
186
  const keyFiles = loadKeyFilesFromSpecs(basePath);
179
187
  const trackedChanges = [];
180
188
  const untrackedChanges = [];
@@ -352,7 +352,19 @@ export interface ClaudeSpec {
352
352
  readonly target?: InstructionTarget | InstructionTarget[];
353
353
  /** npm scripts / shell commands → descriptions. Verified against package.json. */
354
354
  readonly commands?: Record<string, string>;
355
- /** File paths → descriptions. Verified via existsSync. */
355
+ /**
356
+ * File paths → descriptions, compiled into a "## Key Files" list. Verified via existsSync.
357
+ *
358
+ * @deprecated Move each entry out, then remove `keyFiles`. The list loads
359
+ * on every request to tell the agent where files are, which it finds itself.
360
+ * For each entry: delete it if the file or the docs already say it; move a
361
+ * fact about one file into that file's header comment; move a rule for one
362
+ * folder into that folder's nested instruction file; keep a one-line
363
+ * `file()` pointer (verified the same way) only if nothing else leads there.
364
+ * Still compiled for one major — the one that ships this notice — and
365
+ * removed in the major after it; `vigiles lint` reports every use as
366
+ * `deprecated-key-files`. Migration: docs/rules/deprecated-key-files.md.
367
+ */
356
368
  readonly keyFiles?: Record<string, string>;
357
369
  /** Named prose sections — plain strings or tagged templates with file()/cmd()/ref(). */
358
370
  readonly sections?: Record<string, string | InstructionFragment[]>;
@@ -379,6 +391,7 @@ export interface ClaudeSpec {
379
391
  type ClaudeSpecBase = {
380
392
  readonly target?: InstructionTarget | InstructionTarget[];
381
393
  readonly commands?: Record<string, string>;
394
+ /** @deprecated Move each entry out, then remove `keyFiles` — see {@link ClaudeSpec.keyFiles}. Removed in the major after the one that deprecates it. */
382
395
  readonly keyFiles?: Record<string, string>;
383
396
  readonly maxTokens?: number;
384
397
  readonly rules: Record<string, Rule>;
@@ -142,6 +142,14 @@ export interface RulesConfig {
142
142
  * `vigiles lint --update-baseline` records it. Default: "error".
143
143
  */
144
144
  "instruction-weight"?: RuleSeverity;
145
+ /**
146
+ * An instruction spec still uses the deprecated `keyFiles` field — a flat
147
+ * path → description list compiled into the file loaded on every request.
148
+ * Reports the entry count and compiled size with the migration (a `file()`
149
+ * ref in the rule that needs the pointer). The spec is parsed, never run.
150
+ * Default "warn": a deprecation must not turn a consumer's CI red on upgrade.
151
+ */
152
+ "deprecated-key-files"?: RuleSeverity;
145
153
  /**
146
154
  * Near-duplicate rules WITHIN one spec, by NCD similarity — spec bloat, two
147
155
  * rules saying the same thing in different words.
@@ -0,0 +1,35 @@
1
+ import { type KeyFilesUsage } from "./core/keyfiles.js";
2
+ import type { RuleSeverity } from "./core/types.js";
3
+ /** One spec still using `keyFiles`, relative to the directory searched. */
4
+ export interface DeprecatedKeyFilesFinding extends KeyFilesUsage {
5
+ readonly path: string;
6
+ }
7
+ /**
8
+ * The detector: of the given instruction specs, the ones still using
9
+ * `keyFiles`, sorted by path. The file walk and the reads are the caller's
10
+ * (the composition root), so this stays free of I/O.
11
+ */
12
+ export declare function findDeprecatedKeyFiles(io: {
13
+ /** Instruction specs (`**\/*.md.spec.ts`, the glob `compile` uses), relative paths. */
14
+ readonly specPaths: readonly string[];
15
+ readonly read: (path: string) => string;
16
+ }): readonly DeprecatedKeyFilesFinding[];
17
+ /**
18
+ * `lint`'s side of the rule. Runs ONCE over the repository, like the
19
+ * instruction-weight ratchet: a nested bundle's spec would otherwise be
20
+ * reported again by every bundle that encloses it. Warning by default — a
21
+ * deprecation that gated would turn a consumer's CI red on upgrade, which the
22
+ * one-major window exists to prevent.
23
+ */
24
+ export declare function checkDeprecatedKeyFiles(opts: {
25
+ readonly severity: RuleSeverity;
26
+ /** Called only when the rule is on. */
27
+ readonly find: () => readonly DeprecatedKeyFilesFinding[];
28
+ readonly silent: boolean;
29
+ /** One per finding, with the spec's path relative to `root`. */
30
+ readonly annotate: (level: "error" | "warning", message: string, path: string) => void;
31
+ }): {
32
+ readonly issues: number;
33
+ readonly errors: number;
34
+ };
35
+ //# sourceMappingURL=deprecated-key-files.d.ts.map
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.findDeprecatedKeyFiles = findDeprecatedKeyFiles;
4
+ exports.checkDeprecatedKeyFiles = checkDeprecatedKeyFiles;
5
+ /**
6
+ * The `deprecated-key-files` rule: every instruction spec that still uses the
7
+ * deprecated `keyFiles` field. ONE detector, two readers — `lint` (this file's
8
+ * `checkDeprecatedKeyFiles`) and `audit` (`scanPlugin`'s `deprecatedKeyFiles`)
9
+ * both call {@link findDeprecatedKeyFiles}, so they cannot disagree.
10
+ *
11
+ * The decisions — what a spec's `keyFiles` costs, what the finding says — are
12
+ * pure and live in `core/keyfiles.ts` / `core/keyfiles-source.ts`; this module
13
+ * owns what is left: matching specs to findings and printing them. The
14
+ * effects — walking the repository and reading files — happen in the composition root (`cli-main.ts`,
15
+ * `specsUsingKeyFiles`) and are handed in. Migration guide:
16
+ * docs/rules/deprecated-key-files.md.
17
+ */
18
+ const keyfiles_source_js_1 = require("./core/keyfiles-source.js");
19
+ const keyfiles_js_1 = require("./core/keyfiles.js");
20
+ /**
21
+ * The detector: of the given instruction specs, the ones still using
22
+ * `keyFiles`, sorted by path. The file walk and the reads are the caller's
23
+ * (the composition root), so this stays free of I/O.
24
+ */
25
+ function findDeprecatedKeyFiles(io) {
26
+ return [...io.specPaths].sort().flatMap((path) => {
27
+ const usage = (0, keyfiles_source_js_1.findKeyFilesInSpecSource)(io.read(path));
28
+ return usage === null ? [] : [{ path, ...usage }];
29
+ });
30
+ }
31
+ /** The lint message, naming the nested file after the spec's own target. */
32
+ const messageFor = (f) => (0, keyfiles_js_1.keyFilesDeprecationMessage)(f, (0, keyfiles_js_1.instructionFileOfSpec)(f.path));
33
+ /**
34
+ * `lint`'s side of the rule. Runs ONCE over the repository, like the
35
+ * instruction-weight ratchet: a nested bundle's spec would otherwise be
36
+ * reported again by every bundle that encloses it. Warning by default — a
37
+ * deprecation that gated would turn a consumer's CI red on upgrade, which the
38
+ * one-major window exists to prevent.
39
+ */
40
+ function checkDeprecatedKeyFiles(opts) {
41
+ const { severity } = opts;
42
+ if (!severity)
43
+ return { issues: 0, errors: 0 };
44
+ const found = opts.find();
45
+ const mark = severity === "error" ? "✗" : "⚠";
46
+ const level = severity === "error" ? "error" : "warning";
47
+ if (!opts.silent && found.length > 0)
48
+ console.log("\nDeprecated-key-files check:\n");
49
+ // Silent (`--json`, `--summary`) prints nothing at all, annotations
50
+ // included: they go to stdout and would corrupt the JSON.
51
+ if (!opts.silent)
52
+ found.forEach((f) => {
53
+ const message = messageFor(f);
54
+ console.log(` ${mark} ${f.path}: ${message}`);
55
+ opts.annotate(level, message, f.path);
56
+ });
57
+ return {
58
+ issues: found.length,
59
+ errors: severity === "error" ? found.length : 0,
60
+ };
61
+ }
62
+ //# sourceMappingURL=deprecated-key-files.js.map
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Research-index completeness — the deterministic FLOOR keeping the `research/`
3
3
  * corpus and its index (`research/CLAUDE.md.spec.ts`) in sync. The spec's
4
- * `keyFiles` map is the AGENT-FACING index of every research doc; the compiler
5
- * already verifies the OTHER direction (every indexed path EXISTS, else
6
- * `vigiles compile` fails), so the only open gap is a doc that was ADDED but
4
+ * `file()` references are the AGENT-FACING index of every research doc; the
5
+ * compiler already verifies the OTHER direction (every referenced path EXISTS,
6
+ * else `vigiles compile` fails), so the only open gap is a doc that was ADDED but
7
7
  * never indexed. This check closes it: every `research/*.md` (except the
8
8
  * human-facing `README.md`) must appear in the index, or the dogfood test fails.
9
9
  *
@@ -17,13 +17,13 @@ export declare const INDEX_EXEMPT: readonly ["README.md"];
17
17
  /**
18
18
  * Research doc basenames (e.g. `roadmap.md`) NOT referenced anywhere in
19
19
  * `indexContent`. A doc counts as indexed if its repo-relative path
20
- * (`research/<name>.md`) appears in the index — the exact form the spec's
21
- * `keyFiles` keys use. Exempt docs (the README) are never flagged.
20
+ * (`research/<name>.md`) appears in the index — the exact form a `file()`
21
+ * reference takes. Exempt docs (the README) are never flagged.
22
22
  */
23
23
  export declare function unindexedResearchDocs(docFilenames: readonly string[], indexContent: string, exempt?: readonly string[]): string[];
24
24
  /**
25
25
  * Index entries pointing at a `research/<name>.md` that no longer exists on
26
- * disk. The compiler catches this at compile time (a missing `keyFiles` path is
26
+ * disk. The compiler catches this at compile time (a missing `file()` path is
27
27
  * a compile error), so this is a belt-and-suspenders reader for a test that
28
28
  * wants to assert it directly without invoking the compiler.
29
29
  */
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * Research-index completeness — the deterministic FLOOR keeping the `research/`
4
4
  * corpus and its index (`research/CLAUDE.md.spec.ts`) in sync. The spec's
5
- * `keyFiles` map is the AGENT-FACING index of every research doc; the compiler
6
- * already verifies the OTHER direction (every indexed path EXISTS, else
7
- * `vigiles compile` fails), so the only open gap is a doc that was ADDED but
5
+ * `file()` references are the AGENT-FACING index of every research doc; the
6
+ * compiler already verifies the OTHER direction (every referenced path EXISTS,
7
+ * else `vigiles compile` fails), so the only open gap is a doc that was ADDED but
8
8
  * never indexed. This check closes it: every `research/*.md` (except the
9
9
  * human-facing `README.md`) must appear in the index, or the dogfood test fails.
10
10
  *
@@ -22,15 +22,15 @@ exports.INDEX_EXEMPT = ["README.md"];
22
22
  /**
23
23
  * Research doc basenames (e.g. `roadmap.md`) NOT referenced anywhere in
24
24
  * `indexContent`. A doc counts as indexed if its repo-relative path
25
- * (`research/<name>.md`) appears in the index — the exact form the spec's
26
- * `keyFiles` keys use. Exempt docs (the README) are never flagged.
25
+ * (`research/<name>.md`) appears in the index — the exact form a `file()`
26
+ * reference takes. Exempt docs (the README) are never flagged.
27
27
  */
28
28
  function unindexedResearchDocs(docFilenames, indexContent, exempt = exports.INDEX_EXEMPT) {
29
29
  return docFilenames.filter((name) => !exempt.includes(name) && !indexContent.includes(`research/${name}`));
30
30
  }
31
31
  /**
32
32
  * Index entries pointing at a `research/<name>.md` that no longer exists on
33
- * disk. The compiler catches this at compile time (a missing `keyFiles` path is
33
+ * disk. The compiler catches this at compile time (a missing `file()` path is
34
34
  * a compile error), so this is a belt-and-suspenders reader for a test that
35
35
  * wants to assert it directly without invoking the compiler.
36
36
  */
package/dist/scan.d.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { type PluginLayout } from "./core/layout.js";
15
15
  import type { ExcludeSet } from "./exclude.js";
16
+ import type { DeprecatedKeyFilesFinding } from "./deprecated-key-files.js";
16
17
  import type { HarnessDialect } from "./core/dialect.js";
17
18
  import type { ToolIssue } from "./core/tool-contract.js";
18
19
  import { type HookEventIssue } from "./core/hook-events.js";
@@ -329,6 +330,19 @@ export interface ScanReport {
329
330
  * on day one is switched off on day one.
330
331
  */
331
332
  readonly instructionWeight: InstructionWeight | null;
333
+ /**
334
+ * Instruction specs still using the deprecated `keyFiles` — the
335
+ * `deprecated-key-files` rule's own detector (`findDeprecatedKeyFiles`),
336
+ * handed in through `opts` by `audit`, so `lint` and `audit` cannot
337
+ * disagree. Advisory: printed with its fix, never scored.
338
+ *
339
+ * ABSENT when there are none, never `[]`, for the parity reason
340
+ * `coverageCaveats` gives. ⚠️ The browser twin produces none: it would need the
341
+ * TypeScript parser in the site bundle, and the demo does not fetch spec
342
+ * sources. No vendored parity fixture carries a spec, so the gate stays
343
+ * byte-identical; a spec added to one would show this gap as a parity diff.
344
+ */
345
+ readonly deprecatedKeyFiles?: readonly DeprecatedKeyFilesFinding[];
332
346
  /** Skills/agents whose `---` block isn't valid YAML — informational (may still load via salvage). */
333
347
  readonly malformedFrontmatter: readonly FrontmatterParseIssue[];
334
348
  readonly warnings: readonly string[];
@@ -444,6 +458,13 @@ export declare function scanPlugin(dir: string, layout: PluginLayout, dialect: H
444
458
  * context `overBundles` builds, so a new checker cannot forget it).
445
459
  */
446
460
  excludes?: ExcludeSet;
461
+ /**
462
+ * The `deprecated-key-files` findings for this directory, from the rule's
463
+ * own detector (`findDeprecatedKeyFiles`), computed by the composition root
464
+ * — the one place allowed to walk the repository and read specs. `audit`
465
+ * passes them; every other caller leaves the field off the report.
466
+ */
467
+ deprecatedKeyFiles?: readonly DeprecatedKeyFilesFinding[];
447
468
  /**
448
469
  * The repo's `.vigilesrc.json#harnesses`, resolved — every declared harness
449
470
  * with its own layout, dialect and roots, IN DECLARATION ORDER.
package/dist/scan.js CHANGED
@@ -55,6 +55,7 @@ const plugin_loader_js_2 = require("./plugin-loader.js");
55
55
  const score_core_js_1 = require("./score-core.js");
56
56
  const skill_refs_js_1 = require("./skill-refs.js");
57
57
  const layout_js_1 = require("./core/layout.js");
58
+ const keyfiles_js_1 = require("./core/keyfiles.js");
58
59
  const hook_events_js_1 = require("./core/hook-events.js");
59
60
  const mcp_config_js_1 = require("./core/mcp-config.js");
60
61
  const agent_plugins_js_1 = require("./core/agent-plugins.js");
@@ -357,6 +358,7 @@ function scanPlugin(dir, layout, dialect, opts = {}) {
357
358
  evaluable: coverage.total,
358
359
  coverageEvidence: (0, test_coverage_js_1.coverageEvidenceCounts)(coverage),
359
360
  ...(caveats.length > 0 ? { coverageCaveats: caveats } : {}),
361
+ ...deprecatedKeyFilesField(opts.deprecatedKeyFiles),
360
362
  ownTestSignal: ownTestSignalOnDisk(dir),
361
363
  puritySummary,
362
364
  };
@@ -656,6 +658,32 @@ function instructionWeightLines(w) {
656
658
  ...tail,
657
659
  ];
658
660
  }
661
+ /** The scan field, present only when some spec still uses `keyFiles`. */
662
+ function deprecatedKeyFilesField(found) {
663
+ return found && found.length > 0 ? { deprecatedKeyFiles: found } : {};
664
+ }
665
+ /**
666
+ * The audit's `deprecated-key-files` section: one line per spec with what it
667
+ * costs, then the fix once. Next to the instruction weight, because that is
668
+ * the number the field inflates.
669
+ */
670
+ function deprecatedKeyFilesLines(r) {
671
+ const found = r.deprecatedKeyFiles ?? [];
672
+ if (found.length === 0)
673
+ return [];
674
+ return section("Deprecated `keyFiles`", [
675
+ ...found.map((f) => ` ⚠ ${f.path}: ${(0, keyfiles_js_1.keyFilesCost)(f)}`),
676
+ ` → ${(0, keyfiles_js_1.keyFilesFix)(found.map((f) => (0, keyfiles_js_1.instructionFileOfSpec)(f.path)))}.`,
677
+ ` ${keyfiles_js_1.KEYFILES_DOCS_URL} [deprecated-key-files]`,
678
+ ], found.length);
679
+ }
680
+ /** What the instruction file costs: its always-loaded weight, then any deprecated `keyFiles` inflating it. */
681
+ function instructionCostLines(r) {
682
+ const weight = r.instructionWeight
683
+ ? [...instructionWeightLines(r.instructionWeight), ""]
684
+ : [];
685
+ return [...weight, ...deprecatedKeyFilesLines(r)];
686
+ }
659
687
  /** The commands and output-style lines of a scan's facts; a harness without styles says so. */
660
688
  function inventoryFacts(r) {
661
689
  const commands = r.commands > 0 ? [`Commands: ${String(r.commands)}`] : [];
@@ -676,8 +704,7 @@ function formatScanReport(r) {
676
704
  }
677
705
  // Right under the instruction file, because that is the line a reader is
678
706
  // already looking at when they wonder what it costs.
679
- if (r.instructionWeight)
680
- out.push(...instructionWeightLines(r.instructionWeight), ""); // prettier-ignore
707
+ out.push(...instructionCostLines(r));
681
708
  out.push(...section("Skills", r.skills.map(skillLine)));
682
709
  out.push(...section("Agents", r.agents.flatMap(agentLines), r.agents.length));
683
710
  const hookMark = {
@@ -116,7 +116,7 @@ export declare const WORKFLOW_RULES: readonly ["require-instructions-spec", "unt
116
116
  * keep their own default severities. Named for the group taxonomy
117
117
  * (research/install-enforcement-dx.md).
118
118
  */
119
- export declare const NUDGE_RULES: readonly ["skill-description-budget", "frontmatter-valid", "skill-frontmatter", "prefer-compiled-hooks", "unmarked-refs", "lethal-trifecta", "skill-resource-resolves", "skill-missing-fence", "plugin-dir-layout", "delegation-trifecta", "hook-block-ineffective", "hook-matcher", "doc-refs"];
119
+ export declare const NUDGE_RULES: readonly ["skill-description-budget", "frontmatter-valid", "skill-frontmatter", "prefer-compiled-hooks", "unmarked-refs", "lethal-trifecta", "skill-resource-resolves", "skill-missing-fence", "plugin-dir-layout", "delegation-trifecta", "hook-block-ineffective", "hook-matcher", "doc-refs", "deprecated-key-files"];
120
120
  /**
121
121
  * Pure config-merge for what `vigiles init` writes to `.vigilesrc.json`: record
122
122
  * the harnesses if absent, gate the structural rules by EXTENDING the
@@ -119,6 +119,9 @@ exports.NUDGE_RULES = [
119
119
  // 2 582 markdown files, because a fence in prose is a drawing of config rather
120
120
  // than config. See docs/rules/doc-refs.md.
121
121
  "doc-refs",
122
+ // A deprecation notice: it must never gate, or upgrading vigiles would turn a
123
+ // consumer's CI red — the very thing the one-major window exists to avoid.
124
+ "deprecated-key-files",
122
125
  ];
123
126
  /**
124
127
  * Pure config-merge for what `vigiles init` writes to `.vigilesrc.json`: record
@@ -1783,6 +1783,42 @@
1783
1783
  "const": true
1784
1784
  }
1785
1785
  ]
1786
+ },
1787
+ "deprecated-key-files": {
1788
+ "anyOf": [
1789
+ {
1790
+ "type": "string",
1791
+ "const": "warn"
1792
+ },
1793
+ {
1794
+ "type": "string",
1795
+ "const": "error"
1796
+ },
1797
+ {
1798
+ "type": "boolean",
1799
+ "const": false
1800
+ },
1801
+ {
1802
+ "type": "string",
1803
+ "const": "off"
1804
+ },
1805
+ {
1806
+ "type": "number",
1807
+ "const": 0
1808
+ },
1809
+ {
1810
+ "type": "number",
1811
+ "const": 1
1812
+ },
1813
+ {
1814
+ "type": "number",
1815
+ "const": 2
1816
+ },
1817
+ {
1818
+ "type": "boolean",
1819
+ "const": true
1820
+ }
1821
+ ]
1786
1822
  }
1787
1823
  },
1788
1824
  "additionalProperties": false
package/hooks/pre-edit.sh CHANGED
@@ -30,7 +30,7 @@ BLOCKED: ${FILE} is a compiled build artifact — do not edit it directly.
30
30
 
31
31
  Instead:
32
32
  1. Read ${SPEC_FILE} to understand the spec structure
33
- 2. Edit ${SPEC_FILE} to make your changes (add/modify rules, sections, keyFiles, commands)
33
+ 2. Edit ${SPEC_FILE} to make your changes (add/modify rules, sections, commands)
34
34
  3. Run: npx vigiles compile
35
35
  4. The compiled ${FILE} will be regenerated automatically
36
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigiles",
3
- "version": "38.0.0",
3
+ "version": "38.1.0",
4
4
  "description": "Audit, test and measure the harness your AI agent runs on — grade your CLAUDE.md / AGENTS.md, skills, subagents and hooks, run them against a scripted model, and measure whether they actually fire.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -11,7 +11,7 @@ Start a typed `CLAUDE.md.spec.ts` from an existing hand-written CLAUDE.md (or AG
11
11
 
12
12
  Adoption is the **safe, faithful on-ramp — never an upgrade in disguise.** These are non-negotiable:
13
13
 
14
- - **Faithful.** Preserve every rule, command, key file, and prose section as-is. Invent nothing — the spec must compile back to ~the user's existing file.
14
+ - **Faithful.** Preserve every rule, command, and prose section as-is. Invent nothing — the spec must compile back to ~the user's existing file. The one exception is a file map ("Key Files"): never emit the deprecated `keyFiles` field — see the guideline under Step 3.
15
15
  - **Non-destructive.** Never edit the original `CLAUDE.md` / `AGENTS.md`. Only write the new `.spec.ts`. Never auto-`compile` over the file — switching it to spec-managed is a separate, explicit step the user runs with a diff to review.
16
16
  - **Don't escalate enforcement.** Keep `guidance()` as `guidance()`. Upgrading to `enforce()` has a cost (config/plugins, possible false positives) and is a separate opt-in step — the `strengthen` skill. Adoption is **not** turning on strict / `workflow` gating.
17
17
  - **Reversible.** `vigiles eject <file>` hands the file back as plain hand-owned markdown anytime — it's never a one-way door. Tell the user this.
@@ -35,7 +35,7 @@ npm install -D vigiles
35
35
  Identify these sections in the markdown:
36
36
 
37
37
  - **Commands** — lines like `` `npm run build` — description `` or ``- `command` — description``
38
- - **Key files** — lines like `` `src/foo.ts` — description `` listing important files
38
+ - **A file map ("Key Files")** — lines like `` `src/foo.ts` — description `` listing important files. Never becomes `keyFiles` (deprecated); see the guideline under Step 3
39
39
  - **Rules** — `###` headings with `**Enforced by:**` or `**Guidance only**` annotations
40
40
  - **Prose sections** — everything else (positioning, architecture, principles, etc.)
41
41
 
@@ -66,15 +66,9 @@ export default claude({
66
66
  // Prose sections here
67
67
  },
68
68
 
69
- keyFiles: {
70
- // Key files here. A path maps to ONE LINE saying what the file is for —
71
- // aim for 120 characters, never exceed ~200. The entry is a pointer, not a
72
- // summary: how the file works belongs in its own header comment, which is
73
- // read when someone opens it. This file is loaded on every request, so a
74
- // paragraph here is paid for every turn. Prune a stale neighbour whenever
75
- // you add one; `vigiles audit` prints the running total as
76
- // `Always-loaded instructions`.
77
- },
69
+ // No `keyFiles`: the field is deprecated (removed next major). A "Key Files"
70
+ // list in the original file is NOT carried over as a map — see the
71
+ // guideline below.
78
72
 
79
73
  commands: {
80
74
  // Commands here
@@ -89,6 +83,7 @@ export default claude({
89
83
  **Important guidelines:**
90
84
 
91
85
  - Use `file()` refs in sections where file paths appear in backticks — this enables stale reference detection
86
+ - 🔴 **Never emit `keyFiles`.** The field is deprecated and removed in the next major (`vigiles lint` warns as `deprecated-key-files`). For a "Key Files" / file-map section in the original, apply the migration recipe (`docs/rules/deprecated-key-files.md` in the vigiles repository) and show the user, per entry, where it goes — the first that fits: (1) the file's header, the docs or the path already say it (or the path no longer exists) → delete it; (2) a fact about that one file → that file's header comment; (3) a rule for working in one folder → that folder's nested `CLAUDE.md` (its own `<folder>/CLAUDE.md.spec.ts`); (4) nothing else would lead an agent to the file → a one-line `file()` pointer in the section where the decision that needs it is written. Where each entry goes is the user's call; that the map does not become `keyFiles` is not. Until they decide, carry the list over as a plain prose section so adoption stays faithful
92
87
  - Use `cmd()` refs for any `npm run` commands mentioned in sections
93
88
  - Convert `**Enforced by:** \`code-review\``rules to`guidance()` — code review is not a mechanical enforcement
94
89
  - For rules with no annotation, add a `// TODO: classify as enforce() or guidance()` comment
@@ -105,7 +100,7 @@ npm run build
105
100
  npx vigiles compile CLAUDE.md.spec.ts
106
101
  ```
107
102
 
108
- Compare the compiled output against the original file. Key differences are expected (formatting, section ordering), but all rules, commands, key files, and prose content should be preserved.
103
+ Compare the compiled output against the original file. Key differences are expected (formatting, section ordering), but all rules, commands, and prose content should be preserved (a file map is the exception — see the `keyFiles` guideline above).
109
104
 
110
105
  ### Step 5: Present the Result
111
106
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: edit-spec
3
3
  allowed-tools: Read, Edit, Write, Glob, Grep, Bash
4
- description: Edit a vigiles .spec.ts to change a compiled instruction file (CLAUDE.md / AGENTS.md) — add, modify, or remove a rule, section, command, or key file. Use whenever you need to change a CLAUDE.md/AGENTS.md that carries a vigiles hash (edit the spec, never the artifact), including adding a new enforce()/check()/guidance() rule.
4
+ description: Edit a vigiles .spec.ts to change a compiled instruction file (CLAUDE.md / AGENTS.md) — add, modify, or remove a rule, section, command, or file pointer (including a request to add a "key file" — the keyFiles field is deprecated). Use whenever you need to change a CLAUDE.md/AGENTS.md that carries a vigiles hash (edit the spec, never the artifact), including adding a new enforce()/check()/guidance() rule.
5
5
  argument-hint: <what to change — e.g., "add a rule about error handling" or "update the testing section">
6
6
  ---
7
7
 
@@ -13,7 +13,7 @@ $ARGUMENTS — What the user wants to change. Examples:
13
13
 
14
14
  - "add a rule about always using the custom logger"
15
15
  - "update the architecture section"
16
- - "add src/services/auth.ts to key files"
16
+ - "point the auth rule at src/services/auth.ts"
17
17
  - "add npm run lint to commands"
18
18
  - "change the testing guidance"
19
19
 
@@ -49,10 +49,7 @@ export default claude({
49
49
  architecture: "How the codebase is structured...",
50
50
  },
51
51
 
52
- // File paths verified to exist at compile time
53
- keyFiles: {
54
- "src/index.ts": "Main entry point",
55
- },
52
+ // (No `keyFiles` — deprecated; see "A pointer to a file" in Step 3.)
56
53
 
57
54
  // Commands verified against package.json
58
55
  commands: {
@@ -106,24 +103,34 @@ Based on what the user asked for:
106
103
  - Edit the string in `sections`. Sections are plain strings or tagged template literals with `file()`, `cmd()`, `ref()` for verified references
107
104
  - Do NOT add `#` or `##` headers inside sections — they break the document structure
108
105
 
109
- **Adding a key file or command:**
110
-
111
- - Add to `keyFiles` or `commands`. The compiler verifies these exist at compile time
112
- - For commands: must match a script in `package.json`
113
- - For key files: must exist on disk
114
- - 🔴 **KEEP THE DESCRIPTION TO ONE LINE — aim for 120 characters, never exceed
115
- ~200.** A `keyFiles` entry is a POINTER: what the file is for, so a reader knows
116
- whether to open it. The explanation of how it works belongs in that file's own
117
- header comment, where it is read when someone is actually in the file. An
118
- instruction file is loaded on EVERY request, so a paragraph here is paid for
119
- every turn, forever, by every reader — including the ones who never touch that
120
- file.
121
- - **This list only ever grows unless you shrink it.** Every session adds entries
122
- and none removes them, so before adding, check whether a NEARBY entry is now
123
- stale (the file moved, the role changed, the description restates its header)
124
- and fix it in the same edit. Adding without ever pruning is how an instruction
125
- file reaches four times its harness's budget — `vigiles audit` reports that
126
- number as `Always-loaded instructions`, so check it when you touch this list.
106
+ **Adding a command:**
107
+
108
+ - Add to `commands`. The compiler verifies it against `package.json` scripts.
109
+ - Keep the description to one line (aim for 120 characters, never exceed ~200):
110
+ the instruction file is loaded on EVERY request.
111
+
112
+ **A pointer to a file (what used to be a "key file"):**
113
+
114
+ - 🔴 **Never add a `keyFiles` entry — not one, not even to a spec that already
115
+ has the field.** It is deprecated (`vigiles lint` reports it as
116
+ `deprecated-key-files`) and removed in the next major: it compiles a file
117
+ map into the instruction file loaded on every request, and the agent finds
118
+ files itself.
119
+ - Asked to add a file? Place it by the first of these that fits, and say why:
120
+ 1. the file's own header, the docs or the path already say it → add nothing;
121
+ 2. a fact about that one file → write it in **the file's header comment**;
122
+ 3. a rule an agent must follow while working in one folder → that folder's
123
+ **nested `CLAUDE.md`** (its own `<folder>/CLAUDE.md.spec.ts`, compiled);
124
+ 4. only if nothing else would lead an agent there → a **one-line pointer**,
125
+ `${file("src/services/auth.ts")}` inside a `prose` section where the
126
+ decision that needs it is written — compile verifies it exists, exactly
127
+ as it verified a `keyFiles` path. (`guidance()` takes a plain string, so a
128
+ path typed there is not verified.)
129
+ - **Opened a spec that still has `keyFiles`?** Propose removing it, in the same
130
+ reply: go through the entries with the same four steps (the full recipe is
131
+ `docs/rules/deprecated-key-files.md` in the vigiles repository; a path that
132
+ no longer exists is deleted) and show, per entry, where it goes and why.
133
+ Where each entry goes is the user's call; apply it once they confirm.
127
134
 
128
135
  ### Step 4: Compile
129
136
 
@@ -135,7 +142,7 @@ npx vigiles compile
135
142
 
136
143
  This regenerates the compiled instruction file(s). Review the output for any errors:
137
144
 
138
- - `stale-file` — a key file path doesn't exist
145
+ - `stale-file` — a `file()` (or deprecated `keyFiles`) path doesn't exist
139
146
  - `stale-command` — a command isn't in package.json
140
147
  - `invalid-rule` — a linter rule doesn't exist or is disabled
141
148
  - `section-has-header` — a section contains `#` headers (break into separate named sections)