vigiles 6.0.0 → 7.0.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 (65) hide show
  1. package/README.md +78 -68
  2. package/dist/action-gate.js +1 -1
  3. package/dist/adapters/claude-code/agent-runtime.d.ts +46 -11
  4. package/dist/adapters/claude-code/agent-runtime.js +95 -24
  5. package/dist/adapters/claude-code/effect-region.js +1 -1
  6. package/dist/adapters/claude-code/skill-runtime.d.ts +1 -1
  7. package/dist/adapters/claude-code/skill-runtime.js +1 -1
  8. package/dist/adapters/codex/hook-protocol.js +3 -0
  9. package/dist/adapters/codex/mock-model.js +1 -1
  10. package/dist/cli-commands.d.ts +19 -0
  11. package/dist/cli-commands.js +51 -0
  12. package/dist/cli.js +599 -86
  13. package/dist/core/bash-effects.d.ts +12 -0
  14. package/dist/core/bash-effects.js +31 -0
  15. package/dist/core/capability-diff.d.ts +46 -0
  16. package/dist/core/capability-diff.js +97 -0
  17. package/dist/core/guards.d.ts +126 -0
  18. package/dist/core/guards.js +309 -0
  19. package/dist/core/harness-driver.d.ts +1 -1
  20. package/dist/core/hook-program.d.ts +459 -0
  21. package/dist/core/hook-program.js +468 -0
  22. package/dist/core/hook-protocol.d.ts +7 -0
  23. package/dist/core/hook-providers.d.ts +138 -0
  24. package/dist/core/hook-providers.js +155 -0
  25. package/dist/core/hook-spec.d.ts +74 -0
  26. package/dist/core/hook-spec.js +130 -0
  27. package/dist/core/inline.js +1 -1
  28. package/dist/core/mcp-tool.d.ts +12 -0
  29. package/dist/core/mcp-tool.js +20 -0
  30. package/dist/core/mcp.d.ts +13 -0
  31. package/dist/core/mcp.js +67 -0
  32. package/dist/core/types.d.ts +8 -0
  33. package/dist/dialect-drift.d.ts +65 -0
  34. package/dist/dialect-drift.js +216 -0
  35. package/dist/eval.d.ts +40 -5
  36. package/dist/eval.js +59 -5
  37. package/dist/guardrail-check.d.ts +85 -0
  38. package/dist/guardrail-check.js +152 -0
  39. package/dist/harness-assert.d.ts +10 -0
  40. package/dist/harness-assert.js +30 -0
  41. package/dist/hook-install.d.ts +43 -0
  42. package/dist/hook-install.js +91 -0
  43. package/dist/hook.d.ts +52 -0
  44. package/dist/hook.js +98 -0
  45. package/dist/leaderboard.d.ts +6 -0
  46. package/dist/leaderboard.js +43 -1
  47. package/dist/linting.d.ts +9 -5
  48. package/dist/linting.js +17 -5
  49. package/dist/optimize.js +1 -1
  50. package/dist/scaffold-test.js +21 -7
  51. package/dist/scan-behavioral.d.ts +60 -0
  52. package/dist/scan-behavioral.js +239 -1
  53. package/dist/scan.d.ts +14 -0
  54. package/dist/scan.js +33 -1
  55. package/dist/score-explainer.js +1 -1
  56. package/dist/self-command-refs.d.ts +21 -0
  57. package/dist/self-command-refs.js +125 -0
  58. package/dist/testing.d.ts +5 -3
  59. package/dist/testing.js +37 -23
  60. package/dist/tool-intercept.d.ts +4 -4
  61. package/dist/tool-intercept.js +5 -5
  62. package/dist/unit.d.ts +2 -0
  63. package/dist/unit.js +8 -1
  64. package/hooks/refs-nudge.sh +1 -1
  65. package/package.json +5 -3
package/README.md CHANGED
@@ -5,11 +5,11 @@
5
5
  <h1 align="center">vigiles</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Lint, test &amp; measure the harness your AI agent runs on.</strong>
8
+ <strong>Make the harness your AI agent runs on reliable.</strong>
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- Your CLAUDE.md, hooks, and skills steer the agent — but nothing checks they're <em>true</em>, nothing tests they <em>work</em>, and nothing measures whether they actually <em>help</em>. vigiles does all three.
12
+ Your CLAUDE.md, hooks, and skills steer the agent — but nothing checks they're <em>true</em>, nothing stops them entering a <em>bad state</em>, nothing tests they <em>work</em>, and nothing measures whether they actually <em>help</em>. vigiles does all four — deterministically, mostly with no API key.
13
13
  </p>
14
14
 
15
15
  <p align="center">
@@ -20,17 +20,17 @@
20
20
 
21
21
  ---
22
22
 
23
- `Agent = Model + Harness`. You'd never ship an app without a linter, a test suite,
24
- and a benchmark yet the harness steering your agent runs on vibes. vigiles[^name]
25
- is the deterministic layer for it and the only one that can **A/B-test what
26
- actually moves the needle on your Claude subscription**, not metered API. Three
27
- things, adopt any:
23
+ `Agent = Model + Harness`. The model gets the headlines; the **harness** the
24
+ instruction files, hooks, and skills you actually control is the half that
25
+ fails silently. vigiles[^name] turns that harness into a compilable, analyzable,
26
+ testable object instead of vibes. Four instruments, adopt any:
28
27
 
29
- | | |
30
- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
31
- | **🔎 Lint** | Every file path, script, code symbol, and linter rule your CLAUDE.md cites is checked against reality — so a renamed file or a disabled rule can't silently mislead the agent. **[→](docs/verifying-instruction-files.md)** |
32
- | **🧪 Test** | Hooks, skills, and subagents are code. vigiles tests they _do their job_and almost all of it is **deterministic, no API key**; the real-model evals run on your **Claude subscription**, not metered tokens. **[→](docs/harness-testing.md)** |
33
- | **📊 Measure** | Does that skill or plugin actually help or just add cost? **A/B it on real tasks** and read the bill + correctness, not the vendor's headline. The same engine ranks the hyped ecosystem: **what works vs hype**. **[→](docs/measuring-skills.md)** |
28
+ | | |
29
+ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
+ | **🔎 Lint** | Every file path, script, code symbol, and linter rule your CLAUDE.md cites is checked against reality — a lint that asks _is this **true**_, not just _well-formed_ — so a renamed file or a disabled rule can't silently mislead the agent. **[→](docs/verifying-instruction-files.md)** |
31
+ | **🛡 Guard** | Author a hook as a pure typed function and vigiles compiles itmaking **whole classes of hook bugs unrepresentable** (the guard that silently doesn't block). **[→](docs/compiled-hooks.md)** |
32
+ | **🧪 Test** | Hooks, skills, and subagents are code. vigiles tests they _do their job_ and almost all of it is **deterministic, no API key**; the real-model evals run on your **Claude subscription**, not metered tokens. **[→](docs/harness-testing.md)** |
33
+ | **📊 Eval** | Does that skill or plugin actually help — or just add cost? **A/B it on real tasks** and read the bill + correctness, not the vendor's headline — the eval you can afford. The same engine ranks the hyped ecosystem: **what works vs hype**. **[→](docs/measuring-skills.md)** |
34
34
 
35
35
  Pick the one that hurts today. **Works with Claude Code and Codex**
36
36
  ([`vigiles/codex`](docs/harnesses.md)), and you can
@@ -53,24 +53,16 @@ Or do it yourself:
53
53
  npx vigiles init # sets up lint + test: spec + harness test + CI + plugin
54
54
  ```
55
55
 
56
- It's interactive in a terminal and non-interactive for agents/CI (or with
57
- `--yes`), so "set up vigiles" from a Claude Code / Codex prompt Just Works — and
58
- it installs a model-invocable **`test-harness` skill**, so afterward you can just
59
- tell your agent _"test my skills"_ and it picks the tier and writes the test.
56
+ Interactive in a terminal, non-interactive for agents/CI (or `--yes`). It installs
57
+ a model-invocable **`test-harness` skill** too, so you can then tell your agent
58
+ _"test my skills"_ and it writes the test.
60
59
 
61
60
  <details>
62
61
  <summary>What <code>init</code> sets up</summary>
63
62
 
64
- - **Both lint and test** by default; scope with `--lint` / `--test` (one or both).
65
- - Adds `vigiles` to your `devDependencies`.
66
- - Installs the Claude Code plugin (skills + hooks) via the marketplace
67
- globally, never vendored into your repo.
68
- - Wires CI as a `zernie/vigiles@v1` workflow (a composite over the same CLI):
69
-
70
- ```yaml
71
- - uses: actions/checkout@v4
72
- - uses: zernie/vigiles@v1 # lints by default; posts a sticky PR comment + a `valid` output
73
- ```
63
+ - **Both lint and test** by default; scope with `--lint` / `--test`.
64
+ - Adds `vigiles` to `devDependencies`; installs the Claude Code plugin (skills + hooks) via the marketplace — globally, never vendored.
65
+ - Wires CI as a `zernie/vigiles@v1` workflow (a composite over the same CLI) that posts a sticky PR comment + a `valid` output.
74
66
 
75
67
  Prefer to write tests yourself? They can be JS **or** TS
76
68
  (`*.harness.{mjs,ts}`) — run them with `npx vigiles test`.
@@ -79,11 +71,10 @@ Prefer to write tests yourself? They can be JS **or** TS
79
71
 
80
72
  ## ① Lint — your CLAUDE.md lies to your agent
81
73
 
82
- Your CLAUDE.md points the agent at `src/auth/login.ts` and tells it to run
83
- `npm run check`. But the file moved to `src/auth/session.ts` six commits ago, and
84
- the script was renamed. The agent trusts the stale claim and acts on fiction.
85
-
86
- `npx vigiles lint` resolves every reference against reality:
74
+ Your CLAUDE.md points the agent at `src/auth/login.ts` and says to run `npm run
75
+ check`. But the file moved six commits ago and the script was renamed — the agent
76
+ trusts the stale claim and acts on fiction. `npx vigiles lint` resolves every
77
+ reference against reality:
87
78
 
88
79
  ```text
89
80
  CLAUDE.md:
@@ -94,28 +85,54 @@ CLAUDE.md:
94
85
 
95
86
  File paths, scripts, and code symbols — plus linter rules across **7 catalogs**
96
87
  (the rule exists **and is enabled**). Start with one inline comment, no new files;
97
- step up to a typed `.spec.ts` (compiled to CLAUDE.md, compiler-grade) when you want
98
- it and that same spec is what makes your skills & subagents deterministically
99
- **testable** (a `result()` contract → an assert, no LLM judge — see ② below), not
100
- just lint-able. **[Full guide →](docs/verifying-instruction-files.md)**
88
+ step up to a typed `.spec.ts` (compiled to CLAUDE.md) when you want it.
89
+ **[Full guide →](docs/verifying-instruction-files.md)**
101
90
 
102
91
  > **Markdown is prose; a typed spec is a _program_.** Opt in and an agent that
103
- > leaks, exceeds its effect floor, or hands off mismatched data is a **type
104
- > error** — your **multi-agent pipeline won't compile if the handoffs don't line
105
- > up**. Others lint prose after the fact; vigiles is a **compiler for harnesses**.
106
- > Graduated like TypeScript's `strict` — markdown still works, type-safety is
107
- > opt-in. **[The moat →](research/typed-spec-moat.md)**
92
+ > leaks or hands off mismatched data is a **type error** — your multi-agent
93
+ > pipeline won't compile if the handoffs don't line up. Others lint prose; vigiles
94
+ > is a **compiler for harnesses**, graduated like `strict`. **[The moat →](research/typed-spec-moat.md)**
108
95
 
109
96
  **Same cross-reference, any plugin.** `npx vigiles scan` checks a plugin's
110
97
  _contracts_ — every subagent tool, `mcp__server__tool`, `mcp_tool` hook, hook
111
98
  event, and script path actually **exists and resolves**, not just parses (valid
112
- YAML ≠ a tool that's real). A **superset of Anthropic's `claude plugin validate`**,
113
- no key. **[Audit any plugin →](docs/cli.md#scan-dir)**
99
+ YAML ≠ a real tool). A **superset of `claude plugin validate`**, no key. **[Audit any plugin →](docs/cli.md#scan-dir)**
100
+
101
+ ## ② Guard — author a hook that can't be wrong
102
+
103
+ A safety hook is the one place that can _stop_ the agent before something
104
+ irreversible — yet hand-written hooks fail silently (`exit 1` not `2`, the wrong
105
+ JSON field, a `grep` that misses `cd x && git push -f`): they look like a guard
106
+ and block nothing. Write the hook as a pure typed function instead — vigiles
107
+ compiles the protocol for you:
108
+
109
+ ```typescript
110
+ import { defineHook, tool, deny, allow } from "vigiles/hook";
111
+
112
+ export default defineHook({
113
+ on: "PreToolUse",
114
+ match: tool("Bash"),
115
+ decide: (e) =>
116
+ e.command.runs("git push", { force: true })
117
+ ? deny("no force-push to a protected branch")
118
+ : allow(),
119
+ });
120
+ ```
114
121
 
115
- ## Test does your harness do its job?
122
+ This **eliminates an entire class of bugs**: you never write the exit code / JSON
123
+ field (false confidence), the matcher is **AST-backed** (catches the compound
124
+ bypass a glob misses), the only import allowed is `vigiles/hook` (capability = API
125
+ surface), and the artifact is **stamped** (a later hand-edit is refused). A real,
126
+ widely-copied OSS safety hook blocks **2/7** of the disaster battery; the compiled
127
+ rewrite blocks **7/7**. Honest about the cons: this fixes a hook's _logic_, not
128
+ the harness's _delivery_ — a subagent's tool calls still bypass any PreToolUse
129
+ hook ([#34692](https://github.com/anthropics/claude-code/issues/34692)), so it's a
130
+ strong default, not an unbypassable wall. **[Compiled hooks — bug classes + trade-offs →](docs/compiled-hooks.md)**
116
131
 
117
- A hook can be wired wrong. A skill's description can fail to trigger — or hijack
118
- unrelated prompts. Injected context can never reach the model. All of it passes a
132
+ ## Test does your harness do its job?
133
+
134
+ A hook can be wired wrong; a skill's description can fail to trigger — or hijack
135
+ unrelated prompts; injected context can never reach the model. All of it passes a
119
136
  naive "did it run?" check. vigiles tests the assembled harness for real:
120
137
 
121
138
  ```typescript
@@ -133,14 +150,14 @@ It goes well past _"did it fire?"_:
133
150
 
134
151
  - **Hooks block** what they must — `runHook`, or the real agent CLI via `runHarnessTest`.
135
152
  - **Skills trigger** on the right prompts and stay quiet on the wrong ones — recall _and_ precision (`measureTriggerRate`).
136
- - **Behaviour is good** — score a skill's output directly, or A/B it on-vs-off for the real lift (`measure` / `runEval`, with significance testing).
153
+ - **Behaviour is good** — score a skill's output, or A/B it on-vs-off for the real lift (`measure` / `runEval`, with significance testing).
137
154
  - **Safety holds** — the agent _didn't_ push to the wrong branch or hit a paid API; `interceptTools` catches the attempt so the side effect never happens.
138
155
 
139
156
  Almost every tier runs with **no model and no API key** — milliseconds, on every
140
- commit. Only the real-model evals need a model, and they run on your own `claude`
141
- CLI. **[How it works →](docs/harness-testing.md)**
157
+ commit; only the real-model evals need a model, on your own `claude` CLI.
158
+ **[How it works →](docs/harness-testing.md)**
142
159
 
143
- ## Measure — does it actually help, or just cost more?
160
+ ## Eval — does it actually help, or just cost more?
144
161
 
145
162
  A skill claims "65% fewer tokens." A plugin promises "3× faster." Stars and
146
163
  vibes — **zero measurement**. vigiles A/Bs the claim on real coding tasks, the
@@ -155,33 +172,26 @@ const r = await measureArms({
155
172
  arms: { baseline: {}, skill: { files: { "SKILL.md": THE_SKILL } } },
156
173
  measure: (ctx) => ({ cost: ctx.usage.costUsd, correct: check(ctx) }),
157
174
  });
158
- // → is the bill lower? did correctness survive? (not just "fewer output tokens")
159
175
  ```
160
176
 
161
- - **The bill (`costUsd`)** — the honest number; it weights cache ~0.1× and output 1×, so a "saved tokens" headline can't hide behind cheap cache.
162
- - **The optimization target** — whatever the skill claims to move (output tokens, latency, tool calls). Verified on its own terms.
177
+ - **The bill (`costUsd`)** — weights cache ~0.1× / output 1×, so a "saved tokens" headline can't hide behind cheap cache.
178
+ - **The target** — whatever the skill claims to move (output tokens, latency, tool calls), verified on its own terms.
163
179
  - **The blast radius** — correctness, a deterministic 1/0. A token win that breaks the code is **not a win**.
164
180
 
165
- The kicker: every run is **your own `claude` CLI on your Pro/Max subscription** —
166
- so you can measure on every change, and the same engine powers the **ecosystem
167
- benchmark** ("we A/B-tested the most-hyped skills — what works vs hype"). That's
168
- the moat: continuous, sub-affordable measurement nobody metered-by-the-token can
169
- match. **[Measure a skill →](docs/measuring-skills.md)** · **[Why it's affordable →](docs/eval-architecture.md)**
170
-
171
- **The eval you can actually afford.** Most of vigiles needs no model at all; the
172
- rest drives your subscription:
181
+ The kicker: every run is **your own `claude` CLI on your Pro/Max subscription**,
182
+ so you can measure on every change and the same engine powers the **ecosystem
183
+ benchmark** ("what works vs hype"). **[Eval a skill →](docs/measuring-skills.md)** · **[Why it's affordable →](docs/eval-architecture.md)**
173
184
 
174
- | | Runs on | Cost |
175
- | ---------------------- | ----------------------- | ------------------------------------------- |
176
- | promptfoo, DeepEval, | metered API SDK | billed **per token, every run** |
177
- | **vigiles** | your Claude Pro/Max sub | **$0 extra** — and most tiers need no model |
185
+ **The eval you can actually afford.** promptfoo / DeepEval / … hit a metered API
186
+ SDK and bill **per token, every run**; vigiles answers most questions with **no
187
+ model at all** and runs the rest on **your Claude Pro/Max subscription — $0 extra**.
178
188
 
179
189
  ## More
180
190
 
181
- - **[Plugin health leaderboard →](docs/cli.md#scan-dir)** — point `scan` at a marketplace (e.g. `wshobson/agents`) and it ranks every plugin by structural health (0–100, A–F), worst issues first — still **no key**. Add `--trigger` for the model-gated column: do the skills actually fire?
182
- - **[CLI & GitHub Action →](docs/cli.md)** — every command, the Action (inputs / output / versioning), and the Claude Code plugin. The full **[lint rules matrix →](docs/verifying-instruction-files.md#the-validation-rules--the-full-matrix)** lives with the linting guide.
183
- - **[Skills →](docs/skills.md)** — consumer skills installed as a Claude Code plugin: `/plugin marketplace add zernie/vigiles` then `/plugin install vigiles@vigiles` (or let `vigiles init` do it). The model-invocable ones (`test-harness`, `strengthen`, `edit-spec`) fire on their own — ask _"test my skills"_, _"strengthen my rules"_, or _"add a rule to CLAUDE.md"_ and the agent reaches for them; `adopt-spec` and `linter-docs` are user-invoked.
184
- - **[Docs index →](docs/README.md)** · **[Research →](research/README.md)** · **[Related tools →](docs/related-tools.md)** (ast-grep, Dependency Cruiser, Ruler, rulesync).
191
+ - **[Plugin health leaderboard →](docs/cli.md#scan-dir)** — point `scan` at a marketplace (e.g. `wshobson/agents`) and it ranks every plugin by structural health (0–100, A–F), worst issues first — **no key**. Add `--trigger` for the model-gated column: do the skills actually fire?
192
+ - **[CLI & GitHub Action →](docs/cli.md)** — every command (incl. compiled hooks via `compile`), the Action, and the plugin. The full **[lint rules matrix →](docs/verifying-instruction-files.md#the-validation-rules--the-full-matrix)** lives with the linting guide.
193
+ - **[Skills →](docs/skills.md)** — consumer skills installed as a Claude Code plugin. The model-invocable ones (`test-harness`, `strengthen`, `edit-spec`) fire on their own — ask _"test my skills"_ and the agent reaches for them.
194
+ - **[Docs index →](docs/README.md)** · **[Research →](research/README.md)** · **[API reference →](https://zernie.github.io/vigiles/)** (generated) · **[Related tools →](docs/related-tools.md)** (ast-grep, Dependency Cruiser, Ruler, rulesync).
185
195
  - Companion to [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need).
186
196
 
187
197
  ## License
@@ -13,7 +13,7 @@ exports.loadActionGates = loadActionGates;
13
13
  *
14
14
  * It is the same deterministic gate primitive (reuses `runGate` + the
15
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
16
+ * as a PostToolUse hook (`vigiles hook-runtime action`): exit 2 blocks the action and
17
17
  * feeds the reason back, exit 0 allows it.
18
18
  *
19
19
  * Config: `.vigiles/action-gates.json` → `{ "gates": [ { on, gate, when? } ] }`.
@@ -10,7 +10,7 @@
10
10
  * doesn't list.
11
11
  *
12
12
  * This is the same emit-a-hook pattern the skill runtime already ships
13
- * (`src/skill-runtime.ts`): there a `Stop` hook reads the active skill's
13
+ * (`src/adapters/claude-code/skill-runtime.ts`): there a `Stop` hook reads the active skill's
14
14
  * compiled SKILL.md and runs its result gate; here a `PreToolUse` hook reads
15
15
  * the active agent's compiled `.md`, parses its `tools:` allowlist, and
16
16
  * allows/denies the tool call. The compiled markdown's frontmatter is the
@@ -66,21 +66,56 @@ export interface PreToolDecision {
66
66
  * feeding the contract back to the model so it self-corrects.
67
67
  */
68
68
  export declare function decidePreToolUse(allowed: readonly string[] | null, tool: string): PreToolDecision;
69
- /** Record the subagent currently dispatched, so PreToolUse enforces its contract. */
69
+ /**
70
+ * Read the active-agent stack (oldest → newest; the dispatched subagent chain).
71
+ * Back-compat: a legacy single-slot `{ agent: string }` marker reads as a one-frame
72
+ * stack; a malformed file or non-string entries → an empty stack (fail-open).
73
+ */
74
+ export declare function readActiveStack(cwd: string): string[];
75
+ /**
76
+ * Push a dispatched subagent onto the active stack (a `PreToolUse` spawn). The
77
+ * PreToolUse gate then enforces this subagent's contract until it returns. Under
78
+ * nesting each dispatch pushes a frame, so the chain is tracked, not overwritten.
79
+ */
80
+ export declare function pushActiveAgent(cwd: string, agentPath: string): void;
81
+ /**
82
+ * Pop the top frame — the subagent returned (`SubagentStop`), so control returns
83
+ * to its PARENT (the next frame down), whose contract the gate enforces again.
84
+ * This is the nesting-safe close, distinct from {@link clearActiveAgent} (which
85
+ * drops the whole stack). Popping an empty stack is a no-op.
86
+ */
87
+ export declare function popActiveAgent(cwd: string): void;
88
+ /**
89
+ * Push a subagent frame (the manual `agent-start` fallback + the deterministic
90
+ * spawn open-signal both use this). A single call is equivalent to a one-frame
91
+ * stack, so the top — what the gate reads — is this agent. Alias of
92
+ * {@link pushActiveAgent} kept under the historical name.
93
+ */
70
94
  export declare function setActiveAgent(cwd: string, agentPath: string): void;
71
- /** Clear the active-agent marker (the subagent finished). */
95
+ /**
96
+ * Clear the WHOLE stack (a hard reset / session end). Distinct from
97
+ * {@link popActiveAgent}, which returns to the parent frame. Idempotent.
98
+ */
72
99
  export declare function clearActiveAgent(cwd: string): void;
73
- /** The path of the active agent's compiled `.md`, or null when none is active. */
100
+ /**
101
+ * The active agent's compiled `.md` — the STACK TOP — or null when none is active.
102
+ * The gate reads the top, so a returned nested subagent reveals its parent's
103
+ * contract again (the contract-escape fix).
104
+ */
74
105
  export declare function readActiveAgent(cwd: string): string | null;
75
106
  /**
76
- * EXPERIMENTAL — parked (P3), flat-only, do NOT auto-wire. This deterministic
77
- * subagent-window tracking assumes FLAT dispatch (one active subagent at a time);
78
- * it is NOT nesting-safe Claude Code v2.1.172 added depth-5 nested subagents, so
79
- * correct tracking needs a depth-aware STACK (push on dispatch, pop on SubagentStop)
80
- * + verifying the spawn tool name (`Agent` vs `Task`). See
81
- * research/effect-boundary-design.md ("Why dropped").
107
+ * EXPERIMENTAL — parked (P3), do NOT auto-wire. The subagent-window tracking is
108
+ * now nesting-safe: a depth-aware STACK (push on dispatch, pop on SubagentStop —
109
+ * see {@link pushActiveAgent}/{@link popActiveAgent}) closes the contract-escape
110
+ * the flat single-slot model allowed under Claude Code v2.1.172 depth-5 nesting
111
+ * (certified in research/prototypes/.../AgentWindowStack.tla). The open signal
112
+ * recognizes BOTH spawn tool names (`Task` and the nested-spawn `Agent`), gated on
113
+ * a resolvable `subagent_type` so a non-spawn call never opens a frame. Still
114
+ * parked because the `effect()` sub-region goal it served was dropped (see
115
+ * research/effect-boundary-design.md, "Why dropped") — the stack is shipped for
116
+ * when active-agent contract enforcement under nesting is wanted on its own.
82
117
  *
83
- * Resolve a `Task` tool's `subagent_type` to the compiled agent `.md` to
118
+ * Resolve a spawn tool's `subagent_type` to the compiled agent `.md` to
84
119
  * activate, or null when none is found. The DETERMINISTIC open signal that
85
120
  * replaces the model-invoked `agent-start`: Claude Code fires `PreToolUse` for
86
121
  * the parent's `Task` dispatch (and `SubagentStop` when it returns), so the
@@ -11,7 +11,7 @@
11
11
  * doesn't list.
12
12
  *
13
13
  * This is the same emit-a-hook pattern the skill runtime already ships
14
- * (`src/skill-runtime.ts`): there a `Stop` hook reads the active skill's
14
+ * (`src/adapters/claude-code/skill-runtime.ts`): there a `Stop` hook reads the active skill's
15
15
  * compiled SKILL.md and runs its result gate; here a `PreToolUse` hook reads
16
16
  * the active agent's compiled `.md`, parses its `tools:` allowlist, and
17
17
  * allows/denies the tool call. The compiled markdown's frontmatter is the
@@ -28,6 +28,9 @@ exports.parseAgentTools = parseAgentTools;
28
28
  exports.parseAgentToolList = parseAgentToolList;
29
29
  exports.parseAgentPurity = parseAgentPurity;
30
30
  exports.decidePreToolUse = decidePreToolUse;
31
+ exports.readActiveStack = readActiveStack;
32
+ exports.pushActiveAgent = pushActiveAgent;
33
+ exports.popActiveAgent = popActiveAgent;
31
34
  exports.setActiveAgent = setActiveAgent;
32
35
  exports.clearActiveAgent = clearActiveAgent;
33
36
  exports.readActiveAgent = readActiveAgent;
@@ -100,46 +103,114 @@ function decidePreToolUse(allowed, tool) {
100
103
  };
101
104
  }
102
105
  // ---------------------------------------------------------------------------
103
- // Active-agent tracking (mirrors .vigiles/active-skill.json)
106
+ // Active-agent tracking — a depth-aware STACK (mirrors .vigiles/active-skill.json)
104
107
  // ---------------------------------------------------------------------------
108
+ //
109
+ // Claude Code v2.1.172 added nested subagents (a subagent with the spawn tool can
110
+ // dispatch its own, up to depth 5). A single active-agent slot is NOT nesting-safe:
111
+ // when an inner subagent returns, clearing the whole slot drops the OUTER agent's
112
+ // contract while it is still running, so the PreToolUse gate then allows a tool the
113
+ // outer subagent forbids — a CONTRACT ESCAPE. The fix (certified in TLC, see
114
+ // research/prototypes/typed-spec-formal-verification/AgentWindowStack.tla) is a
115
+ // STACK: push on dispatch, pop on SubagentStop (back to the parent), gate on the
116
+ // stack TOP. Counterexample the flat model fails and the stack model passes:
117
+ // Open(writer); Open(writer); Stop; Call(Bash).
105
118
  const ACTIVE_PATH = ".vigiles/active-agent.json";
106
- /** Record the subagent currently dispatched, so PreToolUse enforces its contract. */
107
- function setActiveAgent(cwd, agentPath) {
119
+ /**
120
+ * Read the active-agent stack (oldest → newest; the dispatched subagent chain).
121
+ * Back-compat: a legacy single-slot `{ agent: string }` marker reads as a one-frame
122
+ * stack; a malformed file or non-string entries → an empty stack (fail-open).
123
+ */
124
+ function readActiveStack(cwd) {
125
+ const p = (0, node_path_1.resolve)(cwd, ACTIVE_PATH);
126
+ if (!(0, node_fs_1.existsSync)(p))
127
+ return [];
128
+ try {
129
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(p, "utf-8"));
130
+ if (Array.isArray(parsed.stack)) {
131
+ return parsed.stack.filter((x) => typeof x === "string");
132
+ }
133
+ // legacy single-slot format
134
+ if (typeof parsed.agent === "string")
135
+ return [parsed.agent];
136
+ return [];
137
+ }
138
+ catch {
139
+ return [];
140
+ }
141
+ }
142
+ function writeActiveStack(cwd, stack) {
108
143
  const p = (0, node_path_1.resolve)(cwd, ACTIVE_PATH);
144
+ if (stack.length === 0) {
145
+ if ((0, node_fs_1.existsSync)(p))
146
+ (0, node_fs_1.rmSync)(p);
147
+ return;
148
+ }
109
149
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(p), { recursive: true });
110
- (0, node_fs_1.writeFileSync)(p, JSON.stringify({ agent: agentPath }) + "\n");
150
+ (0, node_fs_1.writeFileSync)(p, JSON.stringify({ stack }) + "\n");
151
+ }
152
+ /**
153
+ * Push a dispatched subagent onto the active stack (a `PreToolUse` spawn). The
154
+ * PreToolUse gate then enforces this subagent's contract until it returns. Under
155
+ * nesting each dispatch pushes a frame, so the chain is tracked, not overwritten.
156
+ */
157
+ function pushActiveAgent(cwd, agentPath) {
158
+ writeActiveStack(cwd, [...readActiveStack(cwd), agentPath]);
159
+ }
160
+ /**
161
+ * Pop the top frame — the subagent returned (`SubagentStop`), so control returns
162
+ * to its PARENT (the next frame down), whose contract the gate enforces again.
163
+ * This is the nesting-safe close, distinct from {@link clearActiveAgent} (which
164
+ * drops the whole stack). Popping an empty stack is a no-op.
165
+ */
166
+ function popActiveAgent(cwd) {
167
+ const stack = readActiveStack(cwd);
168
+ stack.pop();
169
+ writeActiveStack(cwd, stack);
111
170
  }
112
- /** Clear the active-agent marker (the subagent finished). */
171
+ /**
172
+ * Push a subagent frame (the manual `agent-start` fallback + the deterministic
173
+ * spawn open-signal both use this). A single call is equivalent to a one-frame
174
+ * stack, so the top — what the gate reads — is this agent. Alias of
175
+ * {@link pushActiveAgent} kept under the historical name.
176
+ */
177
+ function setActiveAgent(cwd, agentPath) {
178
+ pushActiveAgent(cwd, agentPath);
179
+ }
180
+ /**
181
+ * Clear the WHOLE stack (a hard reset / session end). Distinct from
182
+ * {@link popActiveAgent}, which returns to the parent frame. Idempotent.
183
+ */
113
184
  function clearActiveAgent(cwd) {
114
185
  const p = (0, node_path_1.resolve)(cwd, ACTIVE_PATH);
115
186
  if ((0, node_fs_1.existsSync)(p))
116
187
  (0, node_fs_1.rmSync)(p);
117
188
  }
118
- /** The path of the active agent's compiled `.md`, or null when none is active. */
189
+ /**
190
+ * The active agent's compiled `.md` — the STACK TOP — or null when none is active.
191
+ * The gate reads the top, so a returned nested subagent reveals its parent's
192
+ * contract again (the contract-escape fix).
193
+ */
119
194
  function readActiveAgent(cwd) {
120
- const p = (0, node_path_1.resolve)(cwd, ACTIVE_PATH);
121
- if (!(0, node_fs_1.existsSync)(p))
122
- return null;
123
- try {
124
- const parsed = JSON.parse((0, node_fs_1.readFileSync)(p, "utf-8"));
125
- return typeof parsed.agent === "string" ? parsed.agent : null;
126
- }
127
- catch {
128
- return null;
129
- }
195
+ const stack = readActiveStack(cwd);
196
+ return stack.length > 0 ? stack[stack.length - 1] : null;
130
197
  }
131
198
  // ---------------------------------------------------------------------------
132
199
  // PreToolUse-hook decision
133
200
  // ---------------------------------------------------------------------------
134
201
  /**
135
- * EXPERIMENTAL — parked (P3), flat-only, do NOT auto-wire. This deterministic
136
- * subagent-window tracking assumes FLAT dispatch (one active subagent at a time);
137
- * it is NOT nesting-safe Claude Code v2.1.172 added depth-5 nested subagents, so
138
- * correct tracking needs a depth-aware STACK (push on dispatch, pop on SubagentStop)
139
- * + verifying the spawn tool name (`Agent` vs `Task`). See
140
- * research/effect-boundary-design.md ("Why dropped").
202
+ * EXPERIMENTAL — parked (P3), do NOT auto-wire. The subagent-window tracking is
203
+ * now nesting-safe: a depth-aware STACK (push on dispatch, pop on SubagentStop —
204
+ * see {@link pushActiveAgent}/{@link popActiveAgent}) closes the contract-escape
205
+ * the flat single-slot model allowed under Claude Code v2.1.172 depth-5 nesting
206
+ * (certified in research/prototypes/.../AgentWindowStack.tla). The open signal
207
+ * recognizes BOTH spawn tool names (`Task` and the nested-spawn `Agent`), gated on
208
+ * a resolvable `subagent_type` so a non-spawn call never opens a frame. Still
209
+ * parked because the `effect()` sub-region goal it served was dropped (see
210
+ * research/effect-boundary-design.md, "Why dropped") — the stack is shipped for
211
+ * when active-agent contract enforcement under nesting is wanted on its own.
141
212
  *
142
- * Resolve a `Task` tool's `subagent_type` to the compiled agent `.md` to
213
+ * Resolve a spawn tool's `subagent_type` to the compiled agent `.md` to
143
214
  * activate, or null when none is found. The DETERMINISTIC open signal that
144
215
  * replaces the model-invoked `agent-start`: Claude Code fires `PreToolUse` for
145
216
  * the parent's `Task` dispatch (and `SubagentStop` when it returns), so the
@@ -8,7 +8,7 @@ exports.hasEffectBoundary = hasEffectBoundary;
8
8
  * Effect-boundary position-aware state tracking.
9
9
  * Mirrors active-unit tracking (.vigiles/active-agent.json / .vigiles/active-skill.json).
10
10
  * The PreToolUse hook reads this file to decide whether the agent is inside an
11
- * effect boundary (set by `vigiles effect-enter`) or outside it.
11
+ * effect boundary (set by `vigiles hook-runtime effect-enter`) or outside it.
12
12
  */
13
13
  const node_fs_1 = require("node:fs");
14
14
  const node_path_1 = require("node:path");
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * Safety: this executes the gate commands the skill author declared (e.g.
19
19
  * `npm test`, `validate.py`) via an explicit, user-invoked command
20
- * (`vigiles run-skill`). It is not a silent hook and runs nothing the spec
20
+ * (`vigiles hook-runtime run-skill`). It is not a silent hook and runs nothing the spec
21
21
  * didn't declare as a gate.
22
22
  */
23
23
  import type { PurityLevel } from "../../core/effects.js";
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * Safety: this executes the gate commands the skill author declared (e.g.
20
20
  * `npm test`, `validate.py`) via an explicit, user-invoked command
21
- * (`vigiles run-skill`). It is not a silent hook and runs nothing the spec
21
+ * (`vigiles hook-runtime run-skill`). It is not a silent hook and runs nothing the spec
22
22
  * didn't declare as a gate.
23
23
  */
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -5,6 +5,9 @@ exports.codexHookProtocol = {
5
5
  name: "codex",
6
6
  blockExitCode: 2,
7
7
  denyDecisionValues: ["block", "deny"],
8
+ // Codex matchers are anchored regexes (`matcher = "^Bash$"`), unlike Claude
9
+ // Code's exact tool name / `A|B` alternation.
10
+ matcherStyle: "regex",
8
11
  eventEnvVars: [
9
12
  "session_id",
10
13
  "cwd",
@@ -15,7 +15,7 @@ exports.startCodexMock = startCodexMock;
15
15
  * with the proven `response.created → … → response.completed` SSE sequence that
16
16
  * makes codex emit one assistant text message. There is no count-tokens
17
17
  * endpoint. This is the Codex-side analogue of the Anthropic Messages mock in
18
- * `src/adapters/claude-code/mock-model.ts` (`startMock`).
18
+ * `src/mock-model.ts` (`startMock`).
19
19
  *
20
20
  * The wire format here is NOT guessed — it is PROVEN end-to-end against real
21
21
  * `codex` (codex-cli 0.139.0) via the `-c model_providers.mock.*` recipe in
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The canonical vigiles command surface — the SINGLE SOURCE OF TRUTH the
3
+ * self-command-refs dogfood (`src/self-command-refs.test.ts`) cross-references
4
+ * vigiles's OWN docs + comments against, so a renamed/removed command can't
5
+ * leave a stale `vigiles <cmd>` reference rotting in the docs (the cross-ref
6
+ * moat applied to vigiles itself; the cohesive-cli-surface rule).
7
+ *
8
+ * VERBS are typed by a human/agent/CI. HOOK_RUNTIME_KINDS are the hidden runtime
9
+ * entrypoints under `vigiles hook-runtime <kind>`, emitted into hooks configs and
10
+ * never typed by hand. A behavioural test asserts the dispatch (`src/cli.ts`)
11
+ * recognizes exactly these, so this list can't silently drift from the code.
12
+ */
13
+ /** Human-facing verbs (printed in help; typed by a human/agent/CI). */
14
+ export declare const VERBS: readonly ["init", "compile", "lint", "refs", "test", "eval", "scan", "measure", "explain", "scaffold-test", "generate-types", "generate-schema", "generate-harness", "hook-runtime"];
15
+ /** Runtime entrypoint kinds under `vigiles hook-runtime <kind>` (emitted, not typed). */
16
+ export declare const HOOK_RUNTIME_KINDS: readonly ["run-program", "agent", "agent-start", "agent-done", "skill", "skill-tool", "skill-start", "skill-done", "run-skill", "intercept-tool", "guard", "action", "refs", "effect-enter", "effect-exit"];
17
+ export type Verb = (typeof VERBS)[number];
18
+ export type HookRuntimeKind = (typeof HOOK_RUNTIME_KINDS)[number];
19
+ //# sourceMappingURL=cli-commands.d.ts.map
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * The canonical vigiles command surface — the SINGLE SOURCE OF TRUTH the
4
+ * self-command-refs dogfood (`src/self-command-refs.test.ts`) cross-references
5
+ * vigiles's OWN docs + comments against, so a renamed/removed command can't
6
+ * leave a stale `vigiles <cmd>` reference rotting in the docs (the cross-ref
7
+ * moat applied to vigiles itself; the cohesive-cli-surface rule).
8
+ *
9
+ * VERBS are typed by a human/agent/CI. HOOK_RUNTIME_KINDS are the hidden runtime
10
+ * entrypoints under `vigiles hook-runtime <kind>`, emitted into hooks configs and
11
+ * never typed by hand. A behavioural test asserts the dispatch (`src/cli.ts`)
12
+ * recognizes exactly these, so this list can't silently drift from the code.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.HOOK_RUNTIME_KINDS = exports.VERBS = void 0;
16
+ /** Human-facing verbs (printed in help; typed by a human/agent/CI). */
17
+ exports.VERBS = [
18
+ "init",
19
+ "compile",
20
+ "lint",
21
+ "refs",
22
+ "test",
23
+ "eval",
24
+ "scan",
25
+ "measure",
26
+ "explain",
27
+ "scaffold-test",
28
+ "generate-types",
29
+ "generate-schema",
30
+ "generate-harness",
31
+ "hook-runtime",
32
+ ];
33
+ /** Runtime entrypoint kinds under `vigiles hook-runtime <kind>` (emitted, not typed). */
34
+ exports.HOOK_RUNTIME_KINDS = [
35
+ "run-program",
36
+ "agent",
37
+ "agent-start",
38
+ "agent-done",
39
+ "skill",
40
+ "skill-tool",
41
+ "skill-start",
42
+ "skill-done",
43
+ "run-skill",
44
+ "intercept-tool",
45
+ "guard",
46
+ "action",
47
+ "refs",
48
+ "effect-enter",
49
+ "effect-exit",
50
+ ];
51
+ //# sourceMappingURL=cli-commands.js.map