vigiles 2.3.0 → 2.5.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 +214 -190
- package/dist/agent-result.d.ts +40 -0
- package/dist/agent-result.js +97 -0
- package/dist/agent-runtime.d.ts +64 -0
- package/dist/agent-runtime.js +147 -0
- package/dist/cli.js +155 -1
- package/dist/compile.d.ts +32 -3
- package/dist/compile.js +268 -0
- package/dist/eval-cache.d.ts +33 -0
- package/dist/eval-cache.js +94 -0
- package/dist/eval.d.ts +180 -9
- package/dist/eval.js +319 -57
- package/dist/harness-assert.d.ts +175 -6
- package/dist/harness-assert.js +355 -4
- package/dist/harness-test.d.ts +130 -5
- package/dist/harness-test.js +205 -32
- package/dist/judge.js +2 -0
- package/dist/linters.d.ts +6 -0
- package/dist/linters.js +1 -0
- package/dist/mcp.d.ts +48 -0
- package/dist/mcp.js +247 -0
- package/dist/mock-entry.d.ts +2 -0
- package/dist/mock-entry.js +36 -0
- package/dist/mock-model.d.ts +29 -0
- package/dist/mock-model.js +40 -0
- package/dist/plugin-loader.js +51 -17
- package/dist/sandbox.d.ts +76 -0
- package/dist/sandbox.js +241 -0
- package/dist/spec.d.ts +130 -0
- package/dist/spec.js +55 -0
- package/dist/stats.d.ts +49 -0
- package/dist/stats.js +109 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -21,6 +21,29 @@
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
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
|
+
|
|
24
47
|
Your CLAUDE.md lies to your agent. Here's the fix.
|
|
25
48
|
|
|
26
49
|
Hand-written CLAUDE.md files rot silently. Here's what a typical one looks like:
|
|
@@ -47,6 +70,8 @@ Reads fine. Four things are wrong:
|
|
|
47
70
|
|
|
48
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.
|
|
49
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
|
+
|
|
50
75
|
Three levels. Each is independently useful; adopt as far up as you like.
|
|
51
76
|
|
|
52
77
|
### Level 0 — inline comments (30 seconds, no new files)
|
|
@@ -168,15 +193,7 @@ npx vigiles init
|
|
|
168
193
|
|
|
169
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.
|
|
170
195
|
|
|
171
|
-
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.
|
|
172
|
-
|
|
173
|
-
| Flag | Effect |
|
|
174
|
-
| -------------------- | ----------------------------------------------------- |
|
|
175
|
-
| `--strict` | Sets require-spec and require-skill-spec to `"error"` |
|
|
176
|
-
| `--target=AGENTS.md` | Creates AGENTS.md spec instead of CLAUDE.md |
|
|
177
|
-
| `--no-gha` | Skip adding CI step to GHA workflow |
|
|
178
|
-
|
|
179
|
-
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).
|
|
180
197
|
|
|
181
198
|
## Three Rule Types
|
|
182
199
|
|
|
@@ -198,20 +215,7 @@ Supports ESLint, Stylelint, Ruff, Clippy, Pylint, RuboCop, and Cedar policies. [
|
|
|
198
215
|
"research-first": guidance("Google unfamiliar APIs first."),
|
|
199
216
|
```
|
|
200
217
|
|
|
201
|
-
**`guard()`** — reactive: runs a command when watched files change. One declaration emits hooks for every supported system (Claude Code PostToolUse, husky pre-commit, etc.)
|
|
202
|
-
|
|
203
|
-
```typescript
|
|
204
|
-
"recompile-specs": guard(
|
|
205
|
-
{ watch: "*.spec.ts", run: "npx vigiles compile" },
|
|
206
|
-
"Recompile instruction files when any spec changes.",
|
|
207
|
-
),
|
|
208
|
-
"regen-types": guard(
|
|
209
|
-
{ watch: ["eslint.config.*", "package.json"], run: "npx vigiles generate-types" },
|
|
210
|
-
"Regenerate types when linter config or deps change.",
|
|
211
|
-
),
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
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)
|
|
215
219
|
|
|
216
220
|
## Verified References
|
|
217
221
|
|
|
@@ -234,220 +238,244 @@ export default claude({
|
|
|
234
238
|
});
|
|
235
239
|
```
|
|
236
240
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
### Symbol references (cross-language)
|
|
241
|
+
There's a small family of inline **marks** that `audit` checks, each binding a reference to its real source:
|
|
240
242
|
|
|
241
|
-
|
|
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.
|
|
242
245
|
|
|
243
|
-
|
|
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)
|
|
244
247
|
|
|
245
|
-
##
|
|
246
|
-
|
|
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.
|
|
248
|
-
|
|
249
|
-
```bash
|
|
250
|
-
$ npx vigiles generate-types
|
|
251
|
-
eslint: 64 enabled rules | ruff: 12 | npm scripts: 5 | project files: 42
|
|
252
|
-
✓ Generated .vigiles/generated.d.ts
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
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)
|
|
256
|
-
|
|
257
|
-
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`.
|
|
248
|
+
## Test your Claude Code harness
|
|
258
249
|
|
|
259
|
-
|
|
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`.
|
|
260
254
|
|
|
261
|
-
|
|
262
|
-
npx vigiles init [--target=X.md] # Scaffold a spec (runs full setup wizard by default)
|
|
263
|
-
npx vigiles compile [files...] # Compile .spec.ts → .md
|
|
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)
|
|
268
|
-
npx vigiles generate-types # Emit .d.ts from project state (for spec mode)
|
|
269
|
-
npx vigiles generate-types --check # Verify .d.ts is up to date
|
|
270
|
-
npx vigiles generate-schema # Emit JSON Schema for vigiles: frontmatter (Level 1)
|
|
271
|
-
npx vigiles generate-schema --check # Verify schema.json is up to date
|
|
272
|
-
```
|
|
255
|
+
### Level 1 — test a hook by itself (no AI, milliseconds)
|
|
273
256
|
|
|
274
|
-
|
|
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):
|
|
275
261
|
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
- uses: zernie/vigiles@main
|
|
279
|
-
with:
|
|
280
|
-
command: compile # compile specs in CI
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
To verify generated types are fresh in CI:
|
|
262
|
+
```typescript
|
|
263
|
+
import { runHook } from "vigiles/run-hook";
|
|
284
264
|
|
|
285
|
-
|
|
286
|
-
|
|
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"
|
|
287
271
|
```
|
|
288
272
|
|
|
289
|
-
|
|
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_:
|
|
290
275
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
295
283
|
```
|
|
296
284
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
- **PreToolUse** (Edit/Write) — blocks direct edits to compiled `.md` files and redirects the agent to the `.spec.ts` source
|
|
300
|
-
- **PostToolUse** (Edit/Write) — auto-runs `generate-types` on linter config changes, `compile` on `.spec.ts` changes
|
|
301
|
-
|
|
302
|
-
## Validation
|
|
285
|
+
### Level 2 — does it fire in a real session? (free, scripted "AI")
|
|
303
286
|
|
|
304
|
-
|
|
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.
|
|
305
292
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
| [`require-spec`](docs/rules/require-spec.md) | `"warn"` | Every CLAUDE.md/AGENTS.md has a spec, inline rule, or `vigiles:` frontmatter |
|
|
309
|
-
| [`require-skill-spec`](docs/rules/require-skill-spec.md) | `"warn"` | Every SKILL.md has a `.spec.ts` |
|
|
310
|
-
| [`integrity`](docs/rules/integrity.md) | `"warn"` | Compiled markdown wasn't hand-edited (SHA-256 check) |
|
|
311
|
-
| [`coverage`](docs/rules/coverage.md) | `false` | Spec covers enough of the project surface |
|
|
312
|
-
|
|
313
|
-
Configure in `.vigilesrc.json`:
|
|
293
|
+
```typescript
|
|
294
|
+
import { runHarnessTest, scriptModel } from "vigiles/harness-test";
|
|
314
295
|
|
|
315
|
-
|
|
316
|
-
{
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
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
|
|
323
311
|
```
|
|
324
312
|
|
|
325
|
-
|
|
313
|
+
### Level 3 — does it change what Claude does? (real AI, occasional)
|
|
326
314
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
| Skill | What it does |
|
|
332
|
-
| ---------------------- | ----------------------------------------------------------------------- |
|
|
333
|
-
| `strengthen` | Upgrade `guidance()` → `enforce()` using linter-specific reference docs |
|
|
334
|
-
| `edit-spec` | Edit a spec file — guided workflow with compile step |
|
|
335
|
-
| `migrate-to-spec` | Convert a hand-written CLAUDE.md to a typed `.spec.ts` |
|
|
336
|
-
| `generate-rule` | Add a new `enforce()` / `guidance()` rule to a spec |
|
|
337
|
-
| `pr-to-lint-rule` | Turn a recurring PR review comment into a lint rule + spec entry |
|
|
338
|
-
| `enforce-rules-format` | Validate all rules have enforcement classification |
|
|
339
|
-
| `audit-feedback-loop` | Score your repo's feedback loop maturity |
|
|
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.
|
|
315
|
+
`runEval` runs the **real** model N times with your change **on vs off** and
|
|
316
|
+
reports the gap as **mean ± se** — so you can tell signal from noise instead of
|
|
317
|
+
eyeballing two averages:
|
|
350
318
|
|
|
351
319
|
```typescript
|
|
352
320
|
import { runEval, formatEvalReport } from "vigiles/eval";
|
|
353
321
|
|
|
354
322
|
const report = await runEval({
|
|
355
|
-
|
|
356
|
-
arms: {
|
|
357
|
-
vanilla: {},
|
|
358
|
-
gated: { settings: { hooks: { PostToolUse: [refsHook] } } },
|
|
359
|
-
},
|
|
323
|
+
arms: { off: {}, on: { settings: { hooks: { PostToolUse: [refsHook] } } } },
|
|
360
324
|
task: "Document chargeCard in SKILL.md, referencing it by name.",
|
|
361
325
|
measure: (ctx) => ({
|
|
362
326
|
marked: ctx.sh("grep -c vigiles:symbol SKILL.md") !== "0",
|
|
363
327
|
}),
|
|
364
328
|
trials: 6,
|
|
329
|
+
cache: "readwrite", // replay past runs — editing `measure` re-scores for free
|
|
365
330
|
});
|
|
366
|
-
console.log(formatEvalReport(report));
|
|
331
|
+
console.log(formatEvalReport(report));
|
|
332
|
+
// off marked=0.00 on marked=0.50±0.20 pass^k=0 ($0.07 · 1.2s/run · 4.1k tok)
|
|
367
333
|
```
|
|
368
334
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
335
|
+
`assertSignificant(report, { baseline: "off", arm: "on", metric: "marked" })`
|
|
336
|
+
turns the gap into a CI gate — a Welch t-test decides whether it cleared the
|
|
337
|
+
noise floor, **computed** from the arms' spread, not hand-fed. Runs go
|
|
338
|
+
**concurrently**, track **cost / latency / tokens** (cap them with `maxCostUsd`),
|
|
339
|
+
and the **record/replay cache** makes re-scoring after a `measure` edit free.
|
|
340
|
+
|
|
341
|
+
Same tier, different question: **`measureTriggerRate`** measures how reliably a
|
|
342
|
+
skill's _description fires_ across varied prompts — the #1 skill-authoring pain.
|
|
343
|
+
|
|
344
|
+
### Test your skills for real — and assert on what Claude _did_
|
|
345
|
+
|
|
346
|
+
Install a plugin the way Claude actually does (`pluginDir` → `--plugin-dir`) so
|
|
347
|
+
its **skills genuinely activate**, then assert on the agent's _actions_, not a
|
|
348
|
+
stdout grep:
|
|
372
349
|
|
|
373
350
|
```typescript
|
|
374
|
-
import {
|
|
351
|
+
import { assertSkillResolved, assertToolNotUsed } from "vigiles/harness-assert";
|
|
375
352
|
|
|
376
353
|
const r = await runHarnessTest({
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
{
|
|
381
|
-
hooks: [
|
|
382
|
-
{
|
|
383
|
-
type: "command",
|
|
384
|
-
command: "test -f DONE || { echo 'not done' >&2; exit 2; }",
|
|
385
|
-
},
|
|
386
|
-
],
|
|
387
|
-
},
|
|
388
|
-
],
|
|
389
|
-
},
|
|
390
|
-
},
|
|
354
|
+
pluginDir: "./my-plugin",
|
|
355
|
+
transcript: true, // populate r.toolCalls
|
|
356
|
+
allowedTools: ["Read", "Write", "Bash", "Skill"],
|
|
391
357
|
model: scriptModel([
|
|
392
|
-
{
|
|
393
|
-
{
|
|
394
|
-
{ text: "now done" },
|
|
358
|
+
{ tool: "Skill", input: { skill: "my-plugin:greet" } },
|
|
359
|
+
{ text: "ok" },
|
|
395
360
|
]),
|
|
396
361
|
});
|
|
397
|
-
|
|
362
|
+
assertSkillResolved(r, "my-plugin:greet"); // the skill fired, no error
|
|
363
|
+
assertToolNotUsed(r, /^mcp__github__merge/); // the safety negative: the scary tool was never called
|
|
398
364
|
```
|
|
399
365
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
366
|
+
`assertToolNotUsed` is how you test a safety rule **honestly** — _proving_ the
|
|
367
|
+
dangerous tool was never used, which "the file looks unchanged" can't. It works
|
|
368
|
+
on **real third-party plugins** too: the suite confirms real `obra/superpowers`
|
|
369
|
+
and `wshobson/agents` skills resolve this way, with no markers injected.
|
|
404
370
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
371
|
+
### Did the injected context actually reach the model?
|
|
372
|
+
|
|
373
|
+
A SessionStart hook or a slash command can _fire_ and still inject **nothing** —
|
|
374
|
+
wrong output shape, wrong platform. `trace.modelRequests` records what the model
|
|
375
|
+
actually received (system + messages), so you assert it landed, not just that the
|
|
376
|
+
hook ran — **"fired ≠ landed"**:
|
|
409
377
|
|
|
410
378
|
```typescript
|
|
411
|
-
import {
|
|
379
|
+
import { assertRequestContains } from "vigiles/harness-assert";
|
|
412
380
|
|
|
413
|
-
|
|
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"
|
|
381
|
+
assertRequestContains(r, "You have superpowers"); // the additionalContext reached the model
|
|
419
382
|
```
|
|
420
383
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
384
|
+
(Dogfood: this is exactly how vigiles found that real `obra/superpowers` emits a
|
|
385
|
+
_top-level_ `additionalContext`, which Claude Code — reading the _nested_ form —
|
|
386
|
+
never injects. The hook fired; the context never landed.)
|
|
387
|
+
|
|
388
|
+
### Running an untrusted plugin? It's confined by default
|
|
389
|
+
|
|
390
|
+
Testing a third-party plugin means executing **its** hooks. `runHarnessTest` is
|
|
391
|
+
safe by default: code you wrote (inline `settings`/`files`) runs directly, but an
|
|
392
|
+
external `plugin` / `pluginDir` is **confined under bubblewrap** — a network
|
|
393
|
+
namespace with **no egress** (a malicious hook can't phone home), a read-only
|
|
394
|
+
filesystem, and a **cleared environment** (your `ANTHROPIC_API_KEY` and other
|
|
395
|
+
secrets aren't even visible). If no sandbox is available the run **refuses**
|
|
396
|
+
rather than executing unconfined:
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
runHarnessTest({ pluginDir: "./vendor/some-plugin", model }); // confined, or refuses
|
|
400
|
+
runHarnessTest({ pluginDir: "./audited", model, sandbox: false }); // you vouch for it → direct
|
|
401
|
+
```
|
|
424
402
|
|
|
425
|
-
-
|
|
426
|
-
|
|
403
|
+
Confinement is **Linux-only** (bubblewrap); on macOS / Windows an untrusted run
|
|
404
|
+
refuses unless you pass `sandbox: false`. The suite dogfoods it on real
|
|
405
|
+
`obra/superpowers` — its `SessionStart` hook runs in a no-egress sandbox, and the
|
|
406
|
+
test proves egress is blocked while the scripted mock stays reachable.
|
|
407
|
+
|
|
408
|
+
### Run them in CI
|
|
409
|
+
|
|
410
|
+
`vigiles test` runs `*.harness.mjs` files (free, no key); `vigiles eval` runs
|
|
411
|
+
`*.eval.mjs` files (real model). Point a test at a whole plugin (or `"./"` for
|
|
412
|
+
your repo) to load **what ships** — hooks (with `${CLAUDE_PLUGIN_ROOT}`
|
|
413
|
+
resolved), CLAUDE.md, skills, subagents, commands — and `loadPlugin().warnings`
|
|
414
|
+
flags anything only a real model can drive, so you never silently test an empty
|
|
415
|
+
machine.
|
|
427
416
|
|
|
428
417
|
```bash
|
|
429
418
|
npx vigiles test examples/harness/policy-gate.harness.mjs
|
|
430
419
|
npx vigiles eval --trials=6 examples/harness/skill-outcome.eval.mjs
|
|
431
420
|
```
|
|
432
421
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
422
|
+
### What's covered today — surface × tier
|
|
423
|
+
|
|
424
|
+
| Surface | Unit / static | Integration (no API key) | Eval (real model) |
|
|
425
|
+
| ------------------------------------------------------------- | ---------------------------- | --------------------------- | ----------------- |
|
|
426
|
+
| Hooks — Bash / SessionStart / Stop / UserPromptSubmit | ✅ logic | ✅ fires | ✅ |
|
|
427
|
+
| Hooks — Edit / Write | ✅ logic | ✅ fires | ✅ |
|
|
428
|
+
| Hooks — PreCompact / Notification / SessionEnd / SubagentStop | ✅ logic | — (mock can't trigger) | 🟡 |
|
|
429
|
+
| CLAUDE.md / instructions | ✅ refs | 🟡 present, not behaviour | ✅ behaviour |
|
|
430
|
+
| Skills | 🟡 refs | ✅ resolves via `pluginDir` | ✅ activation |
|
|
431
|
+
| Subagents (`agents/`) | ✅ tool rail · 🟡 refs | 🟡 rail not live-armed | ✅ via Task |
|
|
432
|
+
| Slash commands (`commands/`) | 🟡 refs | 🟡 needs prompt capture | ✅ via `/cmd` |
|
|
433
|
+
| MCP servers | ✅ tool refs (`vigiles:mcp`) | 🔴 | 🔴 |
|
|
434
|
+
| settings.json | 🟡 assert merged | ✅ applied | ✅ |
|
|
435
|
+
| Hook context injection (does it _land_?) | — n/a | ✅ `trace.modelRequests` | ✅ |
|
|
436
|
+
| Untrusted plugin execution | — n/a | ✅ confined (bwrap, Linux) | 🟡 outer sandbox |
|
|
437
|
+
|
|
438
|
+
✅ shipped · 🟡 partial · 🔴 gap · — n/a. Full detail + roadmap: [`research/harness-testing-coverage-matrix.md`](research/harness-testing-coverage-matrix.md).
|
|
439
|
+
|
|
440
|
+
[Full guide → `docs/harness-testing.md`](docs/harness-testing.md) · [benchmarks](research/benchmarks-runtime-gates.md).
|
|
441
|
+
|
|
442
|
+
## CLI & CI
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
npx vigiles init # Scaffold a spec (full setup wizard)
|
|
446
|
+
npx vigiles compile # Compile .spec.ts → .md
|
|
447
|
+
npx vigiles audit # Verify hashes + inline/frontmatter/spec rules + symbols + coverage
|
|
448
|
+
npx vigiles test # Run *.harness.mjs deterministic harness tests (no API key)
|
|
449
|
+
npx vigiles eval # Run *.eval.mjs real-model harness evals (--trials=N)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
`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)
|
|
453
|
+
|
|
454
|
+
## Skills
|
|
455
|
+
|
|
456
|
+
Install with [Vercel Skills](https://github.com/vercel-labs/skills): `npx skills add zernie/vigiles`
|
|
457
|
+
|
|
458
|
+
<details>
|
|
459
|
+
<summary><b>The 7 skills</b></summary>
|
|
460
|
+
|
|
461
|
+
| Skill | What it does |
|
|
462
|
+
| ---------------------- | ----------------------------------------------------------------------- |
|
|
463
|
+
| `strengthen` | Upgrade `guidance()` → `enforce()` using linter-specific reference docs |
|
|
464
|
+
| `edit-spec` | Edit a spec file — guided workflow with compile step |
|
|
465
|
+
| `migrate-to-spec` | Convert a hand-written CLAUDE.md to a typed `.spec.ts` |
|
|
466
|
+
| `generate-rule` | Add a new `enforce()` / `guidance()` rule to a spec |
|
|
467
|
+
| `pr-to-lint-rule` | Turn a recurring PR review comment into a lint rule + spec entry |
|
|
468
|
+
| `enforce-rules-format` | Validate all rules have enforcement classification |
|
|
469
|
+
| `audit-feedback-loop` | Score your repo's feedback loop maturity |
|
|
470
|
+
|
|
471
|
+
</details>
|
|
447
472
|
|
|
448
473
|
## Maturity Levels
|
|
449
474
|
|
|
450
|
-
From [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need):
|
|
475
|
+
From [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need): **Vibes → Guardrails → Architecture as Code → The Organism**.
|
|
476
|
+
|
|
477
|
+
<details>
|
|
478
|
+
<summary>What each level means</summary>
|
|
451
479
|
|
|
452
480
|
| Level | Name | What it means |
|
|
453
481
|
| ----- | -------------------- | ------------------------------------------------------------------- |
|
|
@@ -456,20 +484,16 @@ From [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-al
|
|
|
456
484
|
| 2 | Architecture as Code | Custom lint rules + enforced CLAUDE.md |
|
|
457
485
|
| 3 | The Organism | CI + custom rules + visual tests + observability + scheduled agents |
|
|
458
486
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
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)
|
|
487
|
+
</details>
|
|
462
488
|
|
|
463
489
|
## Related Tools
|
|
464
490
|
|
|
465
|
-
vigiles
|
|
491
|
+
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)
|
|
492
|
+
|
|
493
|
+
## Documentation
|
|
466
494
|
|
|
467
|
-
- **
|
|
468
|
-
- **
|
|
469
|
-
- **Markdown linting** — [markdownlint](https://github.com/DavidAnson/markdownlint). vigiles generates markdown; structure is correct by construction.
|
|
470
|
-
- **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.
|
|
471
|
-
- **Prose quality** — [Vale](https://vale.sh). Different concern.
|
|
472
|
-
- **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.
|
|
495
|
+
- **[docs/](docs/README.md)** — how-to & reference: the adoption ladder, CLI, linter support, the harness-testing guide, skills/agents.
|
|
496
|
+
- **[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.
|
|
473
497
|
|
|
474
498
|
## License
|
|
475
499
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vigiles — parse a subagent's railway result.
|
|
3
|
+
*
|
|
4
|
+
* A subagent with a `result()` contract is told (in its compiled system prompt)
|
|
5
|
+
* to end its turn with exactly one fenced block:
|
|
6
|
+
*
|
|
7
|
+
* ```vigiles:ok
|
|
8
|
+
* { "files": ["a.ts"], "summary": "done" }
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* or `vigiles:err` for the error track. This module extracts and validates that
|
|
12
|
+
* block — the single primitive the railway orchestrator and the harness-test
|
|
13
|
+
* assertions (`assertAgentOk`/`assertAgentErr`) both build on. Pure and
|
|
14
|
+
* model-free: hand it the worker's text, get back a discriminated outcome.
|
|
15
|
+
*
|
|
16
|
+
* "Railway-oriented" is literal here: the parse is `text -> Result<S, E>` with a
|
|
17
|
+
* third `malformed` track for a worker that didn't honor its contract (no block,
|
|
18
|
+
* bad JSON, or a shape that doesn't match the declared schema).
|
|
19
|
+
*/
|
|
20
|
+
import type { OutputContract } from "./spec.js";
|
|
21
|
+
/** The outcome of parsing a worker's result block. */
|
|
22
|
+
export type ParsedAgentResult<S = Record<string, unknown>, E = Record<string, unknown>> = {
|
|
23
|
+
readonly kind: "ok";
|
|
24
|
+
readonly value: S;
|
|
25
|
+
} | {
|
|
26
|
+
readonly kind: "err";
|
|
27
|
+
readonly error: E;
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: "malformed";
|
|
30
|
+
readonly reason: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Parse the last `vigiles:ok` / `vigiles:err` block from a worker's output.
|
|
34
|
+
*
|
|
35
|
+
* With a `contract`, the parsed object is validated against the matching track's
|
|
36
|
+
* shape — a worker that emits the wrong shape is `malformed`, not a silent pass.
|
|
37
|
+
* Without one, any well-formed JSON block is accepted.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseAgentResult(text: string, contract?: OutputContract): ParsedAgentResult;
|
|
40
|
+
//# sourceMappingURL=agent-result.d.ts.map
|