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.
@@ -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`
@@ -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}"`;
@@ -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
- // EXCEPTION: inject hooks. An inject's purpose is to ADD context, not to
436
- // ENFORCE a decision. If it fails to load, the session should degrade
437
- // gracefully (no context injected) rather than wedging the entire harness.
438
- // This is a harness failure, not a gating decision — so we handle it by
439
- // logging the error and exiting 0. Gates (file, bash, prompt, stop) remain
440
- // conservative and fail closed.
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), and \`vigiles compile\` loads the hook through the same ` +
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
- // Exit code depends on hook kind. This heuristic is based on the filename —
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
@@ -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
- const instructionFiles = (0, surface_discovery_fs_js_1.boundedInstructionFiles)((0, node_path_1.resolve)(dir), instructionHarness.layout, opts.excludes);
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.dialect.instructionBudget
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.0.2",
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",