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.
- package/README.md +5 -4
- package/dist/adapters/claude-code/hook-condition.d.ts +46 -0
- package/dist/adapters/claude-code/hook-condition.js +142 -0
- package/dist/adapters/claude-code/hook-protocol.js +5 -0
- package/dist/audit-report.template.html +2 -2
- package/dist/cli.js +67 -115
- package/dist/core/bash-effects.d.ts +22 -0
- package/dist/core/bash-effects.js +10 -0
- package/dist/core/command-files.d.ts +107 -0
- package/dist/core/command-files.js +407 -0
- package/dist/core/hook-condition.d.ts +96 -0
- package/dist/core/hook-condition.js +63 -0
- package/dist/core/hook-matcher.d.ts +50 -0
- package/dist/core/hook-matcher.js +77 -2
- package/dist/core/hook-normalize.d.ts +51 -0
- package/dist/core/hook-normalize.js +61 -1
- package/dist/core/hook-program.d.ts +62 -1
- package/dist/core/hook-program.js +15 -1
- package/dist/core/hook-protocol.d.ts +16 -0
- package/dist/core/linters.js +97 -58
- package/dist/core/shell-vars.d.ts +74 -0
- package/dist/core/shell-vars.js +270 -0
- package/dist/core/skill-resources.d.ts +22 -1
- package/dist/core/skill-resources.js +2 -1
- package/dist/doc-test-script-coverage.d.ts +52 -0
- package/dist/doc-test-script-coverage.js +66 -0
- package/dist/guardrail-check.d.ts +29 -0
- package/dist/guardrail-check.js +69 -10
- package/dist/harness-assert.d.ts +8 -5
- package/dist/harness-assert.js +8 -5
- package/dist/harness-resolve-hooks.mjs +14 -37
- package/dist/hook-state-store.d.ts +143 -0
- package/dist/hook-state-store.js +241 -0
- package/dist/hook.d.ts +3 -1
- package/dist/hook.js +3 -1
- package/dist/run-hook.d.ts +33 -1
- package/dist/run-hook.js +46 -2
- package/dist/run-script.d.ts +94 -0
- package/dist/run-script.js +47 -26
- package/dist/scan-core.js +21 -3
- package/dist/score-core.d.ts +21 -1
- package/dist/score-core.js +30 -6
- package/dist/self-resolve.d.mts +20 -0
- package/dist/self-resolve.mjs +75 -0
- package/dist/spec-hooks.d.mts +10 -0
- package/dist/spec-hooks.mjs +17 -0
- package/dist/test.d.ts +5 -0
- package/dist/test.js +23 -2
- package/dist/verify-plugin-guards.d.ts +194 -0
- package/dist/verify-plugin-guards.js +822 -0
- 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
|
|
186
|
-
output tokens
|
|
187
|
-
|
|
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
|