vigiles 2.4.0 → 2.6.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 +72 -327
- 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/claude-code.d.ts +10 -0
- package/dist/claude-code.js +26 -0
- package/dist/cli.js +106 -0
- package/dist/compile.d.ts +32 -3
- package/dist/compile.js +268 -0
- package/dist/egress-proxy.d.ts +2 -0
- package/dist/egress-proxy.js +60 -0
- package/dist/eval-baseline.d.ts +68 -0
- package/dist/eval-baseline.js +173 -0
- package/dist/eval-cache.d.ts +33 -0
- package/dist/eval-cache.js +94 -0
- package/dist/eval.d.ts +172 -9
- package/dist/eval.js +319 -58
- package/dist/harness-assert.d.ts +175 -13
- package/dist/harness-assert.js +358 -25
- package/dist/harness-test.d.ts +97 -11
- package/dist/harness-test.js +147 -37
- package/dist/judge.js +2 -0
- package/dist/linters.d.ts +6 -0
- package/dist/linters.js +1 -0
- package/dist/linting.d.ts +9 -0
- package/dist/linting.js +25 -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/run-hook.d.ts +81 -1
- package/dist/run-hook.js +189 -11
- package/dist/sandbox.d.ts +107 -0
- package/dist/sandbox.js +307 -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/dist/testing.d.ts +12 -0
- package/dist/testing.js +28 -0
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<strong>
|
|
13
|
-
vigiles <strong>
|
|
12
|
+
<strong>The missing linting + testing layer for agentic coding.</strong><br />
|
|
13
|
+
vigiles <strong>lints</strong> the references your instruction files make — linter rules, file paths, scripts, code symbols — and <strong>tests</strong> whether your hooks, skills, and CLAUDE.md actually change what the agent does.
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
@@ -19,30 +19,39 @@
|
|
|
19
19
|
<a href="https://github.com/zernie/vigiles/blob/main/LICENSE"><img src="https://img.shields.io/github/license/zernie/vigiles" alt="License" /></a>
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
|
+
<p align="center">
|
|
23
|
+
<b>Two pillars →</b>
|
|
24
|
+
<a href="#verify-your-instruction-files">① Verify your instruction files</a>
|
|
25
|
+
·
|
|
26
|
+
<a href="#test-your-claude-code-harness">② Test your Claude Code harness</a>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
22
29
|
---
|
|
23
30
|
|
|
24
31
|
<details>
|
|
25
32
|
<summary><b>Contents</b></summary>
|
|
26
33
|
|
|
27
|
-
**
|
|
34
|
+
- [**Two pillars — pick one or both**](#two-pillars--pick-one-or-both)
|
|
35
|
+
- **Pillar 1** — [verify your instruction files](#verify-your-instruction-files) · full guide: [docs/verifying-instruction-files.md](docs/verifying-instruction-files.md)
|
|
36
|
+
- **Pillar 2** — [test your Claude Code harness](#test-your-claude-code-harness) · full guide: [docs/harness-testing.md](docs/harness-testing.md)
|
|
37
|
+
- [Quick start](#quick-start) · [CLI & CI](#cli--ci) · [Skills](#skills) · [Related tools](#related-tools)
|
|
28
38
|
|
|
29
|
-
|
|
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`
|
|
39
|
+
</details>
|
|
34
40
|
|
|
35
|
-
|
|
41
|
+
## Two pillars — pick one or both
|
|
36
42
|
|
|
37
|
-
|
|
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)
|
|
43
|
+
An agent runs real commands in your repo — it can delete the wrong files, leak a secret, or burn tokens looping on a stale instruction nobody checked. You'd never ship an app without a linter and a test suite. An AI agent steering your codebase is no different — so why is its harness trusted on vibes?
|
|
42
44
|
|
|
43
|
-
**
|
|
45
|
+
`Agent = Model + Harness`. Your harness is everything that steers a run — the **instructions** you write _and_ the **hooks, skills, and settings** that enforce them. vigiles is the **missing linting + testing layer for agentic coding**: it **lints** your instruction files and **tests** your harness. Two pillars of equal weight — adopt either on its own, or both:
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
| | Pillar | What it does |
|
|
48
|
+
| ----- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
49
|
+
| **①** | [**Verify your instruction files**](#verify-your-instruction-files) | Every linter rule, file path, script, and code symbol your CLAUDE.md cites is checked against reality, so stale references can't silently mislead the agent. |
|
|
50
|
+
| **②** | [**Test your harness**](#test-your-claude-code-harness) | Your hooks and skills are code — vigiles tests that they actually fire, **deterministically and for free** (no model, no API key) before you ever pay for a real-model eval. |
|
|
51
|
+
|
|
52
|
+
They share the thesis but not a dependency: verify your instructions without ever writing a harness test, or test your harness without a single `.spec.ts`. Pick the pillar that hurts today.
|
|
53
|
+
|
|
54
|
+
## Verify your instruction files
|
|
46
55
|
|
|
47
56
|
Your CLAUDE.md lies to your agent. Here's the fix.
|
|
48
57
|
|
|
@@ -70,13 +79,11 @@ Reads fine. Four things are wrong:
|
|
|
70
79
|
|
|
71
80
|
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.
|
|
72
81
|
|
|
73
|
-
> **
|
|
82
|
+
> **Set it up — paste this into Claude Code:**
|
|
83
|
+
>
|
|
84
|
+
> > Install vigiles and verify my instruction files. Scan my CLAUDE.md / AGENTS.md, turn its real claims into checked references (linter rules, file paths, scripts, symbols), run `vigiles audit`, and show me what's already stale. Use sensible defaults, but **ask me first** whether to stay in markdown mode or generate a typed `.spec.ts`, and whether to wire the audit into CI and install the edit-blocking hooks.
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
### Level 0 — inline comments (30 seconds, no new files)
|
|
78
|
-
|
|
79
|
-
Add a comment to your existing CLAUDE.md and audit it:
|
|
86
|
+
Or do it by hand — add a marker to your existing CLAUDE.md and audit it, no install, no new files:
|
|
80
87
|
|
|
81
88
|
```md
|
|
82
89
|
<!-- vigiles:enforce eslint/no-console "Route output through logger.ts" -->
|
|
@@ -86,181 +93,46 @@ Add a comment to your existing CLAUDE.md and audit it:
|
|
|
86
93
|
npx vigiles audit CLAUDE.md
|
|
87
94
|
```
|
|
88
95
|
|
|
89
|
-
Each
|
|
90
|
-
|
|
91
|
-
### Level 1 — YAML frontmatter (editor autocomplete, still no TypeScript)
|
|
92
|
-
|
|
93
|
-
Promote your rules into a `vigiles:` block at the top of the file:
|
|
94
|
-
|
|
95
|
-
```yaml
|
|
96
|
-
---
|
|
97
|
-
# yaml-language-server: $schema=./.vigiles/schema.json
|
|
98
|
-
vigiles:
|
|
99
|
-
enforce:
|
|
100
|
-
- rule: "@typescript-eslint/no-explicit-any"
|
|
101
|
-
why: "Use unknown and narrow with type guards."
|
|
102
|
-
- rule: eslint/no-console
|
|
103
|
-
why: "Route output through logger.ts"
|
|
104
|
-
---
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
`npx vigiles generate-schema` emits a JSON Schema from your project's _actual_ enabled rules, so your editor's built-in YAML language server (VS Code, JetBrains, neovim) autocompletes rule names and red-squiggles typos — at edit time, with no TypeScript in the project. `vigiles audit` enforces the same rules in CI. [Markdown mode →](docs/markdown-mode.md)
|
|
108
|
-
|
|
109
|
-
### Level 2 — typed spec (compiler-grade guarantees)
|
|
110
|
-
|
|
111
|
-
When you want the strongest guarantees, compile a typed spec. Every linter rule reference is verified against your real config, every file path against the filesystem, every npm script against package.json. Stale references become compile errors — caught at edit time, not when the agent silently ignores you.
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
// CLAUDE.md.spec.ts
|
|
115
|
-
import { claude, enforce, guidance } from "vigiles/spec";
|
|
116
|
-
|
|
117
|
-
export default claude({
|
|
118
|
-
commands: {
|
|
119
|
-
"npm run build": "Compile TypeScript to dist/",
|
|
120
|
-
"npm test": "Build and run all tests",
|
|
121
|
-
// ✗ "npm run typecheck" → compile error: script not in package.json
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
keyFiles: {
|
|
125
|
-
"src/utils/narrowing.ts": "Type guard utilities",
|
|
126
|
-
// ✗ "src/utils/type-helpers.ts" → compile error: file not found
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
rules: {
|
|
130
|
-
"no-explicit-any": enforce(
|
|
131
|
-
"@typescript-eslint/no-explicit-any",
|
|
132
|
-
"Use unknown and narrow with type guards.",
|
|
133
|
-
),
|
|
134
|
-
// ✗ if rule is disabled in config → compile error
|
|
135
|
-
|
|
136
|
-
"research-first": guidance("Google unfamiliar APIs first."),
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
$ npx vigiles compile
|
|
143
|
-
|
|
144
|
-
✓ CLAUDE.md.spec.ts → CLAUDE.md
|
|
145
|
-
2 rules (1 linter-verified, 1 guidance)
|
|
146
|
-
~180 tokens
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
At this level the spec is the source of truth and CLAUDE.md is a build artifact. The agent edits the spec — hooks auto-compile, types catch typos in the editor, CI catches drift.
|
|
150
|
-
|
|
151
|
-
Companion repo for [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need).
|
|
96
|
+
Each reference is checked against reality — a typo gets a closest-match suggestion, a disabled rule is flagged. That's **markdown mode**; step up to a **typed spec** (`.spec.ts` → compiled CLAUDE.md, compiler-grade guarantees) when you want it.
|
|
152
97
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
### Claude Code
|
|
156
|
-
|
|
157
|
-
| | Without vigiles | With vigiles |
|
|
158
|
-
| ----------------------------------- | ---------------------------- | -------------------------------------------------------------- |
|
|
159
|
-
| **Instructions** | Hand-written CLAUDE.md | Compiled from `.spec.ts` (build artifact) |
|
|
160
|
-
| **Linter rule references** | Trust-based (nobody checks) | Verified at compile time against real config |
|
|
161
|
-
| **File paths** | Rot silently when renamed | `file()` references checked against filesystem |
|
|
162
|
-
| **Commands** | Stale scripts go unnoticed | `cmd()` references checked against package.json |
|
|
163
|
-
| **Direct edits to CLAUDE.md** | Anyone can, nobody knows | PreToolUse hook blocks edits, redirects to spec |
|
|
164
|
-
| **Spec edits** | N/A | PostToolUse hook auto-compiles to markdown |
|
|
165
|
-
| **Linter config changes** | CLAUDE.md drifts out of sync | PostToolUse hook auto-regenerates types |
|
|
166
|
-
| **guidance → enforce upgrades** | Manual guesswork | `/strengthen` reads per-linter docs, suggests upgrades |
|
|
167
|
-
| **New lint rules from PR feedback** | Copy-paste from review | `/pr-to-lint-rule` generates rule + tests + spec entry |
|
|
168
|
-
| **CI** | Nothing to verify | `vigiles audit` catches hand-edits, disabled rules, stale refs |
|
|
169
|
-
|
|
170
|
-
<details>
|
|
171
|
-
<summary><b>Codex</b> (same compile-time checks, no hooks)</summary>
|
|
172
|
-
|
|
173
|
-
| | Without vigiles | With vigiles |
|
|
174
|
-
| ----------------------------- | -------------------------------- | ------------------------------------------------------- |
|
|
175
|
-
| **Instructions** | Hand-written AGENTS.md | Compiled from `.spec.ts` |
|
|
176
|
-
| **Linter rule references** | Trust-based | Verified at compile time |
|
|
177
|
-
| **File paths / commands** | Rot silently | Checked at compile time |
|
|
178
|
-
| **Direct edits to AGENTS.md** | Undetected | CI catches hash mismatch |
|
|
179
|
-
| **Hooks / auto-compile** | Not available (no plugin system) | Not available — run `vigiles compile` manually or in CI |
|
|
180
|
-
| **CI** | Nothing to verify | Same `vigiles audit` pipeline as Claude |
|
|
181
|
-
|
|
182
|
-
</details>
|
|
183
|
-
|
|
184
|
-
Everything vigiles compiles and audits is **deterministic** — same input, same output, no LLM in the loop. The non-deterministic parts (authoring specs, suggesting upgrades, writing custom rules) are agent skills that run outside the compilation pipeline. [Determinism breakdown and flow diagram →](docs/comparison.md)
|
|
98
|
+
→ **Full guide: [docs/verifying-instruction-files.md](docs/verifying-instruction-files.md)** — the adoption ladder, the three rule types (`enforce` / `guidance` / `guard`), verified references (`file` / `cmd` / `symbol` / `ref`), and the before/after tables.
|
|
185
99
|
|
|
186
100
|
## Quick Start
|
|
187
101
|
|
|
188
|
-
The fastest path is markdown mode — add a marker to your existing CLAUDE.md and audit it, no install or new files (see [Level 0 / Level 1](#level-0--inline-comments-30-seconds-no-new-files) above and [docs/markdown-mode.md](docs/markdown-mode.md)). When you want compiler-grade guarantees, scaffold a typed spec:
|
|
189
|
-
|
|
190
102
|
```bash
|
|
191
103
|
npx vigiles init
|
|
192
104
|
```
|
|
193
105
|
|
|
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.
|
|
195
|
-
|
|
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).
|
|
197
|
-
|
|
198
|
-
## Three Rule Types
|
|
199
|
-
|
|
200
|
-
**`enforce()`** — delegated to a linter. vigiles verifies the rule exists in the catalog AND is enabled in your project config. A disabled rule is a compile error.
|
|
201
|
-
|
|
202
|
-
<!-- vigiles:ignore -->
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
"no-any": enforce("@typescript-eslint/no-explicit-any", "Use unknown and narrow."),
|
|
206
|
-
"no-print": enforce("ruff/T201", "Use logging module."),
|
|
207
|
-
"no-unwrap": enforce("clippy/unwrap_used", "Use expect() with context."),
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Supports ESLint, Stylelint, Ruff, Clippy, Pylint, RuboCop, and Cedar policies. [Full linter support details →](docs/linter-support.md)
|
|
211
|
-
|
|
212
|
-
**`guidance()`** — prose advice. No mechanical enforcement, but not untracked: guidance rules participate in the monotonicity proof system. Once a rule exists, it can be strengthened ( `guidance` → `enforce` ) but never weakened or removed without an explicit allowlist. This prevents silent erosion of conventions over time.
|
|
213
|
-
|
|
214
|
-
```typescript
|
|
215
|
-
"research-first": guidance("Google unfamiliar APIs first."),
|
|
216
|
-
```
|
|
217
|
-
|
|
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)
|
|
219
|
-
|
|
220
|
-
## Verified References
|
|
221
|
-
|
|
222
|
-
`file()`, `cmd()`, `symbol()`, and `ref()` catch stale references at compile time:
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
import { claude, file, cmd, symbol, ref, instructions } from "vigiles/spec";
|
|
226
|
-
|
|
227
|
-
export default claude({
|
|
228
|
-
sections: {
|
|
229
|
-
architecture: instructions`
|
|
230
|
-
Core engine in ${file("src/compile.ts")}.
|
|
231
|
-
Compile specs with ${symbol("src/compile.ts", "compileClaude")}.
|
|
232
|
-
Run ${cmd("npm test")} to verify.
|
|
233
|
-
See ${ref("skills/strengthen/SKILL.md")} for the strengthen skill.
|
|
234
|
-
`,
|
|
235
|
-
// If any path / script / symbol is stale → compile error
|
|
236
|
-
},
|
|
237
|
-
// ...
|
|
238
|
-
});
|
|
239
|
-
```
|
|
106
|
+
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. Prefer no new files? Stay in [markdown mode](docs/markdown-mode.md). Start with `guidance()` rules and `/strengthen` them to `enforce()` later; flags and agent usage are in the [CLI reference](docs/cli.md).
|
|
240
107
|
|
|
241
|
-
|
|
242
|
-
|
|
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.
|
|
245
|
-
|
|
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)
|
|
108
|
+
Companion repo for [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need).
|
|
247
109
|
|
|
248
110
|
## Test your Claude Code harness
|
|
249
111
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
112
|
+
Verifying references proves your instructions are _true_ — but the hooks and
|
|
113
|
+
skills that enforce them still have to **actually fire**. A hook can be wired
|
|
114
|
+
wrong, a skill's description can fail to trigger, injected context can never reach
|
|
115
|
+
the model — all silently, all passing a naive "did it run?" check. So vigiles's
|
|
116
|
+
second pillar **tests the harness itself**, as the assembled machine it ships as.
|
|
254
117
|
|
|
255
|
-
|
|
118
|
+
> **Set it up — paste this into Claude Code:**
|
|
119
|
+
>
|
|
120
|
+
> > Install vigiles and use its `test-harness` skill to write and run a harness test for this project. If I didn't say what to test, pick something real from my hooks / skills / settings, choose the cheapest tier (unit / deterministic / eval), write the test, and run it. Use good defaults, but **ask me** whether to gate it in CI and whether to add a real-model eval.
|
|
256
121
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
122
|
+
It's a small library of plain async functions (drops into node:test / vitest /
|
|
123
|
+
jest, or a zero-setup `vigiles test`), with three tiers, cheapest first. The
|
|
124
|
+
design bet is **deterministic and cheap**: the first two tiers never call a model
|
|
125
|
+
or need an API key, so they run on every commit for free — the opposite of
|
|
126
|
+
eval-only frameworks like promptfoo, where every run hits a real model **by
|
|
127
|
+
design**. You only reach for the paid real-model tier when the question genuinely
|
|
128
|
+
needs it.
|
|
129
|
+
|
|
130
|
+
- **Unit-test a hook** — `runHook` hands a hook a fake event and checks block/allow. No `claude`, no model, milliseconds, reaches **every** event type.
|
|
131
|
+
- **Deterministic harness test** — `runHarnessTest` runs the **real** `claude` against a **scripted mock model**, so your hooks fire for real with no API key and the same result every time.
|
|
132
|
+
- **Eval** — `runEval` runs the real model A/B (change on vs off) and reports the gap as **mean ± se**, with a Welch-t-test [significance gate](docs/harness-testing.md#significance--is-the-gap-real), regression baselines, and cost/latency/token tracking.
|
|
261
133
|
|
|
262
134
|
```typescript
|
|
263
|
-
import { runHook } from "vigiles/
|
|
135
|
+
import { runHook } from "vigiles/testing";
|
|
264
136
|
|
|
265
137
|
const r = runHook(guardCommand, {
|
|
266
138
|
hook_event_name: "PreToolUse",
|
|
@@ -270,123 +142,10 @@ const r = runHook(guardCommand, {
|
|
|
270
142
|
assert(r.blocked); // exit 2 / decision:"block" / permissionDecision:"deny"
|
|
271
143
|
```
|
|
272
144
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
```
|
|
284
|
-
|
|
285
|
-
### Level 2 — does it fire in a real session? (free, scripted "AI")
|
|
286
|
-
|
|
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
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
### Level 3 — does it change what Claude does? (real AI, occasional)
|
|
314
|
-
|
|
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
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
### Test your skills for real — and assert on what Claude _did_
|
|
333
|
-
|
|
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
|
|
352
|
-
```
|
|
353
|
-
|
|
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.
|
|
358
|
-
|
|
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.
|
|
367
|
-
|
|
368
|
-
```bash
|
|
369
|
-
npx vigiles test examples/harness/policy-gate.harness.mjs
|
|
370
|
-
npx vigiles eval --trials=6 examples/harness/skill-outcome.eval.mjs
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
### What's covered today — surface × tier
|
|
374
|
-
|
|
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 | ✅ |
|
|
386
|
-
|
|
387
|
-
✅ shipped · 🟡 partial · 🔴 gap · — n/a. Full detail + roadmap: [`research/harness-testing-coverage-matrix.md`](research/harness-testing-coverage-matrix.md).
|
|
388
|
-
|
|
389
|
-
[Full guide → `docs/harness-testing.md`](docs/harness-testing.md) · [benchmarks](research/benchmarks-runtime-gates.md).
|
|
145
|
+
**[Full guide → `docs/harness-testing.md`](docs/harness-testing.md)** — the tier
|
|
146
|
+
walkthrough, testing skills for real, "fired ≠ landed" (`trace.modelRequests`),
|
|
147
|
+
the safe-by-default sandbox for untrusted plugins, the surface × tier coverage
|
|
148
|
+
matrix, and how it compares to promptfoo. Also: [benchmarks](research/benchmarks-runtime-gates.md).
|
|
390
149
|
|
|
391
150
|
## CLI & CI
|
|
392
151
|
|
|
@@ -405,43 +164,29 @@ npx vigiles eval # Run *.eval.mjs real-model harness evals (--trials=N)
|
|
|
405
164
|
Install with [Vercel Skills](https://github.com/vercel-labs/skills): `npx skills add zernie/vigiles`
|
|
406
165
|
|
|
407
166
|
<details>
|
|
408
|
-
<summary><b>The
|
|
409
|
-
|
|
410
|
-
| Skill | What it does
|
|
411
|
-
| ---------------------- |
|
|
412
|
-
| `strengthen` | Upgrade `guidance()` → `enforce()` using linter-specific reference docs
|
|
413
|
-
| `edit-spec` | Edit a spec file — guided workflow with compile step
|
|
414
|
-
| `migrate-to-spec` | Convert a hand-written CLAUDE.md to a typed `.spec.ts`
|
|
415
|
-
| `generate-rule` | Add a new `enforce()` / `guidance()` rule to a spec
|
|
416
|
-
| `pr-to-lint-rule` | Turn a recurring PR review comment into a lint rule + spec entry
|
|
417
|
-
| `enforce-rules-format` | Validate all rules have enforcement classification
|
|
418
|
-
| `audit-feedback-loop` | Score your repo's feedback loop maturity
|
|
419
|
-
|
|
420
|
-
</details>
|
|
421
|
-
|
|
422
|
-
## Maturity Levels
|
|
423
|
-
|
|
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>
|
|
428
|
-
|
|
429
|
-
| Level | Name | What it means |
|
|
430
|
-
| ----- | -------------------- | ------------------------------------------------------------------- |
|
|
431
|
-
| 0 | Vibes | No CI, no linters, no CLAUDE.md |
|
|
432
|
-
| 1 | Guardrails | CI + standard linters, no custom rules |
|
|
433
|
-
| 2 | Architecture as Code | Custom lint rules + enforced CLAUDE.md |
|
|
434
|
-
| 3 | The Organism | CI + custom rules + visual tests + observability + scheduled agents |
|
|
167
|
+
<summary><b>The 8 skills</b></summary>
|
|
168
|
+
|
|
169
|
+
| Skill | What it does |
|
|
170
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------- |
|
|
171
|
+
| `strengthen` | Upgrade `guidance()` → `enforce()` using linter-specific reference docs |
|
|
172
|
+
| `edit-spec` | Edit a spec file — guided workflow with compile step |
|
|
173
|
+
| `migrate-to-spec` | Convert a hand-written CLAUDE.md to a typed `.spec.ts` |
|
|
174
|
+
| `generate-rule` | Add a new `enforce()` / `guidance()` rule to a spec |
|
|
175
|
+
| `pr-to-lint-rule` | Turn a recurring PR review comment into a lint rule + spec entry |
|
|
176
|
+
| `enforce-rules-format` | Validate all rules have enforcement classification |
|
|
177
|
+
| `audit-feedback-loop` | Score your repo's feedback loop maturity |
|
|
178
|
+
| `test-harness` | Test a Claude Code harness — pick the tier (unit / deterministic / eval) and write a passing test |
|
|
435
179
|
|
|
436
180
|
</details>
|
|
437
181
|
|
|
438
182
|
## Related Tools
|
|
439
183
|
|
|
440
|
-
vigiles
|
|
184
|
+
vigiles composes with other tools rather than replacing them: architectural linters ([ast-grep](https://ast-grep.github.io/), [Dependency Cruiser](https://github.com/sverweij/dependency-cruiser)) referenced via `enforce()`, and file-sync tools ([Ruler](https://github.com/intellectronica/ruler), [rulesync](https://github.com/dyoshikawa/rulesync)) that distribute the compiled output. [How it fits with each, and why runtime-LLM rule checkers are the opposite paradigm →](docs/related-tools.md)
|
|
441
185
|
|
|
442
186
|
## Documentation
|
|
443
187
|
|
|
444
|
-
- **[
|
|
188
|
+
- **The two pillar guides:** [verifying instruction files](docs/verifying-instruction-files.md) (Pillar 1) · [testing your harness](docs/harness-testing.md) (Pillar 2).
|
|
189
|
+
- **[docs/](docs/README.md)** — the full how-to & reference index: adoption ladder, CLI, linter support, skills/agents.
|
|
445
190
|
- **[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.
|
|
446
191
|
|
|
447
192
|
## License
|
|
@@ -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
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vigiles — parse a subagent's railway result.
|
|
4
|
+
*
|
|
5
|
+
* A subagent with a `result()` contract is told (in its compiled system prompt)
|
|
6
|
+
* to end its turn with exactly one fenced block:
|
|
7
|
+
*
|
|
8
|
+
* ```vigiles:ok
|
|
9
|
+
* { "files": ["a.ts"], "summary": "done" }
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* or `vigiles:err` for the error track. This module extracts and validates that
|
|
13
|
+
* block — the single primitive the railway orchestrator and the harness-test
|
|
14
|
+
* assertions (`assertAgentOk`/`assertAgentErr`) both build on. Pure and
|
|
15
|
+
* model-free: hand it the worker's text, get back a discriminated outcome.
|
|
16
|
+
*
|
|
17
|
+
* "Railway-oriented" is literal here: the parse is `text -> Result<S, E>` with a
|
|
18
|
+
* third `malformed` track for a worker that didn't honor its contract (no block,
|
|
19
|
+
* bad JSON, or a shape that doesn't match the declared schema).
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.parseAgentResult = parseAgentResult;
|
|
23
|
+
// Capture every vigiles:ok / vigiles:err fenced block; the LAST one is the
|
|
24
|
+
// worker's final answer (earlier ones may be illustrative in its reasoning).
|
|
25
|
+
const BLOCK_RE = /```vigiles:(ok|err)[ \t]*\r?\n([\s\S]*?)```/g;
|
|
26
|
+
/** Does a runtime value match a declared field type? */
|
|
27
|
+
function fieldMatches(value, type) {
|
|
28
|
+
switch (type) {
|
|
29
|
+
case "string":
|
|
30
|
+
return typeof value === "string";
|
|
31
|
+
case "number":
|
|
32
|
+
return typeof value === "number";
|
|
33
|
+
case "boolean":
|
|
34
|
+
return typeof value === "boolean";
|
|
35
|
+
case "string[]":
|
|
36
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Validate a parsed object against a contract track; null when it conforms. */
|
|
40
|
+
function shapeError(obj, shape) {
|
|
41
|
+
for (const [field, type] of Object.entries(shape)) {
|
|
42
|
+
if (!(field in obj))
|
|
43
|
+
return `missing field "${field}"`;
|
|
44
|
+
if (!fieldMatches(obj[field], type)) {
|
|
45
|
+
return `field "${field}" should be ${type}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse the last `vigiles:ok` / `vigiles:err` block from a worker's output.
|
|
52
|
+
*
|
|
53
|
+
* With a `contract`, the parsed object is validated against the matching track's
|
|
54
|
+
* shape — a worker that emits the wrong shape is `malformed`, not a silent pass.
|
|
55
|
+
* Without one, any well-formed JSON block is accepted.
|
|
56
|
+
*/
|
|
57
|
+
function parseAgentResult(text, contract) {
|
|
58
|
+
BLOCK_RE.lastIndex = 0;
|
|
59
|
+
let last = null;
|
|
60
|
+
for (let m = BLOCK_RE.exec(text); m !== null; m = BLOCK_RE.exec(text)) {
|
|
61
|
+
last = { track: m[1], body: m[2] };
|
|
62
|
+
}
|
|
63
|
+
if (!last) {
|
|
64
|
+
return {
|
|
65
|
+
kind: "malformed",
|
|
66
|
+
reason: "no vigiles:ok/vigiles:err block found",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(last.body);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return {
|
|
75
|
+
kind: "malformed",
|
|
76
|
+
reason: `invalid JSON in vigiles:${last.track} block`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
80
|
+
return {
|
|
81
|
+
kind: "malformed",
|
|
82
|
+
reason: `vigiles:${last.track} block must be a JSON object`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const obj = parsed;
|
|
86
|
+
if (contract) {
|
|
87
|
+
const shape = last.track === "ok" ? contract.ok : contract.err;
|
|
88
|
+
const err = shapeError(obj, shape);
|
|
89
|
+
if (err) {
|
|
90
|
+
return { kind: "malformed", reason: `${last.track} block: ${err}` };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return last.track === "ok"
|
|
94
|
+
? { kind: "ok", value: obj }
|
|
95
|
+
: { kind: "err", error: obj };
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=agent-result.js.map
|