vigiles 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +182 -134
  2. package/dist/action-gate.d.ts +28 -0
  3. package/dist/action-gate.js +73 -0
  4. package/dist/cli.js +705 -230
  5. package/dist/community-skills.d.ts +22 -0
  6. package/dist/community-skills.js +86 -0
  7. package/dist/compile-generator.d.ts +48 -0
  8. package/dist/compile-generator.js +322 -0
  9. package/dist/compile.d.ts +4 -0
  10. package/dist/compile.js +239 -45
  11. package/dist/coverage.d.ts +56 -0
  12. package/dist/coverage.js +178 -0
  13. package/dist/doc-refs.d.ts +60 -0
  14. package/dist/doc-refs.js +246 -0
  15. package/dist/eval.d.ts +62 -0
  16. package/dist/eval.js +174 -0
  17. package/dist/evolve.js +60 -125
  18. package/dist/frontmatter.d.ts +75 -0
  19. package/dist/frontmatter.js +263 -0
  20. package/dist/generate-schema.d.ts +51 -0
  21. package/dist/generate-schema.js +125 -0
  22. package/dist/generate-types.js +39 -1
  23. package/dist/harness-test.d.ts +38 -0
  24. package/dist/harness-test.js +129 -0
  25. package/dist/hash.d.ts +8 -0
  26. package/dist/hash.js +16 -0
  27. package/dist/inline.d.ts +22 -4
  28. package/dist/inline.js +60 -13
  29. package/dist/integrity.d.ts +29 -0
  30. package/dist/integrity.js +44 -0
  31. package/dist/linters.d.ts +5 -3
  32. package/dist/linters.js +144 -4
  33. package/dist/mock-model.d.ts +31 -0
  34. package/dist/mock-model.js +189 -0
  35. package/dist/orphans.d.ts +52 -0
  36. package/dist/orphans.js +124 -0
  37. package/dist/proofs.js +22 -16
  38. package/dist/refs.d.ts +44 -0
  39. package/dist/refs.js +144 -0
  40. package/dist/session.d.ts +97 -0
  41. package/dist/session.js +306 -0
  42. package/dist/sidecar.d.ts +35 -0
  43. package/dist/sidecar.js +102 -0
  44. package/dist/skill-driver.d.ts +77 -0
  45. package/dist/skill-driver.js +76 -0
  46. package/dist/skill-runtime.d.ts +101 -0
  47. package/dist/skill-runtime.js +289 -0
  48. package/dist/skill-test.d.ts +47 -0
  49. package/dist/skill-test.js +77 -0
  50. package/dist/spec.d.ts +119 -13
  51. package/dist/spec.js +51 -4
  52. package/dist/symbols.d.ts +30 -0
  53. package/dist/symbols.js +142 -0
  54. package/dist/test-utils.d.ts +8 -0
  55. package/dist/test-utils.js +41 -0
  56. package/dist/types.d.ts +34 -8
  57. package/dist/types.js +19 -0
  58. package/dist/validate.js +5 -3
  59. package/package.json +26 -5
  60. package/.claude/settings.json +0 -46
  61. package/.claude/settings.local.json +0 -8
  62. package/.github/workflows/ci.yml +0 -81
  63. package/.prettierignore +0 -1
  64. package/.vigiles/generated.d.ts +0 -205
  65. package/CLAUDE.md +0 -95
  66. package/CLAUDE.md.spec.ts +0 -142
  67. package/CONTRIBUTING.md +0 -121
  68. package/dist/action.d.ts.map +0 -1
  69. package/dist/action.js.map +0 -1
  70. package/dist/cli.d.ts.map +0 -1
  71. package/dist/cli.js.map +0 -1
  72. package/dist/cli.test.d.ts +0 -2
  73. package/dist/cli.test.d.ts.map +0 -1
  74. package/dist/cli.test.js +0 -650
  75. package/dist/cli.test.js.map +0 -1
  76. package/dist/compile.d.ts.map +0 -1
  77. package/dist/compile.js.map +0 -1
  78. package/dist/evolve.d.ts.map +0 -1
  79. package/dist/evolve.js.map +0 -1
  80. package/dist/freshness.d.ts +0 -67
  81. package/dist/freshness.d.ts.map +0 -1
  82. package/dist/freshness.js +0 -244
  83. package/dist/freshness.js.map +0 -1
  84. package/dist/freshness.test.d.ts +0 -2
  85. package/dist/freshness.test.d.ts.map +0 -1
  86. package/dist/freshness.test.js +0 -356
  87. package/dist/freshness.test.js.map +0 -1
  88. package/dist/generate-types.d.ts.map +0 -1
  89. package/dist/generate-types.js.map +0 -1
  90. package/dist/inline.d.ts.map +0 -1
  91. package/dist/inline.js.map +0 -1
  92. package/dist/inline.test.d.ts +0 -5
  93. package/dist/inline.test.d.ts.map +0 -1
  94. package/dist/inline.test.js +0 -152
  95. package/dist/inline.test.js.map +0 -1
  96. package/dist/linters.d.ts.map +0 -1
  97. package/dist/linters.js.map +0 -1
  98. package/dist/proofs.d.ts.map +0 -1
  99. package/dist/proofs.js.map +0 -1
  100. package/dist/proofs.test.d.ts +0 -9
  101. package/dist/proofs.test.d.ts.map +0 -1
  102. package/dist/proofs.test.js +0 -952
  103. package/dist/proofs.test.js.map +0 -1
  104. package/dist/spec.d.ts.map +0 -1
  105. package/dist/spec.js.map +0 -1
  106. package/dist/spec.test.d.ts +0 -2
  107. package/dist/spec.test.d.ts.map +0 -1
  108. package/dist/spec.test.js +0 -1222
  109. package/dist/spec.test.js.map +0 -1
  110. package/dist/types.d.ts.map +0 -1
  111. package/dist/types.js.map +0 -1
  112. package/dist/validate.d.ts.map +0 -1
  113. package/dist/validate.js.map +0 -1
  114. package/dist/validate.test.d.ts +0 -2
  115. package/dist/validate.test.d.ts.map +0 -1
  116. package/dist/validate.test.js +0 -531
  117. package/dist/validate.test.js.map +0 -1
  118. package/docs/agent-setup.md +0 -85
  119. package/docs/agent-workflows.md +0 -103
  120. package/docs/comparison.md +0 -71
  121. package/docs/freshness.md +0 -124
  122. package/docs/inline-mode.md +0 -119
  123. package/docs/linter-support.md +0 -166
  124. package/docs/spec-format.md +0 -194
  125. package/eslint.config.mjs +0 -79
  126. package/examples/CLAUDE.md +0 -54
  127. package/examples/CLAUDE.md.spec.ts +0 -65
  128. package/examples/SKILL.md +0 -50
  129. package/examples/SKILL.md.spec.ts +0 -57
  130. package/fixtures/example-project/CLAUDE.md +0 -11
  131. package/fixtures/example-project/package.json +0 -9
  132. package/fixtures/example-project/src/index.ts +0 -3
  133. package/fixtures/example-project/src/utils.test.ts +0 -2
  134. package/fixtures/example-project/src/utils.ts +0 -3
  135. package/logo.png +0 -0
  136. package/research/adoption-strategy.md +0 -111
  137. package/research/agent-integration.md +0 -145
  138. package/research/ai-code-quality.md +0 -197
  139. package/research/code-search-for-agents.md +0 -313
  140. package/research/competitive-landscape.md +0 -163
  141. package/research/doc-freshness.md +0 -516
  142. package/research/executable-specs.md +0 -368
  143. package/research/feature-ideas.md +0 -464
  144. package/research/formal-proofs-for-agents.md +0 -338
  145. package/research/fp-for-agent-harness.md +0 -150
  146. package/research/fp-for-deterministic-ai.md +0 -131
  147. package/research/self-evolving-specs.md +0 -298
  148. package/schemas/claude-md-strict.yml +0 -18
  149. package/schemas/claude-md.yml +0 -6
  150. package/schemas/skill-strict.yml +0 -12
  151. package/schemas/skill.yml +0 -5
  152. package/skills/audit-feedback-loop/SKILL.md +0 -76
  153. package/skills/edit-spec/SKILL.md +0 -131
  154. package/skills/enforce-rules-format/SKILL.md +0 -71
  155. package/skills/generate-logo/SKILL.md +0 -102
  156. package/skills/generate-rule/SKILL.md +0 -90
  157. package/skills/linter-docs/clippy.md +0 -241
  158. package/skills/linter-docs/eslint.md +0 -384
  159. package/skills/linter-docs/pylint.md +0 -288
  160. package/skills/linter-docs/rubocop.md +0 -277
  161. package/skills/linter-docs/ruff.md +0 -187
  162. package/skills/linter-docs/stylelint.md +0 -247
  163. package/skills/migrate-to-spec/SKILL.md +0 -124
  164. package/skills/pr-to-lint-rule/SKILL.md +0 -97
  165. package/skills/strengthen/SKILL.md +0 -168
  166. package/src/action.ts +0 -214
  167. package/src/cli.test.ts +0 -914
  168. package/src/cli.ts +0 -1631
  169. package/src/compile.ts +0 -691
  170. package/src/evolve.ts +0 -781
  171. package/src/freshness.test.ts +0 -449
  172. package/src/freshness.ts +0 -299
  173. package/src/generate-types.ts +0 -448
  174. package/src/inline.test.ts +0 -206
  175. package/src/inline.ts +0 -164
  176. package/src/linters.ts +0 -739
  177. package/src/proofs.test.ts +0 -1314
  178. package/src/proofs.ts +0 -849
  179. package/src/spec.test.ts +0 -1471
  180. package/src/spec.ts +0 -427
  181. package/src/types.ts +0 -117
  182. package/src/validate.test.ts +0 -701
  183. package/src/validate.ts +0 -381
  184. package/tsconfig.json +0 -23
package/README.md CHANGED
@@ -16,27 +16,9 @@
16
16
 
17
17
  ---
18
18
 
19
- You wouldn't ship code without a linter. Why ship agent instructions without one?
19
+ Your CLAUDE.md lies to your agent. Here's the fix.
20
20
 
21
- Your CLAUDE.md is a plain text file. Anyone can edit it. Nobody verifies it. The lint rule it references was disabled three months ago. The file path it mentions was renamed. The npm script it suggests was deleted. Your agent reads this, trusts it, and produces code based on lies.
22
-
23
- **Markdown can't be validated. TypeScript can.**
24
-
25
- ```bash
26
- npx vigiles init
27
- ```
28
-
29
- vigiles compiles typed TypeScript specs to instruction files (CLAUDE.md, AGENTS.md). Every linter reference is verified against your actual config — not just that it exists, but that it's enabled. Every file path is checked against the filesystem. Every command is validated against package.json. If something is stale, broken, or disabled — you find out at compile time, not when the agent silently ignores your instructions.
30
-
31
- After setup, the agent edits the spec instead of the markdown. Hooks auto-compile. Types catch typos in the editor. CI catches drift. It's self-maintaining.
32
-
33
- Not using vigiles with AI agents is like not using ESLint with JavaScript. You can do it. You can't afford to.
34
-
35
- Companion repo for [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need).
36
-
37
- ## The Problem
38
-
39
- Hand-written CLAUDE.md files rot silently. Here's what they actually look like:
21
+ Hand-written CLAUDE.md files rot silently. Here's what a typical one looks like:
40
22
 
41
23
  ```markdown
42
24
  ## Code Style
@@ -58,43 +40,45 @@ Reads fine. Four things are wrong:
58
40
  3. `npm run typecheck` — script removed from package.json
59
41
  4. Service/test pairing — no automated check, just a hope
60
42
 
61
- The agent reads this, trusts it, and writes code based on stale claims nobody verified.
43
+ The agent reads this, trusts it, and writes code based on stale claims nobody verified. vigiles **verifies the references in your instruction files** — that each linter rule exists and is enabled, that every file path and script is real, and that referenced **code symbols** (functions, classes, constants) actually exist in the files that define them — and meets you at whatever commitment level you want.
62
44
 
63
- ## What Changes With vigiles
45
+ Three levels. Each is independently useful; adopt as far up as you like.
64
46
 
65
- ### Claude Code
47
+ ### Level 0 — inline comments (30 seconds, no new files)
66
48
 
67
- | | Without vigiles | With vigiles |
68
- | ----------------------------------- | ---------------------------- | -------------------------------------------------------------- |
69
- | **Instructions** | Hand-written CLAUDE.md | Compiled from `.spec.ts` (build artifact) |
70
- | **Linter rule references** | Trust-based (nobody checks) | Verified at compile time against real config |
71
- | **File paths** | Rot silently when renamed | `file()` references checked against filesystem |
72
- | **Commands** | Stale scripts go unnoticed | `cmd()` references checked against package.json |
73
- | **Direct edits to CLAUDE.md** | Anyone can, nobody knows | PreToolUse hook blocks edits, redirects to spec |
74
- | **Linter config changes** | CLAUDE.md drifts out of sync | PostToolUse hook auto-regenerates types |
75
- | **guidance → enforce upgrades** | Manual guesswork | `/strengthen` reads per-linter docs, suggests upgrades |
76
- | **New lint rules from PR feedback** | Copy-paste from review | `/pr-to-lint-rule` generates rule + tests + spec entry |
77
- | **CI** | Nothing to verify | `vigiles audit` catches hash drift, disabled rules, stale refs |
49
+ Add a comment to your existing CLAUDE.md and audit it:
78
50
 
79
- <details>
80
- <summary><b>Codex</b> (same compile-time checks, no hooks)</summary>
51
+ ```md
52
+ <!-- vigiles:enforce eslint/no-console "Route output through logger.ts" -->
53
+ ```
81
54
 
82
- | | Without vigiles | With vigiles |
83
- | ----------------------------- | -------------------------------- | ------------------------------------------------------- |
84
- | **Instructions** | Hand-written AGENTS.md | Compiled from `.spec.ts` |
85
- | **Linter rule references** | Trust-based | Verified at compile time |
86
- | **File paths / commands** | Rot silently | Checked at compile time |
87
- | **Direct edits to AGENTS.md** | Undetected | CI catches hash mismatch |
88
- | **Hooks / auto-compile** | Not available (no plugin system) | Not available — run `vigiles compile` manually or in CI |
89
- | **CI** | Nothing to verify | Same `vigiles audit` pipeline as Claude |
55
+ ```bash
56
+ npx vigiles audit CLAUDE.md
57
+ ```
90
58
 
91
- </details>
59
+ Each rule is checked against your real linter config — typos get closest-match suggestions, disabled rules are flagged. Zero install commitment, zero new files.
92
60
 
93
- 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)
61
+ ### Level 1YAML frontmatter (editor autocomplete, still no TypeScript)
94
62
 
95
- ## The Fix
63
+ Promote your rules into a `vigiles:` block at the top of the file:
96
64
 
97
- Write your conventions as TypeScript. The compiler catches the lies.
65
+ ```yaml
66
+ ---
67
+ # yaml-language-server: $schema=./.vigiles/schema.json
68
+ vigiles:
69
+ enforce:
70
+ - rule: "@typescript-eslint/no-explicit-any"
71
+ why: "Use unknown and narrow with type guards."
72
+ - rule: eslint/no-console
73
+ why: "Route output through logger.ts"
74
+ ---
75
+ ```
76
+
77
+ `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)
78
+
79
+ ### Level 2 — typed spec (compiler-grade guarantees)
80
+
81
+ 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.
98
82
 
99
83
  ```typescript
100
84
  // CLAUDE.md.spec.ts
@@ -132,53 +116,54 @@ $ npx vigiles compile
132
116
  ~180 tokens
133
117
  ```
134
118
 
135
- The spec is the source of truth. CLAUDE.md is a build artifact.
136
-
137
- ## Quick Start
119
+ 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.
138
120
 
139
- ```bash
140
- npx vigiles init
141
- ```
121
+ Companion repo for [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need).
142
122
 
143
- That's it. One command. The wizard auto-detects your project, creates a spec, scans your linters, compiles to markdown, adds a CI step, and installs Claude Code hooks — all automatically.
123
+ ## What Changes With vigiles
144
124
 
145
- ```
146
- npx vigiles init guidance() rules, zero config
147
-
148
-
149
- agent edits spec ◄────────── hooks auto-compile (self-maintaining)
150
-
151
-
152
- npx vigiles audit full verification: hashes + linters + coverage
153
-
154
-
155
- CI catches drift stale refs, disabled rules, typos, duplicates
156
- ```
125
+ ### Claude Code
157
126
 
158
- **After install, it just works:**
127
+ | | Without vigiles | With vigiles |
128
+ | ----------------------------------- | ---------------------------- | -------------------------------------------------------------- |
129
+ | **Instructions** | Hand-written CLAUDE.md | Compiled from `.spec.ts` (build artifact) |
130
+ | **Linter rule references** | Trust-based (nobody checks) | Verified at compile time against real config |
131
+ | **File paths** | Rot silently when renamed | `file()` references checked against filesystem |
132
+ | **Commands** | Stale scripts go unnoticed | `cmd()` references checked against package.json |
133
+ | **Direct edits to CLAUDE.md** | Anyone can, nobody knows | PreToolUse hook blocks edits, redirects to spec |
134
+ | **Spec edits** | N/A | PostToolUse hook auto-compiles to markdown |
135
+ | **Linter config changes** | CLAUDE.md drifts out of sync | PostToolUse hook auto-regenerates types |
136
+ | **guidance → enforce upgrades** | Manual guesswork | `/strengthen` reads per-linter docs, suggests upgrades |
137
+ | **New lint rules from PR feedback** | Copy-paste from review | `/pr-to-lint-rule` generates rule + tests + spec entry |
138
+ | **CI** | Nothing to verify | `vigiles audit` catches hand-edits, disabled rules, stale refs |
159
139
 
160
- - Agent says "update CLAUDE.md" → plugin blocks the edit, redirects to `.spec.ts`
161
- - Agent edits the spec → plugin auto-compiles CLAUDE.md regenerated
162
- - Agent edits `eslint.config.ts` → plugin auto-regenerates types
163
- - CI runs `vigiles audit` → catches stale specs, disabled rules, missing files
140
+ <details>
141
+ <summary><b>Codex</b> (same compile-time checks, no hooks)</summary>
164
142
 
165
- ### Hesitant about a new file type? Try inline mode
143
+ | | Without vigiles | With vigiles |
144
+ | ----------------------------- | -------------------------------- | ------------------------------------------------------- |
145
+ | **Instructions** | Hand-written AGENTS.md | Compiled from `.spec.ts` |
146
+ | **Linter rule references** | Trust-based | Verified at compile time |
147
+ | **File paths / commands** | Rot silently | Checked at compile time |
148
+ | **Direct edits to AGENTS.md** | Undetected | CI catches hash mismatch |
149
+ | **Hooks / auto-compile** | Not available (no plugin system) | Not available — run `vigiles compile` manually or in CI |
150
+ | **CI** | Nothing to verify | Same `vigiles audit` pipeline as Claude |
166
151
 
167
- If a `.spec.ts` feels like too much commitment, you can adopt vigiles one rule at a time by adding HTML comments directly to your existing `CLAUDE.md`:
152
+ </details>
168
153
 
169
- ```md
170
- <!-- vigiles:enforce eslint/no-console "Route output through logger.ts" -->
171
- ```
154
+ 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)
172
155
 
173
- Running `vigiles audit CLAUDE.md` verifies each inline rule against your real linter config with the same closest-match suggestions and disabled-rule detection as spec mode. Zero build step, zero new files, works with any project. See [docs/inline-mode.md](docs/inline-mode.md) for the format and how to graduate to spec mode later.
156
+ ## Quick Start
174
157
 
175
- **It's self-maintaining.** Add a new ESLint rule? The hook regenerates types your spec gets autocomplete for the new rule immediately. Rename a file? The compiler catches the stale reference. The setup doesn't rot because the hooks keep everything in sync.
158
+ 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:
176
159
 
177
- **It evolves automatically.** Start with `guidance()` rules (zero config). When you're ready, run `/strengthen` — it reads your linter configs and per-linter reference docs to find `enforce()` upgrades. Each upgrade adds compiler-verified enforcement.
160
+ ```bash
161
+ npx vigiles init
162
+ ```
178
163
 
179
- **Already have a hand-written CLAUDE.md?** The wizard detects it and suggests migration.
164
+ 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.
180
165
 
181
- **Ready to enforce?** Run `npx vigiles init --strict` to set rules to `"error"` CI fails if any instruction file lacks a spec.
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.
182
167
 
183
168
  | Flag | Effect |
184
169
  | -------------------- | ----------------------------------------------------- |
@@ -188,17 +173,19 @@ Running `vigiles audit CLAUDE.md` verifies each inline rule against your real li
188
173
 
189
174
  Works the same for humans and agents — fully non-interactive. [Agent setup guide →](docs/agent-setup.md) | [Agent workflows →](docs/agent-workflows.md)
190
175
 
191
- ## Two Rule Types
176
+ ## Three Rule Types
192
177
 
193
178
  **`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.
194
179
 
180
+ <!-- vigiles:ignore -->
181
+
195
182
  ```typescript
196
183
  "no-any": enforce("@typescript-eslint/no-explicit-any", "Use unknown and narrow."),
197
184
  "no-print": enforce("ruff/T201", "Use logging module."),
198
185
  "no-unwrap": enforce("clippy/unwrap_used", "Use expect() with context."),
199
186
  ```
200
187
 
201
- Supports ESLint, Stylelint, Ruff, Clippy, Pylint, and RuboCop. [Full linter support details →](docs/linter-support.md)
188
+ Supports ESLint, Stylelint, Ruff, Clippy, Pylint, RuboCop, and Cedar policies. [Full linter support details →](docs/linter-support.md)
202
189
 
203
190
  **`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.
204
191
 
@@ -206,21 +193,37 @@ Supports ESLint, Stylelint, Ruff, Clippy, Pylint, and RuboCop. [Full linter supp
206
193
  "research-first": guidance("Google unfamiliar APIs first."),
207
194
  ```
208
195
 
196
+ **`guard()`** — reactive: runs a command when watched files change. One declaration emits hooks for every supported system (Claude Code PostToolUse, husky pre-commit, etc.). Eliminates copy-pasting the same trigger across `.claude/settings.json`, `.husky/`, and CI configs.
197
+
198
+ ```typescript
199
+ "recompile-specs": guard(
200
+ { watch: "*.spec.ts", run: "npx vigiles compile" },
201
+ "Recompile instruction files when any spec changes.",
202
+ ),
203
+ "regen-types": guard(
204
+ { watch: ["eslint.config.*", "package.json"], run: "npx vigiles generate-types" },
205
+ "Regenerate types when linter config or deps change.",
206
+ ),
207
+ ```
208
+
209
+ Same monotonicity guarantees as `enforce()` — guards can't be silently removed.
210
+
209
211
  ## Verified References
210
212
 
211
- `file()`, `cmd()`, and `ref()` catch stale references at compile time:
213
+ `file()`, `cmd()`, `symbol()`, and `ref()` catch stale references at compile time:
212
214
 
213
215
  ```typescript
214
- import { claude, file, cmd, ref, instructions } from "vigiles/spec";
216
+ import { claude, file, cmd, symbol, ref, instructions } from "vigiles/spec";
215
217
 
216
218
  export default claude({
217
219
  sections: {
218
220
  architecture: instructions`
219
221
  Core engine in ${file("src/compile.ts")}.
222
+ Compile specs with ${symbol("src/compile.ts", "compileClaude")}.
220
223
  Run ${cmd("npm test")} to verify.
221
- See ${ref("skills/deploy/SKILL.md")} for deployment.
224
+ See ${ref("skills/strengthen/SKILL.md")} for the strengthen skill.
222
225
  `,
223
- // If any path is stale → compile error
226
+ // If any path / script / symbol is stale → compile error
224
227
  },
225
228
  // ...
226
229
  });
@@ -228,6 +231,12 @@ export default claude({
228
231
 
229
232
  Skill specs use the same helpers for verified references inside instructions. [Full spec format →](docs/spec-format.md)
230
233
 
234
+ ### Symbol references (cross-language)
235
+
236
+ `symbol("file", "name")` (and the markdown mark `` `vigiles:symbol file#name` ``) verify that the named file actually **defines** the symbol — a function, class, method, or constant — parsed with [ast-grep](https://ast-grep.github.io) across **JS/TS, Python, Ruby, Rust, and CSS**. Rename the function and `audit` fails; no project-wide index, no autoloader guessing — it parses the one named file.
237
+
238
+ In markdown mode the `refs-hook` (PostToolUse) **forces the mark**: it blocks an edit that leaves a code reference bare, telling the agent to write `` `vigiles:symbol path#name` `` or opt out with `<!-- vigiles:ignore -->`. The harness makes the agent mark its references at write time, with full context; `audit` re-verifies them. [Symbol verification →](research/symbol-verification.md)
239
+
231
240
  ## Type-Safe Rule References
232
241
 
233
242
  `vigiles generate-types` scans your linter configs and emits `.vigiles/generated.d.ts`. With this file, `enforce("eslint/no-consolee")` is a red squiggle in your editor — a typo caught at authoring time, not a runtime surprise. Without it, everything falls back to broad types and still works.
@@ -240,14 +249,19 @@ $ npx vigiles generate-types
240
249
 
241
250
  Commit the file to git. CI can verify it's fresh: `npx vigiles generate-types --check`. [How it works →](docs/linter-support.md#generate-types)
242
251
 
252
+ For markdown frontmatter (Level 1), `vigiles generate-schema` gives the same authoring-time feedback without TypeScript: it emits a JSON Schema from your enabled rules, and your editor's YAML language server autocompletes rule names and squiggles typos. CI freshness check: `npx vigiles generate-schema --check`.
253
+
243
254
  ## CLI
244
255
 
245
256
  ```bash
246
257
  npx vigiles init [--target=X.md] # Scaffold a spec (runs full setup wizard by default)
247
258
  npx vigiles compile [files...] # Compile .spec.ts → .md
248
- npx vigiles audit [files...] # Verify hashes + linter rules + coverage + suggest upgrades
249
- npx vigiles generate-types # Emit .d.ts from project state
259
+ npx vigiles audit [files...] # Verify hashes + inline/frontmatter/spec rules + symbols + coverage
260
+ npx vigiles refs <file.md> # Check the symbol references in an instruction file
261
+ npx vigiles generate-types # Emit .d.ts from project state (for spec mode)
250
262
  npx vigiles generate-types --check # Verify .d.ts is up to date
263
+ npx vigiles generate-schema # Emit JSON Schema for vigiles: frontmatter (Level 1)
264
+ npx vigiles generate-schema --check # Verify schema.json is up to date
251
265
  ```
252
266
 
253
267
  ## GitHub Action
@@ -280,17 +294,14 @@ The plugin provides two hooks:
280
294
 
281
295
  ## Validation
282
296
 
283
- `vigiles audit` validates instruction files with three rules:
297
+ `vigiles audit` validates instruction files with four rules:
284
298
 
285
- | Rule | Default | What it checks |
286
- | -------------------- | -------- | --------------------------------------------- |
287
- | `require-spec` | `"warn"` | Every CLAUDE.md/AGENTS.md has a `.spec.ts` |
288
- | `require-skill-spec` | `"warn"` | Every SKILL.md has a `.spec.ts` |
289
- | `freshness` | `"warn"` | Compiled output matches current project state |
290
-
291
- ```bash
292
- npx vigiles audit # checks specs, hashes, freshness, coverage, duplicates
293
- ```
299
+ | Rule | Default | What it checks |
300
+ | -------------------------------------------------------- | -------- | ---------------------------------------------------------------------------- |
301
+ | [`require-spec`](docs/rules/require-spec.md) | `"warn"` | Every CLAUDE.md/AGENTS.md has a spec, inline rule, or `vigiles:` frontmatter |
302
+ | [`require-skill-spec`](docs/rules/require-skill-spec.md) | `"warn"` | Every SKILL.md has a `.spec.ts` |
303
+ | [`integrity`](docs/rules/integrity.md) | `"warn"` | Compiled markdown wasn't hand-edited (SHA-256 check) |
304
+ | [`coverage`](docs/rules/coverage.md) | `false` | Spec covers enough of the project surface |
294
305
 
295
306
  Configure in `.vigilesrc.json`:
296
307
 
@@ -298,41 +309,13 @@ Configure in `.vigilesrc.json`:
298
309
  {
299
310
  "rules": {
300
311
  "require-spec": "error",
301
- "freshness": "error"
312
+ "integrity": "error",
313
+ "coverage": ["warn", { "scripts": 50, "linterRules": 5 }]
302
314
  }
303
315
  }
304
316
  ```
305
317
 
306
- Disable per-file with an HTML comment:
307
-
308
- ```markdown
309
- <!-- vigiles-disable require-spec -->
310
-
311
- # CLAUDE.md
312
-
313
- ...
314
- ```
315
-
316
- ### Freshness
317
-
318
- The `freshness` rule detects when compiled markdown has drifted from project state — disabled linter rules, deleted files, changed configs. Three detection modes:
319
-
320
- | Mode | What it does | Cost |
321
- | -------------------- | ----------------------------------------------------------------------- | ------ |
322
- | `"strict"` (default) | Recompiles in memory, diffs output | 2-5s |
323
- | `"input-hash"` | Checks fingerprint of tracked inputs (spec, linter configs, lock files) | <100ms |
324
- | `"output-hash"` | Only detects hand-edits to compiled markdown | <1ms |
325
-
326
- Strict mode has zero false positives and zero false negatives. Input-hash mode is faster but can false-positive on config whitespace changes. Set the mode in `.vigilesrc.json`:
327
-
328
- ```json
329
- {
330
- "freshnessMode": "input-hash",
331
- "freshnessInputs": ["../../yarn.lock"]
332
- }
333
- ```
334
-
335
- In input-hash mode, vigiles auto-detects lock files across 15 ecosystems (npm, Yarn, pnpm, Bun, Bundler, Poetry, uv, PDM, pip, Cargo, Go, Composer, NuGet, SPM, Mix) and tracks them alongside linter configs, package.json, keyFiles references, and generated types. [Full details →](docs/freshness.md)
318
+ Disable per-file with `<!-- vigiles-disable require-spec -->` at the top of the markdown.
336
319
 
337
320
  ## Skills
338
321
 
@@ -348,6 +331,69 @@ Install with [Vercel Skills](https://github.com/vercel-labs/skills): `npx skills
348
331
  | `enforce-rules-format` | Validate all rules have enforcement classification |
349
332
  | `audit-feedback-loop` | Score your repo's feedback loop maturity |
350
333
 
334
+ ## Test your Claude Code harness
335
+
336
+ vigiles also ships a library for **testing the harness itself** — your hooks,
337
+ settings, skills, and instruction files. `Agent = Model + Harness`; this tests
338
+ the harness, at two levels.
339
+
340
+ **Evals — does my change actually move agent behaviour?** Define a fixture, a set
341
+ of **arms** (a hook on vs off, with/without a CLAUDE.md rule), a task, and a
342
+ metric; `runEval` drives the real `claude` CLI N trials per arm and aggregates.
343
+
344
+ ```typescript
345
+ import { runEval, formatEvalReport } from "vigiles/eval";
346
+
347
+ const report = await runEval({
348
+ fixture: { "src/billing.ts": "export function chargeCard() {}" },
349
+ arms: {
350
+ vanilla: {},
351
+ gated: { settings: { hooks: { PostToolUse: [refsHook] } } },
352
+ },
353
+ task: "Document chargeCard in SKILL.md, referencing it by name.",
354
+ measure: (ctx) => ({
355
+ marked: ctx.sh("grep -c vigiles:symbol SKILL.md") !== "0",
356
+ }),
357
+ trials: 6,
358
+ });
359
+ console.log(formatEvalReport(report)); // vanilla marked=0.00 gated marked=0.50
360
+ ```
361
+
362
+ **Deterministic tests — does my hook fire correctly?** No API key, no cost.
363
+ `runHarnessTest` runs real `claude` against a **scripted mock model**
364
+ (`vigiles/mock-model`), so your real hooks fire but the agent's turns are fixed.
365
+
366
+ ```typescript
367
+ import { runHarnessTest, scriptModel } from "vigiles/harness-test";
368
+
369
+ const r = await runHarnessTest({
370
+ settings: {
371
+ hooks: {
372
+ Stop: [
373
+ {
374
+ hooks: [
375
+ {
376
+ type: "command",
377
+ command: "test -f DONE || { echo 'not done' >&2; exit 2; }",
378
+ },
379
+ ],
380
+ },
381
+ ],
382
+ },
383
+ },
384
+ model: scriptModel([
385
+ { text: "I'm done" }, // tries to stop → blocked
386
+ { tool: "Bash", input: { command: "touch DONE" } },
387
+ { text: "now done" },
388
+ ]),
389
+ });
390
+ assert(JSON.parse(r.stdout).num_turns > 1); // the Stop hook forced more work
391
+ ```
392
+
393
+ The deterministic tier is reliable for **Stop hooks**; tool-event hooks
394
+ (Edit/Write) are headless-gated, so test those via the eval tier. Our own
395
+ findings from this harness live in [`research/benchmarks-runtime-gates.md`](research/benchmarks-runtime-gates.md).
396
+
351
397
  ## Maturity Levels
352
398
 
353
399
  From [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need):
@@ -365,12 +411,14 @@ Specs compile to `CLAUDE.md` by default. Set `target: "AGENTS.md"` or `target: [
365
411
 
366
412
  ## Related Tools
367
413
 
368
- vigiles doesn't try to do everything:
414
+ vigiles doesn't try to do everything. It owns one thing: compile-time verification of typed specs against real linter configs, filesystems, and package.json. Everything else, compose:
369
415
 
370
416
  - **Architectural linting** — [ast-grep](https://ast-grep.github.io/), [Dependency Cruiser](https://github.com/sverweij/dependency-cruiser), [Steiger](https://github.com/feature-sliced/steiger). Reference their rules via `enforce()`.
371
- - **File sync** — [Ruler](https://github.com/intellectronica/ruler), [rulesync](https://github.com/dyoshikawa/rulesync), [block/ai-rules](https://github.com/block/ai-rules). vigiles compiles the source; sync tools distribute.
372
- - **Markdown linting** — [markdownlint](https://github.com/DavidAnson/markdownlint). vigiles generates the markdown; structure is correct by construction.
417
+ - **File sync** across agents — [Ruler](https://github.com/intellectronica/ruler), [rulesync](https://github.com/dyoshikawa/rulesync), [block/ai-rules](https://github.com/block/ai-rules). vigiles compiles the source; sync tools distribute.
418
+ - **Markdown linting** — [markdownlint](https://github.com/DavidAnson/markdownlint). vigiles generates markdown; structure is correct by construction.
419
+ - **Code-block linting in docs** — [eslint-plugin-markdown](https://github.com/eslint/eslint-plugin-markdown) for syntax, [twoslash](https://shikijs.github.io/twoslash/) for TS type-checking.
373
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.
374
422
 
375
423
  ## License
376
424
 
@@ -0,0 +1,28 @@
1
+ import { type RuntimeGate } from "./skill-runtime.js";
2
+ export interface ActionGate {
3
+ /** Tool name to gate, e.g. "Write" | "Edit" | "Bash". */
4
+ readonly on: string;
5
+ /** Deterministic check; a `cmd` command may include `{file}`. */
6
+ readonly gate: RuntimeGate;
7
+ /** Optional substring the (JSON-serialized) tool input must contain. */
8
+ readonly when?: string;
9
+ }
10
+ export interface ActionEvent {
11
+ /** The tool that just ran (PostToolUse `tool_name`). */
12
+ readonly tool: string;
13
+ /** The tool input (`tool_input`), e.g. `{ file_path, command }`. */
14
+ readonly input?: Record<string, unknown>;
15
+ }
16
+ export interface ActionDecision {
17
+ readonly allow: boolean;
18
+ readonly message: string;
19
+ }
20
+ /**
21
+ * Evaluate action gates against a tool event. Runs every gate whose `on`
22
+ * matches the tool (and whose `when` substring matches the input); the first
23
+ * failure blocks. Plan-agnostic — order in any runtime workflow is irrelevant.
24
+ */
25
+ export declare function evaluateAction(event: ActionEvent, gates: readonly ActionGate[], cwd: string): ActionDecision;
26
+ /** Load action gates from `.vigiles/action-gates.json`. */
27
+ export declare function loadActionGates(cwd: string): ActionGate[];
28
+ //# sourceMappingURL=action-gate.d.ts.map
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.evaluateAction = evaluateAction;
4
+ exports.loadActionGates = loadActionGates;
5
+ /**
6
+ * vigiles — Action gates (the dynamic-workflow reframe).
7
+ *
8
+ * A skill gate is bound to a *step* (a fixed position in a plan). When the plan
9
+ * is generated at runtime (dynamic workflows), the step is the wrong unit. An
10
+ * **action gate** binds a deterministic check to a *tool action type* instead —
11
+ * "any time a Write happens to a `.ts` file, eslint must pass on it" — so it
12
+ * fires regardless of where in the runtime plan the action occurs.
13
+ *
14
+ * It is the same deterministic gate primitive (reuses `runGate` + the
15
+ * author-time reference resolution), re-anchored from step → action. Delivered
16
+ * as a PostToolUse hook (`vigiles action-hook`): exit 2 blocks the action and
17
+ * feeds the reason back, exit 0 allows it.
18
+ *
19
+ * Config: `.vigiles/action-gates.json` → `{ "gates": [ { on, gate, when? } ] }`.
20
+ * The gate command may contain `{file}`, substituted with the action's path.
21
+ */
22
+ const node_fs_1 = require("node:fs");
23
+ const node_path_1 = require("node:path");
24
+ const skill_runtime_js_1 = require("./skill-runtime.js");
25
+ /** The file path an action touched, for `{file}` substitution. */
26
+ function fileOf(event) {
27
+ const i = event.input ?? {};
28
+ const v = i.file_path ?? i.path;
29
+ return typeof v === "string" ? v : "";
30
+ }
31
+ /** Substitute `{file}` in a cmd gate with the action's path. */
32
+ function resolveGate(gate, event) {
33
+ if (gate.kind !== "cmd" || !gate.command.includes("{file}"))
34
+ return gate;
35
+ return { ...gate, command: gate.command.replaceAll("{file}", fileOf(event)) };
36
+ }
37
+ /**
38
+ * Evaluate action gates against a tool event. Runs every gate whose `on`
39
+ * matches the tool (and whose `when` substring matches the input); the first
40
+ * failure blocks. Plan-agnostic — order in any runtime workflow is irrelevant.
41
+ */
42
+ function evaluateAction(event, gates, cwd) {
43
+ const inputStr = JSON.stringify(event.input ?? "");
44
+ for (const g of gates) {
45
+ if (g.on !== event.tool)
46
+ continue;
47
+ if (g.when && !inputStr.includes(g.when))
48
+ continue;
49
+ const outcome = (0, skill_runtime_js_1.runGate)(resolveGate(g.gate, event), cwd);
50
+ if (!outcome.ok) {
51
+ const tail = outcome.output ? `\n${outcome.output}` : "";
52
+ return {
53
+ allow: false,
54
+ message: `Action gate failed after ${event.tool}: ${(0, skill_runtime_js_1.gateLabel)(g.gate)} did not pass.${tail}`,
55
+ };
56
+ }
57
+ }
58
+ return { allow: true, message: "" };
59
+ }
60
+ /** Load action gates from `.vigiles/action-gates.json`. */
61
+ function loadActionGates(cwd) {
62
+ const p = (0, node_path_1.resolve)(cwd, ".vigiles/action-gates.json");
63
+ if (!(0, node_fs_1.existsSync)(p))
64
+ return [];
65
+ try {
66
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(p, "utf-8"));
67
+ return Array.isArray(parsed.gates) ? parsed.gates : [];
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ }
73
+ //# sourceMappingURL=action-gate.js.map