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 +176 -152
- package/dist/cli.js +91 -1
- package/dist/eval.d.ts +34 -1
- package/dist/eval.js +55 -20
- package/dist/harness-assert.d.ts +109 -0
- package/dist/harness-assert.js +220 -0
- package/dist/harness-test.d.ts +46 -0
- package/dist/harness-test.js +81 -9
- 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/mcp.d.ts +48 -0
- package/dist/mcp.js +247 -0
- package/dist/plugin-loader.d.ts +37 -0
- package/dist/plugin-loader.js +195 -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/vitest.d.mts +9 -0
- package/dist/vitest.mjs +22 -0
- package/package.json +37 -5
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>
|
|
@@ -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.)
|
|
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
|
-
|
|
241
|
+
There's a small family of inline **marks** that `audit` checks, each binding a reference to its real source:
|
|
233
242
|
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
+
## Test your Claude Code harness
|
|
239
249
|
|
|
240
|
-
|
|
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
|
-
|
|
255
|
+
### Level 1 — test a hook by itself (no AI, milliseconds)
|
|
243
256
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285
|
+
### Level 2 — does it fire in a real session? (free, scripted "AI")
|
|
255
286
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
313
|
+
### Level 3 — does it change what Claude does? (real AI, occasional)
|
|
268
314
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
332
|
+
### Test your skills for real — and assert on what Claude _did_
|
|
277
333
|
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
387
|
+
✅ shipped · 🟡 partial · 🔴 gap · — n/a. Full detail + roadmap: [`research/harness-testing-coverage-matrix.md`](research/harness-testing-coverage-matrix.md).
|
|
298
388
|
|
|
299
|
-
|
|
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
|
-
|
|
391
|
+
## CLI & CI
|
|
307
392
|
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
- **
|
|
417
|
-
- **
|
|
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
|