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.
- package/dist/audit-report.d.ts +8 -0
- package/dist/audit-report.js +12 -3
- package/dist/cli-main.d.ts +7 -0
- package/dist/cli-main.js +52 -10
- package/dist/core/CLAUDE.md.spec.js +0 -4
- package/dist/core/compile.js +8 -5
- package/dist/core/config-schema.d.ts +1 -0
- package/dist/core/config-schema.js +1 -0
- package/dist/core/keyfiles-source.d.ts +9 -0
- package/dist/core/keyfiles-source.js +88 -0
- package/dist/core/keyfiles.d.ts +64 -0
- package/dist/core/keyfiles.js +93 -0
- package/dist/core/orphans.js +1 -1
- package/dist/core/rule-meta.js +9 -0
- package/dist/core/session.d.ts +7 -0
- package/dist/core/session.js +8 -0
- package/dist/core/spec.d.ts +14 -1
- package/dist/core/types.d.ts +8 -0
- package/dist/deprecated-key-files.d.ts +35 -0
- package/dist/deprecated-key-files.js +62 -0
- package/dist/research-index.d.ts +6 -6
- package/dist/research-index.js +6 -6
- package/dist/scan.d.ts +21 -0
- package/dist/scan.js +29 -2
- package/dist/setup-plan.d.ts +1 -1
- package/dist/setup-plan.js +3 -0
- package/dist/vigilesrc.schema.json +36 -0
- package/hooks/pre-edit.sh +1 -1
- package/package.json +1 -1
- package/skills/adopt-spec/SKILL.md +7 -12
- package/skills/edit-spec/SKILL.md +32 -25
package/dist/audit-report.d.ts
CHANGED
|
@@ -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.
|
package/dist/audit-report.js
CHANGED
|
@@ -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
|
|
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
|
package/dist/cli-main.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
},
|
package/dist/core/compile.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
package/dist/core/orphans.js
CHANGED
|
@@ -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,
|
|
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
|
package/dist/core/rule-meta.js
CHANGED
|
@@ -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",
|
package/dist/core/session.d.ts
CHANGED
|
@@ -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. */
|
package/dist/core/session.js
CHANGED
|
@@ -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 = [];
|
package/dist/core/spec.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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>;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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
|
package/dist/research-index.d.ts
CHANGED
|
@@ -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
|
-
* `
|
|
5
|
-
* already verifies the OTHER direction (every
|
|
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
|
|
21
|
-
*
|
|
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 `
|
|
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
|
*/
|
package/dist/research-index.js
CHANGED
|
@@ -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
|
-
* `
|
|
6
|
-
* already verifies the OTHER direction (every
|
|
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
|
|
26
|
-
*
|
|
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 `
|
|
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
|
-
|
|
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 = {
|
package/dist/setup-plan.d.ts
CHANGED
|
@@ -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
|
package/dist/setup-plan.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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,
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
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,
|
|
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
|
-
- "
|
|
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
|
-
//
|
|
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
|
|
110
|
-
|
|
111
|
-
- Add to `
|
|
112
|
-
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
file
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
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)
|