vigiles 38.0.0 → 38.2.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/adapters/claude-code/dialect.js +5 -1
- 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 +53 -10
- package/dist/core/CLAUDE.md.spec.js +0 -4
- package/dist/core/compile.js +15 -9
- package/dist/core/config-schema.d.ts +6 -1
- package/dist/core/config-schema.js +36 -2
- package/dist/core/instruction-baseline.d.ts +26 -0
- package/dist/core/instruction-baseline.js +45 -1
- package/dist/core/instruction-remedies.d.ts +13 -0
- package/dist/core/instruction-remedies.js +24 -0
- package/dist/core/instruction-weight.d.ts +6 -1
- 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 +26 -1
- package/dist/deprecated-key-files.d.ts +35 -0
- package/dist/deprecated-key-files.js +62 -0
- package/dist/instruction-weight-ratchet.d.ts +17 -4
- package/dist/instruction-weight-ratchet.js +71 -22
- 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 +86 -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
|
@@ -86,7 +86,11 @@ exports.claudeCodeDialect = {
|
|
|
86
86
|
unit: "chars",
|
|
87
87
|
limit: 40000,
|
|
88
88
|
onExceed: "warns",
|
|
89
|
-
|
|
89
|
+
// The CLI's warning scales with the model: per file max(40 000, window ×
|
|
90
|
+
// 0.05 × chars-per-token), all files max(120 000, that). 40 000 is its
|
|
91
|
+
// floor (a 200K-token window), kept as the budget on purpose: the weight is
|
|
92
|
+
// re-read on every request whatever the window. Read from the 2.1.296 binary.
|
|
93
|
+
capturedFrom: "claude-code 2.1.296 large-file warning, floor of max(40000, window*0.05*charsPerToken); the rules dir measured 2026-09-16 in a consumer repo",
|
|
90
94
|
},
|
|
91
95
|
// The capability table — what each event CARRIES and HONOURS. Nine of the 31
|
|
92
96
|
// events, each with its basis; see ./event-capability.ts. The three flat lists
|
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
|
|
@@ -1743,6 +1777,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1743
1777
|
const instructionWeight = (0, instruction_weight_ratchet_js_1.checkInstructionWeightRatchet)({
|
|
1744
1778
|
root: frame.root,
|
|
1745
1779
|
severity: (0, types_js_1.ruleSeverity)(config?.rules?.["instruction-weight"]),
|
|
1780
|
+
max: (0, types_js_1.ruleOptions)(config?.rules?.["instruction-weight"])?.max,
|
|
1746
1781
|
update: flags.includes("--update-baseline"),
|
|
1747
1782
|
silent,
|
|
1748
1783
|
measure: () => lintRoots.map((abs) => [
|
|
@@ -1753,6 +1788,17 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1753
1788
|
ghAnnotate(level, message);
|
|
1754
1789
|
},
|
|
1755
1790
|
});
|
|
1791
|
+
// 7w. Deprecated-key-files — an instruction spec still using `keyFiles`. Once
|
|
1792
|
+
// over the repository, like the ratchet: a nested bundle's spec would
|
|
1793
|
+
// otherwise be reported again by every bundle that encloses it.
|
|
1794
|
+
const deprecatedKeyFiles = (0, deprecated_key_files_js_1.checkDeprecatedKeyFiles)({
|
|
1795
|
+
severity: (0, types_js_1.ruleSeverity)(config?.rules?.["deprecated-key-files"]),
|
|
1796
|
+
find: () => specsUsingKeyFiles(process.cwd(), excludes.globIgnore),
|
|
1797
|
+
silent,
|
|
1798
|
+
annotate: (level, message, path) => {
|
|
1799
|
+
ghAnnotate(level, message, frame.repo((0, node_path_1.resolve)(process.cwd(), path)));
|
|
1800
|
+
},
|
|
1801
|
+
});
|
|
1756
1802
|
// 8. Validate vigiles builder calls inside markdown code blocks — the
|
|
1757
1803
|
// `doc-refs` rule, DEFAULT OFF. Illustrative blocks opt out via
|
|
1758
1804
|
// `<!-- vigiles:ignore -->` (single block) or `<!-- vigiles:ignore-file -->`
|
|
@@ -1884,6 +1930,8 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1884
1930
|
hookMatcherErrors: hookMatcher.errors,
|
|
1885
1931
|
instructionWeightIssues: instructionWeight.issues,
|
|
1886
1932
|
instructionWeightErrors: instructionWeight.errors,
|
|
1933
|
+
deprecatedKeyFilesIssues: deprecatedKeyFiles.issues,
|
|
1934
|
+
deprecatedKeyFilesErrors: deprecatedKeyFiles.errors,
|
|
1887
1935
|
// Only the "error" tier gates. At "warn" the findings are printed and
|
|
1888
1936
|
// annotated, and the exit code is untouched — same contract as every other
|
|
1889
1937
|
// opt-in rule here.
|
|
@@ -2516,6 +2564,9 @@ export default instructionFile({${targetLine}
|
|
|
2516
2564
|
// Prose sections become ## headings in the compiled output.
|
|
2517
2565
|
// Do not add # or ## headers inside sections.
|
|
2518
2566
|
// positioning: "What this project does and why.",
|
|
2567
|
+
// A file a decision depends on: name it where the decision is, with
|
|
2568
|
+
// file() inside a prose section — verified to exist at compile time. No
|
|
2569
|
+
// file map: the agent finds files itself, and a map loads on every request.
|
|
2519
2570
|
|
|
2520
2571
|
// This section is included in the compiled output to help agents
|
|
2521
2572
|
// understand how to work with specs. Remove it once your team is familiar.
|
|
@@ -2528,11 +2579,6 @@ export default instructionFile({${targetLine}
|
|
|
2528
2579
|
// "npm test": "Run all tests",
|
|
2529
2580
|
},
|
|
2530
2581
|
|
|
2531
|
-
keyFiles: {
|
|
2532
|
-
// File paths are verified to exist at compile time.
|
|
2533
|
-
// "src/index.ts": "Main entry point",
|
|
2534
|
-
},
|
|
2535
|
-
|
|
2536
2582
|
rules: {
|
|
2537
2583
|
// enforce() — backed by a linter rule, verified to exist AND be enabled:
|
|
2538
2584
|
// "no-console": enforce("eslint/no-console", "Use structured logger."),
|
|
@@ -7298,11 +7344,7 @@ async function main() {
|
|
|
7298
7344
|
// contradiction. Refuse it (exit 2 = "could not do what you asked") rather
|
|
7299
7345
|
// than auditing the first and dropping the rest — silently honouring half
|
|
7300
7346
|
// 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
|
-
}
|
|
7347
|
+
refuseSingleOverMany(single, dirs);
|
|
7306
7348
|
const targets = !single && market && market.onDisk.length > 0
|
|
7307
7349
|
? [...market.onDisk]
|
|
7308
7350
|
: dirs;
|
|
@@ -7392,6 +7434,7 @@ async function main() {
|
|
|
7392
7434
|
// (#281): cwd, unless the audited dir is somebody else's repository.
|
|
7393
7435
|
const frame = (0, frame_js_1.frameFor)(process.cwd(), root);
|
|
7394
7436
|
const report = (0, scan_js_1.scanPlugin)(targets[0], adapter.layout, adapter.dialect, {
|
|
7437
|
+
deprecatedKeyFiles: specsUsingKeyFiles(root, excludes.globIgnore),
|
|
7395
7438
|
sharedDirs: config.sharedDirs,
|
|
7396
7439
|
sharedDirsRoot: frame.root,
|
|
7397
7440
|
// `.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,9 +35,11 @@ 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");
|
|
42
|
+
const instruction_remedies_js_1 = require("./instruction-remedies.js");
|
|
41
43
|
const dialect_js_1 = require("./dialect.js");
|
|
42
44
|
// vigiles's default compile target when a spec names none and no dialect is
|
|
43
45
|
// injected — a product convention (vigiles emits CLAUDE.md by default), not a
|
|
@@ -473,9 +475,10 @@ function sectionBudget(sections, specFile, limits) {
|
|
|
473
475
|
.map(([name, body]) => ({
|
|
474
476
|
type: "section-too-large",
|
|
475
477
|
path: specFile,
|
|
476
|
-
message: `section ${JSON.stringify(name)} is ${
|
|
477
|
-
`(budget ${
|
|
478
|
-
`the normal shape of compiled prose — and every character is loaded on every request
|
|
478
|
+
message: `section ${JSON.stringify(name)} is ${n(body.length)} characters ` +
|
|
479
|
+
`(budget ${n(limits.sectionChars)}). The line guard cannot see this — long lines are ` +
|
|
480
|
+
`the normal shape of compiled prose — and every character is loaded on every request.\n` +
|
|
481
|
+
instruction_remedies_js_1.MOVE_OUT_OF_ALWAYS_LOADED,
|
|
479
482
|
}));
|
|
480
483
|
}
|
|
481
484
|
/** The whole Rules section, as one sum. */
|
|
@@ -492,7 +495,8 @@ function rulesBudget(input, specFile, limits) {
|
|
|
492
495
|
`(budget ${n(limits.rulesChars)}). Render a rule shorter instead of deleting it: ` +
|
|
493
496
|
`enforce(rule, why, { render: "id" }) when a linter already holds it, or ` +
|
|
494
497
|
`guidance(statement, { details: file(...) }) with the long text in that file — ` +
|
|
495
|
-
`the rule stays in the spec, where lint and audit read it
|
|
498
|
+
`the rule stays in the spec, where lint and audit read it.\n` +
|
|
499
|
+
instruction_remedies_js_1.MOVE_OUT_OF_ALWAYS_LOADED,
|
|
496
500
|
},
|
|
497
501
|
];
|
|
498
502
|
}
|
|
@@ -564,12 +568,12 @@ async function compileSectionsSection(spec, basePath, maxSectionLines) {
|
|
|
564
568
|
return { lines, errors };
|
|
565
569
|
}
|
|
566
570
|
function compileKeyFilesSection(spec, basePath) {
|
|
567
|
-
|
|
571
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- keyFiles is deprecated, not removed: it still compiles until the next major
|
|
572
|
+
const keyFiles = spec.keyFiles;
|
|
573
|
+
if (!keyFiles)
|
|
568
574
|
return { lines: [], errors: [] };
|
|
569
|
-
const lines = ["## Key Files", ""];
|
|
570
575
|
const errors = [];
|
|
571
|
-
for (const [filePath, desc] of Object.entries(
|
|
572
|
-
lines.push(`- \`${filePath}\` — ${desc}`);
|
|
576
|
+
for (const [filePath, desc] of Object.entries(keyFiles)) {
|
|
573
577
|
const err = validateFileRef(filePath, basePath);
|
|
574
578
|
if (err)
|
|
575
579
|
errors.push(err);
|
|
@@ -580,7 +584,9 @@ function compileKeyFilesSection(spec, basePath) {
|
|
|
580
584
|
});
|
|
581
585
|
}
|
|
582
586
|
}
|
|
583
|
-
|
|
587
|
+
// The one renderer the `deprecated-key-files` rule measures with, so the
|
|
588
|
+
// size a finding reports is the size of what this emits.
|
|
589
|
+
return { lines: [(0, keyfiles_js_1.renderKeyFiles)(keyFiles)], errors };
|
|
584
590
|
}
|
|
585
591
|
function compileCommandsSection(spec, basePath) {
|
|
586
592
|
if (!spec.commands)
|
|
@@ -149,7 +149,12 @@ export declare const vigilesConfigSchema: z.ZodObject<{
|
|
|
149
149
|
"hook-block-ineffective": 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">>>;
|
|
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
|
-
"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"
|
|
152
|
+
"instruction-weight": z.ZodDefault<z.ZodUnion<readonly [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">>, z.ZodTuple<[z.ZodLiteral<"warn">, z.ZodObject<{
|
|
153
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
154
|
+
}, z.core.$strict>], null>, z.ZodTuple<[z.ZodLiteral<"error">, z.ZodObject<{
|
|
155
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
156
|
+
}, z.core.$strict>], null>]>>;
|
|
157
|
+
"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
158
|
}, z.core.$strict>>;
|
|
154
159
|
files: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
155
160
|
maxRules: z.ZodOptional<z.ZodNumber>;
|
|
@@ -122,6 +122,17 @@ const testCoverageConfig = zod_1.z
|
|
|
122
122
|
testExtension: zod_1.z.string().optional(),
|
|
123
123
|
})
|
|
124
124
|
.strict();
|
|
125
|
+
/**
|
|
126
|
+
* Options for `instruction-weight`. `max` is an opt-in ABSOLUTE ceiling on the
|
|
127
|
+
* always-loaded total, in the harness's own unit — independent of the ratchet's
|
|
128
|
+
* baseline. A positive integer: `0` would fail every repo that has an
|
|
129
|
+
* instruction file, and `"40000"` is a string the author meant as a number.
|
|
130
|
+
*/
|
|
131
|
+
const instructionWeightConfig = zod_1.z
|
|
132
|
+
.object({
|
|
133
|
+
max: zod_1.z.number().int().positive().optional(),
|
|
134
|
+
})
|
|
135
|
+
.strict();
|
|
125
136
|
/**
|
|
126
137
|
* Thresholds for `compile-budget`, in characters. Each one left out keeps its
|
|
127
138
|
* built-in default; `0` turns that one budget off.
|
|
@@ -181,7 +192,8 @@ const rulesSchema = zod_1.z
|
|
|
181
192
|
"hook-block-ineffective": severitySchema.default("warn"),
|
|
182
193
|
"hook-matcher": severitySchema.default("warn"),
|
|
183
194
|
"doc-refs": severitySchema.default(false),
|
|
184
|
-
"instruction-weight":
|
|
195
|
+
"instruction-weight": withOptions(instructionWeightConfig).default("error"),
|
|
196
|
+
"deprecated-key-files": severitySchema.default("warn"),
|
|
185
197
|
})
|
|
186
198
|
.strict();
|
|
187
199
|
/** The rule names, for the "did you mean" on an unknown one. */
|
|
@@ -490,7 +502,7 @@ function formatConfigIssues(issues) {
|
|
|
490
502
|
if (issue.code === "unrecognized_keys")
|
|
491
503
|
return unknownKeyLines(issue);
|
|
492
504
|
if (issue.code === "invalid_union")
|
|
493
|
-
return
|
|
505
|
+
return unionLines(issue);
|
|
494
506
|
return [
|
|
495
507
|
`.vigilesrc.json: ${pathLabel(issue.path ?? [])} — ${issue.message ?? "invalid"}.`,
|
|
496
508
|
];
|
|
@@ -514,6 +526,28 @@ function unknownKeyLines(issue) {
|
|
|
514
526
|
return `.vigilesrc.json: unknown key "${key}" in ${where}. Known: ${listCandidates(known)}.`;
|
|
515
527
|
});
|
|
516
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* A failed union: the one line listing what the key accepts — UNLESS a branch
|
|
531
|
+
* got past the union's own position and failed INSIDE its options
|
|
532
|
+
* (`["error", {"max": 1.5}]` matches the tuple's shape and fails at index 1).
|
|
533
|
+
* That inner issue is the problem the author has; "not one of the accepted
|
|
534
|
+
* values" would send them to re-read a list that was never wrong. Zod already
|
|
535
|
+
* collapses an inner failure of a non-type kind (`max: 0` → "Too small"); a type
|
|
536
|
+
* failure (`1.5`, `"4"`) is the case it leaves as a bare union.
|
|
537
|
+
*
|
|
538
|
+
* "Inside" means a path of two or more: index 0 of a `[severity, options]`
|
|
539
|
+
* tuple is the severity itself, and a wrong severity (`["errr", {}]`) is still
|
|
540
|
+
* the accepted-values line. Both tuple branches (`warn` / `error`) report the
|
|
541
|
+
* same inner issue, so it is de-duplicated.
|
|
542
|
+
*/
|
|
543
|
+
function unionLines(issue) {
|
|
544
|
+
const here = issue.path ?? [];
|
|
545
|
+
const inner = (issue.errors ?? [])
|
|
546
|
+
.flat()
|
|
547
|
+
.filter((e) => (e.path ?? []).length >= 2 && e.code !== "invalid_union")
|
|
548
|
+
.map((e) => `.vigilesrc.json: ${pathLabel([...here, ...(e.path ?? [])])} — ${e.message ?? "invalid"}.`);
|
|
549
|
+
return inner.length === 0 ? [badValueLine(issue)] : [...new Set(inner)];
|
|
550
|
+
}
|
|
517
551
|
/** ONE line for a failed union — never one per branch. */
|
|
518
552
|
function badValueLine(issue) {
|
|
519
553
|
const accepted = acceptedValues(issue);
|
|
@@ -127,12 +127,38 @@ export type RatchetVerdict = {
|
|
|
127
127
|
readonly changes: readonly FileChange[];
|
|
128
128
|
};
|
|
129
129
|
export declare function compareToBaseline(weight: InstructionWeight, recorded: BaselineEntry | undefined): RatchetVerdict;
|
|
130
|
+
/** One file's size, for naming where the weight is. */
|
|
131
|
+
export interface FileWeight {
|
|
132
|
+
readonly path: string;
|
|
133
|
+
readonly size: number;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A total above the opt-in absolute ceiling (`["error", { max }]`).
|
|
137
|
+
*
|
|
138
|
+
* Decided on the same number the ratchet records — `committedTotal`, the SCORED
|
|
139
|
+
* files only — and in the same unit, so the two can never disagree about what
|
|
140
|
+
* the weight is. It does not look at the baseline at all: that independence is
|
|
141
|
+
* what makes it a ceiling and not a second ratchet.
|
|
142
|
+
*/
|
|
143
|
+
export interface CeilingVerdict {
|
|
144
|
+
readonly total: number;
|
|
145
|
+
readonly max: number;
|
|
146
|
+
readonly unit: BaselineEntry["unit"];
|
|
147
|
+
/** The scored files, heaviest first — where the cut has to come from. */
|
|
148
|
+
readonly largest: readonly FileWeight[];
|
|
149
|
+
}
|
|
150
|
+
/** `undefined` when the weight is at or under `max`, or when no ceiling is set. */
|
|
151
|
+
export declare function checkCeiling(weight: InstructionWeight, max: number | undefined): CeilingVerdict | undefined;
|
|
130
152
|
/** Replace the measured bundles' entries; `null` removes one. Others are kept. */
|
|
131
153
|
export declare function recordEntries(previous: InstructionBaseline | undefined, measured: readonly (readonly [string, BaselineEntry | null])[]): InstructionBaseline;
|
|
132
154
|
/** Stable text: sorted keys, two-space indent, trailing newline, no timestamp. */
|
|
133
155
|
export declare function serializeBaseline(baseline: InstructionBaseline): string;
|
|
134
156
|
/** The one-paragraph message a verdict prints as, prefixed by its bundle. */
|
|
135
157
|
export declare function formatVerdict(verdict: RatchetVerdict, at: string): string;
|
|
158
|
+
/** The message for a total over the ceiling, prefixed by its bundle. */
|
|
159
|
+
export declare function formatCeiling(verdict: CeilingVerdict, at: string): string;
|
|
160
|
+
/** Why `--update-baseline` wrote nothing: the ceiling it would have been recorded above. */
|
|
161
|
+
export declare function formatRefusal(verdict: CeilingVerdict, at: string): string;
|
|
136
162
|
/** The compile-time one-liner: weight now, against the baseline when there is one. */
|
|
137
163
|
export declare function formatWeightLine(verdict: RatchetVerdict): string;
|
|
138
164
|
export {};
|
|
@@ -4,9 +4,12 @@ exports.INSTRUCTION_WEIGHT_MEASURE = exports.INSTRUCTION_BASELINE_VERSION = expo
|
|
|
4
4
|
exports.parseInstructionBaseline = parseInstructionBaseline;
|
|
5
5
|
exports.entryFor = entryFor;
|
|
6
6
|
exports.compareToBaseline = compareToBaseline;
|
|
7
|
+
exports.checkCeiling = checkCeiling;
|
|
7
8
|
exports.recordEntries = recordEntries;
|
|
8
9
|
exports.serializeBaseline = serializeBaseline;
|
|
9
10
|
exports.formatVerdict = formatVerdict;
|
|
11
|
+
exports.formatCeiling = formatCeiling;
|
|
12
|
+
exports.formatRefusal = formatRefusal;
|
|
10
13
|
exports.formatWeightLine = formatWeightLine;
|
|
11
14
|
/**
|
|
12
15
|
* The instruction-weight RATCHET: the always-loaded weight may not grow past
|
|
@@ -46,6 +49,7 @@ exports.formatWeightLine = formatWeightLine;
|
|
|
46
49
|
* is the caller's (`src/instruction-weight-ratchet.ts`).
|
|
47
50
|
*/
|
|
48
51
|
const zod_1 = require("zod");
|
|
52
|
+
const instruction_remedies_js_1 = require("./instruction-remedies.js");
|
|
49
53
|
/** Where the baseline lives, relative to the directory `.vigilesrc.json` is read from. */
|
|
50
54
|
exports.INSTRUCTION_BASELINE_FILE = ".vigiles/instruction-weight.json";
|
|
51
55
|
/** The command that writes the baseline — named once, quoted by every message. */
|
|
@@ -153,6 +157,25 @@ function compareToBaseline(weight, recorded) {
|
|
|
153
157
|
changes: changesBetween(recorded.files, current.files),
|
|
154
158
|
};
|
|
155
159
|
}
|
|
160
|
+
/** How many of the heaviest files a ceiling message names. */
|
|
161
|
+
const CEILING_NAMED_FILES = 5;
|
|
162
|
+
/** `undefined` when the weight is at or under `max`, or when no ceiling is set. */
|
|
163
|
+
function checkCeiling(weight, max) {
|
|
164
|
+
if (max === undefined)
|
|
165
|
+
return undefined;
|
|
166
|
+
const { total, unit, files } = entryFor(weight);
|
|
167
|
+
if (total <= max)
|
|
168
|
+
return undefined;
|
|
169
|
+
return {
|
|
170
|
+
total,
|
|
171
|
+
max,
|
|
172
|
+
unit,
|
|
173
|
+
largest: Object.entries(files)
|
|
174
|
+
.map(([path, size]) => ({ path, size }))
|
|
175
|
+
.sort((a, b) => b.size - a.size || a.path.localeCompare(b.path))
|
|
176
|
+
.slice(0, CEILING_NAMED_FILES),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
156
179
|
/** Replace the measured bundles' entries; `null` removes one. Others are kept. */
|
|
157
180
|
function recordEntries(previous, measured) {
|
|
158
181
|
const removed = new Set(measured.filter(([, e]) => e === null).map(([at]) => at));
|
|
@@ -199,12 +222,33 @@ function formatVerdict(verdict, at) {
|
|
|
199
222
|
: "";
|
|
200
223
|
return verdict.kind === "grew"
|
|
201
224
|
? `${where}always-loaded instructions grew ${head} over the recorded baseline.${moved} ` +
|
|
202
|
-
`If the growth is intended, record it with \`${exports.UPDATE_BASELINE_COMMAND}\` — the diff of ${exports.INSTRUCTION_BASELINE_FILE} is the review
|
|
225
|
+
`If the growth is intended, record it with \`${exports.UPDATE_BASELINE_COMMAND}\` — the diff of ${exports.INSTRUCTION_BASELINE_FILE} is the review. If it is not:\n${instruction_remedies_js_1.MOVE_OUT_OF_ALWAYS_LOADED}`
|
|
203
226
|
: `${where}always-loaded instructions shrank ${head}; the baseline still allows the old weight.${moved} ` +
|
|
204
227
|
`Lock the cut in with \`${exports.UPDATE_BASELINE_COMMAND}\`, or the headroom can be regrown without a finding.`;
|
|
205
228
|
}
|
|
206
229
|
}
|
|
207
230
|
}
|
|
231
|
+
/** The message for a total over the ceiling, prefixed by its bundle. */
|
|
232
|
+
function formatCeiling(verdict, at) {
|
|
233
|
+
const where = at === "." ? "" : `${at}: `;
|
|
234
|
+
const biggest = verdict.largest
|
|
235
|
+
.map((f) => `${f.path} ${n(f.size)}`)
|
|
236
|
+
.join(", ");
|
|
237
|
+
return (`${where}always-loaded instructions are ${n(verdict.total)} ${verdict.unit}, ` +
|
|
238
|
+
`max ${n(verdict.max)} — ${n(verdict.total - verdict.max)} over. ` +
|
|
239
|
+
`Largest: ${biggest}.\n` +
|
|
240
|
+
`${instruction_remedies_js_1.MOVE_OUT_OF_ALWAYS_LOADED}\n` +
|
|
241
|
+
`Moving text to another always-loaded file does not help, ` +
|
|
242
|
+
`and re-recording the baseline does not lift the ceiling.`);
|
|
243
|
+
}
|
|
244
|
+
/** Why `--update-baseline` wrote nothing: the ceiling it would have been recorded above. */
|
|
245
|
+
function formatRefusal(verdict, at) {
|
|
246
|
+
const where = at === "." ? "" : `${at}: `;
|
|
247
|
+
return (`${where}refusing to record ${exports.INSTRUCTION_BASELINE_FILE}: ` +
|
|
248
|
+
`${n(verdict.total)} ${verdict.unit} is over the configured max ${n(verdict.max)} ` +
|
|
249
|
+
`(${n(verdict.total - verdict.max)} over). A baseline above the ceiling would bypass it — ` +
|
|
250
|
+
`get under it first, then run \`${exports.UPDATE_BASELINE_COMMAND}\` again.\n${instruction_remedies_js_1.MOVE_OUT_OF_ALWAYS_LOADED}`);
|
|
251
|
+
}
|
|
208
252
|
/** The compile-time one-liner: weight now, against the baseline when there is one. */
|
|
209
253
|
function formatWeightLine(verdict) {
|
|
210
254
|
switch (verdict.kind) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where always-loaded text can go instead of being deleted.
|
|
3
|
+
*
|
|
4
|
+
* Every finding that says "too much is loaded on every request" — the weight
|
|
5
|
+
* ceiling, the ratchet's growth, the refusal to record a heavier baseline, a
|
|
6
|
+
* section or the whole Rules section over its `compile-budget` — ends with
|
|
7
|
+
* this list. Without it the one move the message offers is "cut content", and
|
|
8
|
+
* the reader deletes rules that still matter instead of moving each one to the
|
|
9
|
+
* carrier that loads it only when it applies. The order is the order to try: the cheapest carrier that
|
|
10
|
+
* still fires at the right moment first, deletion last.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MOVE_OUT_OF_ALWAYS_LOADED: string;
|
|
13
|
+
//# sourceMappingURL=instruction-remedies.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MOVE_OUT_OF_ALWAYS_LOADED = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Where always-loaded text can go instead of being deleted.
|
|
6
|
+
*
|
|
7
|
+
* Every finding that says "too much is loaded on every request" — the weight
|
|
8
|
+
* ceiling, the ratchet's growth, the refusal to record a heavier baseline, a
|
|
9
|
+
* section or the whole Rules section over its `compile-budget` — ends with
|
|
10
|
+
* this list. Without it the one move the message offers is "cut content", and
|
|
11
|
+
* the reader deletes rules that still matter instead of moving each one to the
|
|
12
|
+
* carrier that loads it only when it applies. The order is the order to try: the cheapest carrier that
|
|
13
|
+
* still fires at the right moment first, deletion last.
|
|
14
|
+
*/
|
|
15
|
+
exports.MOVE_OUT_OF_ALWAYS_LOADED = [
|
|
16
|
+
"Move what does not apply to every request:",
|
|
17
|
+
" - a rule for one folder → that folder's nested CLAUDE.md or AGENTS.md (loads only there)",
|
|
18
|
+
" - a step tied to one action → a hook",
|
|
19
|
+
" - a procedure for one kind of task → a skill",
|
|
20
|
+
" - a fact a machine can check → a lint rule or a test, then delete the prose",
|
|
21
|
+
" - measurements and history → docs, linked, not loaded",
|
|
22
|
+
" - text another file already says → delete it",
|
|
23
|
+
].join("\n");
|
|
24
|
+
//# sourceMappingURL=instruction-remedies.js.map
|
|
@@ -41,7 +41,12 @@ import type { InstructionChain, InstructionRole, InstructionScope } from "./inst
|
|
|
41
41
|
export interface InstructionBudget {
|
|
42
42
|
/** Claude Code counts characters; Codex counts bytes. Never tokens. */
|
|
43
43
|
readonly unit: "chars" | "bytes";
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* The budget, in `unit`. For Codex it is the harness's own cut-off. For Claude
|
|
46
|
+
* Code it is the harness's FLOOR: the CLI warns per file at 5% of the model's
|
|
47
|
+
* context window in characters (never below 40 000) and on the sum at 120 000
|
|
48
|
+
* or that per-file limit, so on a 1M-token model it warns far later than this.
|
|
49
|
+
*/
|
|
45
50
|
readonly limit: number;
|
|
46
51
|
/**
|
|
47
52
|
* `warns` — the instructions still reach the model (Claude Code).
|
|
@@ -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
|