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,459 @@
1
+ /**
2
+ * Compiled hooks — a hook as a CONSTRAINED TYPED PROGRAM, not arbitrary shell.
3
+ *
4
+ * The pure core behind the public `vigiles/hook` surface (re-exported in
5
+ * `src/hook.ts`; compiled by `vigiles compile`, run by `vigiles hook-runtime
6
+ * run-program`). A hook today is opaque shell (`bash guard.sh`) —
7
+ * un-analyzable (Rice), and the author hand-writes the fragile parts (exit code,
8
+ * JSON field, a `grep` matcher) that the verified #1 pains come from. Invert it:
9
+ * the author writes a PURE typed function `(event) => Decision` against a CLOSED
10
+ * API; vigiles compiles it. The constraint ELIMINATES whole bug classes by
11
+ * construction and buys testability / safety / matching / portability:
12
+ *
13
+ * - TESTABILITY: `decide` is a pure fn — unit-test in-process, no subprocess, no
14
+ * exit-code/JSON plumbing. The false-confidence bug class (exit 1≠2, wrong field)
15
+ * is UNREPRESENTABLE — the author never writes the protocol; `compile` emits it.
16
+ * - SAFETY: capability = API surface. `checkHookImports` rejects any import outside
17
+ * `vigiles/hook` at compile (so the hook can't reach `child_process`/`net`), and
18
+ * `stampHook`/`verifyHookStamp` make the compiled artifact TAMPER-EVIDENT (the
19
+ * integrity.ts pattern) — a hand-edit that smuggles a capability breaks the stamp.
20
+ * - MATCHING: `command.runs("git push", { force })` is AST-backed (leafCommands),
21
+ * so it catches `cd x && git push -f` that the native `Bash(git:*)` glob (#30519)
22
+ * and a hand-written `grep` both miss.
23
+ * - PORTABILITY: one program → each harness's protocol (CC exit-2 here; Codex /
24
+ * OpenCode via the HookProtocol port later — OpenCode hooks ARE in-process TS).
25
+ *
26
+ * Pure core, harness-neutral. HONEST SCOPE (kept in every doc): compile/verify fix
27
+ * the hook's AUTHORING + LOGIC, not DELIVERY — CC's subagent-bypass (#34692) means
28
+ * a PreToolUse hook does not fire for a subagent's tool calls, so a gate is a strong
29
+ * default, never an unbypassable wall. Limits (buy-in, node-startup latency) +
30
+ * full record in research/hook-pain-points.md.
31
+ */
32
+ import { type BashEffect } from "./bash-effects.js";
33
+ import { type SHA256Hash } from "./hash.js";
34
+ import type { HarnessDialect } from "./dialect.js";
35
+ import type { HookProtocol } from "./hook-protocol.js";
36
+ import { type ProviderName, type NeedSpec, type HookCtx } from "./hook-providers.js";
37
+ export type Decision = {
38
+ readonly kind: "allow";
39
+ } | {
40
+ readonly kind: "deny";
41
+ readonly reason: string;
42
+ } | {
43
+ readonly kind: "ask";
44
+ readonly reason: string;
45
+ };
46
+ export declare const allow: () => Decision;
47
+ export declare const deny: (reason: string) => Decision;
48
+ export declare const ask: (reason: string) => Decision;
49
+ /**
50
+ * Whether a gate BLOCKS on a `deny` (`enforce`, the default) or only RECORDS what
51
+ * it WOULD block while letting everything through (`observe` — the shadow / rollout
52
+ * mode: trust a new gate by watching it first, then promote to `enforce`). This is
53
+ * the WAF "shadow mode" pattern, the one essential mode (block vs don't-block-but-
54
+ * record) — not a vocabulary of on-fail actions. HARNESS-NEUTRAL by construction:
55
+ * observe just exits 0 and writes a local record, so it behaves identically on
56
+ * Claude Code and Codex (no harness-specific field names involved).
57
+ */
58
+ export type HookMode = "enforce" | "observe";
59
+ /** A gate's runtime ACTION after applying its {@link HookMode} to its {@link Decision}. Pure. */
60
+ export type GateAction = {
61
+ readonly kind: "block";
62
+ readonly reason: string;
63
+ } | {
64
+ readonly kind: "ask";
65
+ readonly reason: string;
66
+ } | {
67
+ readonly kind: "observe";
68
+ readonly would: "deny" | "ask";
69
+ readonly reason: string;
70
+ } | {
71
+ readonly kind: "allow";
72
+ };
73
+ /**
74
+ * Map a gate's {@link Decision} + {@link HookMode} to what the runtime actually does.
75
+ * `enforce`: deny→block (exit 2), ask→ask, allow→allow. `observe`: a deny/ask is
76
+ * recorded as a no-op `observe` (would-have-blocked) and allowed; allow stays allow.
77
+ * Pure, so a test asserts "in observe mode this deny does NOT block" with no process.
78
+ */
79
+ export declare function gateAction(decision: Decision, mode?: HookMode): GateAction;
80
+ /** An AST-backed view of a Bash command — the author never writes a regex. */
81
+ export interface CommandView {
82
+ readonly raw: string;
83
+ /** True iff a leaf command runs `program` (e.g. "git push"), optionally with --force/-f. */
84
+ runs(program: string, opts?: {
85
+ readonly force?: boolean;
86
+ }): boolean;
87
+ /** True iff the command is provably side-effecting (bash-effects classifier). */
88
+ isSideEffecting(): boolean;
89
+ /**
90
+ * True iff a leaf command references a path under one of the prefixes (e.g.
91
+ * `~/.ssh`, `.env`) — the secret-read / sensitive-path matcher. Sees the path
92
+ * however the command is wrapped (`cd x && cat ~/.ssh/id_rsa`).
93
+ */
94
+ touches(prefixes: readonly string[]): boolean;
95
+ /**
96
+ * True iff the command pipes into a BARE shell interpreter (`curl … | sh`,
97
+ * `… | bash -s`) — the remote-code-execution shape. High-signal: a shell leaf
98
+ * WITH a script-file argument (`sh deploy.sh`) is NOT flagged; only a shell
99
+ * reading from stdin is, which only happens downstream of a pipe.
100
+ */
101
+ pipesToShell(): boolean;
102
+ }
103
+ export declare function commandView(raw: string): CommandView;
104
+ /**
105
+ * The typed event a Bash gate decides over. Generic over the declared context
106
+ * `needs` (`N`) so `e.ctx` exposes ONLY the facts the hook declared — reading an
107
+ * undeclared one is a `tsc` error. The default is the erased (all-providers)
108
+ * shape used by the runtime/`AnyHook`.
109
+ */
110
+ export interface BashToolEvent<N extends readonly NeedSpec[] = readonly ProviderName[]> {
111
+ readonly event: string;
112
+ readonly tool: "Bash";
113
+ readonly command: CommandView;
114
+ /** Host-gathered, DECLARED read-only facts (git branch, …) — see `needs`. */
115
+ readonly ctx: HookCtx<N>;
116
+ }
117
+ /** A hook program: where it fires + the pure decision. */
118
+ export interface HookProgram<N extends readonly NeedSpec[] = readonly ProviderName[]> {
119
+ readonly on: string;
120
+ readonly match: {
121
+ readonly tool: string;
122
+ };
123
+ /** `enforce` (default) blocks on a `deny`; `observe` records + allows. */
124
+ readonly mode?: HookMode;
125
+ /** Declared context providers the trusted runtime gathers into `e.ctx`. */
126
+ readonly needs?: N;
127
+ readonly decide: (e: BashToolEvent<N>) => Decision;
128
+ }
129
+ export declare const tool: (name: string) => {
130
+ tool: string;
131
+ };
132
+ export declare function defineHook<const N extends readonly NeedSpec[] = readonly []>(p: HookProgram<N>): HookProgram<N>;
133
+ /** Build the typed event from a raw PreToolUse event, then decide. */
134
+ export declare function decideProgram<N extends readonly NeedSpec[]>(program: HookProgram<N>, rawEvent: {
135
+ tool_name?: string;
136
+ tool_input?: {
137
+ command?: unknown;
138
+ };
139
+ }, ctx?: Record<string, string | boolean>): Decision;
140
+ /** Map a Decision to the CC hook exit code — the protocol the author never writes. */
141
+ export declare function decisionExitCode(d: Decision): number;
142
+ /**
143
+ * Reject any import/require outside `vigiles/hook` + textual escape hatches
144
+ * (eval / Function / dynamic import). The hook's capabilities are then EXACTLY the
145
+ * sanctioned API — it cannot reach `child_process`, `fs`, or the network. Returns
146
+ * the offending specifiers/constructs (empty = clean). (Probe: a regex scan; a
147
+ * production impl walks the TS AST.)
148
+ */
149
+ export declare function checkHookImports(source: string): string[];
150
+ export declare class HookCompileError extends Error {
151
+ }
152
+ /** A compiled hook program: the harness block + a tamper-evident stamp. */
153
+ export interface CompiledHookProgram {
154
+ readonly hooks: Record<string, readonly {
155
+ /** Tool matcher — omitted for tool-less events (SessionStart/UserPromptSubmit). */
156
+ readonly matcher?: string;
157
+ readonly hooks: readonly {
158
+ readonly type: "command";
159
+ readonly command: string;
160
+ }[];
161
+ }[]>;
162
+ /**
163
+ * The rendered settings block to add to the harness's hooks config — JSON for
164
+ * Claude Code (`.claude/settings.json`), TOML `[[hooks.<event>]]` for Codex
165
+ * (`config.toml`). The CLI prints this; the structured `hooks` above is the
166
+ * Claude-Code-shaped intermediate.
167
+ */
168
+ readonly settingsBlock: string;
169
+ /** SHA-256 of the sanctioned source — the runtime refuses an artifact whose stamp differs. */
170
+ readonly stamp: SHA256Hash;
171
+ }
172
+ /**
173
+ * Per-harness emit inputs (all optional, default to Claude Code) — the ports the
174
+ * CLI threads in from the resolved adapter. Keeps the core harness-agnostic:
175
+ * core depends only on these interfaces, never an adapter (core ⊄ adapter).
176
+ */
177
+ export interface CompileHookOptions {
178
+ /** The command the emitted block routes the event to. */
179
+ readonly gateCommand?: string;
180
+ /** Validate `hook.on` against this harness's hook-event catalog (a typo won't compile). */
181
+ readonly dialect?: HarnessDialect;
182
+ /** Matcher style (exact vs anchored regex). Defaults to Claude Code's `"exact"`. */
183
+ readonly hookProtocol?: HookProtocol;
184
+ /** Settings encoding — `"json"` (Claude Code) or `"toml"` (Codex). From `PluginLayout.settingsFormat`. */
185
+ readonly settingsFormat?: "json" | "toml";
186
+ /** Names of registered providers (`.vigiles/providers/`) a `provider()` ref may resolve to. */
187
+ readonly registeredProviders?: readonly string[];
188
+ }
189
+ /**
190
+ * Every hook shape the closed vocabulary can express. `compileHookProgram`
191
+ * (emit) and the runtime dispatch both range over this union.
192
+ */
193
+ /**
194
+ * The ERASED needs-generic for the {@link AnyHook} union. A gate's `decide`
195
+ * carries `N` contravariantly (in the event param), so no single concrete
196
+ * instantiation is a supertype of every authored hook (a built-in-needs hook and
197
+ * an inline-needs hook have no common `HookProgram<N>`). `any` erases the context
198
+ * generic for the runtime/union view; the decode functions re-narrow + cast `ctx`
199
+ * (they never trust this type). Author-facing types keep the precise `N`.
200
+ */
201
+ type ErasedNeeds = readonly any[];
202
+ export type AnyHook = HookProgram<ErasedNeeds> | FileGateHook<ErasedNeeds> | PromptGateHook<ErasedNeeds> | StopGateHook<ErasedNeeds> | InjectHook | ReactHook;
203
+ /**
204
+ * The runtime dispatch shapes. A bare {@link HookProgram} (no `role`) is a Bash
205
+ * command gate; the role-keyed hooks carry their own shape. The runtime uses this
206
+ * to pick the right decode + output (a gate exits 2, an inject prints
207
+ * `additionalContext`, a react runs its classified command). `prompt-gate` and
208
+ * `stop-gate` are gates on non-tool events — they decode the prompt / stop signal
209
+ * and emit a `Decision` like the tool gates.
210
+ */
211
+ export type DispatchKind = "bash-gate" | "file-gate" | "prompt-gate" | "stop-gate" | "inject" | "react";
212
+ export declare function dispatchKind(hook: AnyHook): DispatchKind;
213
+ /** A gate's {@link HookMode} (`enforce` default); non-gate roles report `enforce` too. */
214
+ export declare function hookMode(hook: AnyHook): HookMode;
215
+ /** The context providers a hook declared via `needs` ([] if none / not a gate). */
216
+ export declare function hookNeeds(hook: AnyHook): readonly NeedSpec[];
217
+ /** Where a hook fires + its tool matcher (undefined for tool-less events). */
218
+ export declare function hookRouting(hook: AnyHook): {
219
+ on: string;
220
+ matcher?: string;
221
+ };
222
+ /**
223
+ * Compile a hook program from its source. Runs the capability check FIRST (an
224
+ * out-of-API import does NOT compile), validates the event against the target
225
+ * harness (a typo won't compile), then stamps the source so the shipped artifact
226
+ * is tamper-evident. The hook supplies the routing (event + matcher), which
227
+ * differs per role — a tool gate matches its tool, a react matches its tools, an
228
+ * inject (SessionStart) has no tool matcher at all.
229
+ *
230
+ * Harness-agnostic by injection: with no `opts` it emits the Claude Code block
231
+ * (back-compatible); pass `{ dialect, hookProtocol, settingsFormat }` from a
232
+ * resolved adapter and it emits that harness's block (e.g. Codex TOML + regex
233
+ * matcher). Core never imports an adapter — only the port interfaces.
234
+ */
235
+ export declare function compileHookProgram(source: string, hook: AnyHook, opts?: CompileHookOptions): CompiledHookProgram;
236
+ /** Stamp a hook's source (the integrity.ts pattern, applied to a hook artifact). */
237
+ export declare function stampHook(source: string): SHA256Hash;
238
+ /** Verify a shipped hook artifact matches its stamp (tamper → false). */
239
+ export declare function verifyHookStamp(source: string, stamp: SHA256Hash): boolean;
240
+ /** An AST-free view of a file path — the matching primitive for file tools. */
241
+ export interface PathView {
242
+ readonly raw: string;
243
+ /** True iff the path sits under at least one allowed prefix (e.g. "src/**"). */
244
+ under(prefixes: readonly string[]): boolean;
245
+ }
246
+ export declare function pathView(raw: string): PathView;
247
+ /** The event a file-tool gate decides over (Edit/Write/Read carry `file_path`). */
248
+ export interface FileToolEvent<N extends readonly NeedSpec[] = readonly ProviderName[]> {
249
+ readonly event: string;
250
+ readonly tool: string;
251
+ readonly path: PathView;
252
+ /** Host-gathered, DECLARED read-only facts — see `needs`. */
253
+ readonly ctx: HookCtx<N>;
254
+ }
255
+ export interface FileGateHook<N extends readonly NeedSpec[] = readonly ProviderName[]> {
256
+ readonly role: "gate";
257
+ readonly on: string;
258
+ readonly match: {
259
+ readonly tools: readonly string[];
260
+ };
261
+ /** `enforce` (default) blocks on a `deny`; `observe` records + allows. */
262
+ readonly mode?: HookMode;
263
+ /** Declared context providers the trusted runtime gathers into `e.ctx`. */
264
+ readonly needs?: N;
265
+ readonly decide: (e: FileToolEvent<N>) => Decision;
266
+ }
267
+ export declare const tools: (...names: string[]) => {
268
+ tools: string[];
269
+ };
270
+ export declare function defineFileGate<const N extends readonly NeedSpec[] = readonly []>(p: Omit<FileGateHook<N>, "role">): FileGateHook<N>;
271
+ /** Run a file-tool gate against a raw PreToolUse event (reads `file_path`). */
272
+ export declare function decideFileGate<N extends readonly NeedSpec[]>(hook: FileGateHook<N>, raw: {
273
+ tool_name?: string;
274
+ tool_input?: {
275
+ file_path?: unknown;
276
+ };
277
+ }, ctx?: Record<string, string | boolean>): Decision;
278
+ /** The event a UserPromptSubmit gate decides over — it sees the prompt TEXT. */
279
+ export interface PromptEvent<N extends readonly NeedSpec[] = readonly ProviderName[]> {
280
+ readonly event: string;
281
+ /** The user's submitted prompt text (empty string if the event carried none). */
282
+ readonly prompt: string;
283
+ /** Host-gathered, DECLARED read-only facts — see `needs`. */
284
+ readonly ctx: HookCtx<N>;
285
+ }
286
+ export interface PromptGateHook<N extends readonly NeedSpec[] = readonly ProviderName[]> {
287
+ readonly role: "prompt-gate";
288
+ /** The event — `UserPromptSubmit`. */
289
+ readonly on: string;
290
+ /** `enforce` (default) blocks on a `deny`; `observe` records + allows. */
291
+ readonly mode?: HookMode;
292
+ /** Declared context providers the trusted runtime gathers into `e.ctx`. */
293
+ readonly needs?: N;
294
+ /** Decide over the prompt. `deny` blocks/erases the prompt; `ask` defers to the user. */
295
+ readonly decide: (e: PromptEvent<N>) => Decision;
296
+ }
297
+ export declare function definePromptGate<const N extends readonly NeedSpec[] = readonly []>(p: Omit<PromptGateHook<N>, "role">): PromptGateHook<N>;
298
+ /** Run a prompt gate against a raw UserPromptSubmit event (reads `prompt`). */
299
+ export declare function decidePromptGate<N extends readonly NeedSpec[]>(hook: PromptGateHook<N>, raw: {
300
+ prompt?: unknown;
301
+ }, ctx?: Record<string, string | boolean>): Decision;
302
+ /**
303
+ * The event a Stop gate decides over. `deny` BLOCKS the agent from ending its
304
+ * turn (the reason is surfaced to the agent — e.g. "tests are red, keep going").
305
+ */
306
+ export interface StopEvent<N extends readonly NeedSpec[] = readonly ProviderName[]> {
307
+ readonly event: string;
308
+ /**
309
+ * True when this Stop is itself the consequence of a PRIOR Stop-block — the
310
+ * loop guard. A gate MUST return `allow` when this is set, or it can wedge the
311
+ * agent in an infinite stop→continue→stop cycle.
312
+ */
313
+ readonly stopHookActive: boolean;
314
+ /** Host-gathered, DECLARED read-only facts — see `needs`. */
315
+ readonly ctx: HookCtx<N>;
316
+ }
317
+ export interface StopGateHook<N extends readonly NeedSpec[] = readonly ProviderName[]> {
318
+ readonly role: "stop-gate";
319
+ /** The event — `Stop` or `SubagentStop`. */
320
+ readonly on: string;
321
+ /** `enforce` (default) blocks on a `deny`; `observe` records + allows. */
322
+ readonly mode?: HookMode;
323
+ /** Declared context providers the trusted runtime gathers into `e.ctx`. */
324
+ readonly needs?: N;
325
+ /** Decide whether the agent may stop. `deny` keeps it going; `allow` lets it stop. */
326
+ readonly decide: (e: StopEvent<N>) => Decision;
327
+ }
328
+ export declare function defineStopGate<const N extends readonly NeedSpec[] = readonly []>(p: Omit<StopGateHook<N>, "role">): StopGateHook<N>;
329
+ /** Run a Stop gate against a raw Stop/SubagentStop event (reads `stop_hook_active`). */
330
+ export declare function decideStopGate<N extends readonly NeedSpec[]>(hook: StopGateHook<N>, raw: {
331
+ stop_hook_active?: unknown;
332
+ }, ctx?: Record<string, string | boolean>): Decision;
333
+ /** The output of an inject hook — text to add to context. NO allow/deny exists here. */
334
+ export interface Injection {
335
+ readonly kind: "inject";
336
+ readonly context: string;
337
+ }
338
+ export declare const inject: (context: string) => Injection;
339
+ /** The event an inject hook produces from (no tool — SessionStart/UserPromptSubmit). */
340
+ export interface SessionEvent {
341
+ readonly event: string;
342
+ readonly source: string;
343
+ }
344
+ export interface InjectHook {
345
+ readonly role: "inject";
346
+ readonly on: string;
347
+ /** Produces context to add. Its return type (Injection) has no `deny` — by design. */
348
+ readonly produce: (e: SessionEvent) => Injection;
349
+ }
350
+ export declare const defineInject: (p: Omit<InjectHook, "role">) => InjectHook;
351
+ /**
352
+ * Run an inject hook → the CC JSON the author never hand-writes. The compiler
353
+ * targets `additionalContext` (the RIGHT field for this event), so the
354
+ * wrong-JSON-field pain can't occur.
355
+ */
356
+ export declare function runInject(hook: InjectHook, raw: {
357
+ source?: string;
358
+ }): {
359
+ hookSpecificOutput: {
360
+ hookEventName: string;
361
+ additionalContext: string;
362
+ };
363
+ };
364
+ /**
365
+ * A view of a tool's RESPONSE (PostToolUse) — the matching primitive a react hook
366
+ * reasons over (e.g. capture/notify only when a command FAILED). The author never
367
+ * parses the raw payload shape.
368
+ */
369
+ export interface ResponseView {
370
+ /** The response as text (an object payload is JSON-stringified). */
371
+ readonly raw: string;
372
+ /**
373
+ * True iff the tool reported a failure — a truthy `error`/`is_error` field on a
374
+ * structured payload, or a leading `Error`/`error:` line on a text one.
375
+ */
376
+ isError(): boolean;
377
+ /** True iff the response text contains `needle`. */
378
+ contains(needle: string): boolean;
379
+ }
380
+ export declare function responseView(raw: unknown): ResponseView;
381
+ /** The event a react hook reacts over — the tool, the file path, AND its response. */
382
+ export interface ReactEvent {
383
+ readonly event: string;
384
+ readonly tool: string;
385
+ readonly path: PathView;
386
+ /** The tool's response (PostToolUse) — react only on an error, capture output, … */
387
+ readonly response: ResponseView;
388
+ }
389
+ export interface RunReaction {
390
+ readonly kind: "run";
391
+ readonly command: string;
392
+ readonly effect: BashEffect;
393
+ }
394
+ export type Reaction = RunReaction | {
395
+ readonly kind: "notice";
396
+ readonly message: string;
397
+ } | {
398
+ readonly kind: "none";
399
+ };
400
+ /** Run a command in reaction — its effect is classified AT CONSTRUCTION (audit/diff-able). */
401
+ export declare const run: (command: string) => RunReaction;
402
+ /** Surface a non-blocking note (no execution). */
403
+ export declare const notice: (message: string) => Reaction;
404
+ /** Do nothing. */
405
+ export declare const nothing: () => Reaction;
406
+ export interface ReactHook {
407
+ readonly role: "react";
408
+ readonly on: string;
409
+ readonly match: {
410
+ readonly tools: readonly string[];
411
+ };
412
+ /** Reacts to a tool that already ran. Returns a Reaction — NO `deny` exists here. */
413
+ readonly react: (e: ReactEvent) => Reaction;
414
+ }
415
+ export declare const defineReact: (p: Omit<ReactHook, "role">) => ReactHook;
416
+ /** Run a react hook against a raw PostToolUse event → the (classified) Reaction. */
417
+ export declare function runReact(hook: ReactHook, raw: {
418
+ tool_name?: string;
419
+ tool_input?: {
420
+ file_path?: unknown;
421
+ };
422
+ tool_response?: unknown;
423
+ }): Reaction;
424
+ /** The raw event fields the decode functions read (the union across roles). */
425
+ export interface RawHookEvent {
426
+ readonly tool_name?: string;
427
+ readonly tool_input?: {
428
+ readonly command?: unknown;
429
+ readonly file_path?: unknown;
430
+ };
431
+ /** PostToolUse response (react). */
432
+ readonly tool_response?: unknown;
433
+ /** SessionStart / UserPromptSubmit (inject). */
434
+ readonly source?: string;
435
+ /** UserPromptSubmit (prompt-gate). */
436
+ readonly prompt?: string;
437
+ /** Stop / SubagentStop (stop-gate) — the prior-block loop guard. */
438
+ readonly stop_hook_active?: boolean;
439
+ }
440
+ /** The normalized outcome of running a hook program — discriminated by role. */
441
+ export type HookProgramOutcome = {
442
+ readonly kind: "decision";
443
+ readonly decision: Decision;
444
+ } | {
445
+ readonly kind: "injection";
446
+ readonly context: string;
447
+ } | {
448
+ readonly kind: "reaction";
449
+ readonly reaction: Reaction;
450
+ };
451
+ /**
452
+ * Evaluate a compiled hook against a raw event, in-process, dispatching by role:
453
+ * a gate → its `Decision`, an inject → the injected context text, a react → its
454
+ * (effect-classified) `Reaction`. Pure — no subprocess, no model. The ergonomic
455
+ * base for testing a compiled hook (see `assertHookDenies` / `assertHookAllows`).
456
+ */
457
+ export declare function runHookProgram(hook: AnyHook, event: RawHookEvent, ctx?: Record<string, string | boolean>): HookProgramOutcome;
458
+ export {};
459
+ //# sourceMappingURL=hook-program.d.ts.map