vigiles 5.2.0 → 7.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.
Files changed (78) hide show
  1. package/README.md +99 -48
  2. package/dist/action-gate.js +1 -1
  3. package/dist/adapters/claude-code/agent-runtime.d.ts +64 -4
  4. package/dist/adapters/claude-code/agent-runtime.js +131 -17
  5. package/dist/adapters/claude-code/dialect.d.ts +34 -0
  6. package/dist/adapters/claude-code/dialect.js +46 -33
  7. package/dist/adapters/claude-code/effect-region.js +1 -1
  8. package/dist/adapters/claude-code/skill-runtime.d.ts +1 -1
  9. package/dist/adapters/claude-code/skill-runtime.js +1 -9
  10. package/dist/adapters/claude-code/typed-spec.d.ts +58 -0
  11. package/dist/adapters/claude-code/typed-spec.js +55 -0
  12. package/dist/adapters/codex/hook-protocol.js +3 -0
  13. package/dist/adapters/codex/mock-model.js +1 -1
  14. package/dist/claude-code.d.ts +1 -0
  15. package/dist/claude-code.js +8 -1
  16. package/dist/cli-commands.d.ts +19 -0
  17. package/dist/cli-commands.js +51 -0
  18. package/dist/cli.js +735 -76
  19. package/dist/core/bash-effects.d.ts +12 -0
  20. package/dist/core/bash-effects.js +31 -0
  21. package/dist/core/capability-diff.d.ts +46 -0
  22. package/dist/core/capability-diff.js +97 -0
  23. package/dist/core/compile.d.ts +1 -1
  24. package/dist/core/compile.js +14 -0
  25. package/dist/core/generate-harness.d.ts +187 -0
  26. package/dist/core/generate-harness.js +337 -0
  27. package/dist/core/guards.d.ts +126 -0
  28. package/dist/core/guards.js +309 -0
  29. package/dist/core/harness-driver.d.ts +1 -1
  30. package/dist/core/hook-program.d.ts +459 -0
  31. package/dist/core/hook-program.js +468 -0
  32. package/dist/core/hook-protocol.d.ts +7 -0
  33. package/dist/core/hook-providers.d.ts +138 -0
  34. package/dist/core/hook-providers.js +155 -0
  35. package/dist/core/hook-spec.d.ts +74 -0
  36. package/dist/core/hook-spec.js +130 -0
  37. package/dist/core/inline.js +1 -1
  38. package/dist/core/mcp-tool.d.ts +12 -0
  39. package/dist/core/mcp-tool.js +20 -0
  40. package/dist/core/mcp.d.ts +13 -0
  41. package/dist/core/mcp.js +67 -0
  42. package/dist/core/spec.d.ts +290 -8
  43. package/dist/core/spec.js +118 -3
  44. package/dist/core/types.d.ts +8 -0
  45. package/dist/dialect-drift.d.ts +65 -0
  46. package/dist/dialect-drift.js +216 -0
  47. package/dist/eval.d.ts +40 -5
  48. package/dist/eval.js +59 -5
  49. package/dist/guardrail-check.d.ts +85 -0
  50. package/dist/guardrail-check.js +152 -0
  51. package/dist/harness-assert.d.ts +10 -0
  52. package/dist/harness-assert.js +30 -0
  53. package/dist/hook-install.d.ts +43 -0
  54. package/dist/hook-install.js +91 -0
  55. package/dist/hook.d.ts +52 -0
  56. package/dist/hook.js +98 -0
  57. package/dist/leaderboard.d.ts +6 -0
  58. package/dist/leaderboard.js +43 -1
  59. package/dist/linting.d.ts +9 -5
  60. package/dist/linting.js +17 -5
  61. package/dist/optimize.js +1 -1
  62. package/dist/scaffold-test.d.ts +28 -0
  63. package/dist/scaffold-test.js +134 -15
  64. package/dist/scan-behavioral.d.ts +60 -0
  65. package/dist/scan-behavioral.js +239 -1
  66. package/dist/scan.d.ts +14 -0
  67. package/dist/scan.js +33 -1
  68. package/dist/score-explainer.js +1 -1
  69. package/dist/self-command-refs.d.ts +21 -0
  70. package/dist/self-command-refs.js +125 -0
  71. package/dist/testing.d.ts +5 -3
  72. package/dist/testing.js +37 -23
  73. package/dist/tool-intercept.d.ts +4 -4
  74. package/dist/tool-intercept.js +5 -5
  75. package/dist/unit.d.ts +2 -0
  76. package/dist/unit.js +8 -1
  77. package/hooks/refs-nudge.sh +1 -1
  78. package/package.json +5 -3
@@ -0,0 +1,468 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defineReact = exports.nothing = exports.notice = exports.run = exports.defineInject = exports.inject = exports.tools = exports.HookCompileError = exports.tool = exports.ask = exports.deny = exports.allow = void 0;
4
+ exports.gateAction = gateAction;
5
+ exports.commandView = commandView;
6
+ exports.defineHook = defineHook;
7
+ exports.decideProgram = decideProgram;
8
+ exports.decisionExitCode = decisionExitCode;
9
+ exports.checkHookImports = checkHookImports;
10
+ exports.dispatchKind = dispatchKind;
11
+ exports.hookMode = hookMode;
12
+ exports.hookNeeds = hookNeeds;
13
+ exports.hookRouting = hookRouting;
14
+ exports.compileHookProgram = compileHookProgram;
15
+ exports.stampHook = stampHook;
16
+ exports.verifyHookStamp = verifyHookStamp;
17
+ exports.pathView = pathView;
18
+ exports.defineFileGate = defineFileGate;
19
+ exports.decideFileGate = decideFileGate;
20
+ exports.definePromptGate = definePromptGate;
21
+ exports.decidePromptGate = decidePromptGate;
22
+ exports.defineStopGate = defineStopGate;
23
+ exports.decideStopGate = decideStopGate;
24
+ exports.runInject = runInject;
25
+ exports.responseView = responseView;
26
+ exports.runReact = runReact;
27
+ exports.runHookProgram = runHookProgram;
28
+ /**
29
+ * Compiled hooks — a hook as a CONSTRAINED TYPED PROGRAM, not arbitrary shell.
30
+ *
31
+ * The pure core behind the public `vigiles/hook` surface (re-exported in
32
+ * `src/hook.ts`; compiled by `vigiles compile`, run by `vigiles hook-runtime
33
+ * run-program`). A hook today is opaque shell (`bash guard.sh`) —
34
+ * un-analyzable (Rice), and the author hand-writes the fragile parts (exit code,
35
+ * JSON field, a `grep` matcher) that the verified #1 pains come from. Invert it:
36
+ * the author writes a PURE typed function `(event) => Decision` against a CLOSED
37
+ * API; vigiles compiles it. The constraint ELIMINATES whole bug classes by
38
+ * construction and buys testability / safety / matching / portability:
39
+ *
40
+ * - TESTABILITY: `decide` is a pure fn — unit-test in-process, no subprocess, no
41
+ * exit-code/JSON plumbing. The false-confidence bug class (exit 1≠2, wrong field)
42
+ * is UNREPRESENTABLE — the author never writes the protocol; `compile` emits it.
43
+ * - SAFETY: capability = API surface. `checkHookImports` rejects any import outside
44
+ * `vigiles/hook` at compile (so the hook can't reach `child_process`/`net`), and
45
+ * `stampHook`/`verifyHookStamp` make the compiled artifact TAMPER-EVIDENT (the
46
+ * integrity.ts pattern) — a hand-edit that smuggles a capability breaks the stamp.
47
+ * - MATCHING: `command.runs("git push", { force })` is AST-backed (leafCommands),
48
+ * so it catches `cd x && git push -f` that the native `Bash(git:*)` glob (#30519)
49
+ * and a hand-written `grep` both miss.
50
+ * - PORTABILITY: one program → each harness's protocol (CC exit-2 here; Codex /
51
+ * OpenCode via the HookProtocol port later — OpenCode hooks ARE in-process TS).
52
+ *
53
+ * Pure core, harness-neutral. HONEST SCOPE (kept in every doc): compile/verify fix
54
+ * the hook's AUTHORING + LOGIC, not DELIVERY — CC's subagent-bypass (#34692) means
55
+ * a PreToolUse hook does not fire for a subagent's tool calls, so a gate is a strong
56
+ * default, never an unbypassable wall. Limits (buy-in, node-startup latency) +
57
+ * full record in research/hook-pain-points.md.
58
+ */
59
+ const bash_effects_js_1 = require("./bash-effects.js");
60
+ const hash_js_1 = require("./hash.js");
61
+ const toml_1 = require("@iarna/toml");
62
+ const hook_events_js_1 = require("./hook-events.js");
63
+ const hook_providers_js_1 = require("./hook-providers.js");
64
+ const allow = () => ({ kind: "allow" });
65
+ exports.allow = allow;
66
+ const deny = (reason) => ({ kind: "deny", reason });
67
+ exports.deny = deny;
68
+ const ask = (reason) => ({ kind: "ask", reason });
69
+ exports.ask = ask;
70
+ /**
71
+ * Map a gate's {@link Decision} + {@link HookMode} to what the runtime actually does.
72
+ * `enforce`: deny→block (exit 2), ask→ask, allow→allow. `observe`: a deny/ask is
73
+ * recorded as a no-op `observe` (would-have-blocked) and allowed; allow stays allow.
74
+ * Pure, so a test asserts "in observe mode this deny does NOT block" with no process.
75
+ */
76
+ function gateAction(decision, mode = "enforce") {
77
+ if (mode === "observe")
78
+ return decision.kind === "allow"
79
+ ? { kind: "allow" }
80
+ : { kind: "observe", would: decision.kind, reason: decision.reason };
81
+ switch (decision.kind) {
82
+ case "deny":
83
+ return { kind: "block", reason: decision.reason };
84
+ case "ask":
85
+ return { kind: "ask", reason: decision.reason };
86
+ case "allow":
87
+ return { kind: "allow" };
88
+ default:
89
+ return (0, hash_js_1.assertNever)(decision);
90
+ }
91
+ }
92
+ const FORCE_FLAG = /^-(?:-force$|[a-z]*f[a-z]*$)/;
93
+ const hasForce = (argv) => argv.some((a) => FORCE_FLAG.test(a));
94
+ const SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
95
+ /** Does `argv` run `tokens` in order (head exact, rest present after it)? */
96
+ function runsSeq(argv, tokens) {
97
+ if (argv[0] !== tokens[0])
98
+ return false;
99
+ let i = 1;
100
+ for (let j = 1; j < argv.length && i < tokens.length; j++) {
101
+ if (argv[j] === tokens[i])
102
+ i++;
103
+ }
104
+ return i === tokens.length;
105
+ }
106
+ /** Does a single token name a path at/under `prefix` (boundary-aware)? */
107
+ function tokenUnder(token, prefix) {
108
+ const t = token.replace(/^\.\//, "");
109
+ return t === prefix || t.startsWith(prefix + "/") || t.endsWith("/" + prefix);
110
+ }
111
+ /** A leaf whose head is a shell reading stdin (no script-file argument). */
112
+ function isBareShellLeaf(argv) {
113
+ return (SHELLS.has(argv[0] ?? "") && argv.slice(1).every((a) => a.startsWith("-")));
114
+ }
115
+ function commandView(raw) {
116
+ const leaves = (0, bash_effects_js_1.leafCommands)(raw);
117
+ return {
118
+ raw,
119
+ runs(program, opts) {
120
+ const tokens = program.split(/\s+/).filter(Boolean);
121
+ return leaves.some((argv) => runsSeq(argv, tokens) && (opts?.force ? hasForce(argv) : true));
122
+ },
123
+ isSideEffecting: () => (0, bash_effects_js_1.classifyBashCommand)(raw) === "side-effecting",
124
+ touches: (prefixes) => leaves.some((argv) => argv.slice(1).some((tok) => prefixes.some((p) => tokenUnder(tok, p)))),
125
+ pipesToShell: () => leaves.some(isBareShellLeaf),
126
+ };
127
+ }
128
+ const tool = (name) => ({ tool: name });
129
+ exports.tool = tool;
130
+ function defineHook(p) {
131
+ return p;
132
+ }
133
+ // ---------------------------------------------------------------------------
134
+ // Run the program against a raw PreToolUse event (the runtime half)
135
+ // ---------------------------------------------------------------------------
136
+ /** Build the typed event from a raw PreToolUse event, then decide. */
137
+ function decideProgram(program, rawEvent, ctx = {}) {
138
+ if (rawEvent.tool_name !== program.match.tool)
139
+ return (0, exports.allow)();
140
+ const command = typeof rawEvent.tool_input?.command === "string"
141
+ ? rawEvent.tool_input.command
142
+ : "";
143
+ return program.decide({
144
+ event: program.on,
145
+ tool: "Bash",
146
+ command: commandView(command),
147
+ ctx: ctx,
148
+ });
149
+ }
150
+ /** Map a Decision to the CC hook exit code — the protocol the author never writes. */
151
+ function decisionExitCode(d) {
152
+ return d.kind === "deny" ? 2 : 0;
153
+ }
154
+ // ---------------------------------------------------------------------------
155
+ // Compile-time safety: capability = the API surface
156
+ // ---------------------------------------------------------------------------
157
+ const ALLOWED_IMPORT = "vigiles/hook";
158
+ /**
159
+ * Reject any import/require outside `vigiles/hook` + textual escape hatches
160
+ * (eval / Function / dynamic import). The hook's capabilities are then EXACTLY the
161
+ * sanctioned API — it cannot reach `child_process`, `fs`, or the network. Returns
162
+ * the offending specifiers/constructs (empty = clean). (Probe: a regex scan; a
163
+ * production impl walks the TS AST.)
164
+ */
165
+ function checkHookImports(source) {
166
+ const out = [];
167
+ const importRe = /\b(?:import|require)\s*(?:\(|[^'"]*from)?\s*['"]([^'"]+)['"]/g;
168
+ for (const m of source.matchAll(importRe)) {
169
+ if (m[1] !== ALLOWED_IMPORT)
170
+ out.push(m[1]);
171
+ }
172
+ if (/\beval\s*\(|\bnew\s+Function\b|\bimport\s*\(/.test(source)) {
173
+ out.push("dynamic-eval");
174
+ }
175
+ return out;
176
+ }
177
+ class HookCompileError extends Error {
178
+ }
179
+ exports.HookCompileError = HookCompileError;
180
+ function dispatchKind(hook) {
181
+ if ("role" in hook)
182
+ return hook.role === "gate" ? "file-gate" : hook.role;
183
+ return "bash-gate";
184
+ }
185
+ /** A gate's {@link HookMode} (`enforce` default); non-gate roles report `enforce` too. */
186
+ function hookMode(hook) {
187
+ return "mode" in hook && hook.mode ? hook.mode : "enforce";
188
+ }
189
+ /** The context providers a hook declared via `needs` ([] if none / not a gate). */
190
+ function hookNeeds(hook) {
191
+ return "needs" in hook && Array.isArray(hook.needs)
192
+ ? hook.needs
193
+ : [];
194
+ }
195
+ /** Where a hook fires + its tool matcher (undefined for tool-less events). */
196
+ function hookRouting(hook) {
197
+ if ("role" in hook) {
198
+ // inject / prompt-gate / stop-gate fire on a whole EVENT — no tool matcher.
199
+ if (hook.role === "inject" ||
200
+ hook.role === "prompt-gate" ||
201
+ hook.role === "stop-gate")
202
+ return { on: hook.on };
203
+ return { on: hook.on, matcher: hook.match.tools.join("|") };
204
+ }
205
+ return { on: hook.on, matcher: hook.match.tool };
206
+ }
207
+ /** Apply a harness's matcher style to the neutral `A|B` matcher join. */
208
+ function styleMatcher(matcher, protocol) {
209
+ if (matcher === undefined)
210
+ return undefined;
211
+ return protocol?.matcherStyle === "regex" ? `^(${matcher})$` : matcher;
212
+ }
213
+ /**
214
+ * Render the settings block a user pastes into their hooks config. JSON for
215
+ * Claude Code (the nested `{event:[{matcher,hooks:[{type,command}]}]}` shape);
216
+ * TOML `[[hooks.<event>]]` with a flat `command` for Codex.
217
+ */
218
+ function renderSettingsBlock(on, matcher, gateCommand, format) {
219
+ if (format === "toml") {
220
+ const entry = matcher === undefined
221
+ ? { command: gateCommand }
222
+ : { matcher, command: gateCommand };
223
+ return (0, toml_1.stringify)({ hooks: { [on]: [entry] } }).trim();
224
+ }
225
+ const entry = matcher === undefined
226
+ ? { hooks: [{ type: "command", command: gateCommand }] }
227
+ : { matcher, hooks: [{ type: "command", command: gateCommand }] };
228
+ return JSON.stringify({ hooks: { [on]: [entry] } }, null, 2);
229
+ }
230
+ /**
231
+ * Compile a hook program from its source. Runs the capability check FIRST (an
232
+ * out-of-API import does NOT compile), validates the event against the target
233
+ * harness (a typo won't compile), then stamps the source so the shipped artifact
234
+ * is tamper-evident. The hook supplies the routing (event + matcher), which
235
+ * differs per role — a tool gate matches its tool, a react matches its tools, an
236
+ * inject (SessionStart) has no tool matcher at all.
237
+ *
238
+ * Harness-agnostic by injection: with no `opts` it emits the Claude Code block
239
+ * (back-compatible); pass `{ dialect, hookProtocol, settingsFormat }` from a
240
+ * resolved adapter and it emits that harness's block (e.g. Codex TOML + regex
241
+ * matcher). Core never imports an adapter — only the port interfaces.
242
+ */
243
+ function compileHookProgram(source, hook, opts = {}) {
244
+ const violations = checkHookImports(source);
245
+ if (violations.length > 0) {
246
+ throw new HookCompileError(`hook program uses capabilities outside \`${ALLOWED_IMPORT}\`: ${violations.join(", ")} — only the sanctioned API is allowed (capability = API surface).`);
247
+ }
248
+ const { on, matcher: rawMatcher } = hookRouting(hook);
249
+ // A hook registered under an event the harness never fires is dead — reject it.
250
+ if (opts.dialect) {
251
+ const issues = (0, hook_events_js_1.verifyHookEvents)([on], opts.dialect);
252
+ if (issues.length > 0) {
253
+ throw new HookCompileError(issues[0].message);
254
+ }
255
+ }
256
+ // A `needs` entry that isn't a built-in provider never resolves — reject it
257
+ // (the typo-won't-compile guarantee, for JS authors the type can't reach).
258
+ const needs = hookNeeds(hook);
259
+ const unknownNeeds = (0, hook_providers_js_1.unknownProviders)(needs, opts.registeredProviders);
260
+ if (unknownNeeds.length > 0) {
261
+ throw new HookCompileError(`unknown context provider(s): ${unknownNeeds.join(", ")} — built-ins are ${Object.keys(hook_providers_js_1.BUILTIN_PROVIDERS).join(", ")}; a provider() ref must resolve to a .vigiles/providers/ file, or use provide()/dangerously() inline (see research/hook-context-providers.md).`);
262
+ }
263
+ // An inline provide() whose command isn't provably read-only must be acknowledged.
264
+ const unsafe = (0, hook_providers_js_1.unsafeInlineProviders)(needs);
265
+ if (unsafe.length > 0) {
266
+ throw new HookCompileError(`inline provider(s) not provably read-only: ${unsafe
267
+ .map((u) => `${u.name} ("${u.run}")`)
268
+ .join(", ")} — use dangerously(name, cmd) to acknowledge a side-effecting/undecidable command, or keep provide() only for a read-only one.`);
269
+ }
270
+ const gateCommand = opts.gateCommand ?? "npx vigiles hook-runtime run-program";
271
+ const matcher = styleMatcher(rawMatcher, opts.hookProtocol);
272
+ const entry = matcher === undefined
273
+ ? { hooks: [{ type: "command", command: gateCommand }] }
274
+ : {
275
+ matcher,
276
+ hooks: [{ type: "command", command: gateCommand }],
277
+ };
278
+ return {
279
+ hooks: { [on]: [entry] },
280
+ settingsBlock: renderSettingsBlock(on, matcher, gateCommand, opts.settingsFormat ?? "json"),
281
+ stamp: stampHook(source),
282
+ };
283
+ }
284
+ /** Stamp a hook's source (the integrity.ts pattern, applied to a hook artifact). */
285
+ function stampHook(source) {
286
+ return (0, hash_js_1.sha256short)(source);
287
+ }
288
+ /** Verify a shipped hook artifact matches its stamp (tamper → false). */
289
+ function verifyHookStamp(source, stamp) {
290
+ return stampHook(source) === stamp;
291
+ }
292
+ function pathView(raw) {
293
+ const norm = raw.replace(/^\.\//, "");
294
+ return {
295
+ raw,
296
+ under: (prefixes) => prefixes.some((p) => {
297
+ const base = p.replace(/\/?\*+$/, "").replace(/\/$/, "");
298
+ return base === "" || norm === base || norm.startsWith(base + "/");
299
+ }),
300
+ };
301
+ }
302
+ const tools = (...names) => ({
303
+ tools: names,
304
+ });
305
+ exports.tools = tools;
306
+ function defineFileGate(p) {
307
+ return { role: "gate", ...p };
308
+ }
309
+ /** Run a file-tool gate against a raw PreToolUse event (reads `file_path`). */
310
+ function decideFileGate(hook, raw, ctx = {}) {
311
+ const t = raw.tool_name ?? "";
312
+ if (!hook.match.tools.includes(t))
313
+ return (0, exports.allow)();
314
+ const fp = typeof raw.tool_input?.file_path === "string"
315
+ ? raw.tool_input.file_path
316
+ : "";
317
+ return hook.decide({
318
+ event: hook.on,
319
+ tool: t,
320
+ path: pathView(fp),
321
+ ctx: ctx,
322
+ });
323
+ }
324
+ function definePromptGate(p) {
325
+ return { role: "prompt-gate", ...p };
326
+ }
327
+ /** Run a prompt gate against a raw UserPromptSubmit event (reads `prompt`). */
328
+ function decidePromptGate(hook, raw, ctx = {}) {
329
+ const prompt = typeof raw.prompt === "string" ? raw.prompt : "";
330
+ return hook.decide({
331
+ event: hook.on,
332
+ prompt,
333
+ ctx: ctx,
334
+ });
335
+ }
336
+ function defineStopGate(p) {
337
+ return { role: "stop-gate", ...p };
338
+ }
339
+ /** Run a Stop gate against a raw Stop/SubagentStop event (reads `stop_hook_active`). */
340
+ function decideStopGate(hook, raw, ctx = {}) {
341
+ return hook.decide({
342
+ event: hook.on,
343
+ stopHookActive: raw.stop_hook_active === true,
344
+ ctx: ctx,
345
+ });
346
+ }
347
+ const inject = (context) => ({
348
+ kind: "inject",
349
+ context,
350
+ });
351
+ exports.inject = inject;
352
+ const defineInject = (p) => ({
353
+ role: "inject",
354
+ ...p,
355
+ });
356
+ exports.defineInject = defineInject;
357
+ /**
358
+ * Run an inject hook → the CC JSON the author never hand-writes. The compiler
359
+ * targets `additionalContext` (the RIGHT field for this event), so the
360
+ * wrong-JSON-field pain can't occur.
361
+ */
362
+ function runInject(hook, raw) {
363
+ const out = hook.produce({ event: hook.on, source: raw.source ?? "startup" });
364
+ return {
365
+ hookSpecificOutput: {
366
+ hookEventName: hook.on,
367
+ additionalContext: out.context,
368
+ },
369
+ };
370
+ }
371
+ /** Normalize an arbitrary tool_response payload to text (object → JSON). */
372
+ function responseText(raw) {
373
+ if (typeof raw === "string")
374
+ return raw;
375
+ if (raw == null)
376
+ return "";
377
+ return JSON.stringify(raw);
378
+ }
379
+ function responseView(raw) {
380
+ const text = responseText(raw);
381
+ const flagged = typeof raw === "object" &&
382
+ raw !== null &&
383
+ (Boolean(raw.error) ||
384
+ raw.is_error === true);
385
+ return {
386
+ raw: text,
387
+ isError: () => flagged || /^\s*error[:\s]/i.test(text),
388
+ contains: (needle) => text.includes(needle),
389
+ };
390
+ }
391
+ /** Run a command in reaction — its effect is classified AT CONSTRUCTION (audit/diff-able). */
392
+ const run = (command) => ({
393
+ kind: "run",
394
+ command,
395
+ effect: (0, bash_effects_js_1.classifyBashCommand)(command),
396
+ });
397
+ exports.run = run;
398
+ /** Surface a non-blocking note (no execution). */
399
+ const notice = (message) => ({
400
+ kind: "notice",
401
+ message,
402
+ });
403
+ exports.notice = notice;
404
+ /** Do nothing. */
405
+ const nothing = () => ({ kind: "none" });
406
+ exports.nothing = nothing;
407
+ const defineReact = (p) => ({
408
+ role: "react",
409
+ ...p,
410
+ });
411
+ exports.defineReact = defineReact;
412
+ /** Run a react hook against a raw PostToolUse event → the (classified) Reaction. */
413
+ function runReact(hook, raw) {
414
+ const t = raw.tool_name ?? "";
415
+ if (!hook.match.tools.includes(t))
416
+ return (0, exports.nothing)();
417
+ const fp = typeof raw.tool_input?.file_path === "string"
418
+ ? raw.tool_input.file_path
419
+ : "";
420
+ return hook.react({
421
+ event: hook.on,
422
+ tool: t,
423
+ path: pathView(fp),
424
+ response: responseView(raw.tool_response),
425
+ });
426
+ }
427
+ /**
428
+ * Evaluate a compiled hook against a raw event, in-process, dispatching by role:
429
+ * a gate → its `Decision`, an inject → the injected context text, a react → its
430
+ * (effect-classified) `Reaction`. Pure — no subprocess, no model. The ergonomic
431
+ * base for testing a compiled hook (see `assertHookDenies` / `assertHookAllows`).
432
+ */
433
+ function runHookProgram(hook, event, ctx = {}) {
434
+ const kind = dispatchKind(hook);
435
+ switch (kind) {
436
+ case "bash-gate":
437
+ return {
438
+ kind: "decision",
439
+ decision: decideProgram(hook, event, ctx),
440
+ };
441
+ case "file-gate":
442
+ return {
443
+ kind: "decision",
444
+ decision: decideFileGate(hook, event, ctx),
445
+ };
446
+ case "prompt-gate":
447
+ return {
448
+ kind: "decision",
449
+ decision: decidePromptGate(hook, event, ctx),
450
+ };
451
+ case "stop-gate":
452
+ return {
453
+ kind: "decision",
454
+ decision: decideStopGate(hook, event, ctx),
455
+ };
456
+ case "inject":
457
+ return {
458
+ kind: "injection",
459
+ context: runInject(hook, event).hookSpecificOutput
460
+ .additionalContext,
461
+ };
462
+ case "react":
463
+ return { kind: "reaction", reaction: runReact(hook, event) };
464
+ default:
465
+ return (0, hash_js_1.assertNever)(kind);
466
+ }
467
+ }
468
+ //# sourceMappingURL=hook-program.js.map
@@ -24,5 +24,12 @@ export interface HookProtocol {
24
24
  * Code passes the event on stdin only; Codex adds session_id/cwd/PLUGIN_ROOT/…).
25
25
  */
26
26
  readonly eventEnvVars: readonly string[];
27
+ /**
28
+ * How a tool matcher is written in the emitted hooks block — `"exact"` (Claude
29
+ * Code: the tool name / `A|B` alternation) or `"regex"` (Codex: an anchored
30
+ * regex `^(A|B)$`). Optional (additive, non-breaking) — absent ⇒ `"exact"`.
31
+ * Used by `compileHookProgram` when rendering the settings block.
32
+ */
33
+ readonly matcherStyle?: "exact" | "regex";
27
34
  }
28
35
  //# sourceMappingURL=hook-protocol.d.ts.map
@@ -0,0 +1,138 @@
1
+ /** The closed set of built-in facts a gate may declare via `needs`, with value types. */
2
+ export interface ProviderResults {
3
+ /** The current git branch, or "" outside a git repo / on an unborn HEAD. */
4
+ readonly "git.branch": string;
5
+ /** True iff the working tree has uncommitted changes (false if not a repo). */
6
+ readonly "git.isDirty": boolean;
7
+ /** The repo's top-level directory, or "" outside a git repo. */
8
+ readonly "git.root": string;
9
+ /** The directory the hook runs in. */
10
+ readonly cwd: string;
11
+ /** The OS platform (`process.platform`: "darwin" | "linux" | "win32" | …). */
12
+ readonly "os.platform": NodeJS.Platform;
13
+ /** True iff running on a CI server (detected via the `ci-info` library). */
14
+ readonly "env.isCI": boolean;
15
+ }
16
+ /** A declarable built-in provider name. */
17
+ export type ProviderName = keyof ProviderResults;
18
+ /**
19
+ * An INLINE one-off provider: a command declared right in `needs` (no registered
20
+ * file), run by the trusted runtime, its stdout becoming `e.ctx[name]` (a string).
21
+ * `dangerous` marks the loud escape — a command not provably read-only. Built via
22
+ * {@link provide} (read-only) or {@link dangerously} (acknowledged).
23
+ */
24
+ export interface InlineProvider<Name extends string = string> {
25
+ readonly kind: "inline";
26
+ readonly name: Name;
27
+ readonly run: string;
28
+ readonly dangerous: boolean;
29
+ }
30
+ /** A `needs` entry — a built-in name, an inline `provide`/`dangerously`, or a `provider()` ref. */
31
+ export type NeedSpec = ProviderName | InlineProvider | RegisteredRef;
32
+ /**
33
+ * Declare an INLINE read-only fact: `provide("k8sCtx", "kubectl config current-context")`.
34
+ * The command MUST be provably read-only (compile rejects it otherwise — use
35
+ * {@link dangerously} to acknowledge a side-effecting/undecidable one). Its stdout
36
+ * is `e.ctx[name]`.
37
+ */
38
+ export declare const provide: <const Name extends string>(name: Name, run: string) => InlineProvider<Name>;
39
+ /**
40
+ * Declare an INLINE fact whose command ISN'T provably read-only — the loud,
41
+ * greppable escape hatch (the `dangerouslySetInnerHTML` / `unsafe` / `http.send`
42
+ * convention; sibling of `purity:'dangerously-unrestricted'`). A security review
43
+ * searches for this one word.
44
+ */
45
+ export declare const dangerously: <const Name extends string>(name: Name, run: string) => InlineProvider<Name>;
46
+ /**
47
+ * A REGISTERED provider — a reusable, named fact authored once in
48
+ * `.vigiles/providers/<name>.{mjs,ts}` (default-exported via {@link defineProvider})
49
+ * and referenced from many hooks by {@link provider} name. The reusable sibling of
50
+ * the one-off inline {@link provide}; same read-only-by-default rule.
51
+ */
52
+ export interface RegisteredProvider<Name extends string = string> {
53
+ readonly kind: "provider-def";
54
+ readonly name: Name;
55
+ readonly run: string;
56
+ readonly dangerous: boolean;
57
+ }
58
+ /** Author a registered provider: `export default defineProvider({ name, run })`. */
59
+ export declare const defineProvider: <const Name extends string>(p: {
60
+ readonly name: Name;
61
+ readonly run: string;
62
+ /** Acknowledge a command that isn't provably read-only (the loud escape). */
63
+ readonly dangerous?: boolean;
64
+ }) => RegisteredProvider<Name>;
65
+ /** A reference to a registered provider, used in a hook's `needs`: `provider("myFact")`. */
66
+ export interface RegisteredRef<Name extends string = string> {
67
+ readonly kind: "provider-ref";
68
+ readonly name: Name;
69
+ }
70
+ /** Reference a registered provider (from `.vigiles/providers/`) by name in `needs`. */
71
+ export declare const provider: <const Name extends string>(name: Name) => RegisteredRef<Name>;
72
+ /** name → its registered provider; the runtime resolves a `provider(name)` ref against this. */
73
+ export type ProviderRegistry = Record<string, RegisteredProvider>;
74
+ type NeedName<E extends NeedSpec> = E extends ProviderName ? E : E extends InlineProvider<infer Nm> ? Nm : E extends RegisteredRef<infer Rn> ? Rn : never;
75
+ type NeedValue<E extends NeedSpec> = E extends ProviderName ? ProviderResults[E] : string;
76
+ /**
77
+ * The typed `e.ctx` for a hook that declared `needs: N` — ONLY the declared facts
78
+ * are present (built-in name → its typed value, inline → string), so reading an
79
+ * undeclared one is a `tsc` error (the typed-purity trick). With `N = readonly []`
80
+ * the ctx is empty. The default `readonly ProviderName[]` is the erased runtime
81
+ * shape (built-ins only — an inline `string` name would force an index signature
82
+ * that clashes with `git.isDirty: boolean`; concrete authored inline names are
83
+ * literals, so they never clash).
84
+ */
85
+ export type HookCtx<N extends readonly NeedSpec[] = readonly ProviderName[]> = {
86
+ readonly [E in N[number] as NeedName<E>]: NeedValue<E>;
87
+ };
88
+ /** The read-only capabilities a provider may use to gather its fact. */
89
+ export interface ProviderIO {
90
+ /** Run a read-only command, return stdout (throws on non-zero exit). */
91
+ readonly exec: (command: string) => string;
92
+ /** The hook's working directory. */
93
+ readonly cwd: string;
94
+ /** The OS platform (`process.platform`). */
95
+ readonly platform: NodeJS.Platform;
96
+ /** Whether the process is on a CI server (the CLI injects `ci-info`'s verdict). */
97
+ readonly isCI: boolean;
98
+ }
99
+ interface ProviderDef<K extends ProviderName> {
100
+ /**
101
+ * The read-only command the provider runs (for audit + the read-only
102
+ * soundness test). Absent for an ambient fact gathered without a subprocess.
103
+ */
104
+ readonly run?: string;
105
+ /** Produce the fact; TOTAL — returns a sensible default on failure, never throws. */
106
+ readonly gather: (io: ProviderIO) => ProviderResults[K];
107
+ }
108
+ /** The closed built-in registry. Each `run` is provably read-only (see the test). */
109
+ export declare const BUILTIN_PROVIDERS: {
110
+ readonly [K in ProviderName]: ProviderDef<K>;
111
+ };
112
+ /**
113
+ * `needs` entries that don't resolve: a built-in NAME that isn't a built-in (a
114
+ * typo), or a `provider()` ref whose name isn't in `registeredNames` (a dangling
115
+ * ref). Inline `provide`/`dangerously` are always self-defined, so never flagged.
116
+ */
117
+ export declare function unknownProviders(needs: readonly NeedSpec[], registeredNames?: readonly string[]): string[];
118
+ /**
119
+ * Inline `provide()` entries whose command ISN'T provably read-only — these must
120
+ * switch to `dangerously()` (the acknowledged escape) or compile is rejected. A
121
+ * `dangerously()` entry is never flagged (it already acknowledged the risk).
122
+ */
123
+ export declare function unsafeInlineProviders(needs: readonly NeedSpec[]): InlineProvider[];
124
+ /**
125
+ * Is a REGISTERED provider's command unsafe (not read-only and not acknowledged
126
+ * `dangerous`)? Reused when compiling a `.vigiles/providers/` file.
127
+ */
128
+ export declare function unsafeProvider(def: RegisteredProvider): boolean;
129
+ /**
130
+ * Gather the DECLARED facts into a context object (the trusted-host step). Only
131
+ * the names in `needs` are gathered, each at most once; a built-in, inline, or
132
+ * registered provider that can't resolve yields its default ("" / false), never
133
+ * throws. Pure over the injected `io` (CLI passes a real execSync; tests a fake)
134
+ * and the `registry` (the loaded `.vigiles/providers/`, for `provider()` refs).
135
+ */
136
+ export declare function gatherContext(needs: readonly NeedSpec[], io: ProviderIO, registry?: ProviderRegistry): Record<string, string | boolean>;
137
+ export {};
138
+ //# sourceMappingURL=hook-providers.d.ts.map