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
package/README.md CHANGED
@@ -182,9 +182,10 @@ A hook that blocks nothing, a skill that hijacks unrelated prompts, context that
182
182
  _"Caveman Mode cuts 65% of your tokens." Says who?_ vigiles A/Bs the claim on real coding tasks and hands you three numbers: the **token bill**, whether it hit its **target**, and whether your code still **works**.
183
183
 
184
184
  ```text
185
- caveman vs verbose · haiku · $0 on your subscription
186
- output tokens 762 842 (+11% — the "saving" reversed)
187
- correctness 1.01.0 (the fact survived)
185
+ caveman vs baseline · sonnet · 7 tasks × 5 trials · $0 on your subscription
186
+ output tokens 6% lower on average (the claim was 65% — and it GREW on 2 of 7 tasks)
187
+ bill $0.5396$0.5334 (flat — output is only ~20% of the cost)
188
+ correctness 1.0 → 1.0 (nothing broke)
188
189
  ```
189
190
 
190
191
  Point it at any harness change that claims a number — does a compression skill pay for itself, is a subagent worth its cost, which model is cheapest here. promptfoo and DeepEval bill **per token, every run**; vigiles runs on your own Claude Pro/Max subscription, so you measure on every change, not once. A committed lock file (like `package-lock`) keeps CI honest without re-calling the model. (Claude Code today; Codex landing.)
@@ -268,7 +269,7 @@ Targets Claude Code and Codex out of the box, or [your own harness](docs/authori
268
269
  > signal you need to look for, since it is on every call site rather than on an
269
270
  > import line you scrolled past. See [Stability](STABILITY.md).
270
271
 
271
- **Project** — [Stability](STABILITY.md) · [Related tools](docs/comparison.md#what-vigiles-composes-with) · ships as an [Agent Plugins](https://agent-plugins.org) 1.0.0 plugin ([how to do the same](docs/for-plugin-authors.md#6-ship-it-in-the-portable-agent-plugins-format))
272
+ **Project** — [Stability](STABILITY.md) · [Contributing](CONTRIBUTING.md) · [Related tools](docs/comparison.md#what-vigiles-composes-with) · ships as an [Agent Plugins](https://agent-plugins.org) 1.0.0 plugin ([how to do the same](docs/for-plugin-authors.md#6-ship-it-in-the-portable-agent-plugins-format))
272
273
 
273
274
  <!-- The "companion to [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need)"
274
275
  link is temporarily removed while AgenticDev paper #1 is under blind review: repo → blog is a
@@ -0,0 +1,46 @@
1
+ /**
2
+ * claudeCodeHookCondition — Claude Code's hook `if` field, evaluated.
3
+ *
4
+ * A CC hook action may carry `if: "Bash(git push *--force*)"`. The hook command
5
+ * runs ONLY when the tool call matches; otherwise it is never spawned. The syntax
6
+ * is CC's permission-rule grammar, which is why this lives in the adapter and not
7
+ * in core. Reached through `HookProtocol.condition`; the neutral shape and the
8
+ * event rule live in `src/core/hook-condition.ts`.
9
+ *
10
+ * SOURCE OF THE SEMANTICS. Every rule below is transcribed from Claude Code's own
11
+ * hooks documentation (the `if` row of the common-fields table + its "Bash if
12
+ * matching" table) and cross-checked against the installed binary, whose bundled
13
+ * schema carries the string verbatim: "Permission rule syntax to filter when this
14
+ * hook runs (e.g., \"Bash(git *)\"). Only runs if the tool call matches the
15
+ * pattern. Avoids spawning hooks for non-matching commands." Measured on
16
+ * @anthropic-ai/claude-code 2.1.263.
17
+ *
18
+ * 🔴 THE MATCHER IS DELIBERATELY CONSERVATIVE IN ONE DIRECTION ONLY, and that is
19
+ * the whole safety argument. CC itself fails OPEN — where it cannot tell what a
20
+ * command name expands to, it runs the hook — so mirroring that means every
21
+ * uncertain input yields `runs: true`. A wrong answer can therefore only ever
22
+ * make a guardrail report say "this hook ran and did not block", never "this hook
23
+ * was skipped". The first is a real finding about the hook; the second would be a
24
+ * false alarm invented by us, and this file must not be able to produce one.
25
+ *
26
+ * THE SEVEN DOCUMENTED BASH CASES, all pinned in hook-condition.test.ts:
27
+ *
28
+ * | pattern | command | runs | why |
29
+ * | ------------------ | --------------------------- | ---- | ------------------------------------ |
30
+ * | `Bash(git *)` | `FOO=bar git push` | yes | leading assignments stripped |
31
+ * | `Bash(git *)` | `npm test && git push` | yes | each subcommand is checked |
32
+ * | `Bash(rm *)` | `echo $(rm -rf /)` | yes | commands inside `$()`/`` ` `` too |
33
+ * | `Bash(rm *)` | `echo $(date)` | no | no subcommand matches |
34
+ * | `Bash(cat *)` | `echo before $(date) after` | no | full command and `date` both checked |
35
+ * | `Bash(git *)` | `$TOOL git push` | yes | the command name is unknowable |
36
+ * | `Bash(git push *)` | `echo $(date)` | yes | argument-bearing pattern + dynamic |
37
+ *
38
+ * The last two rows are the fail-open half, and they are why a blanket "contains a
39
+ * `$` ⇒ run" shortcut is wrong: rows 4 and 5 also contain `$(` and must say NO.
40
+ * CC resolves command NAMES through substitution (it can see `date`, `rm`) but not
41
+ * ARGUMENTS, so a command-name-only pattern (`rm *`) is decided precisely while an
42
+ * argument-bearing one (`git push *`) gives up on any dynamic command.
43
+ */
44
+ import type { HookConditionSupport } from "../../core/hook-condition.js";
45
+ export declare const claudeCodeHookCondition: HookConditionSupport;
46
+ //# sourceMappingURL=hook-condition.d.ts.map
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.claudeCodeHookCondition = void 0;
4
+ const bash_effects_js_1 = require("../../core/bash-effects.js");
5
+ /** The events Claude Code evaluates `if` on. On any other, a hook with `if` never runs. */
6
+ const EVALUATED_ON = [
7
+ "PreToolUse",
8
+ "PostToolUse",
9
+ "PostToolUseFailure",
10
+ "PermissionRequest",
11
+ "PermissionDenied",
12
+ ];
13
+ /**
14
+ * Parse `Tool(pattern)` / `Tool`. Returns `null` on anything else, which the
15
+ * caller treats as "cannot decide" ⇒ run the hook.
16
+ */
17
+ function parseRule(condition) {
18
+ const text = condition.trim();
19
+ if (text === "")
20
+ return null;
21
+ const open = text.indexOf("(");
22
+ if (open === -1)
23
+ return /^[\w.-]+$/.test(text) ? { tool: text, pattern: null } : null;
24
+ if (!text.endsWith(")"))
25
+ return null;
26
+ const tool = text.slice(0, open).trim();
27
+ if (!/^[\w.-]+$/.test(tool))
28
+ return null;
29
+ return { tool, pattern: text.slice(open + 1, -1) };
30
+ }
31
+ /** A permission-rule glob (`*` is the only metacharacter) as a whole-string regex. */
32
+ function globToRegExp(pattern) {
33
+ const body = pattern
34
+ .split("*")
35
+ .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
36
+ .join("[\\s\\S]*");
37
+ return new RegExp(`^${body}$`);
38
+ }
39
+ /**
40
+ * Does the pattern constrain more than the command NAME?
41
+ *
42
+ * `git *` names only the command; `git push *` and `git push *--force*` constrain
43
+ * arguments too. The distinction is exactly the documented fail-open boundary:
44
+ * CC resolves command names through `$()` but not arguments, so only the second
45
+ * kind gives up on a dynamic command.
46
+ */
47
+ function specifiesMoreThanCommandName(pattern) {
48
+ return /\s/.test(pattern.replace(/\*+$/, "").trim());
49
+ }
50
+ /** `$(…)`, a backtick, or a `$VAR`/`${VAR}` expansion — content CC cannot resolve. */
51
+ function hasDynamicContent(command) {
52
+ return /\$\(|`|\$\{?[A-Za-z_]/.test(command);
53
+ }
54
+ /**
55
+ * Every spelling of the command a pattern is tried against: the raw string, plus
56
+ * each leaf command however nested (`&&` chains, pipelines, `$()`, backticks).
57
+ *
58
+ * BOTH leaf extractors are used on purpose. `leafCommands` keeps literal words
59
+ * only; `leafCommandsNormalized` additionally unwraps quotes, so
60
+ * `git commit -m 'skip hooks'` keeps its quoted operand instead of dropping it.
61
+ * More candidates can only make a pattern MATCH, which is the fail-open direction.
62
+ */
63
+ function candidateCommands(command) {
64
+ const out = new Set([command.trim()]);
65
+ for (const argv of (0, bash_effects_js_1.leafCommands)(command))
66
+ out.add(argv.join(" "));
67
+ for (const leaf of (0, bash_effects_js_1.leafCommandsNormalized)(command))
68
+ out.add(leaf.argv.join(" "));
69
+ return [...out];
70
+ }
71
+ /**
72
+ * True when at least one leaf could not be normalized — in practice a leaf whose
73
+ * HEAD is a `$VAR`/substitution, so the command name is unknowable. CC runs the
74
+ * hook in that case (`$TOOL git push` against `Bash(git *)`), so we do too.
75
+ */
76
+ function hasUnresolvableCommandName(command) {
77
+ return (0, bash_effects_js_1.leafCommandsNormalized)(command).length < (0, bash_effects_js_1.leafCommands)(command).length;
78
+ }
79
+ function matchBash(pattern, input) {
80
+ const command = input["command"];
81
+ if (typeof command !== "string")
82
+ return { runs: true, why: "no command to match against — hook runs" };
83
+ const re = globToRegExp(pattern);
84
+ const hit = candidateCommands(command).find((c) => re.test(c));
85
+ if (hit !== undefined)
86
+ return { runs: true, why: `matches \`${pattern}\` via \`${hit}\`` };
87
+ if (hasUnresolvableCommandName(command))
88
+ return {
89
+ runs: true,
90
+ why: `the command name is an expansion, so \`${pattern}\` cannot be ruled out — hook runs`,
91
+ };
92
+ if (specifiesMoreThanCommandName(pattern) && hasDynamicContent(command))
93
+ return {
94
+ runs: true,
95
+ why: `\`${pattern}\` constrains arguments and the command is dynamic — hook runs`,
96
+ };
97
+ return {
98
+ runs: false,
99
+ why: `does not match \`${pattern}\` — Claude Code would not run this hook`,
100
+ };
101
+ }
102
+ /** The string inputs a non-Bash pattern (`Edit(*.ts)`) is matched against. */
103
+ const PATH_KEYS = ["file_path", "path", "notebook_path", "pattern"];
104
+ function matchNonBash(pattern, input) {
105
+ const re = globToRegExp(pattern);
106
+ const values = PATH_KEYS.map((k) => input[k]).filter((v) => typeof v === "string");
107
+ if (values.length === 0)
108
+ return { runs: true, why: "no path input to match against — hook runs" };
109
+ const hit = values.find((v) => re.test(v));
110
+ return hit !== undefined
111
+ ? { runs: true, why: `matches \`${pattern}\` via \`${hit}\`` }
112
+ : {
113
+ runs: false,
114
+ why: `does not match \`${pattern}\` — Claude Code would not run this hook`,
115
+ };
116
+ }
117
+ exports.claudeCodeHookCondition = {
118
+ field: "if",
119
+ evaluatedOn: [...EVALUATED_ON],
120
+ matches(condition, call) {
121
+ const rule = parseRule(condition);
122
+ if (!rule)
123
+ return {
124
+ runs: true,
125
+ why: `\`${condition}\` is not a permission rule we can read — hook runs`,
126
+ };
127
+ if (rule.tool !== call.tool)
128
+ return {
129
+ runs: false,
130
+ why: `\`${condition}\` names ${rule.tool}, the call is ${call.tool} — the hook never runs`,
131
+ };
132
+ if (rule.pattern === null)
133
+ return {
134
+ runs: true,
135
+ why: `\`${condition}\` matches any ${rule.tool} call`,
136
+ };
137
+ return rule.tool === "Bash"
138
+ ? matchBash(rule.pattern, call.input)
139
+ : matchNonBash(rule.pattern, call.input);
140
+ },
141
+ };
142
+ //# sourceMappingURL=hook-condition.js.map
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.claudeCodeHookProtocol = void 0;
4
+ const hook_condition_js_1 = require("./hook-condition.js");
4
5
  exports.claudeCodeHookProtocol = {
5
6
  name: "claude-code",
6
7
  blockExitCode: 2,
@@ -13,5 +14,9 @@ exports.claudeCodeHookProtocol = {
13
14
  // injection). Covers vigiles's shipped inject hooks: the SessionStart lint
14
15
  // summary and the PostToolUse refs / eval-lock nudges.
15
16
  injectableEvents: ["SessionStart", "UserPromptSubmit", "PostToolUse"],
17
+ // The `if` field: a permission-rule pattern deciding whether the hook is spawned
18
+ // at all. See ./hook-condition.ts — without it a conditional guard was reported
19
+ // as blocking every disaster in the battery.
20
+ condition: hook_condition_js_1.claudeCodeHookCondition,
16
21
  };
17
22
  //# sourceMappingURL=hook-protocol.js.map