vigiles 2.1.1 → 2.3.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 +127 -5
- package/dist/action-gate.d.ts +28 -0
- package/dist/action-gate.js +73 -0
- package/dist/cli.js +450 -75
- package/dist/community-skills.d.ts +22 -0
- package/dist/community-skills.js +86 -0
- package/dist/compile-generator.d.ts +48 -0
- package/dist/compile-generator.js +322 -0
- package/dist/compile.d.ts +3 -0
- package/dist/compile.js +217 -26
- package/dist/eval.d.ts +87 -0
- package/dist/eval.js +208 -0
- package/dist/frontmatter.d.ts +24 -6
- package/dist/frontmatter.js +103 -30
- package/dist/generate-schema.js +10 -0
- package/dist/harness-assert.d.ts +68 -0
- package/dist/harness-assert.js +127 -0
- package/dist/harness-test.d.ts +45 -0
- package/dist/harness-test.js +138 -0
- package/dist/inline.d.ts +22 -4
- package/dist/inline.js +60 -13
- package/dist/jest.d.ts +9 -0
- package/dist/jest.js +23 -0
- package/dist/judge.d.ts +29 -0
- package/dist/judge.js +88 -0
- package/dist/linters.js +28 -0
- package/dist/mock-model.d.ts +31 -0
- package/dist/mock-model.js +189 -0
- package/dist/plugin-loader.d.ts +37 -0
- package/dist/plugin-loader.js +195 -0
- package/dist/refs.d.ts +44 -0
- package/dist/refs.js +144 -0
- package/dist/run-hook.d.ts +77 -0
- package/dist/run-hook.js +80 -0
- package/dist/run-scripts.d.ts +20 -0
- package/dist/run-scripts.js +70 -0
- package/dist/skill-driver.d.ts +77 -0
- package/dist/skill-driver.js +76 -0
- package/dist/skill-runtime.d.ts +101 -0
- package/dist/skill-runtime.js +289 -0
- package/dist/skill-test.d.ts +47 -0
- package/dist/skill-test.js +77 -0
- package/dist/spec.d.ts +90 -4
- package/dist/spec.js +29 -0
- package/dist/symbols.d.ts +30 -0
- package/dist/symbols.js +142 -0
- package/dist/vitest.d.mts +9 -0
- package/dist/vitest.mjs +22 -0
- package/package.json +45 -6
package/README.md
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
<em>Quis custodiet ipsos custodes?</em> — Who watches the watchmen?
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
+
<p align="center">
|
|
12
|
+
<strong>Test & verify your Claude Code harness.</strong><br />
|
|
13
|
+
vigiles <strong>verifies the references</strong> your instruction files make — linter rules, file paths, scripts, code symbols — and <strong>evals</strong> whether your hooks, skills, and CLAUDE.md actually change what the agent does.
|
|
14
|
+
</p>
|
|
15
|
+
|
|
11
16
|
<p align="center">
|
|
12
17
|
<a href="https://www.npmjs.com/package/vigiles"><img src="https://img.shields.io/npm/v/vigiles?color=orange" alt="npm version" /></a>
|
|
13
18
|
<a href="https://github.com/zernie/vigiles/actions"><img src="https://img.shields.io/github/actions/workflow/status/zernie/vigiles/ci.yml?branch=main" alt="CI" /></a>
|
|
@@ -40,7 +45,7 @@ Reads fine. Four things are wrong:
|
|
|
40
45
|
3. `npm run typecheck` — script removed from package.json
|
|
41
46
|
4. Service/test pairing — no automated check, just a hope
|
|
42
47
|
|
|
43
|
-
The agent reads this, trusts it, and writes code based on stale claims nobody verified. vigiles **verifies the references in your instruction files** — that each linter rule exists and is enabled, that every file path and script is real — and meets you at whatever commitment level you want.
|
|
48
|
+
The agent reads this, trusts it, and writes code based on stale claims nobody verified. vigiles **verifies the references in your instruction files** — that each linter rule exists and is enabled, that every file path and script is real, and that referenced **code symbols** (functions, classes, constants) actually exist in the files that define them — and meets you at whatever commitment level you want.
|
|
44
49
|
|
|
45
50
|
Three levels. Each is independently useful; adopt as far up as you like.
|
|
46
51
|
|
|
@@ -210,19 +215,20 @@ Same monotonicity guarantees as `enforce()` — guards can't be silently removed
|
|
|
210
215
|
|
|
211
216
|
## Verified References
|
|
212
217
|
|
|
213
|
-
`file()`, `cmd()`, and `ref()` catch stale references at compile time:
|
|
218
|
+
`file()`, `cmd()`, `symbol()`, and `ref()` catch stale references at compile time:
|
|
214
219
|
|
|
215
220
|
```typescript
|
|
216
|
-
import { claude, file, cmd, ref, instructions } from "vigiles/spec";
|
|
221
|
+
import { claude, file, cmd, symbol, ref, instructions } from "vigiles/spec";
|
|
217
222
|
|
|
218
223
|
export default claude({
|
|
219
224
|
sections: {
|
|
220
225
|
architecture: instructions`
|
|
221
226
|
Core engine in ${file("src/compile.ts")}.
|
|
227
|
+
Compile specs with ${symbol("src/compile.ts", "compileClaude")}.
|
|
222
228
|
Run ${cmd("npm test")} to verify.
|
|
223
229
|
See ${ref("skills/strengthen/SKILL.md")} for the strengthen skill.
|
|
224
230
|
`,
|
|
225
|
-
// If any path is stale → compile error
|
|
231
|
+
// If any path / script / symbol is stale → compile error
|
|
226
232
|
},
|
|
227
233
|
// ...
|
|
228
234
|
});
|
|
@@ -230,6 +236,12 @@ export default claude({
|
|
|
230
236
|
|
|
231
237
|
Skill specs use the same helpers for verified references inside instructions. [Full spec format →](docs/spec-format.md)
|
|
232
238
|
|
|
239
|
+
### Symbol references (cross-language)
|
|
240
|
+
|
|
241
|
+
`symbol("file", "name")` (and the markdown mark `` `vigiles:symbol file#name` ``) verify that the named file actually **defines** the symbol — a function, class, method, or constant — parsed with [ast-grep](https://ast-grep.github.io) across **JS/TS, Python, Ruby, Rust, and CSS**. Rename the function and `audit` fails; no project-wide index, no autoloader guessing — it parses the one named file.
|
|
242
|
+
|
|
243
|
+
In markdown mode the `refs-hook` (PostToolUse) **forces the mark**: it blocks an edit that leaves a code reference bare, telling the agent to write `` `vigiles:symbol path#name` `` or opt out with `<!-- vigiles:ignore -->`. The harness makes the agent mark its references at write time, with full context; `audit` re-verifies them. [Symbol verification →](research/symbol-verification.md)
|
|
244
|
+
|
|
233
245
|
## Type-Safe Rule References
|
|
234
246
|
|
|
235
247
|
`vigiles generate-types` scans your linter configs and emits `.vigiles/generated.d.ts`. With this file, `enforce("eslint/no-consolee")` is a red squiggle in your editor — a typo caught at authoring time, not a runtime surprise. Without it, everything falls back to broad types and still works.
|
|
@@ -249,7 +261,10 @@ For markdown frontmatter (Level 1), `vigiles generate-schema` gives the same aut
|
|
|
249
261
|
```bash
|
|
250
262
|
npx vigiles init [--target=X.md] # Scaffold a spec (runs full setup wizard by default)
|
|
251
263
|
npx vigiles compile [files...] # Compile .spec.ts → .md
|
|
252
|
-
npx vigiles audit [files...] # Verify hashes + inline/frontmatter/spec rules + coverage
|
|
264
|
+
npx vigiles audit [files...] # Verify hashes + inline/frontmatter/spec rules + symbols + coverage
|
|
265
|
+
npx vigiles refs <file.md> # Check the symbol references in an instruction file
|
|
266
|
+
npx vigiles test [files...] # Run *.harness.mjs deterministic harness tests (no API key)
|
|
267
|
+
npx vigiles eval [files...] # Run *.eval.mjs real-model harness evals (--trials=N)
|
|
253
268
|
npx vigiles generate-types # Emit .d.ts from project state (for spec mode)
|
|
254
269
|
npx vigiles generate-types --check # Verify .d.ts is up to date
|
|
255
270
|
npx vigiles generate-schema # Emit JSON Schema for vigiles: frontmatter (Level 1)
|
|
@@ -323,6 +338,113 @@ Install with [Vercel Skills](https://github.com/vercel-labs/skills): `npx skills
|
|
|
323
338
|
| `enforce-rules-format` | Validate all rules have enforcement classification |
|
|
324
339
|
| `audit-feedback-loop` | Score your repo's feedback loop maturity |
|
|
325
340
|
|
|
341
|
+
## Test your Claude Code harness
|
|
342
|
+
|
|
343
|
+
vigiles also ships a library for **testing the harness itself** — your hooks,
|
|
344
|
+
settings, skills, and instruction files. `Agent = Model + Harness`; this tests
|
|
345
|
+
the harness, at three levels.
|
|
346
|
+
|
|
347
|
+
**Evals — does my change actually move agent behaviour?** Define a fixture, a set
|
|
348
|
+
of **arms** (a hook on vs off, with/without a CLAUDE.md rule), a task, and a
|
|
349
|
+
metric; `runEval` drives the real `claude` CLI N trials per arm and aggregates.
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import { runEval, formatEvalReport } from "vigiles/eval";
|
|
353
|
+
|
|
354
|
+
const report = await runEval({
|
|
355
|
+
fixture: { "src/billing.ts": "export function chargeCard() {}" },
|
|
356
|
+
arms: {
|
|
357
|
+
vanilla: {},
|
|
358
|
+
gated: { settings: { hooks: { PostToolUse: [refsHook] } } },
|
|
359
|
+
},
|
|
360
|
+
task: "Document chargeCard in SKILL.md, referencing it by name.",
|
|
361
|
+
measure: (ctx) => ({
|
|
362
|
+
marked: ctx.sh("grep -c vigiles:symbol SKILL.md") !== "0",
|
|
363
|
+
}),
|
|
364
|
+
trials: 6,
|
|
365
|
+
});
|
|
366
|
+
console.log(formatEvalReport(report)); // vanilla marked=0.00 gated marked=0.50
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Deterministic tests — does my hook fire correctly?** No API key, no cost.
|
|
370
|
+
`runHarnessTest` runs real `claude` against a **scripted mock model**
|
|
371
|
+
(`vigiles/mock-model`), so your real hooks fire but the agent's turns are fixed.
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
import { runHarnessTest, scriptModel } from "vigiles/harness-test";
|
|
375
|
+
|
|
376
|
+
const r = await runHarnessTest({
|
|
377
|
+
settings: {
|
|
378
|
+
hooks: {
|
|
379
|
+
Stop: [
|
|
380
|
+
{
|
|
381
|
+
hooks: [
|
|
382
|
+
{
|
|
383
|
+
type: "command",
|
|
384
|
+
command: "test -f DONE || { echo 'not done' >&2; exit 2; }",
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
model: scriptModel([
|
|
392
|
+
{ text: "I'm done" }, // tries to stop → blocked
|
|
393
|
+
{ tool: "Bash", input: { command: "touch DONE" } },
|
|
394
|
+
{ text: "now done" },
|
|
395
|
+
]),
|
|
396
|
+
});
|
|
397
|
+
assert(JSON.parse(r.stdout).num_turns > 1); // the Stop hook forced more work
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
The deterministic tier is reliable for **SessionStart, Stop, UserPromptSubmit,
|
|
401
|
+
and Bash PreToolUse/PostToolUse** hooks — the governance/policy shapes most real
|
|
402
|
+
plugins use; Edit/Write tool-event hooks are headless-gated, so test those at the
|
|
403
|
+
unit tier or via the eval tier.
|
|
404
|
+
|
|
405
|
+
**Unit-test a hook — no `claude` at all.** A hook is just a process: `runHook`
|
|
406
|
+
pipes an event JSON to its stdin and reports the block/allow decision —
|
|
407
|
+
milliseconds, and the only tier that reaches **every** event (incl. Edit/Write,
|
|
408
|
+
PreCompact, SessionEnd, which the deterministic mock can't trigger).
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
import { runHook } from "vigiles/run-hook";
|
|
412
|
+
|
|
413
|
+
const r = runHook(guardCommand, {
|
|
414
|
+
hook_event_name: "PreToolUse",
|
|
415
|
+
tool_name: "Bash",
|
|
416
|
+
tool_input: { command: "git commit --no-verify" },
|
|
417
|
+
});
|
|
418
|
+
assert(r.blocked); // exit 2, decision:"block", or permissionDecision:"deny"
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Run them as a CI command.** `vigiles test` discovers `*.harness.mjs` files
|
|
422
|
+
(deterministic, no API key) and `vigiles eval` discovers `*.eval.mjs` files
|
|
423
|
+
(real model). Canonical, real-plugin-shaped examples to copy:
|
|
424
|
+
|
|
425
|
+
- [`examples/harness/policy-gate.harness.mjs`](examples/harness/policy-gate.harness.mjs) — a `PreToolUse` Bash policy gate (block `git commit --no-verify`) and a `SessionStart` setup hook, deterministic.
|
|
426
|
+
- [`examples/harness/skill-outcome.eval.mjs`](examples/harness/skill-outcome.eval.mjs) — does a skill change the agent's output? (the question you ask of any `SKILL.md`).
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
npx vigiles test examples/harness/policy-gate.harness.mjs
|
|
430
|
+
npx vigiles eval --trials=6 examples/harness/skill-outcome.eval.mjs
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Test the whole machine.** Point `plugin` at a plugin (or `"./"` for your repo)
|
|
434
|
+
and the real harness — hooks (with `${CLAUDE_PLUGIN_ROOT}` resolved), CLAUDE.md,
|
|
435
|
+
skills, subagents and commands — is loaded into the sandbox, so you test what
|
|
436
|
+
ships, not a retyped subset. `loadPlugin(...).warnings` flags surfaces only a
|
|
437
|
+
real model can drive (subagents, slash commands, MCP), so loading a whole plugin
|
|
438
|
+
never silently tests an empty machine. The library is plain async functions, so
|
|
439
|
+
it runs in **node:test, vitest, or jest** unchanged (shared `expect.extend`
|
|
440
|
+
matchers for the latter two).
|
|
441
|
+
|
|
442
|
+
[Full guide → `docs/harness-testing.md`](docs/harness-testing.md). The design
|
|
443
|
+
rationale and a coverage assessment against real plugins (protect-mcp,
|
|
444
|
+
obra/superpowers, block-no-verify, 156 wshobson skills) are in
|
|
445
|
+
[`research/harness-testing.md`](research/harness-testing.md); findings from
|
|
446
|
+
running this harness in anger live in [`research/benchmarks-runtime-gates.md`](research/benchmarks-runtime-gates.md).
|
|
447
|
+
|
|
326
448
|
## Maturity Levels
|
|
327
449
|
|
|
328
450
|
From [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need):
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type RuntimeGate } from "./skill-runtime.js";
|
|
2
|
+
export interface ActionGate {
|
|
3
|
+
/** Tool name to gate, e.g. "Write" | "Edit" | "Bash". */
|
|
4
|
+
readonly on: string;
|
|
5
|
+
/** Deterministic check; a `cmd` command may include `{file}`. */
|
|
6
|
+
readonly gate: RuntimeGate;
|
|
7
|
+
/** Optional substring the (JSON-serialized) tool input must contain. */
|
|
8
|
+
readonly when?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ActionEvent {
|
|
11
|
+
/** The tool that just ran (PostToolUse `tool_name`). */
|
|
12
|
+
readonly tool: string;
|
|
13
|
+
/** The tool input (`tool_input`), e.g. `{ file_path, command }`. */
|
|
14
|
+
readonly input?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface ActionDecision {
|
|
17
|
+
readonly allow: boolean;
|
|
18
|
+
readonly message: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Evaluate action gates against a tool event. Runs every gate whose `on`
|
|
22
|
+
* matches the tool (and whose `when` substring matches the input); the first
|
|
23
|
+
* failure blocks. Plan-agnostic — order in any runtime workflow is irrelevant.
|
|
24
|
+
*/
|
|
25
|
+
export declare function evaluateAction(event: ActionEvent, gates: readonly ActionGate[], cwd: string): ActionDecision;
|
|
26
|
+
/** Load action gates from `.vigiles/action-gates.json`. */
|
|
27
|
+
export declare function loadActionGates(cwd: string): ActionGate[];
|
|
28
|
+
//# sourceMappingURL=action-gate.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.evaluateAction = evaluateAction;
|
|
4
|
+
exports.loadActionGates = loadActionGates;
|
|
5
|
+
/**
|
|
6
|
+
* vigiles — Action gates (the dynamic-workflow reframe).
|
|
7
|
+
*
|
|
8
|
+
* A skill gate is bound to a *step* (a fixed position in a plan). When the plan
|
|
9
|
+
* is generated at runtime (dynamic workflows), the step is the wrong unit. An
|
|
10
|
+
* **action gate** binds a deterministic check to a *tool action type* instead —
|
|
11
|
+
* "any time a Write happens to a `.ts` file, eslint must pass on it" — so it
|
|
12
|
+
* fires regardless of where in the runtime plan the action occurs.
|
|
13
|
+
*
|
|
14
|
+
* It is the same deterministic gate primitive (reuses `runGate` + the
|
|
15
|
+
* author-time reference resolution), re-anchored from step → action. Delivered
|
|
16
|
+
* as a PostToolUse hook (`vigiles action-hook`): exit 2 blocks the action and
|
|
17
|
+
* feeds the reason back, exit 0 allows it.
|
|
18
|
+
*
|
|
19
|
+
* Config: `.vigiles/action-gates.json` → `{ "gates": [ { on, gate, when? } ] }`.
|
|
20
|
+
* The gate command may contain `{file}`, substituted with the action's path.
|
|
21
|
+
*/
|
|
22
|
+
const node_fs_1 = require("node:fs");
|
|
23
|
+
const node_path_1 = require("node:path");
|
|
24
|
+
const skill_runtime_js_1 = require("./skill-runtime.js");
|
|
25
|
+
/** The file path an action touched, for `{file}` substitution. */
|
|
26
|
+
function fileOf(event) {
|
|
27
|
+
const i = event.input ?? {};
|
|
28
|
+
const v = i.file_path ?? i.path;
|
|
29
|
+
return typeof v === "string" ? v : "";
|
|
30
|
+
}
|
|
31
|
+
/** Substitute `{file}` in a cmd gate with the action's path. */
|
|
32
|
+
function resolveGate(gate, event) {
|
|
33
|
+
if (gate.kind !== "cmd" || !gate.command.includes("{file}"))
|
|
34
|
+
return gate;
|
|
35
|
+
return { ...gate, command: gate.command.replaceAll("{file}", fileOf(event)) };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Evaluate action gates against a tool event. Runs every gate whose `on`
|
|
39
|
+
* matches the tool (and whose `when` substring matches the input); the first
|
|
40
|
+
* failure blocks. Plan-agnostic — order in any runtime workflow is irrelevant.
|
|
41
|
+
*/
|
|
42
|
+
function evaluateAction(event, gates, cwd) {
|
|
43
|
+
const inputStr = JSON.stringify(event.input ?? "");
|
|
44
|
+
for (const g of gates) {
|
|
45
|
+
if (g.on !== event.tool)
|
|
46
|
+
continue;
|
|
47
|
+
if (g.when && !inputStr.includes(g.when))
|
|
48
|
+
continue;
|
|
49
|
+
const outcome = (0, skill_runtime_js_1.runGate)(resolveGate(g.gate, event), cwd);
|
|
50
|
+
if (!outcome.ok) {
|
|
51
|
+
const tail = outcome.output ? `\n${outcome.output}` : "";
|
|
52
|
+
return {
|
|
53
|
+
allow: false,
|
|
54
|
+
message: `Action gate failed after ${event.tool}: ${(0, skill_runtime_js_1.gateLabel)(g.gate)} did not pass.${tail}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { allow: true, message: "" };
|
|
59
|
+
}
|
|
60
|
+
/** Load action gates from `.vigiles/action-gates.json`. */
|
|
61
|
+
function loadActionGates(cwd) {
|
|
62
|
+
const p = (0, node_path_1.resolve)(cwd, ".vigiles/action-gates.json");
|
|
63
|
+
if (!(0, node_fs_1.existsSync)(p))
|
|
64
|
+
return [];
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(p, "utf-8"));
|
|
67
|
+
return Array.isArray(parsed.gates) ? parsed.gates : [];
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=action-gate.js.map
|