vigiles 38.1.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/cli-main.js +1 -0
- package/dist/core/compile.js +7 -4
- package/dist/core/config-schema.d.ts +5 -1
- package/dist/core/config-schema.js +35 -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/types.d.ts +18 -1
- package/dist/instruction-weight-ratchet.d.ts +17 -4
- package/dist/instruction-weight-ratchet.js +71 -22
- package/dist/vigilesrc.schema.json +76 -26
- package/package.json +1 -1
|
@@ -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/cli-main.js
CHANGED
|
@@ -1777,6 +1777,7 @@ async function runLint(restArgs, flags, excludes, config) {
|
|
|
1777
1777
|
const instructionWeight = (0, instruction_weight_ratchet_js_1.checkInstructionWeightRatchet)({
|
|
1778
1778
|
root: frame.root,
|
|
1779
1779
|
severity: (0, types_js_1.ruleSeverity)(config?.rules?.["instruction-weight"]),
|
|
1780
|
+
max: (0, types_js_1.ruleOptions)(config?.rules?.["instruction-weight"])?.max,
|
|
1780
1781
|
update: flags.includes("--update-baseline"),
|
|
1781
1782
|
silent,
|
|
1782
1783
|
measure: () => lintRoots.map((abs) => [
|
package/dist/core/compile.js
CHANGED
|
@@ -39,6 +39,7 @@ const keyfiles_js_1 = require("./keyfiles.js");
|
|
|
39
39
|
const linters_js_1 = require("./linters.js");
|
|
40
40
|
const tool_contract_js_1 = require("./tool-contract.js");
|
|
41
41
|
const effects_js_1 = require("./effects.js");
|
|
42
|
+
const instruction_remedies_js_1 = require("./instruction-remedies.js");
|
|
42
43
|
const dialect_js_1 = require("./dialect.js");
|
|
43
44
|
// vigiles's default compile target when a spec names none and no dialect is
|
|
44
45
|
// injected — a product convention (vigiles emits CLAUDE.md by default), not a
|
|
@@ -474,9 +475,10 @@ function sectionBudget(sections, specFile, limits) {
|
|
|
474
475
|
.map(([name, body]) => ({
|
|
475
476
|
type: "section-too-large",
|
|
476
477
|
path: specFile,
|
|
477
|
-
message: `section ${JSON.stringify(name)} is ${
|
|
478
|
-
`(budget ${
|
|
479
|
-
`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,
|
|
480
482
|
}));
|
|
481
483
|
}
|
|
482
484
|
/** The whole Rules section, as one sum. */
|
|
@@ -493,7 +495,8 @@ function rulesBudget(input, specFile, limits) {
|
|
|
493
495
|
`(budget ${n(limits.rulesChars)}). Render a rule shorter instead of deleting it: ` +
|
|
494
496
|
`enforce(rule, why, { render: "id" }) when a linter already holds it, or ` +
|
|
495
497
|
`guidance(statement, { details: file(...) }) with the long text in that file — ` +
|
|
496
|
-
`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,
|
|
497
500
|
},
|
|
498
501
|
];
|
|
499
502
|
}
|
|
@@ -149,7 +149,11 @@ 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>]>>;
|
|
153
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">>>;
|
|
154
158
|
}, z.core.$strict>>;
|
|
155
159
|
files: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
@@ -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,7 @@ 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"),
|
|
185
196
|
"deprecated-key-files": severitySchema.default("warn"),
|
|
186
197
|
})
|
|
187
198
|
.strict();
|
|
@@ -491,7 +502,7 @@ function formatConfigIssues(issues) {
|
|
|
491
502
|
if (issue.code === "unrecognized_keys")
|
|
492
503
|
return unknownKeyLines(issue);
|
|
493
504
|
if (issue.code === "invalid_union")
|
|
494
|
-
return
|
|
505
|
+
return unionLines(issue);
|
|
495
506
|
return [
|
|
496
507
|
`.vigilesrc.json: ${pathLabel(issue.path ?? [])} — ${issue.message ?? "invalid"}.`,
|
|
497
508
|
];
|
|
@@ -515,6 +526,28 @@ function unknownKeyLines(issue) {
|
|
|
515
526
|
return `.vigilesrc.json: unknown key "${key}" in ${where}. Known: ${listCandidates(known)}.`;
|
|
516
527
|
});
|
|
517
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
|
+
}
|
|
518
551
|
/** ONE line for a failed union — never one per branch. */
|
|
519
552
|
function badValueLine(issue) {
|
|
520
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).
|
package/dist/core/types.d.ts
CHANGED
|
@@ -55,6 +55,19 @@ export interface CoverageThresholds {
|
|
|
55
55
|
/** Min % of npm scripts documented in spec commands. */
|
|
56
56
|
scripts?: number;
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Options for the `instruction-weight` rule: `["error", { max: 40000 }]`.
|
|
60
|
+
*/
|
|
61
|
+
export interface InstructionWeightConfig {
|
|
62
|
+
/**
|
|
63
|
+
* An opt-in ABSOLUTE ceiling on the always-loaded instruction weight, in the
|
|
64
|
+
* harness's own unit (characters for Claude Code, bytes for Codex). A total
|
|
65
|
+
* above it is a finding at the rule's severity regardless of the baseline, and
|
|
66
|
+
* `vigiles lint --update-baseline` refuses to record a baseline above it.
|
|
67
|
+
* Omitted → only the ratchet applies. A positive integer.
|
|
68
|
+
*/
|
|
69
|
+
max?: number;
|
|
70
|
+
}
|
|
58
71
|
/**
|
|
59
72
|
* Options for the orphan-docs check. The PRESENCE of this block in
|
|
60
73
|
* `.vigilesrc.json` OPTS THE REPO IN — the scan is off unless declared,
|
|
@@ -140,8 +153,12 @@ export interface RulesConfig {
|
|
|
140
153
|
* repo's own past, not a fixed budget. No baseline file → a one-line note,
|
|
141
154
|
* never a finding, so turning this on fails no repository on day one.
|
|
142
155
|
* `vigiles lint --update-baseline` records it. Default: "error".
|
|
156
|
+
*
|
|
157
|
+
* `["error", { max: 40000 }]` adds an opt-in absolute ceiling on top of the
|
|
158
|
+
* ratchet: a total above `max` is a finding whatever the baseline says, and
|
|
159
|
+
* the baseline cannot be re-recorded above it.
|
|
143
160
|
*/
|
|
144
|
-
"instruction-weight"?:
|
|
161
|
+
"instruction-weight"?: RuleWithOptions<InstructionWeightConfig>;
|
|
145
162
|
/**
|
|
146
163
|
* An instruction spec still uses the deprecated `keyFiles` field — a flat
|
|
147
164
|
* path → description list compiled into the file loaded on every request.
|
|
@@ -24,23 +24,35 @@ export interface RatchetLine {
|
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Pure: the lines a run prints, given what was measured and what is on disk.
|
|
27
|
+
* `max` is the opt-in absolute ceiling; a bundle over it adds a finding of its
|
|
28
|
+
* own, whatever the baseline says (including when there is none, or it is
|
|
29
|
+
* unreadable).
|
|
27
30
|
*
|
|
28
31
|
* A bundle with no entry is a nudge while the repo has NO baseline file (it has
|
|
29
32
|
* not opted in, and a fresh repo must not fail on day one) — but a FINDING once
|
|
30
33
|
* the file exists: a bundle that appeared after the baseline was recorded is
|
|
31
34
|
* weight nobody signed off on.
|
|
32
35
|
*/
|
|
33
|
-
export declare function ratchetLines(onDisk: BaselineOnDisk, measured: readonly MeasuredBundle[]): readonly RatchetLine[];
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
+
export declare function ratchetLines(onDisk: BaselineOnDisk, measured: readonly MeasuredBundle[], max?: number): readonly RatchetLine[];
|
|
37
|
+
/**
|
|
38
|
+
* The bundles `--update-baseline` must not record: each one over the ceiling,
|
|
39
|
+
* as the refusal text. Empty when there is no ceiling or nothing exceeds it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function updateRefusals(measured: readonly MeasuredBundle[], max: number | undefined): readonly string[];
|
|
42
|
+
/** The options {@link checkInstructionWeightRatchet} takes. */
|
|
43
|
+
interface RatchetOptions {
|
|
36
44
|
readonly root: string;
|
|
37
45
|
readonly severity: RuleSeverity;
|
|
46
|
+
/** The opt-in absolute ceiling (`["error", { max }]`); `undefined` = ratchet only. */
|
|
47
|
+
readonly max?: number;
|
|
38
48
|
readonly update: boolean;
|
|
39
49
|
readonly silent: boolean;
|
|
40
50
|
/** Called only when the rule is on or an update was asked for. */
|
|
41
51
|
readonly measure: () => readonly MeasuredBundle[];
|
|
42
52
|
readonly annotate: (level: "error" | "warning", message: string) => void;
|
|
43
|
-
}
|
|
53
|
+
}
|
|
54
|
+
/** Run the ratchet for `lint`: print, annotate, count. */
|
|
55
|
+
export declare function checkInstructionWeightRatchet(opts: RatchetOptions): {
|
|
44
56
|
issues: number;
|
|
45
57
|
errors: number;
|
|
46
58
|
};
|
|
@@ -50,4 +62,5 @@ export declare function checkInstructionWeightRatchet(opts: {
|
|
|
50
62
|
* `lint` stays the single gate. `null` when there is nothing to weigh.
|
|
51
63
|
*/
|
|
52
64
|
export declare function compileWeightLine(root: string, weight: InstructionWeight | null): string | null;
|
|
65
|
+
export {};
|
|
53
66
|
//# sourceMappingURL=instruction-weight-ratchet.d.ts.map
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.readInstructionBaseline = readInstructionBaseline;
|
|
4
4
|
exports.writeInstructionBaseline = writeInstructionBaseline;
|
|
5
5
|
exports.ratchetLines = ratchetLines;
|
|
6
|
+
exports.updateRefusals = updateRefusals;
|
|
6
7
|
exports.checkInstructionWeightRatchet = checkInstructionWeightRatchet;
|
|
7
8
|
exports.compileWeightLine = compileWeightLine;
|
|
8
9
|
/**
|
|
@@ -41,13 +42,29 @@ function writeInstructionBaseline(root, onDisk, measured) {
|
|
|
41
42
|
}
|
|
42
43
|
/**
|
|
43
44
|
* Pure: the lines a run prints, given what was measured and what is on disk.
|
|
45
|
+
* `max` is the opt-in absolute ceiling; a bundle over it adds a finding of its
|
|
46
|
+
* own, whatever the baseline says (including when there is none, or it is
|
|
47
|
+
* unreadable).
|
|
44
48
|
*
|
|
45
49
|
* A bundle with no entry is a nudge while the repo has NO baseline file (it has
|
|
46
50
|
* not opted in, and a fresh repo must not fail on day one) — but a FINDING once
|
|
47
51
|
* the file exists: a bundle that appeared after the baseline was recorded is
|
|
48
52
|
* weight nobody signed off on.
|
|
49
53
|
*/
|
|
50
|
-
function ratchetLines(onDisk, measured) {
|
|
54
|
+
function ratchetLines(onDisk, measured, max) {
|
|
55
|
+
return [
|
|
56
|
+
...baselineLines(onDisk, measured),
|
|
57
|
+
...measured.flatMap(([at, weight]) => ceilingLine(at, weight, max)),
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
/** One bundle's ceiling finding, or nothing when it is at or under `max`. */
|
|
61
|
+
function ceilingLine(at, weight, max) {
|
|
62
|
+
const over = weight === null ? undefined : (0, instruction_baseline_js_1.checkCeiling)(weight, max);
|
|
63
|
+
return over === undefined
|
|
64
|
+
? []
|
|
65
|
+
: [{ at, status: "finding", text: (0, instruction_baseline_js_1.formatCeiling)(over, at) }];
|
|
66
|
+
}
|
|
67
|
+
function baselineLines(onDisk, measured) {
|
|
51
68
|
if (onDisk.kind === "invalid") {
|
|
52
69
|
return [
|
|
53
70
|
{
|
|
@@ -77,41 +94,73 @@ function ratchetLines(onDisk, measured) {
|
|
|
77
94
|
return [{ at, status, text: `${(0, instruction_baseline_js_1.formatVerdict)(verdict, at)}${unfollowed}` }];
|
|
78
95
|
});
|
|
79
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* The bundles `--update-baseline` must not record: each one over the ceiling,
|
|
99
|
+
* as the refusal text. Empty when there is no ceiling or nothing exceeds it.
|
|
100
|
+
*/
|
|
101
|
+
function updateRefusals(measured, max) {
|
|
102
|
+
return measured.flatMap(([at, weight]) => {
|
|
103
|
+
const over = weight === null ? undefined : (0, instruction_baseline_js_1.checkCeiling)(weight, max);
|
|
104
|
+
return over === undefined ? [] : [(0, instruction_baseline_js_1.formatRefusal)(over, at)];
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* What `lint` compares against: the file as it is, or — under `--update-baseline`
|
|
109
|
+
* and when nothing is over the ceiling — the file just written. A baseline above
|
|
110
|
+
* the ceiling would bypass it (re-recording is how a ratchet gets loosened), so
|
|
111
|
+
* then nothing is written and the refusals are returned for the caller to fail on.
|
|
112
|
+
*/
|
|
113
|
+
function recordOrRead(opts, measured) {
|
|
114
|
+
const before = readInstructionBaseline(opts.root);
|
|
115
|
+
const refusals = opts.update ? updateRefusals(measured, opts.max) : [];
|
|
116
|
+
const record = opts.update && refusals.length === 0;
|
|
117
|
+
return {
|
|
118
|
+
refusals,
|
|
119
|
+
onDisk: record
|
|
120
|
+
? {
|
|
121
|
+
kind: "ok",
|
|
122
|
+
baseline: writeInstructionBaseline(opts.root, before, measured),
|
|
123
|
+
}
|
|
124
|
+
: before,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** The human-readable report: what was recorded, then one line per bundle. */
|
|
128
|
+
function printReport(lines, severity, recorded) {
|
|
129
|
+
console.log("\nInstruction-weight ratchet:\n");
|
|
130
|
+
if (recorded)
|
|
131
|
+
console.log(` ✓ recorded ${instruction_baseline_js_1.INSTRUCTION_BASELINE_FILE}`);
|
|
132
|
+
const marks = {
|
|
133
|
+
finding: severity === "error" ? "✗" : "⚠",
|
|
134
|
+
ok: "✓",
|
|
135
|
+
note: "ℹ",
|
|
136
|
+
};
|
|
137
|
+
lines.forEach((l) => {
|
|
138
|
+
console.log(` ${marks[l.status]} ${l.text}`);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
80
141
|
/** Run the ratchet for `lint`: print, annotate, count. */
|
|
81
142
|
function checkInstructionWeightRatchet(opts) {
|
|
82
143
|
const { severity, update, silent } = opts;
|
|
83
144
|
if (!severity && !update)
|
|
84
145
|
return { issues: 0, errors: 0 };
|
|
85
146
|
const measured = opts.measure();
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
: before;
|
|
93
|
-
const lines = severity ? ratchetLines(onDisk, measured) : [];
|
|
147
|
+
const { onDisk, refusals } = recordOrRead(opts, measured);
|
|
148
|
+
refusals.forEach((message) => {
|
|
149
|
+
console.error(`✗ ${message}`);
|
|
150
|
+
opts.annotate("error", message);
|
|
151
|
+
});
|
|
152
|
+
const lines = severity ? ratchetLines(onDisk, measured, opts.max) : [];
|
|
94
153
|
const findings = lines.filter((l) => l.status === "finding");
|
|
95
154
|
if (!silent && (update || lines.length > 0)) {
|
|
96
|
-
|
|
97
|
-
if (update)
|
|
98
|
-
console.log(` ✓ recorded ${instruction_baseline_js_1.INSTRUCTION_BASELINE_FILE}`);
|
|
99
|
-
const marks = {
|
|
100
|
-
finding: severity === "error" ? "✗" : "⚠",
|
|
101
|
-
ok: "✓",
|
|
102
|
-
note: "ℹ",
|
|
103
|
-
};
|
|
104
|
-
lines.forEach((l) => {
|
|
105
|
-
console.log(` ${marks[l.status]} ${l.text}`);
|
|
106
|
-
});
|
|
155
|
+
printReport(lines, severity, update && refusals.length === 0);
|
|
107
156
|
}
|
|
108
157
|
const level = severity === "error" ? "error" : "warning";
|
|
109
158
|
findings.forEach((l) => {
|
|
110
159
|
opts.annotate(level, l.text);
|
|
111
160
|
});
|
|
112
161
|
return {
|
|
113
|
-
issues: findings.length,
|
|
114
|
-
errors: severity === "error" ? findings.length : 0,
|
|
162
|
+
issues: findings.length + refusals.length,
|
|
163
|
+
errors: (severity === "error" ? findings.length : 0) + refusals.length,
|
|
115
164
|
};
|
|
116
165
|
}
|
|
117
166
|
/**
|
|
@@ -1751,36 +1751,86 @@
|
|
|
1751
1751
|
"instruction-weight": {
|
|
1752
1752
|
"anyOf": [
|
|
1753
1753
|
{
|
|
1754
|
-
"
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1754
|
+
"anyOf": [
|
|
1755
|
+
{
|
|
1756
|
+
"type": "string",
|
|
1757
|
+
"const": "warn"
|
|
1758
|
+
},
|
|
1759
|
+
{
|
|
1760
|
+
"type": "string",
|
|
1761
|
+
"const": "error"
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
"type": "boolean",
|
|
1765
|
+
"const": false
|
|
1766
|
+
},
|
|
1767
|
+
{
|
|
1768
|
+
"type": "string",
|
|
1769
|
+
"const": "off"
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
"type": "number",
|
|
1773
|
+
"const": 0
|
|
1774
|
+
},
|
|
1775
|
+
{
|
|
1776
|
+
"type": "number",
|
|
1777
|
+
"const": 1
|
|
1778
|
+
},
|
|
1779
|
+
{
|
|
1780
|
+
"type": "number",
|
|
1781
|
+
"const": 2
|
|
1782
|
+
},
|
|
1783
|
+
{
|
|
1784
|
+
"type": "boolean",
|
|
1785
|
+
"const": true
|
|
1786
|
+
}
|
|
1787
|
+
]
|
|
1776
1788
|
},
|
|
1777
1789
|
{
|
|
1778
|
-
"type": "
|
|
1779
|
-
"
|
|
1790
|
+
"type": "array",
|
|
1791
|
+
"prefixItems": [
|
|
1792
|
+
{
|
|
1793
|
+
"type": "string",
|
|
1794
|
+
"const": "warn"
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
"type": "object",
|
|
1798
|
+
"properties": {
|
|
1799
|
+
"max": {
|
|
1800
|
+
"type": "integer",
|
|
1801
|
+
"exclusiveMinimum": 0,
|
|
1802
|
+
"maximum": 9007199254740991
|
|
1803
|
+
}
|
|
1804
|
+
},
|
|
1805
|
+
"additionalProperties": false
|
|
1806
|
+
}
|
|
1807
|
+
],
|
|
1808
|
+
"items": false,
|
|
1809
|
+
"minItems": 2,
|
|
1810
|
+
"maxItems": 2
|
|
1780
1811
|
},
|
|
1781
1812
|
{
|
|
1782
|
-
"type": "
|
|
1783
|
-
"
|
|
1813
|
+
"type": "array",
|
|
1814
|
+
"prefixItems": [
|
|
1815
|
+
{
|
|
1816
|
+
"type": "string",
|
|
1817
|
+
"const": "error"
|
|
1818
|
+
},
|
|
1819
|
+
{
|
|
1820
|
+
"type": "object",
|
|
1821
|
+
"properties": {
|
|
1822
|
+
"max": {
|
|
1823
|
+
"type": "integer",
|
|
1824
|
+
"exclusiveMinimum": 0,
|
|
1825
|
+
"maximum": 9007199254740991
|
|
1826
|
+
}
|
|
1827
|
+
},
|
|
1828
|
+
"additionalProperties": false
|
|
1829
|
+
}
|
|
1830
|
+
],
|
|
1831
|
+
"items": false,
|
|
1832
|
+
"minItems": 2,
|
|
1833
|
+
"maxItems": 2
|
|
1784
1834
|
}
|
|
1785
1835
|
]
|
|
1786
1836
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "38.
|
|
3
|
+
"version": "38.2.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",
|