vigiles 2.2.0 → 2.4.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 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>
@@ -16,6 +21,29 @@
16
21
 
17
22
  ---
18
23
 
24
+ <details>
25
+ <summary><b>Contents</b></summary>
26
+
27
+ **Pillar 1 — verify your instruction files** · references your CLAUDE.md makes that a linter, the filesystem, and package.json can prove
28
+
29
+ - Three adoption levels: [inline comments](#level-0--inline-comments-30-seconds-no-new-files) → [YAML frontmatter](#level-1--yaml-frontmatter-editor-autocomplete-still-no-typescript) → [typed spec](#level-2--typed-spec-compiler-grade-guarantees)
30
+ - [What changes with vigiles](#what-changes-with-vigiles)
31
+ - [Quick start](#quick-start)
32
+ - [Three rule types](#three-rule-types) — `enforce` / `guidance` / `guard`
33
+ - [Verified references](#verified-references) — `file` / `cmd` / `symbol` / `ref`
34
+
35
+ **Pillar 2 — [test your Claude Code harness](#test-your-claude-code-harness)** · eval whether your hooks, skills, and CLAUDE.md actually change what the agent does
36
+
37
+ - [Level 1 — unit-test a hook (no AI)](#level-1--test-a-hook-by-itself-no-ai-milliseconds)
38
+ - [Level 2 — does it fire in a real session?](#level-2--does-it-fire-in-a-real-session-free-scripted-ai)
39
+ - [Level 3 — does it change behaviour?](#level-3--does-it-change-what-claude-does-real-ai-occasional)
40
+ - [Test skills for real + assert on actions](#test-your-skills-for-real--and-assert-on-what-claude-did)
41
+ - [Run them in CI](#run-them-in-ci)
42
+
43
+ **More** — [CLI & CI](#cli--ci) · [Skills](#skills) · [Maturity levels](#maturity-levels) · [Related tools](#related-tools)
44
+
45
+ </details>
46
+
19
47
  Your CLAUDE.md lies to your agent. Here's the fix.
20
48
 
21
49
  Hand-written CLAUDE.md files rot silently. Here's what a typical one looks like:
@@ -42,6 +70,8 @@ Reads fine. Four things are wrong:
42
70
 
43
71
  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
72
 
73
+ > **See it in 60 seconds:** `npm run demo` runs `vigiles audit` against a deliberately-broken instruction file and catches a renamed symbol and a missing MCP tool (_"did you mean `purge`?"_), while the truthful references pass silently. [examples/demo →](examples/demo)
74
+
45
75
  Three levels. Each is independently useful; adopt as far up as you like.
46
76
 
47
77
  ### Level 0 — inline comments (30 seconds, no new files)
@@ -163,15 +193,7 @@ npx vigiles init
163
193
 
164
194
  The wizard auto-detects your project, creates a spec, scans your linters, compiles to markdown, adds a CI step, and installs Claude Code hooks. After install: the agent edits the spec (hooks block direct CLAUDE.md edits), the spec auto-compiles on save, and `vigiles audit` catches drift in CI.
165
195
 
166
- Start with `guidance()` rules (zero config). When you're ready, run `/strengthen` to find rules that can be upgraded to compile-verified `enforce()`. Already have a hand-written CLAUDE.md? The wizard detects it and offers migration.
167
-
168
- | Flag | Effect |
169
- | -------------------- | ----------------------------------------------------- |
170
- | `--strict` | Sets require-spec and require-skill-spec to `"error"` |
171
- | `--target=AGENTS.md` | Creates AGENTS.md spec instead of CLAUDE.md |
172
- | `--no-gha` | Skip adding CI step to GHA workflow |
173
-
174
- Works the same for humans and agents — fully non-interactive. [Agent setup guide →](docs/agent-setup.md) | [Agent workflows →](docs/agent-workflows.md)
196
+ Start with `guidance()` rules (zero config). When you're ready, run `/strengthen` to find rules that can be upgraded to compile-verified `enforce()`. Already have a hand-written CLAUDE.md? The wizard detects it and offers migration. Flags (`--strict`, `--target=AGENTS.md`, `--no-gha`) and non-interactive agent usage are in the [CLI reference](docs/cli.md) and [agent setup guide](docs/agent-setup.md).
175
197
 
176
198
  ## Three Rule Types
177
199
 
@@ -193,20 +215,7 @@ Supports ESLint, Stylelint, Ruff, Clippy, Pylint, RuboCop, and Cedar policies. [
193
215
  "research-first": guidance("Google unfamiliar APIs first."),
194
216
  ```
195
217
 
196
- **`guard()`** — reactive: runs a command when watched files change. One declaration emits hooks for every supported system (Claude Code PostToolUse, husky pre-commit, etc.). Eliminates copy-pasting the same trigger across `.claude/settings.json`, `.husky/`, and CI configs.
197
-
198
- ```typescript
199
- "recompile-specs": guard(
200
- { watch: "*.spec.ts", run: "npx vigiles compile" },
201
- "Recompile instruction files when any spec changes.",
202
- ),
203
- "regen-types": guard(
204
- { watch: ["eslint.config.*", "package.json"], run: "npx vigiles generate-types" },
205
- "Regenerate types when linter config or deps change.",
206
- ),
207
- ```
208
-
209
- Same monotonicity guarantees as `enforce()` — guards can't be silently removed.
218
+ **`guard()`** — reactive: runs a command when watched files change (e.g. `*.spec.ts` → `npx vigiles compile`). One declaration emits hooks for every supported system (Claude Code PostToolUse, husky pre-commit, etc.) no copy-pasting the same trigger across `.claude/settings.json`, `.husky/`, and CI. Same monotonicity guarantees as `enforce()`. [Full spec format →](docs/spec-format.md)
210
219
 
211
220
  ## Verified References
212
221
 
@@ -229,98 +238,175 @@ export default claude({
229
238
  });
230
239
  ```
231
240
 
232
- Skill specs use the same helpers for verified references inside instructions. [Full spec format →](docs/spec-format.md)
241
+ There's a small family of inline **marks** that `audit` checks, each binding a reference to its real source:
233
242
 
234
- ### Symbol references (cross-language)
243
+ - `` `vigiles:symbol file#name` `` — the named file actually **defines** that symbol (function, class, method, constant), parsed with [ast-grep](https://ast-grep.github.io) across **JS/TS, Python, Ruby, Rust, and CSS**. Rename it and `audit` fails; in markdown mode the `refs-hook` **forces the mark**, blocking edits that leave a code reference bare. [Details →](research/symbol-verification.md)
244
+ - `` `vigiles:mcp server#tool` `` — the referenced **MCP tool exists** on its server. `audit` reads `.mcp.json`, starts the server, lists its tools, and flags a renamed/removed one with a "did you mean" — catching e.g. the GitHub MCP server renaming `create_issue` → `issue_write`, which otherwise fails silently.
235
245
 
236
- `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.
246
+ **Typo-safe at authoring time, too.** `vigiles generate-types` emits a `.vigiles/generated.d.ts` so `enforce("eslint/no-consolee")` red-squiggles in your editor; `generate-schema` gives Level 1 frontmatter the same via your YAML language server. Both have `--check` CI freshness modes. [How it works →](docs/linter-support.md#generate-types)
237
247
 
238
- 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)
248
+ ## Test your Claude Code harness
239
249
 
240
- ## Type-Safe Rule References
250
+ You wrote hooks, a skill, a CLAUDE.md rule — how do you know they work, beyond
251
+ running Claude and eyeballing it? vigiles ships a library to **test the harness
252
+ itself**, at three levels, cheapest first. It's plain async functions, so it
253
+ drops into **node:test / vitest / jest**, or a zero-setup `vigiles test`.
241
254
 
242
- `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.
255
+ ### Level 1 test a hook by itself (no AI, milliseconds)
243
256
 
244
- ```bash
245
- $ npx vigiles generate-types
246
- eslint: 64 enabled rules | ruff: 12 | npm scripts: 5 | project files: 42
247
- ✓ Generated .vigiles/generated.d.ts
257
+ A hook is just a process that's handed a "Claude is about to do X" event and
258
+ answers block/allow. Hand it a fake event and check the answer — no `claude`, no
259
+ model, and **every** event type is reachable (incl. Edit/Write, PreCompact,
260
+ SessionEnd):
261
+
262
+ ```typescript
263
+ import { runHook } from "vigiles/run-hook";
264
+
265
+ const r = runHook(guardCommand, {
266
+ hook_event_name: "PreToolUse",
267
+ tool_name: "Bash",
268
+ tool_input: { command: "git commit --no-verify" },
269
+ });
270
+ assert(r.blocked); // exit 2 / decision:"block" / permissionDecision:"deny"
248
271
  ```
249
272
 
250
- Commit the file to git. CI can verify it's fresh: `npx vigiles generate-types --check`. [How it works →](docs/linter-support.md#generate-types)
273
+ The same shape governs **MCP tools** the dominant real MCP test with no
274
+ server running, because the hook only sees the tool _name_:
251
275
 
252
- For markdown frontmatter (Level 1), `vigiles generate-schema` gives the same authoring-time feedback without TypeScript: it emits a JSON Schema from your enabled rules, and your editor's YAML language server autocompletes rule names and squiggles typos. CI freshness check: `npx vigiles generate-schema --check`.
276
+ ```typescript
277
+ // block the destructive github-MCP tool; read-only ones pass
278
+ runHook(guard, {
279
+ hook_event_name: "PreToolUse",
280
+ tool_name: "mcp__github__merge_pull_request",
281
+ tool_input: { pull_number: 42 },
282
+ }).blocked; // true
283
+ ```
253
284
 
254
- ## CLI
285
+ ### Level 2 — does it fire in a real session? (free, scripted "AI")
255
286
 
256
- ```bash
257
- npx vigiles init [--target=X.md] # Scaffold a spec (runs full setup wizard by default)
258
- npx vigiles compile [files...] # Compile .spec.ts .md
259
- npx vigiles audit [files...] # Verify hashes + inline/frontmatter/spec rules + symbols + coverage
260
- npx vigiles refs <file.md> # Check the symbol references in an instruction file
261
- npx vigiles generate-types # Emit .d.ts from project state (for spec mode)
262
- npx vigiles generate-types --check # Verify .d.ts is up to date
263
- npx vigiles generate-schema # Emit JSON Schema for vigiles: frontmatter (Level 1)
264
- npx vigiles generate-schema --check # Verify schema.json is up to date
287
+ Right logic ≠ wired in correctly. `runHarnessTest` runs the **real** `claude`
288
+ against a **scripted mock model** you control your hooks fire for real, the
289
+ agent's turns are fixed, no API key, same result every time. Covers the
290
+ governance shapes: SessionStart, Stop, UserPromptSubmit, and Bash **and
291
+ Edit/Write** Pre/PostToolUse.
292
+
293
+ ```typescript
294
+ import { runHarnessTest, scriptModel } from "vigiles/harness-test";
295
+
296
+ const r = await runHarnessTest({
297
+ settings: {
298
+ hooks: {
299
+ Stop: [
300
+ { hooks: [{ type: "command", command: "test -f DONE || exit 2" }] },
301
+ ],
302
+ },
303
+ },
304
+ model: scriptModel([
305
+ { text: "I'm done" }, // tries to stop → blocked (no DONE)
306
+ { tool: "Bash", input: { command: "touch DONE" } },
307
+ { text: "now done" },
308
+ ]),
309
+ });
310
+ assert(JSON.parse(r.stdout).num_turns > 1); // the Stop hook forced more work
265
311
  ```
266
312
 
267
- ## GitHub Action
313
+ ### Level 3 — does it change what Claude does? (real AI, occasional)
268
314
 
269
- ```yaml
270
- - uses: zernie/vigiles@main # runs `audit` by default
271
- - uses: zernie/vigiles@main
272
- with:
273
- command: compile # compile specs in CI
315
+ `runEval` runs the **real** model N times with your change **on vs off** and
316
+ reports the gap. Costs tokens, so you run it now and then — not on every save:
317
+
318
+ ```typescript
319
+ import { runEval, formatEvalReport } from "vigiles/eval";
320
+
321
+ const report = await runEval({
322
+ arms: { off: {}, on: { settings: { hooks: { PostToolUse: [refsHook] } } } },
323
+ task: "Document chargeCard in SKILL.md, referencing it by name.",
324
+ measure: (ctx) => ({
325
+ marked: ctx.sh("grep -c vigiles:symbol SKILL.md") !== "0",
326
+ }),
327
+ trials: 6,
328
+ });
329
+ console.log(formatEvalReport(report)); // off marked=0.00 on marked=0.50
274
330
  ```
275
331
 
276
- To verify generated types are fresh in CI:
332
+ ### Test your skills for real and assert on what Claude _did_
277
333
 
278
- ```yaml
279
- - run: npx vigiles generate-types --check
334
+ Install a plugin the way Claude actually does (`pluginDir` → `--plugin-dir`) so
335
+ its **skills genuinely activate**, then assert on the agent's _actions_, not a
336
+ stdout grep:
337
+
338
+ ```typescript
339
+ import { assertSkillResolved, assertToolNotUsed } from "vigiles/harness-assert";
340
+
341
+ const r = await runHarnessTest({
342
+ pluginDir: "./my-plugin",
343
+ transcript: true, // populate r.toolCalls
344
+ allowedTools: ["Read", "Write", "Bash", "Skill"],
345
+ model: scriptModel([
346
+ { tool: "Skill", input: { skill: "my-plugin:greet" } },
347
+ { text: "ok" },
348
+ ]),
349
+ });
350
+ assertSkillResolved(r, "my-plugin:greet"); // the skill fired, no error
351
+ assertToolNotUsed(r, /^mcp__github__merge/); // the safety negative: the scary tool was never called
280
352
  ```
281
353
 
282
- ## Claude Code Plugin
354
+ `assertToolNotUsed` is how you test a safety rule **honestly** — _proving_ the
355
+ dangerous tool was never used, which "the file looks unchanged" can't. It works
356
+ on **real third-party plugins** too: the suite confirms real `obra/superpowers`
357
+ and `wshobson/agents` skills resolve this way, with no markers injected.
283
358
 
284
- **Install the plugin.** Without it, you're responsible for manually running `compile` and `generate-types`. With it, the agent works with fresh instruction files automatically.
359
+ ### Run them in CI
360
+
361
+ `vigiles test` runs `*.harness.mjs` files (free, no key); `vigiles eval` runs
362
+ `*.eval.mjs` files (real model). Point a test at a whole plugin (or `"./"` for
363
+ your repo) to load **what ships** — hooks (with `${CLAUDE_PLUGIN_ROOT}`
364
+ resolved), CLAUDE.md, skills, subagents, commands — and `loadPlugin().warnings`
365
+ flags anything only a real model can drive, so you never silently test an empty
366
+ machine.
285
367
 
286
368
  ```bash
287
- npx skills add zernie/vigiles
369
+ npx vigiles test examples/harness/policy-gate.harness.mjs
370
+ npx vigiles eval --trials=6 examples/harness/skill-outcome.eval.mjs
288
371
  ```
289
372
 
290
- The plugin provides two hooks:
291
-
292
- - **PreToolUse** (Edit/Write) — blocks direct edits to compiled `.md` files and redirects the agent to the `.spec.ts` source
293
- - **PostToolUse** (Edit/Write) — auto-runs `generate-types` on linter config changes, `compile` on `.spec.ts` changes
373
+ ### What's covered today — surface × tier
294
374
 
295
- ## Validation
375
+ | Surface | Unit / static | Integration (no API key) | Eval (real model) |
376
+ | ------------------------------------------------------------- | ---------------------------- | --------------------------- | ----------------- |
377
+ | Hooks — Bash / SessionStart / Stop / UserPromptSubmit | ✅ logic | ✅ fires | ✅ |
378
+ | Hooks — Edit / Write | ✅ logic | ✅ fires | ✅ |
379
+ | Hooks — PreCompact / Notification / SessionEnd / SubagentStop | ✅ logic | — (mock can't trigger) | 🟡 |
380
+ | CLAUDE.md / instructions | ✅ refs | 🟡 present, not behaviour | ✅ behaviour |
381
+ | Skills | 🟡 refs | ✅ resolves via `pluginDir` | ✅ activation |
382
+ | Subagents (`agents/`) | 🟡 refs | 🔴 hard | ✅ via Task |
383
+ | Slash commands (`commands/`) | 🟡 refs | 🟡 needs prompt capture | ✅ via `/cmd` |
384
+ | MCP servers | ✅ tool refs (`vigiles:mcp`) | 🔴 | 🔴 |
385
+ | settings.json | 🟡 assert merged | ✅ applied | ✅ |
296
386
 
297
- `vigiles audit` validates instruction files with four rules:
387
+ shipped · 🟡 partial · 🔴 gap · — n/a. Full detail + roadmap: [`research/harness-testing-coverage-matrix.md`](research/harness-testing-coverage-matrix.md).
298
388
 
299
- | Rule | Default | What it checks |
300
- | -------------------------------------------------------- | -------- | ---------------------------------------------------------------------------- |
301
- | [`require-spec`](docs/rules/require-spec.md) | `"warn"` | Every CLAUDE.md/AGENTS.md has a spec, inline rule, or `vigiles:` frontmatter |
302
- | [`require-skill-spec`](docs/rules/require-skill-spec.md) | `"warn"` | Every SKILL.md has a `.spec.ts` |
303
- | [`integrity`](docs/rules/integrity.md) | `"warn"` | Compiled markdown wasn't hand-edited (SHA-256 check) |
304
- | [`coverage`](docs/rules/coverage.md) | `false` | Spec covers enough of the project surface |
389
+ [Full guide `docs/harness-testing.md`](docs/harness-testing.md) · [benchmarks](research/benchmarks-runtime-gates.md).
305
390
 
306
- Configure in `.vigilesrc.json`:
391
+ ## CLI & CI
307
392
 
308
- ```json
309
- {
310
- "rules": {
311
- "require-spec": "error",
312
- "integrity": "error",
313
- "coverage": ["warn", { "scripts": 50, "linterRules": 5 }]
314
- }
315
- }
393
+ ```bash
394
+ npx vigiles init # Scaffold a spec (full setup wizard)
395
+ npx vigiles compile # Compile .spec.ts → .md
396
+ npx vigiles audit # Verify hashes + inline/frontmatter/spec rules + symbols + coverage
397
+ npx vigiles test # Run *.harness.mjs deterministic harness tests (no API key)
398
+ npx vigiles eval # Run *.eval.mjs real-model harness evals (--trials=N)
316
399
  ```
317
400
 
318
- Disable per-file with `<!-- vigiles-disable require-spec -->` at the top of the markdown.
401
+ `vigiles audit` enforces four rules — `require-spec`, `require-skill-spec`, `integrity`, `coverage` — configurable in `.vigilesrc.json`. The GitHub Action runs `audit` by default; the Claude Code plugin (`npx skills add zernie/vigiles`) adds the Pre/PostToolUse hooks that block direct `.md` edits and auto-compile specs. [Full CLI, Action, plugin & validation reference →](docs/cli.md)
319
402
 
320
403
  ## Skills
321
404
 
322
405
  Install with [Vercel Skills](https://github.com/vercel-labs/skills): `npx skills add zernie/vigiles`
323
406
 
407
+ <details>
408
+ <summary><b>The 7 skills</b></summary>
409
+
324
410
  | Skill | What it does |
325
411
  | ---------------------- | ----------------------------------------------------------------------- |
326
412
  | `strengthen` | Upgrade `guidance()` → `enforce()` using linter-specific reference docs |
@@ -331,72 +417,14 @@ Install with [Vercel Skills](https://github.com/vercel-labs/skills): `npx skills
331
417
  | `enforce-rules-format` | Validate all rules have enforcement classification |
332
418
  | `audit-feedback-loop` | Score your repo's feedback loop maturity |
333
419
 
334
- ## Test your Claude Code harness
335
-
336
- vigiles also ships a library for **testing the harness itself** — your hooks,
337
- settings, skills, and instruction files. `Agent = Model + Harness`; this tests
338
- the harness, at two levels.
339
-
340
- **Evals — does my change actually move agent behaviour?** Define a fixture, a set
341
- of **arms** (a hook on vs off, with/without a CLAUDE.md rule), a task, and a
342
- metric; `runEval` drives the real `claude` CLI N trials per arm and aggregates.
343
-
344
- ```typescript
345
- import { runEval, formatEvalReport } from "vigiles/eval";
346
-
347
- const report = await runEval({
348
- fixture: { "src/billing.ts": "export function chargeCard() {}" },
349
- arms: {
350
- vanilla: {},
351
- gated: { settings: { hooks: { PostToolUse: [refsHook] } } },
352
- },
353
- task: "Document chargeCard in SKILL.md, referencing it by name.",
354
- measure: (ctx) => ({
355
- marked: ctx.sh("grep -c vigiles:symbol SKILL.md") !== "0",
356
- }),
357
- trials: 6,
358
- });
359
- console.log(formatEvalReport(report)); // vanilla marked=0.00 gated marked=0.50
360
- ```
361
-
362
- **Deterministic tests — does my hook fire correctly?** No API key, no cost.
363
- `runHarnessTest` runs real `claude` against a **scripted mock model**
364
- (`vigiles/mock-model`), so your real hooks fire but the agent's turns are fixed.
365
-
366
- ```typescript
367
- import { runHarnessTest, scriptModel } from "vigiles/harness-test";
368
-
369
- const r = await runHarnessTest({
370
- settings: {
371
- hooks: {
372
- Stop: [
373
- {
374
- hooks: [
375
- {
376
- type: "command",
377
- command: "test -f DONE || { echo 'not done' >&2; exit 2; }",
378
- },
379
- ],
380
- },
381
- ],
382
- },
383
- },
384
- model: scriptModel([
385
- { text: "I'm done" }, // tries to stop → blocked
386
- { tool: "Bash", input: { command: "touch DONE" } },
387
- { text: "now done" },
388
- ]),
389
- });
390
- assert(JSON.parse(r.stdout).num_turns > 1); // the Stop hook forced more work
391
- ```
392
-
393
- The deterministic tier is reliable for **Stop hooks**; tool-event hooks
394
- (Edit/Write) are headless-gated, so test those via the eval tier. Our own
395
- findings from this harness live in [`research/benchmarks-runtime-gates.md`](research/benchmarks-runtime-gates.md).
420
+ </details>
396
421
 
397
422
  ## Maturity Levels
398
423
 
399
- From [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need):
424
+ From [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need): **Vibes → Guardrails → Architecture as Code → The Organism**.
425
+
426
+ <details>
427
+ <summary>What each level means</summary>
400
428
 
401
429
  | Level | Name | What it means |
402
430
  | ----- | -------------------- | ------------------------------------------------------------------- |
@@ -405,20 +433,16 @@ From [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-al
405
433
  | 2 | Architecture as Code | Custom lint rules + enforced CLAUDE.md |
406
434
  | 3 | The Organism | CI + custom rules + visual tests + observability + scheduled agents |
407
435
 
408
- ## Output Targets
409
-
410
- Specs compile to `CLAUDE.md` by default. Set `target: "AGENTS.md"` or `target: ["CLAUDE.md", "AGENTS.md"]` for multiple outputs from one spec. For non-markdown formats (`.cursorrules`, Copilot), use [rule-porter](https://github.com/nichochar/rule-porter) or [rulesync](https://github.com/dyoshikawa/rulesync) to convert. [Spec format →](docs/spec-format.md)
436
+ </details>
411
437
 
412
438
  ## Related Tools
413
439
 
414
- vigiles doesn't try to do everything. It owns one thing: compile-time verification of typed specs against real linter configs, filesystems, and package.json. Everything else, compose:
440
+ vigiles owns one thing: compile-time verification of typed specs against real linter configs, filesystems, and package.json, plus testing the harness those specs describe. Everything else it composes with rather than replaces — architectural linters ([ast-grep](https://ast-grep.github.io/), [Dependency Cruiser](https://github.com/sverweij/dependency-cruiser)) referenced via `enforce()`, file-sync tools ([Ruler](https://github.com/intellectronica/ruler), [rulesync](https://github.com/dyoshikawa/rulesync)) that distribute the compiled output, and markdown/prose linters that check a different layer. [How vigiles composes with each, and why runtime-LLM rule checkers are the opposite paradigm →](docs/related-tools.md)
441
+
442
+ ## Documentation
415
443
 
416
- - **Architectural linting** — [ast-grep](https://ast-grep.github.io/), [Dependency Cruiser](https://github.com/sverweij/dependency-cruiser), [Steiger](https://github.com/feature-sliced/steiger). Reference their rules via `enforce()`.
417
- - **File sync** across agents — [Ruler](https://github.com/intellectronica/ruler), [rulesync](https://github.com/dyoshikawa/rulesync), [block/ai-rules](https://github.com/block/ai-rules). vigiles compiles the source; sync tools distribute.
418
- - **Markdown linting** — [markdownlint](https://github.com/DavidAnson/markdownlint). vigiles generates markdown; structure is correct by construction.
419
- - **Code-block linting in docs** — [eslint-plugin-markdown](https://github.com/eslint/eslint-plugin-markdown) for syntax, [twoslash](https://shikijs.github.io/twoslash/) for TS type-checking.
420
- - **Prose quality** — [Vale](https://vale.sh). Different concern.
421
- - **Runtime LLM rule checking** (e.g. ai-rulez `"AI-Powered Rule Enforcement"`) — opposite paradigm. Those tools send your code to a model on every check, costing tokens and giving non-reproducible verdicts. vigiles compiles once and checks deterministically forever after with `eslint`, `ruff`, `tsc`, Cedar evaluation — tools as deterministic as their inputs.
444
+ - **[docs/](docs/README.md)** how-to & reference: the adoption ladder, CLI, linter support, the harness-testing guide, skills/agents.
445
+ - **[research/](research/README.md)** — the thinking behind it: design docs, the [harness-testing coverage roadmap](research/harness-testing-coverage-matrix.md), benchmark findings, landscape, and parked ideas.
422
446
 
423
447
  ## License
424
448
 
package/dist/cli.js CHANGED
@@ -24,8 +24,11 @@ const generate_schema_js_1 = require("./generate-schema.js");
24
24
  const compile_generator_js_1 = require("./compile-generator.js");
25
25
  const action_gate_js_1 = require("./action-gate.js");
26
26
  const refs_js_1 = require("./refs.js");
27
+ const mcp_js_1 = require("./mcp.js");
27
28
  const skill_runtime_js_1 = require("./skill-runtime.js");
28
29
  const linters_js_1 = require("./linters.js");
30
+ const harness_test_js_1 = require("./harness-test.js");
31
+ const run_scripts_js_1 = require("./run-scripts.js");
29
32
  const integrity_js_1 = require("./integrity.js");
30
33
  const coverage_js_1 = require("./coverage.js");
31
34
  const orphans_js_1 = require("./orphans.js");
@@ -431,6 +434,46 @@ function verifyMarkdownSymbols(files, silent) {
431
434
  }
432
435
  return errors;
433
436
  }
437
+ /**
438
+ * Verify `vigiles:mcp server#tool` marks in instruction files against the live
439
+ * MCP servers declared in `.mcp.json` — the referenced tool must exist on the
440
+ * server (it gets started for the check). No `.mcp.json` ⇒ skipped; a server is
441
+ * only started if a mark actually references it. Returns the count of broken
442
+ * references. Async because it speaks to real servers.
443
+ */
444
+ async function verifyMarkdownMcpRefs(files, silent) {
445
+ const cwd = process.cwd();
446
+ const servers = (0, mcp_js_1.loadMcpServers)(cwd);
447
+ if (files.length === 0 || Object.keys(servers).length === 0)
448
+ return 0;
449
+ let printedHeader = false;
450
+ let errors = 0;
451
+ for (const f of files) {
452
+ let markdown;
453
+ try {
454
+ markdown = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd, f), "utf-8");
455
+ }
456
+ catch {
457
+ continue;
458
+ }
459
+ const broken = await (0, mcp_js_1.verifyMcpRefs)(markdown, servers);
460
+ if (broken.length === 0)
461
+ continue;
462
+ if (!silent) {
463
+ if (!printedHeader) {
464
+ console.log("\nMCP reference check:\n");
465
+ printedHeader = true;
466
+ }
467
+ for (const b of broken) {
468
+ const msg = (0, mcp_js_1.mcpRefMessage)(b);
469
+ console.log(` ✗ ${f}:${String(b.line)} ${msg}`);
470
+ ghAnnotate("error", msg, f, b.line);
471
+ }
472
+ }
473
+ errors += broken.length;
474
+ }
475
+ return errors;
476
+ }
434
477
  /** Exit codes: 0 clean, 1 warnings only, 2 hard errors. */
435
478
  function auditExitCode(report) {
436
479
  if (report.hashErrors > 0 ||
@@ -439,7 +482,8 @@ function auditExitCode(report) {
439
482
  report.frontmatterErrors > 0 ||
440
483
  report.integrityErrors > 0 ||
441
484
  report.coverageErrors > 0 ||
442
- report.symbolRefErrors > 0)
485
+ report.symbolRefErrors > 0 ||
486
+ report.mcpRefErrors > 0)
443
487
  return 2;
444
488
  if (report.duplicatePairs > 0 ||
445
489
  report.orphanCount > 0 ||
@@ -728,6 +772,9 @@ async function audit(restArgs, flags, config) {
728
772
  }
729
773
  // 9. Verify code-shaped symbol references live (see src/refs.ts).
730
774
  const symbolRefErrors = verifyMarkdownSymbols(files, silent);
775
+ // 10. Verify `vigiles:mcp server#tool` marks against live MCP servers
776
+ // (only when a .mcp.json declares them). See src/mcp.ts.
777
+ const mcpRefErrors = await verifyMarkdownMcpRefs(files, silent);
731
778
  const report = {
732
779
  hashErrors: hashResult.hashErrors,
733
780
  validationErrors: hashResult.validationErrors,
@@ -744,6 +791,7 @@ async function audit(restArgs, flags, config) {
744
791
  orphanCount: orphanReport.orphans.length,
745
792
  docRefErrors: docRefReport.errors.length,
746
793
  symbolRefErrors,
794
+ mcpRefErrors,
747
795
  files,
748
796
  };
749
797
  if (summary) {
@@ -773,6 +821,8 @@ function printAuditSummary(report) {
773
821
  parts.push(`${String(report.docRefErrors)} broken doc refs`);
774
822
  if (report.symbolRefErrors > 0)
775
823
  parts.push(`${String(report.symbolRefErrors)} broken symbol refs`);
824
+ if (report.mcpRefErrors > 0)
825
+ parts.push(`${String(report.mcpRefErrors)} broken MCP refs`);
776
826
  const undocumented = report.coverageEnabled - report.coverageDocumented;
777
827
  if (undocumented > 0)
778
828
  parts.push(`${String(undocumented)} undocumented rules`);
@@ -1413,6 +1463,38 @@ function handleGenerateSchema(args, restArgs) {
1413
1463
  console.log(" Add to your markdown frontmatter:\n" +
1414
1464
  ` # yaml-language-server: $schema=./${outPath}`);
1415
1465
  }
1466
+ /**
1467
+ * `vigiles test` / `vigiles eval` — discover and run the two-tier harness
1468
+ * scripts (deterministic `*.harness.mjs` / real-model `*.eval.mjs`) as child
1469
+ * `node` processes, aggregating exit codes so they work as a CI command. See
1470
+ * src/run-scripts.ts.
1471
+ *
1472
+ * `vigiles test` skips clean when the `claude` CLI is absent (the deterministic
1473
+ * tier needs it, just like the node:test suite). `--trials=N` is forwarded to
1474
+ * eval scripts via the `VIGILES_TRIALS` env var.
1475
+ */
1476
+ function handleRunScripts(kind, args, restArgs) {
1477
+ const cwd = process.cwd();
1478
+ const defaultGlob = kind === "test" ? "**/*.harness.mjs" : "**/*.eval.mjs";
1479
+ if (kind === "test" && !(0, harness_test_js_1.claudeAvailable)()) {
1480
+ console.log("vigiles test: `claude` CLI not found — skipping harness tests.");
1481
+ return;
1482
+ }
1483
+ const files = (0, run_scripts_js_1.discoverScripts)(restArgs, defaultGlob, cwd);
1484
+ if (files.length === 0) {
1485
+ console.log(`No ${defaultGlob} files found.`);
1486
+ return;
1487
+ }
1488
+ const trialsFlag = args.find((a) => a.startsWith("--trials="));
1489
+ const env = {};
1490
+ if (trialsFlag)
1491
+ env.VIGILES_TRIALS = trialsFlag.split("=")[1];
1492
+ console.log(`Running ${String(files.length)} ${kind} file(s):\n`);
1493
+ const results = (0, run_scripts_js_1.runScripts)(files, cwd, env);
1494
+ console.log("\n" + (0, run_scripts_js_1.formatScriptSummary)(results));
1495
+ if (results.some((r) => r.code !== 0))
1496
+ process.exit(1);
1497
+ }
1416
1498
  function printUsage(command) {
1417
1499
  console.log("vigiles — compile typed specs to instruction files");
1418
1500
  console.log("");
@@ -1420,6 +1502,8 @@ function printUsage(command) {
1420
1502
  console.log(" vigiles init [flags] Setup project (--target=X.md, --strict, --no-gha)");
1421
1503
  console.log(" vigiles compile [files...] Compile .spec.ts → .md");
1422
1504
  console.log(" vigiles audit [files...] Verify, find gaps, suggest improvements");
1505
+ console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
1506
+ console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N)");
1423
1507
  console.log("");
1424
1508
  console.log("Examples:");
1425
1509
  console.log(" vigiles init Auto-detect project, create specs, wire CI");
@@ -1725,6 +1809,12 @@ async function main() {
1725
1809
  }
1726
1810
  break;
1727
1811
  }
1812
+ case "test":
1813
+ handleRunScripts("test", args, restArgs);
1814
+ break;
1815
+ case "eval":
1816
+ handleRunScripts("eval", args, restArgs);
1817
+ break;
1728
1818
  // --- Plumbing ---
1729
1819
  case "generate-types":
1730
1820
  handleGenerateTypes(args, restArgs);
package/dist/eval.d.ts CHANGED
@@ -4,6 +4,20 @@ export interface EvalArm {
4
4
  readonly files?: Record<string, string>;
5
5
  /** `.claude/settings.json` (hooks/permissions) for this arm; omit for none. */
6
6
  readonly settings?: unknown;
7
+ /**
8
+ * Path to a real plugin/repo to load for this arm (hooks + CLAUDE.md +
9
+ * skills). Lets an arm be "the whole plugin on" vs "off". See
10
+ * src/plugin-loader.ts.
11
+ */
12
+ readonly plugin?: string;
13
+ /**
14
+ * Path to a plugin dir to install NATIVELY (`claude --plugin-dir`) for this
15
+ * arm, so its skills/commands/agents activate the real way — the real model
16
+ * can trigger a skill by its description (vs. `plugin`, which materializes a
17
+ * file subset that does not register skills). Point at a COMPLETE plugin. Lets
18
+ * an arm be "skill installed" vs "off" to measure real activation.
19
+ */
20
+ readonly pluginDir?: string;
7
21
  }
8
22
  /** Context handed to `measure` after a run, to compute that run's metrics. */
9
23
  export interface RunContext {
@@ -39,10 +53,23 @@ export interface EvalSpec<M extends Metrics> {
39
53
  /** Seconds to wait between runs (avoid rate-limit bursts). Default 4. */
40
54
  readonly spacingSec?: number;
41
55
  }
56
+ /** Per-metric summary statistics across an arm's runs. */
57
+ export interface MetricStat {
58
+ /** Mean (numbers) / fraction-true (booleans). */
59
+ readonly mean: number;
60
+ /** Sample standard deviation (0 when n < 2). */
61
+ readonly std: number;
62
+ /** Standard error of the mean (std / √n). */
63
+ readonly se: number;
64
+ /** Number of runs the metric was observed in. */
65
+ readonly n: number;
66
+ }
42
67
  export interface ArmReport {
43
68
  readonly runs: number;
44
69
  /** Aggregated metrics: mean for numbers, fraction-true (0..1) for booleans. */
45
70
  readonly metrics: Record<string, number>;
71
+ /** Per-metric mean / std / se / n, so an A/B gap can be read for significance. */
72
+ readonly stats: Record<string, MetricStat>;
46
73
  }
47
74
  export interface EvalReport {
48
75
  readonly name: string;
@@ -51,12 +78,18 @@ export interface EvalReport {
51
78
  }
52
79
  /** Aggregate per-run metrics: mean for numbers, fraction-true (0..1) for booleans. */
53
80
  export declare function aggregate(rows: readonly Metrics[]): Record<string, number>;
81
+ /**
82
+ * Aggregate per-run metrics with spread: mean, sample std, standard error, and
83
+ * n. The se/std let you judge whether an A/B gap between arms is real or noise —
84
+ * a difference smaller than the combined se is not yet significant.
85
+ */
86
+ export declare function aggregateStats(rows: readonly Metrics[]): Record<string, MetricStat>;
54
87
  /**
55
88
  * Run the eval: every arm × every trial against the real `claude` CLI, with the
56
89
  * metric computed per run and aggregated per arm. Requires `claude` on PATH and
57
90
  * working model auth (e.g. `ANTHROPIC_API_KEY`).
58
91
  */
59
92
  export declare function runEval<M extends Metrics>(spec: EvalSpec<M>): Promise<EvalReport>;
60
- /** Format an eval report as a compact table for the console. */
93
+ /** Format an eval report as a compact table for the console (mean ± se). */
61
94
  export declare function formatEvalReport(report: EvalReport): string;
62
95
  //# sourceMappingURL=eval.d.ts.map