vigiles 32.0.2 → 32.1.1
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/instruction-chain.js +36 -2
- package/dist/cli-flag-check.js +7 -1
- package/dist/cli-main.d.ts +2 -0
- package/dist/cli-main.js +38 -5
- package/dist/core/config-schema.d.ts +1 -0
- package/dist/core/config-schema.js +1 -0
- package/dist/core/hook-program.d.ts +67 -1
- package/dist/core/hook-program.js +73 -1
- package/dist/core/instruction-baseline.d.ts +139 -0
- package/dist/core/instruction-baseline.js +224 -0
- package/dist/core/instruction-weight.d.ts +17 -7
- package/dist/core/instruction-weight.js +25 -9
- package/dist/core/rule-meta.js +8 -0
- package/dist/core/types.d.ts +8 -0
- package/dist/coverage-probe.d.ts +15 -0
- package/dist/coverage-probe.js +29 -0
- package/dist/hook-check-install.d.ts +3 -0
- package/dist/hook-check-install.js +57 -0
- package/dist/hook-check.d.ts +36 -0
- package/dist/hook-check.js +125 -0
- package/dist/hook-install.d.ts +0 -36
- package/dist/hook-install.js +0 -38
- package/dist/hook-runtime.js +17 -24
- package/dist/instruction-weight-ratchet.d.ts +53 -0
- package/dist/instruction-weight-ratchet.js +132 -0
- package/dist/local-files.js +2 -0
- package/dist/scan.d.ts +10 -0
- package/dist/scan.js +30 -4
- package/dist/vigilesrc.schema.json +36 -0
- package/package.json +1 -1
package/dist/hook-install.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { DispatchKind } from "./core/hook-program.js";
|
|
2
1
|
/** The agnostic, committed home for hook SOURCE — one dir, cross-adapter. */
|
|
3
2
|
export declare const HOOKS_DIR = ".vigiles/hooks";
|
|
4
3
|
/** The committed home for registered context-provider SOURCE (v2). */
|
|
@@ -73,41 +72,6 @@ export declare function normalizeHookRef(hookPath: string, cwd?: string): string
|
|
|
73
72
|
* it can be given — see {@link hookGateRef} for the same fallback.
|
|
74
73
|
*/
|
|
75
74
|
export declare function hookRuntimeRef(projectRootTokens: readonly string[] | undefined): string;
|
|
76
|
-
/**
|
|
77
|
-
* What the shell must do when the runtime above CANNOT START — a missing
|
|
78
|
-
* `node_modules/vigiles`, an unreadable file, an interpreter that dies before a
|
|
79
|
-
* single line of ours runs. No code of ours executes in that case, so the policy
|
|
80
|
-
* has to be expressed in the emitted command or not at all.
|
|
81
|
-
*
|
|
82
|
-
* 🔴 THIS IS NOT A NEW POLICY. `runHookProgramCommand`'s load-failure branch has
|
|
83
|
-
* decided it since 2026-08: *"an inject's purpose is to ADD context, not to
|
|
84
|
-
* ENFORCE a decision … Gates (file, bash, prompt, stop) remain conservative and
|
|
85
|
-
* fail closed."* That branch only reaches failures that happen AFTER the runtime
|
|
86
|
-
* starts. This carries the same rule one layer out, to the failures that happen
|
|
87
|
-
* before it.
|
|
88
|
-
*
|
|
89
|
-
* WHY THE SPLIT, RATHER THAN ONE ANSWER FOR EVERYTHING — the two failures are
|
|
90
|
-
* not comparable:
|
|
91
|
-
*
|
|
92
|
-
* A GATE THAT SILENTLY PASSES IS WORSE THAN NO GATE. Its whole value is the
|
|
93
|
-
* refusal, and a harness that reports protection it is not providing is the
|
|
94
|
-
* one state worse than admitting it has none. So a gate whose runtime is
|
|
95
|
-
* missing exits 2: loud, blocking, and the cause is on stderr.
|
|
96
|
-
*
|
|
97
|
-
* A NUDGE THAT BLOCKS COSTS THE WHOLE REPOSITORY. Measured here 2026-08-10:
|
|
98
|
-
* merge-conflict markers in `package.json` stopped every hook loading, the
|
|
99
|
-
* Bash gate then refused `git merge --abort` — the one command that undoes the
|
|
100
|
-
* cause — and the session could not be repaired from inside. A reminder is
|
|
101
|
-
* never worth that, so a nudge exits 0 and says nothing it cannot say.
|
|
102
|
-
*
|
|
103
|
-
* The role is not a flag someone can flip: `Reaction` has no `deny` and an
|
|
104
|
-
* inject returns context, so "nudge" is a fact about the TYPE the author chose.
|
|
105
|
-
*
|
|
106
|
-
* (Industry does not agree on one answer either — husky and lefthook skip,
|
|
107
|
-
* pre-commit fails. Which is itself the argument for deciding by role instead
|
|
108
|
-
* of picking one and imposing it on both.)
|
|
109
|
-
*/
|
|
110
|
-
export declare function hookRuntimeMissingExit(kind: DispatchKind): 0 | 2;
|
|
111
75
|
export declare function hookGateRef(ref: string, projectRootTokens: readonly string[] | undefined): string;
|
|
112
76
|
/**
|
|
113
77
|
* Idempotently merge a compiled hook's block into an existing `settings.json`
|
package/dist/hook-install.js
CHANGED
|
@@ -5,7 +5,6 @@ exports.discoverHookFiles = discoverHookFiles;
|
|
|
5
5
|
exports.discoverProviderFiles = discoverProviderFiles;
|
|
6
6
|
exports.normalizeHookRef = normalizeHookRef;
|
|
7
7
|
exports.hookRuntimeRef = hookRuntimeRef;
|
|
8
|
-
exports.hookRuntimeMissingExit = hookRuntimeMissingExit;
|
|
9
8
|
exports.hookGateRef = hookGateRef;
|
|
10
9
|
exports.mergeHooksJson = mergeHooksJson;
|
|
11
10
|
exports.mergeHooksToml = mergeHooksToml;
|
|
@@ -122,43 +121,6 @@ function hookRuntimeRef(projectRootTokens) {
|
|
|
122
121
|
const token = projectRootTokens?.[0];
|
|
123
122
|
return token === undefined ? `node ${rel}` : `node "${token}/${rel}"`;
|
|
124
123
|
}
|
|
125
|
-
/**
|
|
126
|
-
* What the shell must do when the runtime above CANNOT START — a missing
|
|
127
|
-
* `node_modules/vigiles`, an unreadable file, an interpreter that dies before a
|
|
128
|
-
* single line of ours runs. No code of ours executes in that case, so the policy
|
|
129
|
-
* has to be expressed in the emitted command or not at all.
|
|
130
|
-
*
|
|
131
|
-
* 🔴 THIS IS NOT A NEW POLICY. `runHookProgramCommand`'s load-failure branch has
|
|
132
|
-
* decided it since 2026-08: *"an inject's purpose is to ADD context, not to
|
|
133
|
-
* ENFORCE a decision … Gates (file, bash, prompt, stop) remain conservative and
|
|
134
|
-
* fail closed."* That branch only reaches failures that happen AFTER the runtime
|
|
135
|
-
* starts. This carries the same rule one layer out, to the failures that happen
|
|
136
|
-
* before it.
|
|
137
|
-
*
|
|
138
|
-
* WHY THE SPLIT, RATHER THAN ONE ANSWER FOR EVERYTHING — the two failures are
|
|
139
|
-
* not comparable:
|
|
140
|
-
*
|
|
141
|
-
* A GATE THAT SILENTLY PASSES IS WORSE THAN NO GATE. Its whole value is the
|
|
142
|
-
* refusal, and a harness that reports protection it is not providing is the
|
|
143
|
-
* one state worse than admitting it has none. So a gate whose runtime is
|
|
144
|
-
* missing exits 2: loud, blocking, and the cause is on stderr.
|
|
145
|
-
*
|
|
146
|
-
* A NUDGE THAT BLOCKS COSTS THE WHOLE REPOSITORY. Measured here 2026-08-10:
|
|
147
|
-
* merge-conflict markers in `package.json` stopped every hook loading, the
|
|
148
|
-
* Bash gate then refused `git merge --abort` — the one command that undoes the
|
|
149
|
-
* cause — and the session could not be repaired from inside. A reminder is
|
|
150
|
-
* never worth that, so a nudge exits 0 and says nothing it cannot say.
|
|
151
|
-
*
|
|
152
|
-
* The role is not a flag someone can flip: `Reaction` has no `deny` and an
|
|
153
|
-
* inject returns context, so "nudge" is a fact about the TYPE the author chose.
|
|
154
|
-
*
|
|
155
|
-
* (Industry does not agree on one answer either — husky and lefthook skip,
|
|
156
|
-
* pre-commit fails. Which is itself the argument for deciding by role instead
|
|
157
|
-
* of picking one and imposing it on both.)
|
|
158
|
-
*/
|
|
159
|
-
function hookRuntimeMissingExit(kind) {
|
|
160
|
-
return kind === "inject" || kind === "react" ? 0 : 2;
|
|
161
|
-
}
|
|
162
124
|
function hookGateRef(ref, projectRootTokens) {
|
|
163
125
|
const token = projectRootTokens?.[0];
|
|
164
126
|
return token === undefined ? ref : `"${token}/${ref}"`;
|
package/dist/hook-runtime.js
CHANGED
|
@@ -432,12 +432,16 @@ async function runHookProgramCommand(file) {
|
|
|
432
432
|
// the author are different from a `deny`'s: name the real cause, and leave a
|
|
433
433
|
// way back.
|
|
434
434
|
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
435
|
+
// A hook that cannot be loaded always exits 2 here, whatever kind of hook it
|
|
436
|
+
// is. We cannot tell: the hook did not load, so there is nothing to ask
|
|
437
|
+
// whether it is a gate (can block) or a nudge (reminder only). This code used
|
|
438
|
+
// to guess from the file name (`file.includes("inject")`), and the guess
|
|
439
|
+
// disagreed with the `|| exit N` ending that `vigiles compile` writes on the
|
|
440
|
+
// command: a nudge without "inject" in its name blocked the session, and a
|
|
441
|
+
// gate with it let every command through (#312).
|
|
442
|
+
// Now only that ending decides. A nudge is wired `|| exit 0`, so the shell
|
|
443
|
+
// turns our 2 into a 0 and it degrades; a gate is wired `|| exit 2` and
|
|
444
|
+
// blocks. A command with no such ending is treated as a gate, the safe default.
|
|
441
445
|
//
|
|
442
446
|
// Escapes, both announced loudly on stderr:
|
|
443
447
|
// - the stale-stamp one (an edit to the hook itself / `vigiles compile`),
|
|
@@ -476,11 +480,13 @@ async function runHookProgramCommand(file) {
|
|
|
476
480
|
announceRepairEscape(file, cause);
|
|
477
481
|
return;
|
|
478
482
|
}
|
|
479
|
-
// Log the error, but for inject hooks, degrade gracefully (exit 0).
|
|
480
483
|
const errorMsg = `vigiles: hook ${file} ${cause}.\n` +
|
|
481
484
|
`vigiles: this is the state of the HARNESS, not a decision about your ` +
|
|
482
485
|
`command — the gate never ran. Blocking anyway (a gate that cannot run ` +
|
|
483
486
|
`must not pass traffic).\n` +
|
|
487
|
+
`vigiles: if this hook is a NUDGE (an inject or a react), it must not ` +
|
|
488
|
+
`block: its wiring should end in \`|| exit 0\`, which \`vigiles compile\` ` +
|
|
489
|
+
`writes. A hook wired by hand with no such ending is treated as a gate.\n` +
|
|
484
490
|
`vigiles: the way out is a FILE WRITE, not a command — under a tool that ` +
|
|
485
491
|
`WRITES (Write/Edit/MultiEdit); a Read of the same path repairs nothing ` +
|
|
486
492
|
`and is refused. Fix whichever of ` +
|
|
@@ -493,24 +499,11 @@ async function runHookProgramCommand(file) {
|
|
|
493
499
|
`repair this failure.\n` +
|
|
494
500
|
`vigiles: no command is allowed, deliberately. \`git merge --abort\` and ` +
|
|
495
501
|
`\`git checkout\` RUN \`.git/hooks/*\` (measured: reference-transaction, ` +
|
|
496
|
-
`post-checkout),
|
|
497
|
-
`resolver that just failed
|
|
502
|
+
`post-checkout), \`vigiles compile\` loads the hook through the same ` +
|
|
503
|
+
`resolver that just failed, and an install runs the lifecycle scripts of ` +
|
|
504
|
+
`every package in the lockfile.`;
|
|
498
505
|
console.error(errorMsg);
|
|
499
|
-
|
|
500
|
-
// a more robust approach would parse the stamp or metadata, but that requires
|
|
501
|
-
// the hook to load. Inject hooks typically have "inject" in the name; fall
|
|
502
|
-
// back to blocking (exit 2) for safety on gates.
|
|
503
|
-
const isLikelyInject = file.includes("inject");
|
|
504
|
-
if (isLikelyInject) {
|
|
505
|
-
// Inject hook: degrade gracefully. Log the error but don't wedge the session.
|
|
506
|
-
console.error(`vigiles: ${file} is an inject hook; degrading gracefully (no context injected).`);
|
|
507
|
-
process.exit(0);
|
|
508
|
-
}
|
|
509
|
-
else {
|
|
510
|
-
// Gate hook: fail closed.
|
|
511
|
-
process.exit(2);
|
|
512
|
-
}
|
|
513
|
-
return;
|
|
506
|
+
process.exit(2);
|
|
514
507
|
}
|
|
515
508
|
verifyStampOrRefuse(file, event);
|
|
516
509
|
switch ((0, hook_program_js_1.dispatchKind)(program)) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type InstructionBaseline, type ParsedBaseline } from "./core/instruction-baseline.js";
|
|
2
|
+
import type { InstructionWeight } from "./core/instruction-weight.js";
|
|
3
|
+
import type { RuleSeverity } from "./core/types.js";
|
|
4
|
+
/** The file on disk, read once: absent, parsed, or refused. */
|
|
5
|
+
export type BaselineOnDisk = {
|
|
6
|
+
readonly kind: "absent";
|
|
7
|
+
} | ParsedBaseline;
|
|
8
|
+
export declare function readInstructionBaseline(root: string): BaselineOnDisk;
|
|
9
|
+
/** One bundle's measurement: where it is, and its weight (`null` = nothing to weigh). */
|
|
10
|
+
export type MeasuredBundle = readonly [
|
|
11
|
+
at: string,
|
|
12
|
+
weight: InstructionWeight | null
|
|
13
|
+
];
|
|
14
|
+
/** Write the baseline for every measured bundle; returns what was written. */
|
|
15
|
+
export declare function writeInstructionBaseline(root: string, onDisk: BaselineOnDisk, measured: readonly MeasuredBundle[]): InstructionBaseline;
|
|
16
|
+
/**
|
|
17
|
+
* One line of lint output. `finding` counts toward the exit code; `ok` is the
|
|
18
|
+
* baseline holding; `note` is the opt-in nudge on a repo with no baseline.
|
|
19
|
+
*/
|
|
20
|
+
export interface RatchetLine {
|
|
21
|
+
readonly at: string;
|
|
22
|
+
readonly status: "finding" | "ok" | "note";
|
|
23
|
+
readonly text: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Pure: the lines a run prints, given what was measured and what is on disk.
|
|
27
|
+
*
|
|
28
|
+
* A bundle with no entry is a nudge while the repo has NO baseline file (it has
|
|
29
|
+
* not opted in, and a fresh repo must not fail on day one) — but a FINDING once
|
|
30
|
+
* the file exists: a bundle that appeared after the baseline was recorded is
|
|
31
|
+
* weight nobody signed off on.
|
|
32
|
+
*/
|
|
33
|
+
export declare function ratchetLines(onDisk: BaselineOnDisk, measured: readonly MeasuredBundle[]): readonly RatchetLine[];
|
|
34
|
+
/** Run the ratchet for `lint`: print, annotate, count. */
|
|
35
|
+
export declare function checkInstructionWeightRatchet(opts: {
|
|
36
|
+
readonly root: string;
|
|
37
|
+
readonly severity: RuleSeverity;
|
|
38
|
+
readonly update: boolean;
|
|
39
|
+
readonly silent: boolean;
|
|
40
|
+
/** Called only when the rule is on or an update was asked for. */
|
|
41
|
+
readonly measure: () => readonly MeasuredBundle[];
|
|
42
|
+
readonly annotate: (level: "error" | "warning", message: string) => void;
|
|
43
|
+
}): {
|
|
44
|
+
issues: number;
|
|
45
|
+
errors: number;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The line `compile` prints after writing instruction files: the weight NOW,
|
|
49
|
+
* against the baseline, at the moment the author changed it. Report-only —
|
|
50
|
+
* `lint` stays the single gate. `null` when there is nothing to weigh.
|
|
51
|
+
*/
|
|
52
|
+
export declare function compileWeightLine(root: string, weight: InstructionWeight | null): string | null;
|
|
53
|
+
//# sourceMappingURL=instruction-weight-ratchet.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readInstructionBaseline = readInstructionBaseline;
|
|
4
|
+
exports.writeInstructionBaseline = writeInstructionBaseline;
|
|
5
|
+
exports.ratchetLines = ratchetLines;
|
|
6
|
+
exports.checkInstructionWeightRatchet = checkInstructionWeightRatchet;
|
|
7
|
+
exports.compileWeightLine = compileWeightLine;
|
|
8
|
+
/**
|
|
9
|
+
* `lint`'s side of the instruction-weight ratchet: measure each scored bundle,
|
|
10
|
+
* compare it with `.vigiles/instruction-weight.json`, and — only under the
|
|
11
|
+
* explicit `--update-baseline` — write that file.
|
|
12
|
+
*
|
|
13
|
+
* The decisions (what a verdict is, what it says) live in
|
|
14
|
+
* `core/instruction-baseline.ts`; this module owns the two effects the core
|
|
15
|
+
* must not have, reading and writing the committed file, plus turning verdicts
|
|
16
|
+
* into lint counters.
|
|
17
|
+
*
|
|
18
|
+
* WHY THE FILE IS COMMITTED, unlike `.vigiles/coverage.json`. Coverage records
|
|
19
|
+
* what tests ran on THIS machine, so a committed copy would credit a checkout
|
|
20
|
+
* where nothing ran. The baseline records a property of the COMMIT: it is built
|
|
21
|
+
* from `committedTotal`, which already leaves out every per-machine file
|
|
22
|
+
* (`CLAUDE.local.md`, a gitignored settings sibling), so two clones of one
|
|
23
|
+
* commit compute byte-identical files. That is what lets CI compare against it.
|
|
24
|
+
*/
|
|
25
|
+
const node_fs_1 = require("node:fs");
|
|
26
|
+
const node_path_1 = require("node:path");
|
|
27
|
+
const instruction_baseline_js_1 = require("./core/instruction-baseline.js");
|
|
28
|
+
function readInstructionBaseline(root) {
|
|
29
|
+
const path = (0, node_path_1.join)(root, instruction_baseline_js_1.INSTRUCTION_BASELINE_FILE);
|
|
30
|
+
return (0, node_fs_1.existsSync)(path)
|
|
31
|
+
? (0, instruction_baseline_js_1.parseInstructionBaseline)((0, node_fs_1.readFileSync)(path, "utf-8"))
|
|
32
|
+
: { kind: "absent" };
|
|
33
|
+
}
|
|
34
|
+
/** Write the baseline for every measured bundle; returns what was written. */
|
|
35
|
+
function writeInstructionBaseline(root, onDisk, measured) {
|
|
36
|
+
const next = (0, instruction_baseline_js_1.recordEntries)(onDisk.kind === "ok" ? onDisk.baseline : undefined, measured.map(([at, w]) => [at, w === null ? null : (0, instruction_baseline_js_1.entryFor)(w)]));
|
|
37
|
+
const path = (0, node_path_1.join)(root, instruction_baseline_js_1.INSTRUCTION_BASELINE_FILE);
|
|
38
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
39
|
+
(0, node_fs_1.writeFileSync)(path, (0, instruction_baseline_js_1.serializeBaseline)(next));
|
|
40
|
+
return next;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Pure: the lines a run prints, given what was measured and what is on disk.
|
|
44
|
+
*
|
|
45
|
+
* A bundle with no entry is a nudge while the repo has NO baseline file (it has
|
|
46
|
+
* not opted in, and a fresh repo must not fail on day one) — but a FINDING once
|
|
47
|
+
* the file exists: a bundle that appeared after the baseline was recorded is
|
|
48
|
+
* weight nobody signed off on.
|
|
49
|
+
*/
|
|
50
|
+
function ratchetLines(onDisk, measured) {
|
|
51
|
+
if (onDisk.kind === "invalid") {
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
at: ".",
|
|
55
|
+
status: "finding",
|
|
56
|
+
text: `${instruction_baseline_js_1.INSTRUCTION_BASELINE_FILE} cannot be read (${onDisk.message}) — ` +
|
|
57
|
+
`re-record it with \`${instruction_baseline_js_1.UPDATE_BASELINE_COMMAND}\``,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
const recorded = onDisk.kind === "ok" ? onDisk.baseline.bundles : {};
|
|
62
|
+
return measured.flatMap(([at, weight]) => {
|
|
63
|
+
if (weight === null)
|
|
64
|
+
return [];
|
|
65
|
+
const verdict = (0, instruction_baseline_js_1.compareToBaseline)(weight, Object.hasOwn(recorded, at) ? recorded[at] : undefined);
|
|
66
|
+
const status = verdict.kind === "held"
|
|
67
|
+
? "ok"
|
|
68
|
+
: verdict.kind === "unrecorded" && onDisk.kind !== "ok"
|
|
69
|
+
? "note"
|
|
70
|
+
: "finding";
|
|
71
|
+
// An import named and not read is weight this number lacks — a second
|
|
72
|
+
// hop, or a path that does not resolve. Said on the line itself, because
|
|
73
|
+
// the gate holding is only as true as the sum it holds.
|
|
74
|
+
const unfollowed = weight.unreadImports.length > 0
|
|
75
|
+
? ` (${String(weight.unreadImports.length)} import(s) not followed: ${weight.unreadImports.join(", ")} — their size is not in this number)`
|
|
76
|
+
: "";
|
|
77
|
+
return [{ at, status, text: `${(0, instruction_baseline_js_1.formatVerdict)(verdict, at)}${unfollowed}` }];
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Run the ratchet for `lint`: print, annotate, count. */
|
|
81
|
+
function checkInstructionWeightRatchet(opts) {
|
|
82
|
+
const { severity, update, silent } = opts;
|
|
83
|
+
if (!severity && !update)
|
|
84
|
+
return { issues: 0, errors: 0 };
|
|
85
|
+
const measured = opts.measure();
|
|
86
|
+
const before = readInstructionBaseline(opts.root);
|
|
87
|
+
const onDisk = update
|
|
88
|
+
? {
|
|
89
|
+
kind: "ok",
|
|
90
|
+
baseline: writeInstructionBaseline(opts.root, before, measured),
|
|
91
|
+
}
|
|
92
|
+
: before;
|
|
93
|
+
const lines = severity ? ratchetLines(onDisk, measured) : [];
|
|
94
|
+
const findings = lines.filter((l) => l.status === "finding");
|
|
95
|
+
if (!silent && (update || lines.length > 0)) {
|
|
96
|
+
console.log("\nInstruction-weight ratchet:\n");
|
|
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
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const level = severity === "error" ? "error" : "warning";
|
|
109
|
+
findings.forEach((l) => {
|
|
110
|
+
opts.annotate(level, l.text);
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
issues: findings.length,
|
|
114
|
+
errors: severity === "error" ? findings.length : 0,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The line `compile` prints after writing instruction files: the weight NOW,
|
|
119
|
+
* against the baseline, at the moment the author changed it. Report-only —
|
|
120
|
+
* `lint` stays the single gate. `null` when there is nothing to weigh.
|
|
121
|
+
*/
|
|
122
|
+
function compileWeightLine(root, weight) {
|
|
123
|
+
if (weight === null)
|
|
124
|
+
return null;
|
|
125
|
+
const onDisk = readInstructionBaseline(root);
|
|
126
|
+
if (onDisk.kind === "invalid") {
|
|
127
|
+
return `always-loaded: ${instruction_baseline_js_1.INSTRUCTION_BASELINE_FILE} cannot be read (${onDisk.message})`;
|
|
128
|
+
}
|
|
129
|
+
const recorded = onDisk.kind === "ok" ? onDisk.baseline.bundles : {};
|
|
130
|
+
return (0, instruction_baseline_js_1.formatWeightLine)((0, instruction_baseline_js_1.compareToBaseline)(weight, Object.hasOwn(recorded, ".") ? recorded["."] : undefined));
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=instruction-weight-ratchet.js.map
|
package/dist/local-files.js
CHANGED
|
@@ -100,6 +100,7 @@ exports.LOCAL_FILES = [
|
|
|
100
100
|
*/
|
|
101
101
|
exports.COMMITTED_PATHS = [
|
|
102
102
|
"hooks", // hook sources + their stamps (`hook-install.ts`)
|
|
103
|
+
"hook-check.sh", // the start-of-session check, generated by compile (`hook-check.ts`)
|
|
103
104
|
"providers", // registered hook-context providers
|
|
104
105
|
"eval-locks", // committed eval staleness stamps (`eval-lock.ts`)
|
|
105
106
|
"eval-baseline.json", // the committed regression baseline (`eval-baseline.ts`)
|
|
@@ -107,6 +108,7 @@ exports.COMMITTED_PATHS = [
|
|
|
107
108
|
"schema.json", // YAML-LSP frontmatter schema (`vigiles init`)
|
|
108
109
|
"guards.json", // the declared guard set (`core/guards.ts`)
|
|
109
110
|
"action-gates.json", // declared action gates (`action-gate.ts`)
|
|
111
|
+
"instruction-weight.json", // the instruction-weight ratchet's baseline (`core/instruction-baseline.ts`)
|
|
110
112
|
];
|
|
111
113
|
/** The header comment on a `.vigiles/.gitignore` vigiles creates. */
|
|
112
114
|
/** The ignore file vigiles keeps inside `.vigiles/`. One spelling for every reader. */
|
package/dist/scan.d.ts
CHANGED
|
@@ -411,6 +411,16 @@ export interface ScanHarness {
|
|
|
411
411
|
readonly dialect: HarnessDialect;
|
|
412
412
|
readonly roots: readonly string[];
|
|
413
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* The always-loaded instruction weight of ONE directory under ONE harness —
|
|
416
|
+
* the bounded read, the harness's chain, and the sum, in one place.
|
|
417
|
+
*
|
|
418
|
+
* `scanPlugin` reports it, `lint`'s instruction-weight ratchet gates on it and
|
|
419
|
+
* `compile` prints it, so all three must arrive at the same number by the same
|
|
420
|
+
* route; this function is that route. `null` when the harness publishes no
|
|
421
|
+
* budget (there is no unit to count in).
|
|
422
|
+
*/
|
|
423
|
+
export declare function measureInstructionWeight(dir: string, harness: Pick<ScanHarness, "layout" | "dialect">): InstructionWeight | null;
|
|
414
424
|
/** Scan a plugin/repo directory and report its surfaces + structural issues. */
|
|
415
425
|
export declare function scanPlugin(dir: string, layout: PluginLayout, dialect: HarnessDialect, opts?: {
|
|
416
426
|
sharedDirs?: readonly string[];
|
package/dist/scan.js
CHANGED
|
@@ -27,6 +27,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
27
27
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
28
28
|
};
|
|
29
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.measureInstructionWeight = measureInstructionWeight;
|
|
30
31
|
exports.scanPlugin = scanPlugin;
|
|
31
32
|
exports.verifyLiveMcpTools = verifyLiveMcpTools;
|
|
32
33
|
exports.formatMcpContractReport = formatMcpContractReport;
|
|
@@ -153,6 +154,25 @@ function ownTestSignalOnDisk(dir) {
|
|
|
153
154
|
existsSync: node_fs_1.existsSync,
|
|
154
155
|
});
|
|
155
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* The always-loaded instruction weight of ONE directory under ONE harness —
|
|
159
|
+
* the bounded read, the harness's chain, and the sum, in one place.
|
|
160
|
+
*
|
|
161
|
+
* `scanPlugin` reports it, `lint`'s instruction-weight ratchet gates on it and
|
|
162
|
+
* `compile` prints it, so all three must arrive at the same number by the same
|
|
163
|
+
* route; this function is that route. `null` when the harness publishes no
|
|
164
|
+
* budget (there is no unit to count in).
|
|
165
|
+
*/
|
|
166
|
+
function measureInstructionWeight(dir, harness) {
|
|
167
|
+
// No `exclude` parameter, on purpose — see the note in `scanPlugin`.
|
|
168
|
+
return weighBoundedInstructions(harness, (0, surface_discovery_fs_js_1.boundedInstructionFiles)((0, node_path_1.resolve)(dir), harness.layout));
|
|
169
|
+
}
|
|
170
|
+
function weighBoundedInstructions(harness, instructionFiles) {
|
|
171
|
+
const budget = harness.dialect.instructionBudget;
|
|
172
|
+
return budget
|
|
173
|
+
? (0, instruction_weight_js_1.weighInstructions)(harness.layout.instructionChain(instructionFiles), instructionFiles, budget)
|
|
174
|
+
: null;
|
|
175
|
+
}
|
|
156
176
|
/** Scan a plugin/repo directory and report its surfaces + structural issues. */
|
|
157
177
|
function scanPlugin(dir, layout, dialect, opts = {}) {
|
|
158
178
|
const lay = layout;
|
|
@@ -195,7 +215,15 @@ function scanPlugin(dir, layout, dialect, opts = {}) {
|
|
|
195
215
|
// expanded an ADAPTER's globs by walking the whole tree; the bound and the
|
|
196
216
|
// classification are now separate jobs held by separate modules, and only the
|
|
197
217
|
// second is the adapter's. See `core/instruction-chain.ts`.
|
|
198
|
-
|
|
218
|
+
//
|
|
219
|
+
// 🔴 `exclude` DOES NOT REACH IT. `exclude` means "vigiles does not lint
|
|
220
|
+
// this"; the harness still loads the file, so hiding it from the weight
|
|
221
|
+
// under-reports — and since the weight is a committed baseline, one more
|
|
222
|
+
// `exclude` line would silently lower the ratchet. The harness's own
|
|
223
|
+
// `claudeMdExcludes` is the channel that removes a file, because it is also
|
|
224
|
+
// what stops the harness loading it. The browser twin never applied `exclude`
|
|
225
|
+
// here either, so this is also where the two engines agree.
|
|
226
|
+
const instructionFiles = (0, surface_discovery_fs_js_1.boundedInstructionFiles)((0, node_path_1.resolve)(dir), instructionHarness.layout);
|
|
199
227
|
const instructions = loaded.files[instructionFile] !== undefined
|
|
200
228
|
? {
|
|
201
229
|
file: instructionFile,
|
|
@@ -321,9 +349,7 @@ function scanPlugin(dir, layout, dialect, opts = {}) {
|
|
|
321
349
|
// unit (Claude Code counts 40 000 chars, Codex 32 768 bytes), so there is no
|
|
322
350
|
// meaningful sum across two; reporting the one that owns the file that
|
|
323
351
|
// exists is the only reading that is true of something.
|
|
324
|
-
instructionWeight: instructionHarness
|
|
325
|
-
? (0, instruction_weight_js_1.weighInstructions)(instructionHarness.layout.instructionChain(instructionFiles), instructionFiles, instructionHarness.dialect.instructionBudget)
|
|
326
|
-
: null,
|
|
352
|
+
instructionWeight: weighBoundedInstructions(instructionHarness, instructionFiles),
|
|
327
353
|
untested: coverage.untested.length,
|
|
328
354
|
untestedHarness: coverage.harness.untested.length,
|
|
329
355
|
unevaluated: coverage.evals.untested.length,
|
|
@@ -1519,6 +1519,42 @@
|
|
|
1519
1519
|
"const": true
|
|
1520
1520
|
}
|
|
1521
1521
|
]
|
|
1522
|
+
},
|
|
1523
|
+
"instruction-weight": {
|
|
1524
|
+
"anyOf": [
|
|
1525
|
+
{
|
|
1526
|
+
"type": "string",
|
|
1527
|
+
"const": "warn"
|
|
1528
|
+
},
|
|
1529
|
+
{
|
|
1530
|
+
"type": "string",
|
|
1531
|
+
"const": "error"
|
|
1532
|
+
},
|
|
1533
|
+
{
|
|
1534
|
+
"type": "boolean",
|
|
1535
|
+
"const": false
|
|
1536
|
+
},
|
|
1537
|
+
{
|
|
1538
|
+
"type": "string",
|
|
1539
|
+
"const": "off"
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
"type": "number",
|
|
1543
|
+
"const": 0
|
|
1544
|
+
},
|
|
1545
|
+
{
|
|
1546
|
+
"type": "number",
|
|
1547
|
+
"const": 1
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
"type": "number",
|
|
1551
|
+
"const": 2
|
|
1552
|
+
},
|
|
1553
|
+
{
|
|
1554
|
+
"type": "boolean",
|
|
1555
|
+
"const": true
|
|
1556
|
+
}
|
|
1557
|
+
]
|
|
1522
1558
|
}
|
|
1523
1559
|
},
|
|
1524
1560
|
"additionalProperties": false
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "32.
|
|
3
|
+
"version": "32.1.1",
|
|
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",
|