vigiles 32.1.0 → 33.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/adapters/claude-code/run-scripts.d.ts +19 -3
- package/dist/adapters/claude-code/run-scripts.js +12 -4
- package/dist/cli-main.js +59 -14
- package/dist/core/hook-program.d.ts +67 -1
- package/dist/core/hook-program.js +74 -2
- package/dist/core/hook-providers.d.ts +1 -1
- package/dist/coverage-evidence.d.ts +1 -3
- package/dist/coverage-evidence.js +7 -35
- package/dist/coverage-probe.d.ts +15 -0
- package/dist/coverage-probe.js +29 -0
- package/dist/eval-define.js +3 -3
- package/dist/eval-lock.js +2 -1
- 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 +93 -40
- package/dist/hook-install.js +235 -54
- package/dist/hook-runtime.d.ts +1 -1
- package/dist/hook-runtime.js +25 -26
- package/dist/hook-state-store.d.ts +1 -1
- package/dist/hook.js +1 -1
- package/dist/load-hook.d.ts +1 -1
- package/dist/load-hook.js +1 -1
- package/dist/local-files.js +1 -0
- package/dist/source-kinds.d.ts +128 -0
- package/dist/source-kinds.js +162 -0
- package/dist/test-coverage-files.js +2 -17
- package/dist/test-coverage.js +11 -8
- package/package.json +1 -1
package/dist/hook-install.js
CHANGED
|
@@ -3,9 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.PROVIDERS_DIR = exports.HOOKS_DIR = void 0;
|
|
4
4
|
exports.discoverHookFiles = discoverHookFiles;
|
|
5
5
|
exports.discoverProviderFiles = discoverProviderFiles;
|
|
6
|
+
exports.partitionHookArgs = partitionHookArgs;
|
|
7
|
+
exports.invalidArgMessage = invalidArgMessage;
|
|
8
|
+
exports.discoveredHookArgs = discoveredHookArgs;
|
|
9
|
+
exports.renameCommand = renameCommand;
|
|
10
|
+
exports.unclaimedReport = unclaimedReport;
|
|
11
|
+
exports.unclaimedMessage = unclaimedMessage;
|
|
6
12
|
exports.normalizeHookRef = normalizeHookRef;
|
|
7
13
|
exports.hookRuntimeRef = hookRuntimeRef;
|
|
8
|
-
exports.hookRuntimeMissingExit = hookRuntimeMissingExit;
|
|
9
14
|
exports.hookGateRef = hookGateRef;
|
|
10
15
|
exports.mergeHooksJson = mergeHooksJson;
|
|
11
16
|
exports.mergeHooksToml = mergeHooksToml;
|
|
@@ -18,7 +23,8 @@ exports.serializeConfig = serializeConfig;
|
|
|
18
23
|
* The typed hook program is harness-NEUTRAL — it imports `vigiles/hook` and
|
|
19
24
|
* compiles to whatever harness — so its SOURCE lives in the agnostic,
|
|
20
25
|
* committed {@link HOOKS_DIR} (`.vigiles/hooks/`), never in a harness's own
|
|
21
|
-
* `.claude/`. `compile` discovers each hook there
|
|
26
|
+
* `.claude/`. `compile` discovers each hook there — a hook is a `.hook.` file
|
|
27
|
+
* and nothing else, decided by `source-kinds.ts` — compiles it, and MERGES the
|
|
22
28
|
* result into the active harness's native config (`.claude/settings.json` JSON
|
|
23
29
|
* / `config.toml` TOML) — so the harness is actually wired, not handed a
|
|
24
30
|
* paste-this block. The merge is idempotent: an entry is keyed by the runtime
|
|
@@ -30,6 +36,7 @@ exports.serializeConfig = serializeConfig;
|
|
|
30
36
|
*/
|
|
31
37
|
const node_fs_1 = require("node:fs");
|
|
32
38
|
const node_path_1 = require("node:path");
|
|
39
|
+
const source_kinds_js_1 = require("./source-kinds.js");
|
|
33
40
|
/**
|
|
34
41
|
* Lazy for the same reason as in `core/hook-program.ts` — and this module is
|
|
35
42
|
* reached from the hook runtime through `hook-state-store.ts` (`normalizeHookRef`),
|
|
@@ -41,25 +48,218 @@ const stringifyToml = (value) => require("@iarna/toml").stringify(value);
|
|
|
41
48
|
exports.HOOKS_DIR = ".vigiles/hooks";
|
|
42
49
|
/** The committed home for registered context-provider SOURCE (v2). */
|
|
43
50
|
exports.PROVIDERS_DIR = ".vigiles/providers";
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
/**
|
|
52
|
+
* The ONE place a directory's role meets {@link SourceKind}. The `switch` is
|
|
53
|
+
* exhaustive, so a kind added to the table in `source-kinds.ts` is a compile
|
|
54
|
+
* error here until someone decides what hooks and providers do with it.
|
|
55
|
+
*
|
|
56
|
+
* `leave` is for files that are in these directories on purpose and are none of
|
|
57
|
+
* `compile`'s business: tests, declarations, stamps, a README.
|
|
58
|
+
*/
|
|
59
|
+
function claimOf(role, found) {
|
|
60
|
+
switch (found.kind) {
|
|
61
|
+
case "hook":
|
|
62
|
+
case "provider":
|
|
63
|
+
// A provider in the hooks directory is as unclaimed as a stray helper:
|
|
64
|
+
// nothing here would ever load it, and saying so beats a silent miss.
|
|
65
|
+
return found.kind === role ? "claim" : "refuse";
|
|
66
|
+
case "unclaimed":
|
|
67
|
+
return "refuse";
|
|
68
|
+
case "vigiles-test":
|
|
69
|
+
case "declaration":
|
|
70
|
+
case "stamp":
|
|
71
|
+
case "non-source":
|
|
72
|
+
return "leave";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** List the files under `dir` (relative to cwd), split by what `role` does with each. */
|
|
76
|
+
function discoverSources(cwd, dir, role) {
|
|
48
77
|
const abs = (0, node_path_1.join)(cwd, dir);
|
|
49
78
|
if (!(0, node_fs_1.existsSync)(abs))
|
|
50
|
-
return [];
|
|
51
|
-
|
|
52
|
-
.filter((f) => HOOK_SOURCE_RE.test(f) && !f.endsWith(".d.ts"))
|
|
79
|
+
return { claimed: [], unclaimed: [] };
|
|
80
|
+
const verdicts = (0, node_fs_1.readdirSync)(abs)
|
|
53
81
|
.sort()
|
|
54
|
-
.map((f) => (
|
|
82
|
+
.map((f) => ({
|
|
83
|
+
path: (0, node_path_1.join)(dir, f),
|
|
84
|
+
claim: claimOf(role, (0, source_kinds_js_1.classifySource)(f)),
|
|
85
|
+
}));
|
|
86
|
+
const pick = (want) => verdicts.filter((v) => v.claim === want).map((v) => v.path);
|
|
87
|
+
return { claimed: pick("claim"), unclaimed: pick("refuse") };
|
|
55
88
|
}
|
|
56
|
-
/** Discover hook
|
|
89
|
+
/** Discover hook sources under {@link HOOKS_DIR}: `.hook.` files, and the runnable files that are nobody's. */
|
|
57
90
|
function discoverHookFiles(cwd) {
|
|
58
|
-
return discoverSources(cwd, exports.HOOKS_DIR);
|
|
91
|
+
return discoverSources(cwd, exports.HOOKS_DIR, "hook");
|
|
59
92
|
}
|
|
60
|
-
/** Discover registered-provider
|
|
93
|
+
/** Discover registered-provider sources under {@link PROVIDERS_DIR}. */
|
|
61
94
|
function discoverProviderFiles(cwd) {
|
|
62
|
-
return discoverSources(cwd, exports.PROVIDERS_DIR);
|
|
95
|
+
return discoverSources(cwd, exports.PROVIDERS_DIR, "provider");
|
|
96
|
+
}
|
|
97
|
+
const realProbe = (path) => {
|
|
98
|
+
const st = (0, node_fs_1.statSync)((0, node_path_1.resolve)(process.cwd(), path), { throwIfNoEntry: false });
|
|
99
|
+
if (st === undefined)
|
|
100
|
+
return "missing";
|
|
101
|
+
return st.isFile() ? "file" : st.isDirectory() ? "directory" : "other";
|
|
102
|
+
};
|
|
103
|
+
/** Is `path` directly inside `dir` (a repo-relative POSIX dir), either separator, any prefix? */
|
|
104
|
+
function insideDir(path, dir) {
|
|
105
|
+
const parent = (0, source_kinds_js_1.dirName)(path)
|
|
106
|
+
.replace(/\\/g, "/")
|
|
107
|
+
.replace(/\/$/, "")
|
|
108
|
+
.replace(/^\.\//, "");
|
|
109
|
+
return parent === dir || parent.endsWith(`/${dir}`);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Sort explicit `compile` arguments by the same classifier discovery uses, so
|
|
113
|
+
* `compile guard.mjs` and a bare `compile` give the same answer about it — for a
|
|
114
|
+
* path ANYWHERE, not just under `.vigiles/hooks/`: `compile .claude/hooks/x.mjs`
|
|
115
|
+
* or a plugin's `hooks/guard.mjs` is refused unless it carries `.hook.`.
|
|
116
|
+
*
|
|
117
|
+
* A path that is missing, a directory or not a regular file is an error (it used
|
|
118
|
+
* to fail, and the first version of the marker rule turned it into a green run).
|
|
119
|
+
* The one lenient case exists for a shell glob (`compile .vigiles/hooks/*`): a
|
|
120
|
+
* test, declaration or stamp is reported as skipped, and so is a README or
|
|
121
|
+
* `.gitkeep` — but only inside the two vigiles source directories, where a glob
|
|
122
|
+
* would sweep one up. A `/etc/passwd` is not a README.
|
|
123
|
+
*/
|
|
124
|
+
function partitionHookArgs(args, probe = realProbe) {
|
|
125
|
+
const hooks = [];
|
|
126
|
+
const unclaimed = [];
|
|
127
|
+
const skipped = [];
|
|
128
|
+
const invalid = [];
|
|
129
|
+
for (const path of args) {
|
|
130
|
+
const what = probe(path);
|
|
131
|
+
if (what === "missing" || what === "directory") {
|
|
132
|
+
invalid.push({ path, reason: what });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (what === "other") {
|
|
136
|
+
invalid.push({ path, reason: "not-a-source" });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const found = (0, source_kinds_js_1.classifySource)((0, source_kinds_js_1.baseName)(path));
|
|
140
|
+
switch (found.kind) {
|
|
141
|
+
case "hook":
|
|
142
|
+
hooks.push(path);
|
|
143
|
+
break;
|
|
144
|
+
case "unclaimed":
|
|
145
|
+
unclaimed.push(path);
|
|
146
|
+
break;
|
|
147
|
+
case "provider":
|
|
148
|
+
// Valid, and compile validates providers whenever it compiles a hook —
|
|
149
|
+
// except in the hooks directory, where nothing would ever load it.
|
|
150
|
+
if (insideDir(path, exports.HOOKS_DIR))
|
|
151
|
+
unclaimed.push(path);
|
|
152
|
+
else
|
|
153
|
+
skipped.push({ path, kind: found.kind });
|
|
154
|
+
break;
|
|
155
|
+
case "vigiles-test":
|
|
156
|
+
case "declaration":
|
|
157
|
+
case "stamp":
|
|
158
|
+
skipped.push({ path, kind: found.kind });
|
|
159
|
+
break;
|
|
160
|
+
case "non-source":
|
|
161
|
+
if (insideDir(path, exports.HOOKS_DIR) || insideDir(path, exports.PROVIDERS_DIR))
|
|
162
|
+
skipped.push({ path, kind: found.kind });
|
|
163
|
+
else
|
|
164
|
+
invalid.push({ path, reason: "not-a-source" });
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { hooks, unclaimed, skipped, invalid };
|
|
169
|
+
}
|
|
170
|
+
/** The line `compile` prints for an argument it cannot treat as a hook source at all. */
|
|
171
|
+
function invalidArgMessage(path, reason) {
|
|
172
|
+
switch (reason) {
|
|
173
|
+
case "missing":
|
|
174
|
+
return `${path} — no such file.`;
|
|
175
|
+
case "directory":
|
|
176
|
+
return `${path} — is a directory. Name hook files, or run \`vigiles compile\` with no arguments to discover them.`;
|
|
177
|
+
case "not-a-source":
|
|
178
|
+
return `${path} — not a hook source (a hook is a \`<name>.hook.<ext>\` file).`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Discovery, shaped like {@link partitionHookArgs}'s answer, so `compile` handles
|
|
183
|
+
* "no arguments" and "these paths" through one code path afterwards.
|
|
184
|
+
*/
|
|
185
|
+
function discoveredHookArgs(cwd) {
|
|
186
|
+
const { claimed, unclaimed } = discoverHookFiles(cwd);
|
|
187
|
+
return { hooks: claimed, unclaimed, skipped: [], invalid: [] };
|
|
188
|
+
}
|
|
189
|
+
/** POSIX single-quote one word: safe for any file name, a newline and `$(…)` included. */
|
|
190
|
+
const shq = (word) => `'${word.replace(/'/g, `'\\''`)}'`;
|
|
191
|
+
/**
|
|
192
|
+
* The ONE shell command that renames an unclaimed file to its marked name and
|
|
193
|
+
* (for a hook) recompiles it. Every path is single-quoted: this is printed by a
|
|
194
|
+
* tool and pasted by a person, and a repository can plant a file name that is a
|
|
195
|
+
* command.
|
|
196
|
+
*
|
|
197
|
+
* 🔴 THE HOOK COMMAND COPIES FIRST AND DELETES LAST, and that order is the
|
|
198
|
+
* point. Already-wired hooks keep running after an upgrade: the runtime loads
|
|
199
|
+
* whatever path `settings.json` names, and never asks what a file is called. So
|
|
200
|
+
* the break is the NEXT compile, and a plain `mv` + `compile` has one bad
|
|
201
|
+
* moment — the old path is gone and the wiring still names it. For a gate
|
|
202
|
+
* wired `|| exit 2` that is a hook that cannot load, which blocks every Bash
|
|
203
|
+
* call (the repair is a file write, not a command). Compiling the renamed copy
|
|
204
|
+
* while the old file still exists, and deleting the old one only after that
|
|
205
|
+
* compile succeeded, has no such moment: if anything fails the old hook is
|
|
206
|
+
* still on disk and still wired. `compile` replaces the old wiring itself
|
|
207
|
+
* ({@link ownedRefs}), so nothing is left naming the deleted path.
|
|
208
|
+
*
|
|
209
|
+
* 🔴 `cp -n`, because the marked name may already exist: a plain `cp` replaced
|
|
210
|
+
* an edited `guard.hook.mjs` with the stale `guard.mjs`, and the weaker hook
|
|
211
|
+
* became the wired gate. {@link unclaimedMessage} checks first and prints no
|
|
212
|
+
* command at all in that case; `-n` is for the file that appears in between.
|
|
213
|
+
*/
|
|
214
|
+
function renameCommand(path, role) {
|
|
215
|
+
const target = (0, source_kinds_js_1.markedName)(path, role);
|
|
216
|
+
switch (role) {
|
|
217
|
+
case "hook":
|
|
218
|
+
return (`cp -n -- ${shq(path)} ${shq(target)} && ` +
|
|
219
|
+
`npx vigiles compile ${shq(target)} && ` +
|
|
220
|
+
`rm -f -- ${shq(path)} ${shq(`${exports.HOOKS_DIR}/${(0, node_path_1.basename)(path)}.json`)}`);
|
|
221
|
+
case "provider":
|
|
222
|
+
return `mv -n -- ${shq(path)} ${shq(target)}`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* {@link unclaimedMessage} with the one fact it cannot know, read off the disk:
|
|
227
|
+
* whether the marked name is already there.
|
|
228
|
+
*/
|
|
229
|
+
function unclaimedReport(path, role, cwd = process.cwd()) {
|
|
230
|
+
return unclaimedMessage(path, role, (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, (0, source_kinds_js_1.markedName)(path, role))));
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The text `compile` prints for one file it will not compile, with the exact
|
|
234
|
+
* command to fix it. `targetExists` is whether the marked name is already on
|
|
235
|
+
* disk: then there is nothing safe to copy, and the message says to merge by
|
|
236
|
+
* hand instead.
|
|
237
|
+
*/
|
|
238
|
+
function unclaimedMessage(path, role, targetExists = false) {
|
|
239
|
+
const dir = (0, source_kinds_js_1.dirName)(path);
|
|
240
|
+
const target = (0, source_kinds_js_1.markedName)(path, role);
|
|
241
|
+
const found = (0, source_kinds_js_1.classifySource)((0, source_kinds_js_1.baseName)(path)).kind;
|
|
242
|
+
const head = `${path} — not compiled: a ${role} source must carry \`.${role}.\` before its extension (${(0, source_kinds_js_1.baseName)(target)}), and this name carries no marker vigiles knows.`;
|
|
243
|
+
const misplaced = found === "provider" || found === "hook"
|
|
244
|
+
? ` It is marked as a ${found}, which belongs in ${found === "hook" ? exports.HOOKS_DIR : exports.PROVIDERS_DIR}/ — move it there.`
|
|
245
|
+
: undefined;
|
|
246
|
+
const notIt = ` If it is not a ${role}, move it out of ${dir || "this directory"} — a test belongs in a \`.harness.\` or \`.eval.\` file.`;
|
|
247
|
+
if (misplaced !== undefined)
|
|
248
|
+
return [head, misplaced].join("\n");
|
|
249
|
+
if (targetExists)
|
|
250
|
+
return [
|
|
251
|
+
head,
|
|
252
|
+
` ${target} already exists beside it, so nothing is printed that could overwrite it. If ${path} is the ${role}, compare the two by hand, keep the content you want in ${target}, delete ${path}${role === "hook" ? ` and its stamp, and run \`npx vigiles compile ${target}\`` : ""}.`,
|
|
253
|
+
notIt,
|
|
254
|
+
].join("\n");
|
|
255
|
+
return [
|
|
256
|
+
head,
|
|
257
|
+
role === "hook"
|
|
258
|
+
? ` If it is a hook, rename it. Copy first and delete last, so a failure leaves the old hook in place and wired:`
|
|
259
|
+
: ` If it is a provider, rename it (providers are found by directory, never wired by path):`,
|
|
260
|
+
` ${renameCommand(path, role)}`,
|
|
261
|
+
notIt,
|
|
262
|
+
].join("\n");
|
|
63
263
|
}
|
|
64
264
|
/**
|
|
65
265
|
* The CANONICAL form of a hook-source reference — how the path is written into
|
|
@@ -122,49 +322,30 @@ function hookRuntimeRef(projectRootTokens) {
|
|
|
122
322
|
const token = projectRootTokens?.[0];
|
|
123
323
|
return token === undefined ? `node ${rel}` : `node "${token}/${rel}"`;
|
|
124
324
|
}
|
|
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
325
|
function hookGateRef(ref, projectRootTokens) {
|
|
163
326
|
const token = projectRootTokens?.[0];
|
|
164
327
|
return token === undefined ? ref : `"${token}/${ref}"`;
|
|
165
328
|
}
|
|
166
329
|
/**
|
|
167
|
-
*
|
|
330
|
+
* The wiring references a hook file owns: its own, and — for `x.hook.ts` — the
|
|
331
|
+
* one an older vigiles wrote when the file was still called `x.ts`.
|
|
332
|
+
*
|
|
333
|
+
* 🔴 WITHOUT THE SECOND, RENAMING A HOOK TO CARRY ITS MARKER LEAVES A DEAD ENTRY.
|
|
334
|
+
* Wiring is keyed by path, so compiling `guard.hook.mjs` appended a new entry and
|
|
335
|
+
* left the one for `guard.mjs` in `settings.json`, pointing at a file that is
|
|
336
|
+
* about to be deleted. That entry cannot load; wired as a gate it blocks every
|
|
337
|
+
* Bash call, the repair included (see {@link unclaimedMessage}). The marker rename
|
|
338
|
+
* is the one rename vigiles itself asks of every consumer, so it is the one the
|
|
339
|
+
* merge is told about: the renamed file takes over the old file's wiring.
|
|
340
|
+
*/
|
|
341
|
+
function ownedRefs(hookPath) {
|
|
342
|
+
const ref = normalizeHookRef(hookPath);
|
|
343
|
+
const before = (0, source_kinds_js_1.preMarkerName)(ref, "hook");
|
|
344
|
+
return before === undefined ? [ref] : [ref, before];
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* True when an entry's command routes through the runtime for `hookPath`
|
|
348
|
+
* (or for the name it had before it carried its marker, see {@link ownedRefs}).
|
|
168
349
|
*
|
|
169
350
|
* Compares CANONICALIZED path tokens rather than testing for a raw substring:
|
|
170
351
|
* `./x.hook.ts` and `x.hook.ts` are the same file (so the entry is replaced,
|
|
@@ -172,10 +353,10 @@ function hookGateRef(ref, projectRootTokens) {
|
|
|
172
353
|
* `x.hook.ts` and `my-x.hook.ts` are not (a substring test said they were).
|
|
173
354
|
*/
|
|
174
355
|
function managesHook(entry, hookPath) {
|
|
175
|
-
const
|
|
356
|
+
const refs = ownedRefs(hookPath);
|
|
176
357
|
return entry.hooks.some((h) => h.command.split(/\s+/).some((token) => {
|
|
177
358
|
const bare = bareToken(token);
|
|
178
|
-
return bare !== "" && normalizeHookRef(bare)
|
|
359
|
+
return bare !== "" && refs.includes(normalizeHookRef(bare));
|
|
179
360
|
}));
|
|
180
361
|
}
|
|
181
362
|
/**
|
package/dist/hook-runtime.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ import { loadHook } from "./load-hook.js";
|
|
|
44
44
|
* loads in a test loads identically here (one loader, no drift).
|
|
45
45
|
*/
|
|
46
46
|
export declare const loadHookProgram: typeof loadHook;
|
|
47
|
-
/** Load a registered provider (`.vigiles/providers/<name>`) → its definition. */
|
|
47
|
+
/** Load a registered provider (`.vigiles/providers/<name>.provider.<ext>`) → its definition. */
|
|
48
48
|
export declare function loadProvider(file: string, root?: string): Promise<RegisteredProvider>;
|
|
49
49
|
/** Path of the tamper-evident stamp sidecar for a hook file. */
|
|
50
50
|
export declare function hookStampPath(file: string, root?: string): string;
|
package/dist/hook-runtime.js
CHANGED
|
@@ -75,7 +75,7 @@ function injectableEventsFor(root) {
|
|
|
75
75
|
* loads in a test loads identically here (one loader, no drift).
|
|
76
76
|
*/
|
|
77
77
|
exports.loadHookProgram = load_hook_js_1.loadHook;
|
|
78
|
-
/** Load a registered provider (`.vigiles/providers/<name>`) → its definition. */
|
|
78
|
+
/** Load a registered provider (`.vigiles/providers/<name>.provider.<ext>`) → its definition. */
|
|
79
79
|
async function loadProvider(file, root = process.cwd()) {
|
|
80
80
|
const abs = (0, node_path_1.resolve)(root, file);
|
|
81
81
|
const { pathToFileURL } = require("node:url");
|
|
@@ -155,7 +155,13 @@ async function gatherHookContext(program, file, root) {
|
|
|
155
155
|
*/
|
|
156
156
|
async function loadProviderRegistry(root) {
|
|
157
157
|
const registry = {};
|
|
158
|
-
|
|
158
|
+
// Tolerant on purpose, unlike `compile`: a provider file written before the
|
|
159
|
+
// `.provider.` marker existed is `unclaimed`, and a hook compiled yesterday
|
|
160
|
+
// still resolves `provider("x")` against it today. `compile` refuses such a
|
|
161
|
+
// file out loud; the runtime must not turn that into a silently empty value
|
|
162
|
+
// for a hook already wired. Tests and declarations are never loaded.
|
|
163
|
+
const { claimed, unclaimed } = (0, hook_install_js_1.discoverProviderFiles)(root);
|
|
164
|
+
for (const file of [...claimed, ...unclaimed]) {
|
|
159
165
|
try {
|
|
160
166
|
const def = await loadProvider(file, root);
|
|
161
167
|
registry[def.name] = def;
|
|
@@ -432,12 +438,16 @@ async function runHookProgramCommand(file) {
|
|
|
432
438
|
// the author are different from a `deny`'s: name the real cause, and leave a
|
|
433
439
|
// way back.
|
|
434
440
|
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
+
// A hook that cannot be loaded always exits 2 here, whatever kind of hook it
|
|
442
|
+
// is. We cannot tell: the hook did not load, so there is nothing to ask
|
|
443
|
+
// whether it is a gate (can block) or a nudge (reminder only). This code used
|
|
444
|
+
// to guess from the file name (`file.includes("inject")`), and the guess
|
|
445
|
+
// disagreed with the `|| exit N` ending that `vigiles compile` writes on the
|
|
446
|
+
// command: a nudge without "inject" in its name blocked the session, and a
|
|
447
|
+
// gate with it let every command through (#312).
|
|
448
|
+
// Now only that ending decides. A nudge is wired `|| exit 0`, so the shell
|
|
449
|
+
// turns our 2 into a 0 and it degrades; a gate is wired `|| exit 2` and
|
|
450
|
+
// blocks. A command with no such ending is treated as a gate, the safe default.
|
|
441
451
|
//
|
|
442
452
|
// Escapes, both announced loudly on stderr:
|
|
443
453
|
// - the stale-stamp one (an edit to the hook itself / `vigiles compile`),
|
|
@@ -476,11 +486,13 @@ async function runHookProgramCommand(file) {
|
|
|
476
486
|
announceRepairEscape(file, cause);
|
|
477
487
|
return;
|
|
478
488
|
}
|
|
479
|
-
// Log the error, but for inject hooks, degrade gracefully (exit 0).
|
|
480
489
|
const errorMsg = `vigiles: hook ${file} ${cause}.\n` +
|
|
481
490
|
`vigiles: this is the state of the HARNESS, not a decision about your ` +
|
|
482
491
|
`command — the gate never ran. Blocking anyway (a gate that cannot run ` +
|
|
483
492
|
`must not pass traffic).\n` +
|
|
493
|
+
`vigiles: if this hook is a NUDGE (an inject or a react), it must not ` +
|
|
494
|
+
`block: its wiring should end in \`|| exit 0\`, which \`vigiles compile\` ` +
|
|
495
|
+
`writes. A hook wired by hand with no such ending is treated as a gate.\n` +
|
|
484
496
|
`vigiles: the way out is a FILE WRITE, not a command — under a tool that ` +
|
|
485
497
|
`WRITES (Write/Edit/MultiEdit); a Read of the same path repairs nothing ` +
|
|
486
498
|
`and is refused. Fix whichever of ` +
|
|
@@ -493,24 +505,11 @@ async function runHookProgramCommand(file) {
|
|
|
493
505
|
`repair this failure.\n` +
|
|
494
506
|
`vigiles: no command is allowed, deliberately. \`git merge --abort\` and ` +
|
|
495
507
|
`\`git checkout\` RUN \`.git/hooks/*\` (measured: reference-transaction, ` +
|
|
496
|
-
`post-checkout),
|
|
497
|
-
`resolver that just failed
|
|
508
|
+
`post-checkout), \`vigiles compile\` loads the hook through the same ` +
|
|
509
|
+
`resolver that just failed, and an install runs the lifecycle scripts of ` +
|
|
510
|
+
`every package in the lockfile.`;
|
|
498
511
|
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;
|
|
512
|
+
process.exit(2);
|
|
514
513
|
}
|
|
515
514
|
verifyStampOrRefuse(file, event);
|
|
516
515
|
switch ((0, hook_program_js_1.dispatchKind)(program)) {
|
|
@@ -91,7 +91,7 @@ export interface HookStateHandle {
|
|
|
91
91
|
* OLD fact, and "old" is not something a test can produce by waiting.
|
|
92
92
|
*
|
|
93
93
|
* ```js
|
|
94
|
-
* const st = experimental_hookState(".vigiles/hooks/nag.mjs", { cwd });
|
|
94
|
+
* const st = experimental_hookState(".vigiles/hooks/nag.hook.mjs", { cwd });
|
|
95
95
|
* st.seed("retro.nagged", { ago: "4d" }); // → the hook speaks
|
|
96
96
|
* st.seed("retro.nagged", { ago: "10m" }); // → the hook stays quiet
|
|
97
97
|
* ```
|
package/dist/hook.js
CHANGED
|
@@ -143,7 +143,7 @@ Object.defineProperty(exports, "HookStateError", { enumerable: true, get: functi
|
|
|
143
143
|
// and `pipesToShell()` match over, so a guard written against the closed
|
|
144
144
|
// vocabulary sees the OPERATION rather than the literal tokens.
|
|
145
145
|
//
|
|
146
|
-
// It is exported because `examples/harness/safe-bash-guard-v2.mjs` needs it —
|
|
146
|
+
// It is exported because `examples/harness/safe-bash-guard-v2.hook.mjs` needs it —
|
|
147
147
|
// and that need is the finding, not the feature. v2 is an UNRUN experiment (no
|
|
148
148
|
// test executes it) covering three things the vocabulary cannot yet express: a
|
|
149
149
|
// flag with a value (`--index-url`), an env-assignment prefix, and a
|
package/dist/load-hook.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { type AnyHook } from "./core/hook-program.js";
|
|
|
10
10
|
* ```js
|
|
11
11
|
* import { loadHook, assertHookDenies } from "vigiles";
|
|
12
12
|
*
|
|
13
|
-
* const guard = await loadHook(".vigiles/hooks/guard.mjs");
|
|
13
|
+
* const guard = await loadHook(".vigiles/hooks/guard.hook.mjs");
|
|
14
14
|
* assertHookDenies(guard, {
|
|
15
15
|
* tool_name: "Bash",
|
|
16
16
|
* tool_input: { command: "git push --force" },
|
package/dist/load-hook.js
CHANGED
|
@@ -29,7 +29,7 @@ const hook_program_js_1 = require("./core/hook-program.js");
|
|
|
29
29
|
* ```js
|
|
30
30
|
* import { loadHook, assertHookDenies } from "vigiles";
|
|
31
31
|
*
|
|
32
|
-
* const guard = await loadHook(".vigiles/hooks/guard.mjs");
|
|
32
|
+
* const guard = await loadHook(".vigiles/hooks/guard.hook.mjs");
|
|
33
33
|
* assertHookDenies(guard, {
|
|
34
34
|
* tool_name: "Bash",
|
|
35
35
|
* tool_input: { command: "git push --force" },
|
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`)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a file in a vigiles-owned source directory IS — decided in ONE place.
|
|
3
|
+
*
|
|
4
|
+
* `.vigiles/hooks/` and `.vigiles/providers/` hold several kinds of file side by
|
|
5
|
+
* side: the programs `vigiles compile` turns into wiring, the tests that exercise
|
|
6
|
+
* them, type declarations, and the `.json` stamp `compile` writes next to each
|
|
7
|
+
* hook. Discovery used to ask "does it end in a runnable extension?" and treat
|
|
8
|
+
* every yes as a hook (#278): a `task-list-nudge.harness.mjs` beside its
|
|
9
|
+
* `task-list-nudge.hook.ts` was compiled as a hook and failed the build. The
|
|
10
|
+
* obvious patch — subtract the names that bit (`.harness.`, `.test.`) — is a list
|
|
11
|
+
* of what a hook is NOT, and a list of that kind is open: the next helper file
|
|
12
|
+
* somebody colocates is the next report.
|
|
13
|
+
*
|
|
14
|
+
* So a hook is defined by what it IS. A file's kind comes from ONE marker, the
|
|
15
|
+
* LAST dot-separated segment before the extension (`guard.hook.mjs` → `hook`),
|
|
16
|
+
* looked up in ONE table ({@link MARKERS}). Because exactly one segment decides,
|
|
17
|
+
* a name has exactly one kind; because the table is closed, every consumer can
|
|
18
|
+
* `switch` over {@link SourceKind} exhaustively and the compiler names the next
|
|
19
|
+
* one that forgets a case. A runnable file whose marker is not in the table is
|
|
20
|
+
* `unclaimed` — a kind of its own, not an absence — so a renamed-away hook is a
|
|
21
|
+
* reported fact rather than a silent hole.
|
|
22
|
+
*
|
|
23
|
+
* The patterns test discovery uses (`vigiles test`, `vigiles eval`, the coverage
|
|
24
|
+
* defaults, the browser twin) are DERIVED from the same table. They used to be
|
|
25
|
+
* five hand-kept copies of one fact: `*.harness.*` and `*.eval.*` are tests.
|
|
26
|
+
*
|
|
27
|
+
* Pure and import-free on purpose. `hook-install.ts` imports this and is reached
|
|
28
|
+
* from the hook runtime, where every module is a per-call cost, and the browser
|
|
29
|
+
* twin of coverage detection needs it too — nothing here may touch `node:*`.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Every extension Node executes directly. `.mts`/`.cts` are real (TS 4.7+) and
|
|
33
|
+
* Node 22 strips their types with no toolchain — measured, not assumed.
|
|
34
|
+
*/
|
|
35
|
+
export declare const RUNNABLE_EXTS: readonly ["mjs", "cjs", "js", "mts", "cts", "ts"];
|
|
36
|
+
/** Which vigiles test runner takes a file: `vigiles test` or `vigiles eval`. */
|
|
37
|
+
export type TestTier = "harness" | "eval";
|
|
38
|
+
/**
|
|
39
|
+
* The closed union of what a file in a source directory can be. Exactly one per
|
|
40
|
+
* name; see {@link classifySource}.
|
|
41
|
+
*/
|
|
42
|
+
export type SourceKind =
|
|
43
|
+
/** A compiled-hook program: `guard.hook.mjs`. */
|
|
44
|
+
{
|
|
45
|
+
readonly kind: "hook";
|
|
46
|
+
}
|
|
47
|
+
/** A registered context provider: `k8s.provider.mjs`. */
|
|
48
|
+
| {
|
|
49
|
+
readonly kind: "provider";
|
|
50
|
+
}
|
|
51
|
+
/** A vigiles test: `guard.harness.mjs` (`vigiles test`) or `guard.eval.mjs` (`vigiles eval`). */
|
|
52
|
+
| {
|
|
53
|
+
readonly kind: "vigiles-test";
|
|
54
|
+
readonly test: TestTier;
|
|
55
|
+
}
|
|
56
|
+
/** A type declaration: `x.d.ts`, `x.d.mts`, `x.d.cts`. */
|
|
57
|
+
| {
|
|
58
|
+
readonly kind: "declaration";
|
|
59
|
+
}
|
|
60
|
+
/** The `.json` sidecar `compile` writes beside a source: `guard.hook.mjs.json`. */
|
|
61
|
+
| {
|
|
62
|
+
readonly kind: "stamp";
|
|
63
|
+
}
|
|
64
|
+
/** Not a runnable source at all: `README.md`, `.gitkeep`, a stray `.json`. */
|
|
65
|
+
| {
|
|
66
|
+
readonly kind: "non-source";
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A runnable file whose name carries no marker vigiles knows: `guard.mjs`
|
|
70
|
+
* (a hook from before the marker), `util.ts`, `guard.test.ts`. Nobody's — and
|
|
71
|
+
* therefore never compiled, and never silently ignored either.
|
|
72
|
+
*/
|
|
73
|
+
| {
|
|
74
|
+
readonly kind: "unclaimed";
|
|
75
|
+
};
|
|
76
|
+
/** The markers that name a file a vigiles consumer COMPILES (as opposed to runs or ignores). */
|
|
77
|
+
export type CompilableMarker = "hook" | "provider";
|
|
78
|
+
/**
|
|
79
|
+
* The kind of a file, from its BASENAME. Total: every string has an answer, and
|
|
80
|
+
* the answer is one member of {@link SourceKind}.
|
|
81
|
+
*
|
|
82
|
+
* Order matters and is the whole algorithm: a declaration is a declaration even
|
|
83
|
+
* when its infix says `hook` (`x.hook.d.ts`); a `.json` beside a source is its
|
|
84
|
+
* stamp; then the last infix is looked up. Case-sensitive, like the file systems
|
|
85
|
+
* these names mostly live on.
|
|
86
|
+
*/
|
|
87
|
+
export declare function classifySource(filename: string): SourceKind;
|
|
88
|
+
/**
|
|
89
|
+
* Is this basename an eval script — the PAID tier, the one that spends real
|
|
90
|
+
* model calls? It decides which of two runners owns a test file, and has been
|
|
91
|
+
* wrong in both directions:
|
|
92
|
+
*
|
|
93
|
+
* - As the full suffix `.eval.mjs`: `foo.eval.ts` fell into the free branch and
|
|
94
|
+
* would have spent real model calls on every push.
|
|
95
|
+
* - As the bare INFIX `.eval.`: `parser.eval.test.ts`, an ordinary deterministic
|
|
96
|
+
* test, was credited to the paid tier and dropped from the free one — though
|
|
97
|
+
* `vigiles eval` cannot discover that name at all.
|
|
98
|
+
*
|
|
99
|
+
* The answer that is wrong in neither direction is the runner's own: `.eval.`
|
|
100
|
+
* followed by a runnable extension AT THE END of the name, which is exactly what
|
|
101
|
+
* the last-infix rule of {@link classifySource} says.
|
|
102
|
+
*
|
|
103
|
+
* @param filename a BASENAME — callers strip the directory with their own path
|
|
104
|
+
* helper (`node:path` on disk, `posix-path` in the browser twin).
|
|
105
|
+
*/
|
|
106
|
+
export declare function isEvalScript(filename: string): boolean;
|
|
107
|
+
/** Is this basename a vigiles test of either tier? */
|
|
108
|
+
export declare function isVigilesTest(filename: string): boolean;
|
|
109
|
+
/** The glob `vigiles test` / `vigiles eval` discover a tier's scripts with. */
|
|
110
|
+
export declare function testGlob(tier: TestTier): string;
|
|
111
|
+
/** The patterns coverage credits tests with by default — one per tier, from the table. */
|
|
112
|
+
export declare const DEFAULT_TEST_GLOBS: readonly string[];
|
|
113
|
+
/** The last path segment, whichever separator precedes it. */
|
|
114
|
+
export declare const baseName: (path: string) => string;
|
|
115
|
+
/** Everything up to and including the last separator, or "" when there is none. */
|
|
116
|
+
export declare const dirName: (path: string) => string;
|
|
117
|
+
/**
|
|
118
|
+
* `guard.mjs` → `guard.hook.mjs`: the name a file must carry to be claimed as
|
|
119
|
+
* `marker`. Keeps the directory. The inverse of {@link preMarkerName}.
|
|
120
|
+
*/
|
|
121
|
+
export declare function markedName(path: string, marker: CompilableMarker): string;
|
|
122
|
+
/**
|
|
123
|
+
* `guard.hook.mjs` → `guard.mjs`: the name a file carried before the marker
|
|
124
|
+
* existed, or `undefined` when the name has no such marker. Used to recognise
|
|
125
|
+
* the wiring an older vigiles wrote for the same file.
|
|
126
|
+
*/
|
|
127
|
+
export declare function preMarkerName(path: string, marker: CompilableMarker): string | undefined;
|
|
128
|
+
//# sourceMappingURL=source-kinds.d.ts.map
|