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.
Files changed (49) hide show
  1. package/README.md +127 -5
  2. package/dist/action-gate.d.ts +28 -0
  3. package/dist/action-gate.js +73 -0
  4. package/dist/cli.js +450 -75
  5. package/dist/community-skills.d.ts +22 -0
  6. package/dist/community-skills.js +86 -0
  7. package/dist/compile-generator.d.ts +48 -0
  8. package/dist/compile-generator.js +322 -0
  9. package/dist/compile.d.ts +3 -0
  10. package/dist/compile.js +217 -26
  11. package/dist/eval.d.ts +87 -0
  12. package/dist/eval.js +208 -0
  13. package/dist/frontmatter.d.ts +24 -6
  14. package/dist/frontmatter.js +103 -30
  15. package/dist/generate-schema.js +10 -0
  16. package/dist/harness-assert.d.ts +68 -0
  17. package/dist/harness-assert.js +127 -0
  18. package/dist/harness-test.d.ts +45 -0
  19. package/dist/harness-test.js +138 -0
  20. package/dist/inline.d.ts +22 -4
  21. package/dist/inline.js +60 -13
  22. package/dist/jest.d.ts +9 -0
  23. package/dist/jest.js +23 -0
  24. package/dist/judge.d.ts +29 -0
  25. package/dist/judge.js +88 -0
  26. package/dist/linters.js +28 -0
  27. package/dist/mock-model.d.ts +31 -0
  28. package/dist/mock-model.js +189 -0
  29. package/dist/plugin-loader.d.ts +37 -0
  30. package/dist/plugin-loader.js +195 -0
  31. package/dist/refs.d.ts +44 -0
  32. package/dist/refs.js +144 -0
  33. package/dist/run-hook.d.ts +77 -0
  34. package/dist/run-hook.js +80 -0
  35. package/dist/run-scripts.d.ts +20 -0
  36. package/dist/run-scripts.js +70 -0
  37. package/dist/skill-driver.d.ts +77 -0
  38. package/dist/skill-driver.js +76 -0
  39. package/dist/skill-runtime.d.ts +101 -0
  40. package/dist/skill-runtime.js +289 -0
  41. package/dist/skill-test.d.ts +47 -0
  42. package/dist/skill-test.js +77 -0
  43. package/dist/spec.d.ts +90 -4
  44. package/dist/spec.js +29 -0
  45. package/dist/symbols.d.ts +30 -0
  46. package/dist/symbols.js +142 -0
  47. package/dist/vitest.d.mts +9 -0
  48. package/dist/vitest.mjs +22 -0
  49. 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 &amp; 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