vigiles 26.2.0 → 27.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit-score.js +1 -1
- package/dist/cli-commands.d.ts +1 -1
- package/dist/cli-commands.js +1 -1
- package/dist/cli-main.d.ts +146 -0
- package/dist/cli-main.js +6811 -0
- package/dist/cli.d.ts +42 -126
- package/dist/cli.js +76 -7278
- package/dist/core/bash-effects.js +44 -29
- package/dist/core/compile.d.ts +0 -6
- package/dist/core/compile.js +16 -6
- package/dist/core/hook-program.d.ts +0 -6
- package/dist/core/hook-program.js +48 -7
- package/dist/core/linters.d.ts +11 -2
- package/dist/core/linters.js +12 -3
- package/dist/core/types.d.ts +4 -2
- package/dist/coverage-evidence.d.ts +1 -1
- package/dist/coverage-evidence.js +2 -2
- package/dist/exclude.js +8 -72
- package/dist/hook-install.js +8 -2
- package/dist/hook-runtime.d.ts +63 -0
- package/dist/hook-runtime.js +540 -0
- package/dist/hook.d.ts +1 -1
- package/dist/hook.js +2 -3
- package/dist/test-coverage.d.ts +3 -3
- package/dist/test-coverage.js +7 -7
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compiled-hook RUNTIME — `vigiles hook-runtime run-program <file>`, the
|
|
3
|
+
* process the harness spawns on every matching tool call.
|
|
4
|
+
*
|
|
5
|
+
* WHY IT IS ITS OWN MODULE, and why that is the whole point (#216): this code
|
|
6
|
+
* used to live in `src/cli.ts` beside `compile` / `lint` / `audit` / `eval`, and
|
|
7
|
+
* CommonJS resolves a module's top-level imports before a single argument is
|
|
8
|
+
* parsed — so deciding `allow` on `ls -la` loaded the report template loader, the
|
|
9
|
+
* linter catalogs, the adapter registry and ~30 more barrels. MEASURED
|
|
10
|
+
* 2026-09-08 (Node 22.22.2, `tools/measure-hook-startup.mjs`, median of 20
|
|
11
|
+
* spawns): `require("dist/cli.js")` cost 623 ms against a 42 ms bare Node start,
|
|
12
|
+
* and a real `run-program` spawn cost 610-661 ms. A shell hook doing the same job
|
|
13
|
+
* costs ~12 ms.
|
|
14
|
+
*
|
|
15
|
+
* So `src/cli.ts` is now a dispatcher shim that `require`s THIS file for
|
|
16
|
+
* `hook-runtime run-program` and the verb barrel for everything else. The
|
|
17
|
+
* emitted command is unchanged and MUST stay unchanged — it is baked into every
|
|
18
|
+
* already-emitted settings block and into the SHA stamp beside each hook.
|
|
19
|
+
*
|
|
20
|
+
* KEEP THIS MODULE'S TOP-LEVEL IMPORTS MINIMAL. Everything imported here is paid
|
|
21
|
+
* on every gated tool call. Two deliberately-lazy edges, each with the measurement
|
|
22
|
+
* at its call site: `@iarna/toml` (compile-time only, in `core/hook-program.ts`
|
|
23
|
+
* and `hook-install.ts`) and `mvdan-sh` (bash predicates only, in
|
|
24
|
+
* `core/bash-effects.ts`). `src/hook-runtime-graph.test.ts` asserts the graph
|
|
25
|
+
* deterministically, because a timing threshold on a shared CI runner would be
|
|
26
|
+
* flaky and would be the first thing quarantined.
|
|
27
|
+
*
|
|
28
|
+
* Harness-neutral: the gate protocol (deny → exit 2) is byte-identical on Claude
|
|
29
|
+
* Code and Codex, and the ONE harness-specific fact the runtime needs — which
|
|
30
|
+
* events accept injected context — is read from the resolved adapter's
|
|
31
|
+
* `HookProtocol.injectableEvents`, never a literal. That resolution is itself a
|
|
32
|
+
* lazy `require` because only the react role needs it (the adapter registry
|
|
33
|
+
* measured 279 ms on its own).
|
|
34
|
+
*
|
|
35
|
+
* This is COMPOSITION-ROOT code (`src/` root), not `src/core/`: it does real I/O
|
|
36
|
+
* (stdin, the stamp sidecar, state writes, `spawnSync`) and reaches the adapter
|
|
37
|
+
* registry, so `core ⊄ adapter` keeps it out of the domain layer.
|
|
38
|
+
*/
|
|
39
|
+
import { type RegisteredProvider } from "./core/hook-providers.js";
|
|
40
|
+
import { loadHook } from "./load-hook.js";
|
|
41
|
+
/**
|
|
42
|
+
* Load a compiled-hook program's default export — the SHARED loader, also the
|
|
43
|
+
* public `vigiles` `loadHook` a `.harness.mjs` test uses, so a hook that
|
|
44
|
+
* loads in a test loads identically here (one loader, no drift).
|
|
45
|
+
*/
|
|
46
|
+
export declare const loadHookProgram: typeof loadHook;
|
|
47
|
+
/** Load a registered provider (`.vigiles/providers/<name>`) → its definition. */
|
|
48
|
+
export declare function loadProvider(file: string): Promise<RegisteredProvider>;
|
|
49
|
+
/** Path of the tamper-evident stamp sidecar for a hook file. */
|
|
50
|
+
export declare function hookStampPath(file: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* `vigiles hook-runtime run-program <file>` — the runtime the compiled hooks block
|
|
53
|
+
* points at. Reads the live event on stdin, loads the typed program, verifies
|
|
54
|
+
* its stamp, and dispatches by role: a gate exits 2 + reason on `deny`; an
|
|
55
|
+
* inject prints `additionalContext`; a react runs its effect-classified
|
|
56
|
+
* command. A hook that won't load — or whose stamp is stale — fails CLOSED
|
|
57
|
+
* (exit 2), never silent-allow, with two loudly-announced exceptions: the repair
|
|
58
|
+
* action itself ({@link isStampRepairEvent}), and — on a LOAD failure only — the
|
|
59
|
+
* load-path repair WRITE ({@link isLoadPathRepairEvent}), or the repo wedges
|
|
60
|
+
* with no way to fix whatever broke the load path.
|
|
61
|
+
*/
|
|
62
|
+
export declare function runHookProgramCommand(file: string | undefined): Promise<void>;
|
|
63
|
+
//# sourceMappingURL=hook-runtime.d.ts.map
|
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The compiled-hook RUNTIME — `vigiles hook-runtime run-program <file>`, the
|
|
4
|
+
* process the harness spawns on every matching tool call.
|
|
5
|
+
*
|
|
6
|
+
* WHY IT IS ITS OWN MODULE, and why that is the whole point (#216): this code
|
|
7
|
+
* used to live in `src/cli.ts` beside `compile` / `lint` / `audit` / `eval`, and
|
|
8
|
+
* CommonJS resolves a module's top-level imports before a single argument is
|
|
9
|
+
* parsed — so deciding `allow` on `ls -la` loaded the report template loader, the
|
|
10
|
+
* linter catalogs, the adapter registry and ~30 more barrels. MEASURED
|
|
11
|
+
* 2026-09-08 (Node 22.22.2, `tools/measure-hook-startup.mjs`, median of 20
|
|
12
|
+
* spawns): `require("dist/cli.js")` cost 623 ms against a 42 ms bare Node start,
|
|
13
|
+
* and a real `run-program` spawn cost 610-661 ms. A shell hook doing the same job
|
|
14
|
+
* costs ~12 ms.
|
|
15
|
+
*
|
|
16
|
+
* So `src/cli.ts` is now a dispatcher shim that `require`s THIS file for
|
|
17
|
+
* `hook-runtime run-program` and the verb barrel for everything else. The
|
|
18
|
+
* emitted command is unchanged and MUST stay unchanged — it is baked into every
|
|
19
|
+
* already-emitted settings block and into the SHA stamp beside each hook.
|
|
20
|
+
*
|
|
21
|
+
* KEEP THIS MODULE'S TOP-LEVEL IMPORTS MINIMAL. Everything imported here is paid
|
|
22
|
+
* on every gated tool call. Two deliberately-lazy edges, each with the measurement
|
|
23
|
+
* at its call site: `@iarna/toml` (compile-time only, in `core/hook-program.ts`
|
|
24
|
+
* and `hook-install.ts`) and `mvdan-sh` (bash predicates only, in
|
|
25
|
+
* `core/bash-effects.ts`). `src/hook-runtime-graph.test.ts` asserts the graph
|
|
26
|
+
* deterministically, because a timing threshold on a shared CI runner would be
|
|
27
|
+
* flaky and would be the first thing quarantined.
|
|
28
|
+
*
|
|
29
|
+
* Harness-neutral: the gate protocol (deny → exit 2) is byte-identical on Claude
|
|
30
|
+
* Code and Codex, and the ONE harness-specific fact the runtime needs — which
|
|
31
|
+
* events accept injected context — is read from the resolved adapter's
|
|
32
|
+
* `HookProtocol.injectableEvents`, never a literal. That resolution is itself a
|
|
33
|
+
* lazy `require` because only the react role needs it (the adapter registry
|
|
34
|
+
* measured 279 ms on its own).
|
|
35
|
+
*
|
|
36
|
+
* This is COMPOSITION-ROOT code (`src/` root), not `src/core/`: it does real I/O
|
|
37
|
+
* (stdin, the stamp sidecar, state writes, `spawnSync`) and reaches the adapter
|
|
38
|
+
* registry, so `core ⊄ adapter` keeps it out of the domain layer.
|
|
39
|
+
*/
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.loadHookProgram = void 0;
|
|
42
|
+
exports.loadProvider = loadProvider;
|
|
43
|
+
exports.hookStampPath = hookStampPath;
|
|
44
|
+
exports.runHookProgramCommand = runHookProgramCommand;
|
|
45
|
+
const node_fs_1 = require("node:fs");
|
|
46
|
+
const node_path_1 = require("node:path");
|
|
47
|
+
const hook_program_js_1 = require("./core/hook-program.js");
|
|
48
|
+
const merge_conflict_js_1 = require("./core/merge-conflict.js");
|
|
49
|
+
const hook_install_js_1 = require("./hook-install.js");
|
|
50
|
+
const hook_providers_js_1 = require("./core/hook-providers.js");
|
|
51
|
+
const hook_state_store_js_1 = require("./hook-state-store.js");
|
|
52
|
+
const observe_js_1 = require("./observe.js");
|
|
53
|
+
const load_hook_js_1 = require("./load-hook.js");
|
|
54
|
+
/**
|
|
55
|
+
* Which events accept injected context, from the ACTIVE adapter — the one
|
|
56
|
+
* harness-specific fact a react needs, read through the `HookProtocol` port so
|
|
57
|
+
* this stays harness-neutral (never a Claude Code literal).
|
|
58
|
+
*
|
|
59
|
+
* LAZY on purpose: `./adapter-registry.js` pulls every registered adapter and
|
|
60
|
+
* its ports, and measured 279 ms to require on its own (2026-09-08, Node
|
|
61
|
+
* 22.22.2) — more than the whole rest of this module's graph. Only the REACT
|
|
62
|
+
* role needs it; a gate or an inject must not pay for it.
|
|
63
|
+
*/
|
|
64
|
+
function injectableEventsFor(root) {
|
|
65
|
+
const { resolveAdapter } = require("./adapter-registry.js");
|
|
66
|
+
return resolveAdapter(root).hookProtocol?.injectableEvents ?? [];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Load a compiled-hook program's default export — the SHARED loader, also the
|
|
70
|
+
* public `vigiles` `loadHook` a `.harness.mjs` test uses, so a hook that
|
|
71
|
+
* loads in a test loads identically here (one loader, no drift).
|
|
72
|
+
*/
|
|
73
|
+
exports.loadHookProgram = load_hook_js_1.loadHook;
|
|
74
|
+
/** Load a registered provider (`.vigiles/providers/<name>`) → its definition. */
|
|
75
|
+
async function loadProvider(file) {
|
|
76
|
+
const abs = (0, node_path_1.resolve)(process.cwd(), file);
|
|
77
|
+
const { pathToFileURL } = require("node:url");
|
|
78
|
+
let mod;
|
|
79
|
+
try {
|
|
80
|
+
mod = (await import(pathToFileURL(abs).href));
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
throw new hook_program_js_1.HookCompileError(`Cannot load provider "${file}": ${e.message}`);
|
|
84
|
+
}
|
|
85
|
+
const def = mod.default?.default ?? mod.default;
|
|
86
|
+
if (!def ||
|
|
87
|
+
typeof def !== "object" ||
|
|
88
|
+
def.kind !== "provider-def") {
|
|
89
|
+
throw new hook_program_js_1.HookCompileError(`${file} has no default-exported provider ` +
|
|
90
|
+
`(use \`export default defineProvider({…})\`).`);
|
|
91
|
+
}
|
|
92
|
+
return def;
|
|
93
|
+
}
|
|
94
|
+
/** Path of the tamper-evident stamp sidecar for a hook file. */
|
|
95
|
+
function hookStampPath(file) {
|
|
96
|
+
return (0, node_path_1.resolve)(process.cwd(), ".vigiles/hooks", (0, node_path_1.basename)(file) + ".json");
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Perform the state writes a hook declared, after its output has been emitted.
|
|
100
|
+
* A refused write (a hand-built record object with a key `record()` would have
|
|
101
|
+
* thrown on) is announced — silence here would be a hook that believes it
|
|
102
|
+
* remembered something.
|
|
103
|
+
*/
|
|
104
|
+
function applyHookWrites(file, outcome) {
|
|
105
|
+
const { ok, refused } = (0, hook_program_js_1.outcomeWrites)(outcome);
|
|
106
|
+
for (const name of refused) {
|
|
107
|
+
console.error(`vigiles: refused to record ${name} from ${file} — not a valid state key.`);
|
|
108
|
+
}
|
|
109
|
+
for (const w of ok) {
|
|
110
|
+
try {
|
|
111
|
+
(0, hook_state_store_js_1.writeHookState)(file, w);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
console.error(`vigiles: could not record ${w.name} from ${file}: ${String(e)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Gather a gate's DECLARED context providers (the trusted-host I/O step). Runs
|
|
120
|
+
* each declared read-only command via execSync in the hook's cwd; a provider
|
|
121
|
+
* that can't resolve yields its default (never throws). The pure registry +
|
|
122
|
+
* decision logic live in core/hook-providers.ts — this only injects the real IO.
|
|
123
|
+
*/
|
|
124
|
+
async function gatherHookContext(program, file) {
|
|
125
|
+
const needs = (0, hook_program_js_1.hookNeeds)(program);
|
|
126
|
+
if (needs.length === 0)
|
|
127
|
+
return {};
|
|
128
|
+
// Only load the registered-provider registry if a provider() ref is declared.
|
|
129
|
+
const hasRef = needs.some((n) => typeof n !== "string" && n.kind === "provider-ref");
|
|
130
|
+
const registry = hasRef ? await loadProviderRegistry() : {};
|
|
131
|
+
const { execSync } = require("node:child_process");
|
|
132
|
+
const { isCI } = require("ci-info");
|
|
133
|
+
return (0, hook_providers_js_1.gatherContext)(needs, {
|
|
134
|
+
exec: (command) => execSync(command, {
|
|
135
|
+
encoding: "utf-8",
|
|
136
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
137
|
+
}),
|
|
138
|
+
cwd: process.cwd(),
|
|
139
|
+
platform: process.platform,
|
|
140
|
+
isCI,
|
|
141
|
+
// The namespace is bound HERE, from the hook's own path — core never sees
|
|
142
|
+
// it, so no key a hook can spell reaches another owner's store.
|
|
143
|
+
readState: (key) => (0, hook_state_store_js_1.readHookState)(file, key),
|
|
144
|
+
now: Date.now(),
|
|
145
|
+
}, registry);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Load the registered providers (`.vigiles/providers/`) into a name→def registry
|
|
149
|
+
* for `provider()` ref resolution. A bad/unloadable provider file is skipped (the
|
|
150
|
+
* ref then yields its default ""), never crashes a live session.
|
|
151
|
+
*/
|
|
152
|
+
async function loadProviderRegistry() {
|
|
153
|
+
const registry = {};
|
|
154
|
+
for (const file of (0, hook_install_js_1.discoverProviderFiles)(process.cwd())) {
|
|
155
|
+
try {
|
|
156
|
+
const def = await loadProvider(file);
|
|
157
|
+
registry[def.name] = def;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
/* skip an unloadable provider file */
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return registry;
|
|
164
|
+
}
|
|
165
|
+
/** Append an observe-mode record to `.vigiles/hook-observations.jsonl` (best-effort). */
|
|
166
|
+
function recordObservation(file, on, would, reason) {
|
|
167
|
+
try {
|
|
168
|
+
const dir = (0, node_path_1.resolve)(process.cwd(), ".vigiles");
|
|
169
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
170
|
+
const line = JSON.stringify({
|
|
171
|
+
ts: new Date().toISOString(),
|
|
172
|
+
hook: file,
|
|
173
|
+
event: on,
|
|
174
|
+
would,
|
|
175
|
+
reason,
|
|
176
|
+
}) + "\n";
|
|
177
|
+
(0, node_fs_1.appendFileSync)((0, node_path_1.resolve)(dir, "hook-observations.jsonl"), line);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
/* recording is best-effort — never let it break a live session */
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Emit a gate Decision in the harness protocol — the author never writes it.
|
|
185
|
+
* `observe` mode turns a would-be block/ask into a recorded no-op (exit 0): the
|
|
186
|
+
* shadow/rollout path. Harness-neutral — exit 2 / exit 0 are identical on Claude
|
|
187
|
+
* Code and Codex; the record is vigiles-local.
|
|
188
|
+
*/
|
|
189
|
+
function emitGate(decision, on, mode, file) {
|
|
190
|
+
const action = (0, hook_program_js_1.gateAction)(decision, mode);
|
|
191
|
+
switch (action.kind) {
|
|
192
|
+
case "block":
|
|
193
|
+
(0, observe_js_1.appendObservation)({
|
|
194
|
+
kind: "hook",
|
|
195
|
+
event: on,
|
|
196
|
+
decision: "deny",
|
|
197
|
+
mode: "enforce",
|
|
198
|
+
rule: file,
|
|
199
|
+
reason: action.reason,
|
|
200
|
+
});
|
|
201
|
+
console.error(action.reason);
|
|
202
|
+
process.exit(2);
|
|
203
|
+
return;
|
|
204
|
+
case "ask":
|
|
205
|
+
(0, observe_js_1.appendObservation)({
|
|
206
|
+
kind: "hook",
|
|
207
|
+
event: on,
|
|
208
|
+
decision: "ask",
|
|
209
|
+
mode: "enforce",
|
|
210
|
+
rule: file,
|
|
211
|
+
reason: action.reason,
|
|
212
|
+
});
|
|
213
|
+
process.stdout.write(JSON.stringify({
|
|
214
|
+
hookSpecificOutput: {
|
|
215
|
+
hookEventName: on,
|
|
216
|
+
permissionDecision: "ask",
|
|
217
|
+
permissionDecisionReason: action.reason,
|
|
218
|
+
},
|
|
219
|
+
}) + "\n");
|
|
220
|
+
return;
|
|
221
|
+
case "observe":
|
|
222
|
+
(0, observe_js_1.appendObservation)({
|
|
223
|
+
kind: "hook",
|
|
224
|
+
event: on,
|
|
225
|
+
decision: action.would,
|
|
226
|
+
mode: "observe",
|
|
227
|
+
rule: file,
|
|
228
|
+
reason: action.reason,
|
|
229
|
+
});
|
|
230
|
+
recordObservation(file, on, action.would, action.reason);
|
|
231
|
+
console.error(`⚠ [vigiles observe] ${on}: would ${action.would} — ${action.reason}`);
|
|
232
|
+
return; // exit 0 — observe never blocks
|
|
233
|
+
case "allow":
|
|
234
|
+
return; // emit nothing, exit 0
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Every `package.json` between the hook file and the filesystem root, plus the
|
|
239
|
+
* project's `.vigilesrc.json` — the files Node and the runtime must PARSE for a
|
|
240
|
+
* compiled hook to load at all. Repo-relative-ish paths, for a message.
|
|
241
|
+
*
|
|
242
|
+
* Walking UP is not decoration: `vigiles/hook` is a bare specifier, so Node reads
|
|
243
|
+
* the nearest `package.json` (and every one above it) while resolving it. The
|
|
244
|
+
* observed wedge came from a `package.json` the author was not thinking about at
|
|
245
|
+
* the time — it had merge-conflict markers in it, nothing to do with hooks.
|
|
246
|
+
*/
|
|
247
|
+
function hookLoadPathFiles(hookFile) {
|
|
248
|
+
const files = [];
|
|
249
|
+
let dir = (0, node_path_1.dirname)((0, node_path_1.resolve)(process.cwd(), hookFile));
|
|
250
|
+
for (;;) {
|
|
251
|
+
const pkg = (0, node_path_1.resolve)(dir, "package.json");
|
|
252
|
+
files.push(pkg);
|
|
253
|
+
// 🔴 THE WALK STOPS AT THE FIRST ONE THAT EXISTS, and this list is now also
|
|
254
|
+
// the set of writes the repair door accepts, so its length is a blast
|
|
255
|
+
// radius. Unbounded, it reached `/home/package.json` and `/package.json` —
|
|
256
|
+
// files Node never opens once a nearer one is found. MEASURED against this
|
|
257
|
+
// runtime, hook at `gp/p/repo/.claude/hooks/`, conflict markers planted at
|
|
258
|
+
// one ancestor:
|
|
259
|
+
//
|
|
260
|
+
// repo pkg PRESENT , parent conflicted → loads fine
|
|
261
|
+
// repo pkg absent , parent conflicted → WEDGES (cause: ../package.json)
|
|
262
|
+
// repo pkg absent , parent absent, gp broken → WEDGES (cause: ../../package.json)
|
|
263
|
+
// repo pkg PRESENT , parent absent, gp broken → loads fine
|
|
264
|
+
// repo pkg absent , parent HEALTHY, gp broken→ loads fine
|
|
265
|
+
//
|
|
266
|
+
// A missing one is still pushed before the check: `package.json` may be the
|
|
267
|
+
// file the author has to CREATE, and it is the commonest repair of all.
|
|
268
|
+
if ((0, node_fs_1.existsSync)(pkg))
|
|
269
|
+
break;
|
|
270
|
+
const up = (0, node_path_1.dirname)(dir);
|
|
271
|
+
if (up === dir)
|
|
272
|
+
break;
|
|
273
|
+
dir = up;
|
|
274
|
+
}
|
|
275
|
+
files.push((0, node_path_1.resolve)(process.cwd(), ".vigilesrc.json"));
|
|
276
|
+
return files;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* The conflicted files on this hook's load path, if any — the difference between
|
|
280
|
+
* "your hook is broken" and "your repo is mid-merge and the hook is collateral".
|
|
281
|
+
*/
|
|
282
|
+
function conflictedLoadPathFiles(hookFile) {
|
|
283
|
+
return hookLoadPathFiles(hookFile)
|
|
284
|
+
.filter((p) => {
|
|
285
|
+
try {
|
|
286
|
+
return ((0, node_fs_1.existsSync)(p) && (0, merge_conflict_js_1.hasMergeConflictMarkers)((0, node_fs_1.readFileSync)(p, "utf-8")));
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return false; // unreadable is a different problem; don't guess about it
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
.map((p) => (0, node_path_1.relative)(process.cwd(), p) || p);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Print the loud stderr banner that accompanies a REPAIR-only pass-through, and
|
|
296
|
+
* return true — the caller allows exactly this one tool call. Shared by the two
|
|
297
|
+
* refusal paths (stale stamp, unloadable program) so the wording can't drift.
|
|
298
|
+
*/
|
|
299
|
+
function announceRepairEscape(file, why) {
|
|
300
|
+
console.error(`vigiles: hook ${file} ${why}.\n` +
|
|
301
|
+
`vigiles: ALLOWING this one call because it is a repair or recovery action ` +
|
|
302
|
+
`(a write to ${file}, to ${hookStampPath(file)}, or to one of ` +
|
|
303
|
+
`${merge_conflict_js_1.HARNESS_CONFIG_FILES.join(", ")}) ` +
|
|
304
|
+
`— without this the gate blocks the only actions that can fix it.\n` +
|
|
305
|
+
`vigiles: every OTHER tool call stays BLOCKED until the hook loads again.`);
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Fail closed if a stamp sidecar exists and the on-disk source no longer
|
|
310
|
+
* matches it — a hand-edit that smuggles in a capability breaks the stamp.
|
|
311
|
+
* No sidecar → run uncompiled (e.g. a test fixture or a not-yet-compiled hook).
|
|
312
|
+
*
|
|
313
|
+
* ONE exception, and it is loud: the author's own REPAIR action
|
|
314
|
+
* ({@link isStampRepairEvent}) is let through, or a repo WEDGES. A stale stamp on
|
|
315
|
+
* a PreToolUse Bash gate blocks every Bash command — including `vigiles compile`,
|
|
316
|
+
* the only command that regenerates the stamp — so a normal edit-compile cycle
|
|
317
|
+
* could paint you into a corner whose only escape was hand-editing
|
|
318
|
+
* `.claude/settings.json` to unwire the gate. Observed 2026-08-03.
|
|
319
|
+
*/
|
|
320
|
+
function verifyStampOrRefuse(file, event) {
|
|
321
|
+
const stampPath = hookStampPath(file);
|
|
322
|
+
if (!(0, node_fs_1.existsSync)(stampPath))
|
|
323
|
+
return;
|
|
324
|
+
try {
|
|
325
|
+
const { stamp } = JSON.parse((0, node_fs_1.readFileSync)(stampPath, "utf-8"));
|
|
326
|
+
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), file), "utf-8");
|
|
327
|
+
if (stamp && !(0, hook_program_js_1.verifyHookStamp)(source, stamp)) {
|
|
328
|
+
if ((0, hook_program_js_1.isStampRepairEvent)(event, file, process.cwd())) {
|
|
329
|
+
announceRepairEscape(file, "does not match its compiled stamp");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
console.error(`vigiles: hook ${file} does not match its compiled stamp (tampered).\n` +
|
|
333
|
+
`vigiles: if YOU edited it, the way out is a FILE WRITE, not a command — ` +
|
|
334
|
+
`this refusal blocks the recompile too. Either edit ${file} back to what ` +
|
|
335
|
+
`was compiled, or clear its stamp by writing \`{}\` into ` +
|
|
336
|
+
`${stampPath}. The hook then runs UNSTAMPED but still ENFORCES, so ` +
|
|
337
|
+
`\`vigiles compile ${file}\` goes through the normal gate.`);
|
|
338
|
+
process.exit(2);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
/* unreadable sidecar → don't block a live session on it */
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Say so, ON STDERR, when this event's path cannot be matched against a
|
|
347
|
+
* repo-relative prefix — an absolute `file_path` and no project root anywhere.
|
|
348
|
+
* The failure it announces is otherwise invisible: the hook runs, exits 0, and
|
|
349
|
+
* decides on nothing. Deliberately silent for a relative `file_path` (decidable
|
|
350
|
+
* without a root) so it cannot be mistaken for a react hook's `notice`.
|
|
351
|
+
*/
|
|
352
|
+
function warnIfPathUndecidable(event, root) {
|
|
353
|
+
const warning = (0, hook_program_js_1.undecidablePathWarning)(event.tool_input?.file_path, root);
|
|
354
|
+
if (warning !== undefined)
|
|
355
|
+
console.error(warning);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* `vigiles hook-runtime run-program <file>` — the runtime the compiled hooks block
|
|
359
|
+
* points at. Reads the live event on stdin, loads the typed program, verifies
|
|
360
|
+
* its stamp, and dispatches by role: a gate exits 2 + reason on `deny`; an
|
|
361
|
+
* inject prints `additionalContext`; a react runs its effect-classified
|
|
362
|
+
* command. A hook that won't load — or whose stamp is stale — fails CLOSED
|
|
363
|
+
* (exit 2), never silent-allow, with two loudly-announced exceptions: the repair
|
|
364
|
+
* action itself ({@link isStampRepairEvent}), and — on a LOAD failure only — the
|
|
365
|
+
* load-path repair WRITE ({@link isLoadPathRepairEvent}), or the repo wedges
|
|
366
|
+
* with no way to fix whatever broke the load path.
|
|
367
|
+
*/
|
|
368
|
+
async function runHookProgramCommand(file) {
|
|
369
|
+
if (!file) {
|
|
370
|
+
console.error("Usage: vigiles hook-runtime run-program <hook-file>");
|
|
371
|
+
process.exit(2);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
let raw = "";
|
|
375
|
+
try {
|
|
376
|
+
raw = (0, node_fs_1.readFileSync)(0, "utf-8");
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
/* no stdin */
|
|
380
|
+
}
|
|
381
|
+
let event = {};
|
|
382
|
+
try {
|
|
383
|
+
event = JSON.parse(raw);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
/* malformed → empty event */
|
|
387
|
+
}
|
|
388
|
+
// The root repo-relative path prefixes resolve against. `$CLAUDE_PROJECT_DIR`
|
|
389
|
+
// first (the same root the harness resolved THIS hook's own path against),
|
|
390
|
+
// then the payload's `cwd`; never `process.cwd()`, which under a git worktree
|
|
391
|
+
// can be a different checkout. See `projectRootOf`.
|
|
392
|
+
const projectRoot = (0, hook_program_js_1.projectRootOf)(event, process.env);
|
|
393
|
+
let program;
|
|
394
|
+
try {
|
|
395
|
+
program = await (0, exports.loadHookProgram)(file);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
// A LOAD failure is a fact about the harness, not a verdict about the
|
|
399
|
+
// command that happened to arrive — so it must still fail CLOSED (a gate
|
|
400
|
+
// that cannot run must not wave traffic through), but the two things it owes
|
|
401
|
+
// the author are different from a `deny`'s: name the real cause, and leave a
|
|
402
|
+
// way back.
|
|
403
|
+
//
|
|
404
|
+
// Escapes, both announced loudly on stderr:
|
|
405
|
+
// - the stale-stamp one (an edit to the hook itself / `vigiles compile`),
|
|
406
|
+
// for the hook broken mid-edit;
|
|
407
|
+
// - the RECOVERY set, for the case where the hook is fine and something
|
|
408
|
+
// else on its load path is not (observed 2026-08-10: `package.json` left
|
|
409
|
+
// holding merge-conflict markers → Node can't resolve `vigiles/hook` →
|
|
410
|
+
// no compiled hook loads → the Bash gate refuses `git merge --abort`,
|
|
411
|
+
// the one command that undoes the cause. Irreversible from inside the
|
|
412
|
+
// session; it was fixed by hand-editing the JSON, because file tools do
|
|
413
|
+
// not go through PreToolUse(Bash)).
|
|
414
|
+
// Everything else stays BLOCKED, and the escapes are whitelists of commands
|
|
415
|
+
// that are WRITES — see `isLoadPathRepairEvent` for why no command is one.
|
|
416
|
+
const conflicted = conflictedLoadPathFiles(file);
|
|
417
|
+
const cause = conflicted.length > 0
|
|
418
|
+
? `cannot be loaded — ${conflicted.join(", ")} contains merge-conflict ` +
|
|
419
|
+
`markers, so Node cannot resolve \`vigiles/hook\` from it (the hook itself ` +
|
|
420
|
+
`may be fine)`
|
|
421
|
+
: "cannot be loaded";
|
|
422
|
+
if ((0, hook_program_js_1.isLoadPathRepairEvent)(event, file, {
|
|
423
|
+
// The root the REST of this runtime already uses: `hookStampPath` and
|
|
424
|
+
// `verifyStampOrRefuse` read the hook and its sidecar via `process.cwd()`,
|
|
425
|
+
// so a repair accepted against any other root would name a file the
|
|
426
|
+
// runtime never reads. The hook's own path cannot supply it (a hook sits
|
|
427
|
+
// at any depth, and a `.git` probe would be a disk read core does not do).
|
|
428
|
+
root: process.cwd(),
|
|
429
|
+
loadPathFiles: hookLoadPathFiles(file),
|
|
430
|
+
})) {
|
|
431
|
+
announceRepairEscape(file, cause);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
console.error(`vigiles: hook ${file} ${cause}.\n` +
|
|
435
|
+
`vigiles: this is the state of the HARNESS, not a decision about your ` +
|
|
436
|
+
`command — the gate never ran. Blocking anyway (a gate that cannot run ` +
|
|
437
|
+
`must not pass traffic).\n` +
|
|
438
|
+
`vigiles: the way out is a FILE WRITE, not a command — under a tool that ` +
|
|
439
|
+
`WRITES (Write/Edit/MultiEdit); a Read of the same path repairs nothing ` +
|
|
440
|
+
`and is refused. Fix whichever of ` +
|
|
441
|
+
`${file}, ${merge_conflict_js_1.HARNESS_CONFIG_FILES.join(", ")} is broken — those writes are ` +
|
|
442
|
+
`allowed even while this refuses, and a Bash gate never gated file tools ` +
|
|
443
|
+
`at all. The hook then loads and the gate decides normally again.\n` +
|
|
444
|
+
`vigiles: those paths resolve under ${process.cwd()} — plus any ancestor ` +
|
|
445
|
+
`\`package.json\` Node actually reads, so whatever is named above as the ` +
|
|
446
|
+
`cause is writable. A path in a DIFFERENT checkout is refused: it cannot ` +
|
|
447
|
+
`repair this failure.\n` +
|
|
448
|
+
`vigiles: no command is allowed, deliberately. \`git merge --abort\` and ` +
|
|
449
|
+
`\`git checkout\` RUN \`.git/hooks/*\` (measured: reference-transaction, ` +
|
|
450
|
+
`post-checkout), and \`vigiles compile\` loads the hook through the same ` +
|
|
451
|
+
`resolver that just failed.`);
|
|
452
|
+
process.exit(2);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
verifyStampOrRefuse(file, event);
|
|
456
|
+
switch ((0, hook_program_js_1.dispatchKind)(program)) {
|
|
457
|
+
case "inject": {
|
|
458
|
+
const ctx = await gatherHookContext(program, file);
|
|
459
|
+
const injection = (0, hook_program_js_1.injectionOf)(program, event, ctx);
|
|
460
|
+
process.stdout.write(JSON.stringify({
|
|
461
|
+
hookSpecificOutput: {
|
|
462
|
+
hookEventName: program.on,
|
|
463
|
+
additionalContext: injection.context,
|
|
464
|
+
},
|
|
465
|
+
}) + "\n");
|
|
466
|
+
// Writes land AFTER the output is emitted: a hook that recorded "I spoke"
|
|
467
|
+
// must not have recorded it if emitting threw.
|
|
468
|
+
applyHookWrites(file, {
|
|
469
|
+
kind: "injection",
|
|
470
|
+
context: injection.context,
|
|
471
|
+
records: injection.records,
|
|
472
|
+
});
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
case "react": {
|
|
476
|
+
const ctx = await gatherHookContext(program, file);
|
|
477
|
+
warnIfPathUndecidable(event, projectRoot);
|
|
478
|
+
const reaction = (0, hook_program_js_1.runReact)(program, event, ctx, projectRoot);
|
|
479
|
+
// A notice has to REACH someone. stderr at exit 0 goes to the debug log
|
|
480
|
+
// and nothing else (the host's docs are explicit: "Claude never sees it"),
|
|
481
|
+
// and a react always exits 0 because its type has no `deny` — so stderr
|
|
482
|
+
// alone delivered nowhere. Emit the same `additionalContext` shape the
|
|
483
|
+
// shipped refs/eval-lock nudges use, gated on the ACTIVE adapter's
|
|
484
|
+
// `injectableEvents` so this is per-harness fact, not a CC literal.
|
|
485
|
+
const injectable = injectableEventsFor(projectRoot ?? process.cwd());
|
|
486
|
+
const delivery = (0, hook_program_js_1.noticeDelivery)(reaction, program.on, injectable);
|
|
487
|
+
if (delivery.kind === "inject") {
|
|
488
|
+
process.stdout.write(JSON.stringify({
|
|
489
|
+
hookSpecificOutput: {
|
|
490
|
+
hookEventName: program.on,
|
|
491
|
+
additionalContext: delivery.context,
|
|
492
|
+
},
|
|
493
|
+
}) + "\n");
|
|
494
|
+
}
|
|
495
|
+
// The stderr copy STAYS, deliberately. It is what the debug log and every
|
|
496
|
+
// `runHook`-based probe already read, it costs nothing, and on an event
|
|
497
|
+
// this harness does not inject it is the only trace that exists at all.
|
|
498
|
+
// Removing it would break existing consumers to gain nothing.
|
|
499
|
+
if (reaction.kind === "notice")
|
|
500
|
+
console.error(reaction.message);
|
|
501
|
+
applyHookWrites(file, { kind: "reaction", reaction });
|
|
502
|
+
if (reaction.kind === "run") {
|
|
503
|
+
const { spawnSync } = require("node:child_process");
|
|
504
|
+
const res = spawnSync(reaction.command, {
|
|
505
|
+
shell: true,
|
|
506
|
+
stdio: "inherit",
|
|
507
|
+
});
|
|
508
|
+
process.exit(res.status ?? 0);
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
case "file-gate": {
|
|
513
|
+
const ctx = await gatherHookContext(program, file);
|
|
514
|
+
warnIfPathUndecidable(event, projectRoot);
|
|
515
|
+
emitGate((0, hook_program_js_1.decideFileGate)(program, event, ctx, projectRoot), program.on, (0, hook_program_js_1.hookMode)(program), file);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
case "bash-gate": {
|
|
519
|
+
const ctx = await gatherHookContext(program, file);
|
|
520
|
+
// The same `projectRoot` the file gates get: without it every
|
|
521
|
+
// repo-relative prefix in a DENYLIST matcher (`touches`/`writesTo`) is
|
|
522
|
+
// matched by over-blocking alone, and with it an absolute token is placed
|
|
523
|
+
// exactly. Measured bypass this closes: `sed -i s/a/b/ <abs>/paper.tex`
|
|
524
|
+
// exited 0 against a guard that blocked the relative spelling.
|
|
525
|
+
emitGate((0, hook_program_js_1.decideProgram)(program, event, ctx, projectRoot), program.on, (0, hook_program_js_1.hookMode)(program), file);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
case "prompt-gate": {
|
|
529
|
+
const ctx = await gatherHookContext(program, file);
|
|
530
|
+
emitGate((0, hook_program_js_1.decidePromptGate)(program, event, ctx), program.on, (0, hook_program_js_1.hookMode)(program), file);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
case "stop-gate": {
|
|
534
|
+
const ctx = await gatherHookContext(program, file);
|
|
535
|
+
emitGate((0, hook_program_js_1.decideStopGate)(program, event, ctx), program.on, (0, hook_program_js_1.hookMode)(program), file);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
//# sourceMappingURL=hook-runtime.js.map
|
package/dist/hook.d.ts
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
* entirely (#45427 / #32376 — a Bash heredoc instead of `Write`), so a gate is a
|
|
50
50
|
* strong default and is NEVER an unbypassable wall. See `docs/compiled-hooks.md`.
|
|
51
51
|
*/
|
|
52
|
-
export { experimental_defineHook, experimental_defineFileGate, experimental_definePromptGate, experimental_defineStopGate,
|
|
52
|
+
export { experimental_defineHook, experimental_defineFileGate, experimental_definePromptGate, experimental_defineStopGate, tools, allow, deny, ask, commandView, pathView, gateAction, hookMode, experimental_defineInject, inject, experimental_defineReact, run, notice, nothing, responseView, decideProgram, decideFileGate, decidePromptGate, decideStopGate, runInject, runReact, runHookProgram, decisionExitCode, dispatchKind, hookRouting, hookNeeds, injectionOf, outcomeWrites, matchesTool, invalidToolPatterns, compileHookProgram, checkHookImports, stampHook, verifyHookStamp, HookCompileError, } from "./core/hook-program.js";
|
|
53
53
|
export type { Decision, HookMode, GateAction, CommandView, PathView, ResponseView, BashToolEvent, FileToolEvent, PromptEvent, StopEvent, ReactEvent, SessionEvent, HookProgram, FileGateHook, PromptGateHook, StopGateHook, InjectHook, ReactHook, AnyHook, DispatchKind, Injection, Reaction, RunReaction, CompiledHookProgram, CompileHookOptions, RawHookEvent, HookProgramOutcome, } from "./core/hook-program.js";
|
|
54
54
|
export { provide, dangerously, defineProvider, provider, } from "./core/hook-providers.js";
|
|
55
55
|
export { state, record, stateFact, isValidStateKey, isStateNeed, isStateWrite, admissibleWrites, durationSeconds, HookStateError, } from "./core/hook-state.js";
|
package/dist/hook.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isStateWrite = exports.isStateNeed = exports.isValidStateKey = exports.stateFact = exports.record = exports.state = exports.provider = exports.defineProvider = exports.dangerously = exports.provide = exports.HookCompileError = exports.verifyHookStamp = exports.stampHook = exports.checkHookImports = exports.compileHookProgram = exports.invalidToolPatterns = exports.matchesTool = exports.outcomeWrites = exports.injectionOf = exports.hookNeeds = exports.hookRouting = exports.dispatchKind = exports.decisionExitCode = exports.runHookProgram = exports.runReact = exports.runInject = exports.decideStopGate = exports.decidePromptGate = exports.decideFileGate = exports.decideProgram = exports.responseView = exports.nothing = exports.notice = exports.run = exports.experimental_defineReact = exports.inject = exports.experimental_defineInject = exports.hookMode = exports.gateAction = exports.pathView = exports.commandView = exports.ask = exports.deny = exports.allow = exports.tools = exports.
|
|
4
|
-
exports.leafCommandsNormalized = exports.HookStateError = exports.durationSeconds =
|
|
3
|
+
exports.admissibleWrites = exports.isStateWrite = exports.isStateNeed = exports.isValidStateKey = exports.stateFact = exports.record = exports.state = exports.provider = exports.defineProvider = exports.dangerously = exports.provide = exports.HookCompileError = exports.verifyHookStamp = exports.stampHook = exports.checkHookImports = exports.compileHookProgram = exports.invalidToolPatterns = exports.matchesTool = exports.outcomeWrites = exports.injectionOf = exports.hookNeeds = exports.hookRouting = exports.dispatchKind = exports.decisionExitCode = exports.runHookProgram = exports.runReact = exports.runInject = exports.decideStopGate = exports.decidePromptGate = exports.decideFileGate = exports.decideProgram = exports.responseView = exports.nothing = exports.notice = exports.run = exports.experimental_defineReact = exports.inject = exports.experimental_defineInject = exports.hookMode = exports.gateAction = exports.pathView = exports.commandView = exports.ask = exports.deny = exports.allow = exports.tools = exports.experimental_defineStopGate = exports.experimental_definePromptGate = exports.experimental_defineFileGate = exports.experimental_defineHook = void 0;
|
|
4
|
+
exports.leafCommandsNormalized = exports.HookStateError = exports.durationSeconds = void 0;
|
|
5
5
|
/**
|
|
6
6
|
* `vigiles/hook` — the **closed vocabulary** for authoring a compiled hook.
|
|
7
7
|
*
|
|
@@ -78,7 +78,6 @@ Object.defineProperty(exports, "experimental_defineHook", { enumerable: true, ge
|
|
|
78
78
|
Object.defineProperty(exports, "experimental_defineFileGate", { enumerable: true, get: function () { return hook_program_js_1.experimental_defineFileGate; } });
|
|
79
79
|
Object.defineProperty(exports, "experimental_definePromptGate", { enumerable: true, get: function () { return hook_program_js_1.experimental_definePromptGate; } });
|
|
80
80
|
Object.defineProperty(exports, "experimental_defineStopGate", { enumerable: true, get: function () { return hook_program_js_1.experimental_defineStopGate; } });
|
|
81
|
-
Object.defineProperty(exports, "tool", { enumerable: true, get: function () { return hook_program_js_1.tool; } });
|
|
82
81
|
Object.defineProperty(exports, "tools", { enumerable: true, get: function () { return hook_program_js_1.tools; } });
|
|
83
82
|
Object.defineProperty(exports, "allow", { enumerable: true, get: function () { return hook_program_js_1.allow; } });
|
|
84
83
|
Object.defineProperty(exports, "deny", { enumerable: true, get: function () { return hook_program_js_1.deny; } });
|
package/dist/test-coverage.d.ts
CHANGED
|
@@ -173,7 +173,7 @@ export interface UntestedReport {
|
|
|
173
173
|
*/
|
|
174
174
|
readonly staleRuns?: readonly StaleRun[];
|
|
175
175
|
/**
|
|
176
|
-
* DETERMINISTIC coverage only — `*.harness.*`, plus any custom `
|
|
176
|
+
* DETERMINISTIC coverage only — `*.harness.*`, plus any custom `include`.
|
|
177
177
|
* Free, millisecond, every-push. Answers "does this gate still catch what it
|
|
178
178
|
* claims?" (`*.test.*` has NOT counted since 15.x — see DEFAULT_TEST_GLOBS.)
|
|
179
179
|
*/
|
|
@@ -194,7 +194,7 @@ export interface TestCoverageOptions {
|
|
|
194
194
|
/** Scan hook scripts referenced from plugin.json / settings.json. Default true. */
|
|
195
195
|
readonly hooks?: boolean;
|
|
196
196
|
/** Globs of test files that count as coverage. */
|
|
197
|
-
readonly
|
|
197
|
+
readonly include?: readonly string[];
|
|
198
198
|
/**
|
|
199
199
|
* Which extension a GENERATED test gets. Detection (a tsconfig.json, a
|
|
200
200
|
* typescript dependency) decides by default; this field exists only to
|
|
@@ -206,7 +206,7 @@ export interface TestCoverageOptions {
|
|
|
206
206
|
* (or the `-subagent` / `-hook` twin — the three share their options). The
|
|
207
207
|
* previous wording here said only "from `.vigilesrc.json`", which read as a
|
|
208
208
|
* promise the CLI did not keep: `TestCoverageConfig` had no such key and
|
|
209
|
-
* `checkUntestedSurfaces` forwarded only `
|
|
209
|
+
* `checkUntestedSurfaces` forwarded only `include`/`exclude`, so a configured
|
|
210
210
|
* `mjs` was silently ignored on any TypeScript-shaped repo.
|
|
211
211
|
*/
|
|
212
212
|
readonly testExtension?: string;
|