vigiles 26.0.1 → 26.2.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 (51) hide show
  1. package/README.md +5 -4
  2. package/dist/adapters/claude-code/hook-condition.d.ts +46 -0
  3. package/dist/adapters/claude-code/hook-condition.js +142 -0
  4. package/dist/adapters/claude-code/hook-protocol.js +5 -0
  5. package/dist/audit-report.template.html +2 -2
  6. package/dist/cli.js +67 -115
  7. package/dist/core/bash-effects.d.ts +22 -0
  8. package/dist/core/bash-effects.js +10 -0
  9. package/dist/core/command-files.d.ts +107 -0
  10. package/dist/core/command-files.js +407 -0
  11. package/dist/core/hook-condition.d.ts +96 -0
  12. package/dist/core/hook-condition.js +63 -0
  13. package/dist/core/hook-matcher.d.ts +50 -0
  14. package/dist/core/hook-matcher.js +77 -2
  15. package/dist/core/hook-normalize.d.ts +51 -0
  16. package/dist/core/hook-normalize.js +61 -1
  17. package/dist/core/hook-program.d.ts +62 -1
  18. package/dist/core/hook-program.js +15 -1
  19. package/dist/core/hook-protocol.d.ts +16 -0
  20. package/dist/core/linters.js +97 -58
  21. package/dist/core/shell-vars.d.ts +74 -0
  22. package/dist/core/shell-vars.js +270 -0
  23. package/dist/core/skill-resources.d.ts +22 -1
  24. package/dist/core/skill-resources.js +2 -1
  25. package/dist/doc-test-script-coverage.d.ts +52 -0
  26. package/dist/doc-test-script-coverage.js +66 -0
  27. package/dist/guardrail-check.d.ts +29 -0
  28. package/dist/guardrail-check.js +69 -10
  29. package/dist/harness-assert.d.ts +8 -5
  30. package/dist/harness-assert.js +8 -5
  31. package/dist/harness-resolve-hooks.mjs +14 -37
  32. package/dist/hook-state-store.d.ts +143 -0
  33. package/dist/hook-state-store.js +241 -0
  34. package/dist/hook.d.ts +3 -1
  35. package/dist/hook.js +3 -1
  36. package/dist/run-hook.d.ts +33 -1
  37. package/dist/run-hook.js +46 -2
  38. package/dist/run-script.d.ts +94 -0
  39. package/dist/run-script.js +47 -26
  40. package/dist/scan-core.js +21 -3
  41. package/dist/score-core.d.ts +21 -1
  42. package/dist/score-core.js +30 -6
  43. package/dist/self-resolve.d.mts +20 -0
  44. package/dist/self-resolve.mjs +75 -0
  45. package/dist/spec-hooks.d.mts +10 -0
  46. package/dist/spec-hooks.mjs +17 -0
  47. package/dist/test.d.ts +5 -0
  48. package/dist/test.js +23 -2
  49. package/dist/verify-plugin-guards.d.ts +194 -0
  50. package/dist/verify-plugin-guards.js +822 -0
  51. package/package.json +1 -1
@@ -0,0 +1,194 @@
1
+ import { type NonCommandHookAction } from "./core/hook-normalize.js";
2
+ export type { NonCommandHookAction } from "./core/hook-normalize.js";
3
+ import type { HarnessAdapter } from "./core/adapter.js";
4
+ import { type DisasterCategory, type DisasterEvent, type GuardrailResult } from "./guardrail-check.js";
5
+ import type { RunHookOptions } from "./run-hook.js";
6
+ /** The hook a sweep looked at, as its config declares it. */
7
+ export interface SweptHook {
8
+ /** The event it registers under, e.g. `"PreToolUse"`. */
9
+ readonly event: string;
10
+ /** Its tool matcher, or `null` when it declares none (matches everything). */
11
+ readonly matcher: string | null;
12
+ /** Its condition as written (Claude Code's `if`), or `null` when unconditional. */
13
+ readonly condition: string | null;
14
+ /** The command, with the harness's plugin-root token already expanded. */
15
+ readonly command: string;
16
+ /**
17
+ * Its position in the flattened registration list, so two hooks sharing a
18
+ * command are still distinguishable in a report. Stable for one sweep of one
19
+ * directory; not an identity across versions.
20
+ */
21
+ readonly index: number;
22
+ }
23
+ /**
24
+ * What happened to one hook. A DISCRIMINATED UNION, not a result list plus three
25
+ * nullable fields: `results` exists on exactly the outcome that has them, so
26
+ * "read the score of a hook that was never run" is a type error rather than a
27
+ * `0/7` someone quotes.
28
+ */
29
+ export type SweptHookOutcome = {
30
+ /** The battery reached this hook; `results` holds one entry per event. */
31
+ readonly status: "measured";
32
+ readonly hook: SweptHook;
33
+ /** One result per battery event, in catalog order. */
34
+ readonly results: readonly GuardrailResult[];
35
+ /** Ids of the events it denied. */
36
+ readonly blocked: readonly string[];
37
+ /** Ids it ran on and let through. */
38
+ readonly allowed: readonly string[];
39
+ /** Ids the harness would never have handed it (condition did not match). */
40
+ readonly notRun: readonly string[];
41
+ } | {
42
+ /**
43
+ * The battery does not apply to this hook — a different event, or a matcher
44
+ * that selects none of the battery's tools. NOT a score of zero.
45
+ */
46
+ readonly status: "not-applicable";
47
+ readonly hook: SweptHook;
48
+ /** One line naming which of the two it is, and against what. */
49
+ readonly reason: string;
50
+ } | {
51
+ /**
52
+ * The command names a variable nothing has set, so the program we would run
53
+ * is not the program the harness runs. Refusing is the honest answer; the
54
+ * fix is in the caller's hands (pass `env`).
55
+ */
56
+ readonly status: "unresolved";
57
+ readonly hook: SweptHook;
58
+ /** One line naming the unset variables. */
59
+ readonly reason: string;
60
+ };
61
+ /** The whole sweep. */
62
+ export interface PluginGuardReport {
63
+ /** The directory swept, resolved to an absolute path. */
64
+ readonly dir: string;
65
+ /** The adapter that read it, e.g. `"claude-code"`. */
66
+ readonly harness: string;
67
+ /** The event each disaster was delivered as (default `"PreToolUse"`). */
68
+ readonly event: string;
69
+ /** The battery that was used, so a report says what it measured against. */
70
+ readonly events: readonly DisasterEvent[];
71
+ /** One outcome per declared COMMAND hook, in config order. */
72
+ readonly hooks: readonly SweptHookOutcome[];
73
+ /**
74
+ * Declared actions this tier cannot drive because they are not commands —
75
+ * `prompt`, `http`, `mcp_tool`, `agent`.
76
+ *
77
+ * 🔴 THEY USED TO BE DROPPED, AND DROPPING THEM MANUFACTURED THE FALSE EMPTY
78
+ * `notes` exists to prevent. A repository whose hooks are all `prompt` actions
79
+ * has declared guards; it was reported as declaring none, in the words of the
80
+ * one sentence this report writes to be sure nobody reads an empty result as a
81
+ * clean bill of health. Not measured is a limit of the tier and says so; not
82
+ * declared is an accusation about the repository, and it was not true.
83
+ *
84
+ * They carry no score and never will here — a shell battery cannot drive a
85
+ * prompt — so they are a separate list rather than a fourth outcome status
86
+ * with an invented command.
87
+ */
88
+ readonly unmeasurable: readonly NonCommandHookAction[];
89
+ /**
90
+ * Why the sweep measured less than a reader might assume — no COMMAND hooks
91
+ * declared, a harness with no shell hooks, every hook on another event, or an
92
+ * action this tier cannot drive. Empty only when at least one hook was
93
+ * measured AND nothing was left undrivable: an undrivable action is a gap in
94
+ * COVERAGE, so it is said even when other hooks scored.
95
+ *
96
+ * 🔴 THIS IS THE EMPTY CASE'S VOICE. `hooks: []` on its own reads as a clean
97
+ * bill of health, which is the exact false confidence the battery exists to
98
+ * remove — so a sweep that measured nothing always says so in words.
99
+ */
100
+ readonly notes: readonly string[];
101
+ }
102
+ /** Options for {@link experimental_verifyPluginGuards}. */
103
+ export interface VerifyPluginGuardsOptions extends Omit<RunHookOptions, "condition" | "protocol"> {
104
+ /**
105
+ * The harness to read the repo as. Defaults to Claude Code, so an existing
106
+ * Claude Code repo needs nothing. The condition grammar and the block protocol
107
+ * both come from this adapter's `hookProtocol`.
108
+ */
109
+ readonly adapter?: HarnessAdapter;
110
+ /** Restrict the battery to these categories (default: the whole catalog). */
111
+ readonly categories?: readonly DisasterCategory[];
112
+ /** Override the battery entirely. */
113
+ readonly events?: readonly DisasterEvent[];
114
+ /** The event each disaster is delivered as (default `"PreToolUse"`). */
115
+ readonly event?: string;
116
+ }
117
+ /**
118
+ * Run the disaster battery against every hook a plugin or repo declares, using
119
+ * each hook's OWN event, matcher and condition, and report per hook.
120
+ *
121
+ * ```ts
122
+ * import { experimental_verifyPluginGuards } from "vigiles";
123
+ *
124
+ * const report = experimental_verifyPluginGuards(".");
125
+ * for (const h of report.hooks) {
126
+ * if (h.status === "measured")
127
+ * console.log(`${h.blocked.length}/${h.results.length} ${h.hook.command}`);
128
+ * else console.log(`⊘ ${h.status} ${h.hook.command} — ${h.reason}`);
129
+ * }
130
+ * for (const note of report.notes) console.log(note);
131
+ * ```
132
+ *
133
+ * Nothing here needs a model or a key. The hooks it finds are the ones the
134
+ * harness would load, so a hook that is present on disk but not registered is
135
+ * absent from the report by construction — which is the correct answer, and the
136
+ * one you would not get by globbing `hooks/*.sh`.
137
+ *
138
+ * ⚠️ It RUNS each reachable hook. A hook is a program you did not necessarily
139
+ * write, so point this at a repo whose hooks you are willing to execute, or pass
140
+ * `trusted: false` / `sandbox: "auto"` (inherited from {@link RunHookOptions}) to
141
+ * confine them. `verifyGuardrail` has always had the same property; sweeping a
142
+ * whole plugin makes it worth saying out loud.
143
+ *
144
+ * @experimental Days old, with no consumer outside this repository. The REPORT
145
+ * SHAPE is the part most likely to move — specifically whether `not-applicable`
146
+ * stays one status or splits by cause, and whether the per-hook counts stay id
147
+ * arrays. The prefix comes off when that shape survives sweeping several real
148
+ * third-party repos unchanged; see docs/experimental.md.
149
+ *
150
+ * @param dir - the plugin or repo root to read hooks from.
151
+ */
152
+ export declare function experimental_verifyPluginGuards(dir: string, opts?: VerifyPluginGuardsOptions): PluginGuardReport;
153
+ /**
154
+ * Render a {@link PluginGuardReport} as terminal text.
155
+ *
156
+ * ```ts
157
+ * import {
158
+ * experimental_verifyPluginGuards,
159
+ * experimental_formatPluginGuardReport,
160
+ * } from "vigiles";
161
+ *
162
+ * console.log(
163
+ * experimental_formatPluginGuardReport(experimental_verifyPluginGuards(".")),
164
+ * );
165
+ * ```
166
+ *
167
+ * NEUTRAL, the same way {@link formatGuardrailReport} is: it reports what each
168
+ * hook blocks without deciding whether that was the hook's job. A repo's config
169
+ * never says which of its hooks is meant to be a bash-safety guard, so a verdict
170
+ * here would be invented rather than read.
171
+ *
172
+ * 🔴 A HOOK THE BATTERY NEVER REACHED IS NEVER GIVEN A NUMBER. A `measured` hook
173
+ * prints `blocks n/7`; a `not-applicable` or `unresolved` one prints its REASON
174
+ * under a `⊘` heading and no count at all, because a rendered `0/7` is the same
175
+ * false confidence the discriminated union exists to prevent, reintroduced one
176
+ * layer up where the type system can no longer see it. For the same reason the
177
+ * report's `notes` are printed FIRST and in full: a sweep that measured nothing
178
+ * has to say so in words, since an output with no rows reads as a clean bill of
179
+ * health.
180
+ *
181
+ * MANY HOOKS STAY READABLE by grouping the unmeasured half BY REASON — a repo
182
+ * with thirty hooks usually has two or three distinct reasons — and naming at
183
+ * most {@link HOOKS_PER_REASON} hooks per reason before counting the rest. The
184
+ * measured half is never collapsed: those are the hooks you came for.
185
+ *
186
+ * @experimental It renders {@link PluginGuardReport}, whose SHAPE is the part
187
+ * most likely to move (see {@link experimental_verifyPluginGuards}), so a stable
188
+ * name here would promise a stability its only input does not have. The prefix
189
+ * comes off with the same change that takes it off the report.
190
+ *
191
+ * @param report - a sweep from {@link experimental_verifyPluginGuards}.
192
+ */
193
+ export declare function experimental_formatPluginGuardReport(report: PluginGuardReport): string;
194
+ //# sourceMappingURL=verify-plugin-guards.d.ts.map