vigiles 5.1.0 → 6.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.
- package/README.md +59 -18
- package/dist/adapters/claude-code/adapter.js +1 -0
- package/dist/adapters/claude-code/agent-runtime.d.ts +45 -6
- package/dist/adapters/claude-code/agent-runtime.js +94 -8
- package/dist/adapters/claude-code/dialect.d.ts +34 -0
- package/dist/adapters/claude-code/dialect.js +51 -19
- package/dist/adapters/claude-code/effect-region.d.ts +9 -0
- package/dist/adapters/claude-code/effect-region.js +45 -0
- package/dist/adapters/claude-code/layout.js +3 -0
- package/dist/adapters/claude-code/skill-runtime.d.ts +25 -0
- package/dist/adapters/claude-code/skill-runtime.js +40 -0
- package/dist/adapters/claude-code/typed-spec.d.ts +58 -0
- package/dist/adapters/claude-code/typed-spec.js +55 -0
- package/dist/adapters/codex/adapter.js +3 -0
- package/dist/adapters/codex/layout.js +3 -0
- package/dist/adapters/opencode/adapter.js +1 -0
- package/dist/adapters/opencode/layout.js +3 -0
- package/dist/check.d.ts +8 -0
- package/dist/check.js +27 -3
- package/dist/claude-code.d.ts +1 -0
- package/dist/claude-code.js +8 -1
- package/dist/cli.js +469 -88
- package/dist/core/adapter.d.ts +10 -0
- package/dist/core/bash-effects.d.ts +41 -0
- package/dist/core/bash-effects.js +405 -0
- package/dist/core/compile.d.ts +3 -1
- package/dist/core/compile.js +176 -39
- package/dist/core/dialect.d.ts +10 -0
- package/dist/core/effects.d.ts +172 -0
- package/dist/core/effects.js +245 -0
- package/dist/core/generate-harness.d.ts +187 -0
- package/dist/core/generate-harness.js +337 -0
- package/dist/core/layout.d.ts +6 -0
- package/dist/core/mcp-tool.d.ts +1 -1
- package/dist/core/orphans.js +21 -0
- package/dist/core/spec.d.ts +432 -11
- package/dist/core/spec.js +166 -3
- package/dist/core/tool-contract.d.ts +1 -1
- package/dist/core/types.d.ts +6 -6
- package/dist/core/validate.js +4 -4
- package/dist/harness-test.d.ts +7 -0
- package/dist/harness-test.js +19 -7
- package/dist/leaderboard.d.ts +2 -0
- package/dist/leaderboard.js +2 -0
- package/dist/optimize.d.ts +74 -0
- package/dist/optimize.js +94 -0
- package/dist/scaffold-test.d.ts +58 -0
- package/dist/scaffold-test.js +263 -0
- package/dist/scan.d.ts +40 -0
- package/dist/scan.js +91 -43
- package/dist/score-explainer.d.ts +69 -0
- package/dist/score-explainer.js +169 -0
- package/dist/test-coverage.d.ts +7 -0
- package/dist/test-coverage.js +39 -24
- package/package.json +2 -1
- package/skills/{migrate-to-spec → adopt-spec}/SKILL.md +4 -4
- package/skills/edit-spec/SKILL.md +1 -1
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 &
|
|
8
|
+
<strong>Lint, test & measure the harness your AI agent runs on.</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>,
|
|
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.
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
@@ -20,14 +20,17 @@
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
-
`Agent = Model + Harness`. You'd never ship an app without a linter
|
|
24
|
-
|
|
25
|
-
deterministic layer for it
|
|
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:
|
|
26
28
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
| **🔎 Lint**
|
|
30
|
-
| **🧪 Test**
|
|
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)** |
|
|
31
34
|
|
|
32
35
|
Pick the one that hurts today. **Works with Claude Code and Codex**
|
|
33
36
|
([`vigiles/codex`](docs/harnesses.md)), and you can
|
|
@@ -92,7 +95,16 @@ CLAUDE.md:
|
|
|
92
95
|
File paths, scripts, and code symbols — plus linter rules across **7 catalogs**
|
|
93
96
|
(the rule exists **and is enabled**). Start with one inline comment, no new files;
|
|
94
97
|
step up to a typed `.spec.ts` (compiled to CLAUDE.md, compiler-grade) when you want
|
|
95
|
-
it
|
|
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)**
|
|
101
|
+
|
|
102
|
+
> **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)**
|
|
96
108
|
|
|
97
109
|
**Same cross-reference, any plugin.** `npx vigiles scan` checks a plugin's
|
|
98
110
|
_contracts_ — every subagent tool, `mcp__server__tool`, `mcp_tool` hook, hook
|
|
@@ -121,25 +133,54 @@ It goes well past _"did it fire?"_:
|
|
|
121
133
|
|
|
122
134
|
- **Hooks block** what they must — `runHook`, or the real agent CLI via `runHarnessTest`.
|
|
123
135
|
- **Skills trigger** on the right prompts and stay quiet on the wrong ones — recall _and_ precision (`measureTriggerRate`).
|
|
124
|
-
- **Behaviour is good** — score a skill's output directly, or A/B it on-vs-off for the real lift
|
|
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).
|
|
125
137
|
- **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.
|
|
126
138
|
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
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)**
|
|
142
|
+
|
|
143
|
+
## ③ Measure — does it actually help, or just cost more?
|
|
144
|
+
|
|
145
|
+
A skill claims "65% fewer tokens." A plugin promises "3× faster." Stars and
|
|
146
|
+
vibes — **zero measurement**. vigiles A/Bs the claim on real coding tasks, the
|
|
147
|
+
harness loaded exactly as it ships, and reports the **metric triple**:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { measureArms } from "vigiles/testing";
|
|
151
|
+
|
|
152
|
+
const r = await measureArms({
|
|
153
|
+
fixture: { "in.txt": "Implement a slug helper." },
|
|
154
|
+
task: "Read in.txt, write slugify() to slug.js, explain. Stop.",
|
|
155
|
+
arms: { baseline: {}, skill: { files: { "SKILL.md": THE_SKILL } } },
|
|
156
|
+
measure: (ctx) => ({ cost: ctx.usage.costUsd, correct: check(ctx) }),
|
|
157
|
+
});
|
|
158
|
+
// → is the bill lower? did correctness survive? (not just "fewer output tokens")
|
|
159
|
+
```
|
|
160
|
+
|
|
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.
|
|
163
|
+
- **The blast radius** — correctness, a deterministic 1/0. A token win that breaks the code is **not a win**.
|
|
164
|
+
|
|
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:
|
|
129
173
|
|
|
130
174
|
| | Runs on | Cost |
|
|
131
175
|
| ---------------------- | ----------------------- | ------------------------------------------- |
|
|
132
176
|
| promptfoo, DeepEval, … | metered API SDK | billed **per token, every run** |
|
|
133
177
|
| **vigiles** | your Claude Pro/Max sub | **$0 extra** — and most tiers need no model |
|
|
134
178
|
|
|
135
|
-
That's why you can eval your harness on every change, not just once.
|
|
136
|
-
**[How it works →](docs/harness-testing.md)** · **[Why it's affordable →](docs/eval-architecture.md)** · **[Safety model →](docs/safety.md)**
|
|
137
|
-
|
|
138
179
|
## More
|
|
139
180
|
|
|
140
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?
|
|
141
|
-
- **[CLI & GitHub Action →](docs/cli.md)** — every command, the Action (inputs / output / versioning), the Claude Code plugin
|
|
142
|
-
- **[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; `
|
|
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.
|
|
143
184
|
- **[Docs index →](docs/README.md)** · **[Research →](research/README.md)** · **[Related tools →](docs/related-tools.md)** (ast-grep, Dependency Cruiser, Ruler, rulesync).
|
|
144
185
|
- Companion to [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need).
|
|
145
186
|
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* (mirrors `.vigiles/active-skill.json`). The decision logic below is
|
|
23
23
|
* harness-agnostic and fully testable.
|
|
24
24
|
*/
|
|
25
|
+
import type { PurityLevel } from "../../core/effects.js";
|
|
25
26
|
/**
|
|
26
27
|
* Parse an agent's allowed-tools contract from its compiled markdown.
|
|
27
28
|
*
|
|
@@ -41,6 +42,14 @@ export declare function parseAgentTools(markdown: string): string[] | null;
|
|
|
41
42
|
* rail (`tools:`) and the `disallowed-tools-contract` scan/lint.
|
|
42
43
|
*/
|
|
43
44
|
export declare function parseAgentToolList(markdown: string, key: string): string[] | null;
|
|
45
|
+
/**
|
|
46
|
+
* Parse the declared purity floor from a compiled agent's `.md` — the
|
|
47
|
+
* `<!-- vigiles:purity:LEVEL -->` marker `compile` emits (see `purityMarker`).
|
|
48
|
+
* Returns null when no marker is present (the unit declared no floor, so the
|
|
49
|
+
* purity gate imposes no constraint). The single source of truth the runtime
|
|
50
|
+
* gate reads, exactly like `tools:` for the tool-contract rail.
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseAgentPurity(markdown: string): PurityLevel | null;
|
|
44
53
|
export interface PreToolDecision {
|
|
45
54
|
/** Whether the tool call is allowed (true) or blocked (false). */
|
|
46
55
|
readonly allow: boolean;
|
|
@@ -64,11 +73,41 @@ export declare function clearActiveAgent(cwd: string): void;
|
|
|
64
73
|
/** The path of the active agent's compiled `.md`, or null when none is active. */
|
|
65
74
|
export declare function readActiveAgent(cwd: string): string | null;
|
|
66
75
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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").
|
|
82
|
+
*
|
|
83
|
+
* Resolve a `Task` tool's `subagent_type` to the compiled agent `.md` to
|
|
84
|
+
* activate, or null when none is found. The DETERMINISTIC open signal that
|
|
85
|
+
* replaces the model-invoked `agent-start`: Claude Code fires `PreToolUse` for
|
|
86
|
+
* the parent's `Task` dispatch (and `SubagentStop` when it returns), so the
|
|
87
|
+
* harness — not the model — brackets the subagent's active window. The name is
|
|
88
|
+
* the last ":"-segment (a `--plugin-dir` subagent_type is namespaced
|
|
89
|
+
* "plugin:name"); searched in `agents/` under the cwd then the plugin root. A
|
|
90
|
+
* path under the cwd is returned relative (readActiveAgent resolves vs cwd); a
|
|
91
|
+
* plugin-root hit is absolute. Returns null on an unknown agent (fail-open: an
|
|
92
|
+
* unresolved subagent is simply not gated, exactly as before agent-start ran).
|
|
93
|
+
*/
|
|
94
|
+
export declare function resolveDispatchedAgent(subagentType: string, cwd: string, pluginRoot?: string): string | null;
|
|
95
|
+
/**
|
|
96
|
+
* The agent `.md` to activate for a `PreToolUse(Task)` event, or null. Pure
|
|
97
|
+
* (reads `tool_input.subagent_type`, resolves via {@link resolveDispatchedAgent}).
|
|
98
|
+
*/
|
|
99
|
+
export declare function decideTaskDispatch(toolInput: unknown, cwd: string, pluginRoot?: string): string | null;
|
|
100
|
+
/**
|
|
101
|
+
* PreToolUse-hook decision. If an agent is active, enforce BOTH deterministic
|
|
102
|
+
* rails its compiled `.md` declares, in order:
|
|
103
|
+
*
|
|
104
|
+
* 1. the tool-contract rail (`tools:`) — allow only listed tools;
|
|
105
|
+
* 2. the purity gate (`vigiles:purity:`) — allow only calls within the declared
|
|
106
|
+
* effect floor, refining `Bash` by the live `command` (`decidePurityGate`).
|
|
107
|
+
*
|
|
108
|
+
* The first to deny wins, feeding its reason back to the model. With no active
|
|
109
|
+
* agent (or one that declared neither contract), always allow — the rails only
|
|
110
|
+
* constrain agents that opted in.
|
|
72
111
|
*/
|
|
73
|
-
export declare function evaluatePreToolUse(cwd: string, tool: string): PreToolDecision;
|
|
112
|
+
export declare function evaluatePreToolUse(cwd: string, tool: string, command?: string): PreToolDecision;
|
|
74
113
|
//# sourceMappingURL=agent-runtime.d.ts.map
|
|
@@ -26,14 +26,20 @@
|
|
|
26
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
27
|
exports.parseAgentTools = parseAgentTools;
|
|
28
28
|
exports.parseAgentToolList = parseAgentToolList;
|
|
29
|
+
exports.parseAgentPurity = parseAgentPurity;
|
|
29
30
|
exports.decidePreToolUse = decidePreToolUse;
|
|
30
31
|
exports.setActiveAgent = setActiveAgent;
|
|
31
32
|
exports.clearActiveAgent = clearActiveAgent;
|
|
32
33
|
exports.readActiveAgent = readActiveAgent;
|
|
34
|
+
exports.resolveDispatchedAgent = resolveDispatchedAgent;
|
|
35
|
+
exports.decideTaskDispatch = decideTaskDispatch;
|
|
33
36
|
exports.evaluatePreToolUse = evaluatePreToolUse;
|
|
34
37
|
const node_fs_1 = require("node:fs");
|
|
35
38
|
const node_path_1 = require("node:path");
|
|
36
39
|
const frontmatter_read_js_1 = require("../../core/frontmatter-read.js");
|
|
40
|
+
const effects_js_1 = require("../../core/effects.js");
|
|
41
|
+
const dialect_js_1 = require("./dialect.js");
|
|
42
|
+
const effect_region_js_1 = require("./effect-region.js");
|
|
37
43
|
// ---------------------------------------------------------------------------
|
|
38
44
|
// Parse the tool contract from a compiled agent .md
|
|
39
45
|
// ---------------------------------------------------------------------------
|
|
@@ -60,6 +66,18 @@ function parseAgentTools(markdown) {
|
|
|
60
66
|
function parseAgentToolList(markdown, key) {
|
|
61
67
|
return (0, frontmatter_read_js_1.frontmatterList)((0, frontmatter_read_js_1.readFrontmatter)(markdown), key);
|
|
62
68
|
}
|
|
69
|
+
const PURITY_RE = /<!--\s*vigiles:purity:(pure|bounded|unrestricted)\s*-->/;
|
|
70
|
+
/**
|
|
71
|
+
* Parse the declared purity floor from a compiled agent's `.md` — the
|
|
72
|
+
* `<!-- vigiles:purity:LEVEL -->` marker `compile` emits (see `purityMarker`).
|
|
73
|
+
* Returns null when no marker is present (the unit declared no floor, so the
|
|
74
|
+
* purity gate imposes no constraint). The single source of truth the runtime
|
|
75
|
+
* gate reads, exactly like `tools:` for the tool-contract rail.
|
|
76
|
+
*/
|
|
77
|
+
function parseAgentPurity(markdown) {
|
|
78
|
+
const m = PURITY_RE.exec(markdown);
|
|
79
|
+
return m ? m[1] : null;
|
|
80
|
+
}
|
|
63
81
|
/**
|
|
64
82
|
* Decide whether `tool` is allowed under an agent's tool contract. Pure, so the
|
|
65
83
|
* rail is unit-testable without spawning anything.
|
|
@@ -114,20 +132,88 @@ function readActiveAgent(cwd) {
|
|
|
114
132
|
// PreToolUse-hook decision
|
|
115
133
|
// ---------------------------------------------------------------------------
|
|
116
134
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
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").
|
|
141
|
+
*
|
|
142
|
+
* Resolve a `Task` tool's `subagent_type` to the compiled agent `.md` to
|
|
143
|
+
* activate, or null when none is found. The DETERMINISTIC open signal that
|
|
144
|
+
* replaces the model-invoked `agent-start`: Claude Code fires `PreToolUse` for
|
|
145
|
+
* the parent's `Task` dispatch (and `SubagentStop` when it returns), so the
|
|
146
|
+
* harness — not the model — brackets the subagent's active window. The name is
|
|
147
|
+
* the last ":"-segment (a `--plugin-dir` subagent_type is namespaced
|
|
148
|
+
* "plugin:name"); searched in `agents/` under the cwd then the plugin root. A
|
|
149
|
+
* path under the cwd is returned relative (readActiveAgent resolves vs cwd); a
|
|
150
|
+
* plugin-root hit is absolute. Returns null on an unknown agent (fail-open: an
|
|
151
|
+
* unresolved subagent is simply not gated, exactly as before agent-start ran).
|
|
152
|
+
*/
|
|
153
|
+
function resolveDispatchedAgent(subagentType, cwd, pluginRoot) {
|
|
154
|
+
const name = subagentType.split(":").pop()?.trim();
|
|
155
|
+
if (!name)
|
|
156
|
+
return null;
|
|
157
|
+
const rel = (0, node_path_1.join)("agents", `${name}.md`);
|
|
158
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, rel)))
|
|
159
|
+
return rel;
|
|
160
|
+
if (pluginRoot && (0, node_fs_1.existsSync)((0, node_path_1.resolve)(pluginRoot, rel))) {
|
|
161
|
+
return (0, node_path_1.resolve)(pluginRoot, rel);
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The agent `.md` to activate for a `PreToolUse(Task)` event, or null. Pure
|
|
167
|
+
* (reads `tool_input.subagent_type`, resolves via {@link resolveDispatchedAgent}).
|
|
122
168
|
*/
|
|
123
|
-
function
|
|
169
|
+
function decideTaskDispatch(toolInput, cwd, pluginRoot) {
|
|
170
|
+
const st = toolInput?.subagent_type;
|
|
171
|
+
if (typeof st !== "string" || !st)
|
|
172
|
+
return null;
|
|
173
|
+
return resolveDispatchedAgent(st, cwd, pluginRoot);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* PreToolUse-hook decision. If an agent is active, enforce BOTH deterministic
|
|
177
|
+
* rails its compiled `.md` declares, in order:
|
|
178
|
+
*
|
|
179
|
+
* 1. the tool-contract rail (`tools:`) — allow only listed tools;
|
|
180
|
+
* 2. the purity gate (`vigiles:purity:`) — allow only calls within the declared
|
|
181
|
+
* effect floor, refining `Bash` by the live `command` (`decidePurityGate`).
|
|
182
|
+
*
|
|
183
|
+
* The first to deny wins, feeding its reason back to the model. With no active
|
|
184
|
+
* agent (or one that declared neither contract), always allow — the rails only
|
|
185
|
+
* constrain agents that opted in.
|
|
186
|
+
*/
|
|
187
|
+
function evaluatePreToolUse(cwd, tool, command) {
|
|
124
188
|
const agentPath = readActiveAgent(cwd);
|
|
125
189
|
if (!agentPath)
|
|
126
190
|
return { allow: true, message: "" };
|
|
127
191
|
const full = (0, node_path_1.resolve)(cwd, agentPath);
|
|
128
192
|
if (!(0, node_fs_1.existsSync)(full))
|
|
129
193
|
return { allow: true, message: "" };
|
|
130
|
-
const
|
|
131
|
-
|
|
194
|
+
const md = (0, node_fs_1.readFileSync)(full, "utf-8");
|
|
195
|
+
// 1) Tool-contract rail — the declared allowlist.
|
|
196
|
+
const rail = decidePreToolUse(parseAgentTools(md), tool);
|
|
197
|
+
if (!rail.allow)
|
|
198
|
+
return rail;
|
|
199
|
+
// 2) Purity gate — the declared effect floor, refined by the live command.
|
|
200
|
+
// If an effect boundary is declared, tighten to "pure" outside it and apply
|
|
201
|
+
// the declared purity (or "unrestricted") inside.
|
|
202
|
+
const purity = parseAgentPurity(md);
|
|
203
|
+
const boundary = (0, effect_region_js_1.hasEffectBoundary)(md);
|
|
204
|
+
if (boundary) {
|
|
205
|
+
const effective = (0, effect_region_js_1.readEffectActive)(cwd)
|
|
206
|
+
? (purity ?? "unrestricted")
|
|
207
|
+
: "pure";
|
|
208
|
+
const gate = (0, effects_js_1.decidePurityGate)(effective, tool, command, dialect_js_1.claudeCodeDialect);
|
|
209
|
+
if (!gate.allow)
|
|
210
|
+
return gate;
|
|
211
|
+
}
|
|
212
|
+
else if (purity) {
|
|
213
|
+
const gate = (0, effects_js_1.decidePurityGate)(purity, tool, command, dialect_js_1.claudeCodeDialect);
|
|
214
|
+
if (!gate.allow)
|
|
215
|
+
return gate;
|
|
216
|
+
}
|
|
217
|
+
return { allow: true, message: "" };
|
|
132
218
|
}
|
|
133
219
|
//# sourceMappingURL=agent-runtime.js.map
|
|
@@ -8,6 +8,40 @@
|
|
|
8
8
|
* in its adapter (e.g. `src/adapters/codex/dialect.ts` exporting `codexDialect`).
|
|
9
9
|
*/
|
|
10
10
|
import type { HarnessDialect } from "../../core/dialect.js";
|
|
11
|
+
/**
|
|
12
|
+
* The Claude Code built-in subagent tool catalog as a `const` tuple, so a typed
|
|
13
|
+
* authoring surface can derive a LITERAL union (`ClaudeCodeBuiltinTool`) from it.
|
|
14
|
+
* `claudeCodeDialect.builtinAgentTools` references this same array — one source
|
|
15
|
+
* of truth for the runtime catalog AND the compile-time tool vocabulary.
|
|
16
|
+
*/
|
|
17
|
+
export declare const claudeCodeBuiltinAgentTools: readonly ["Read", "Write", "Edit", "MultiEdit", "Bash", "BashOutput", "KillBash", "Grep", "Glob", "LS", "WebSearch", "WebFetch", "NotebookEdit", "TodoWrite", "Task", "Skill"];
|
|
18
|
+
/**
|
|
19
|
+
* The Claude Code side-effecting tools as a `const` tuple (the complement of
|
|
20
|
+
* read-only within `builtinAgentTools`). `claudeCodeDialect.sideEffectingTools`
|
|
21
|
+
* references this; the typed vocabulary derives the read-only / bounded splits.
|
|
22
|
+
*/
|
|
23
|
+
export declare const claudeCodeSideEffectingTools: readonly ["Bash", "BashOutput", "KillBash", "Edit", "MultiEdit", "Write", "NotebookEdit", "WebFetch", "WebSearch", "Skill", "Task", "TodoWrite"];
|
|
11
24
|
export declare const claudeCodeDialect: HarnessDialect;
|
|
25
|
+
/** Every Claude Code built-in subagent tool (literal union). */
|
|
26
|
+
export type ClaudeCodeBuiltinTool = (typeof claudeCodeBuiltinAgentTools)[number];
|
|
27
|
+
/** The side-effecting subset (literal union). */
|
|
28
|
+
export type ClaudeCodeSideEffectingTool = (typeof claudeCodeSideEffectingTools)[number];
|
|
29
|
+
/**
|
|
30
|
+
* Read-only built-in tools — the complement of the side-effecting set within
|
|
31
|
+
* the built-in catalog. The tools a `pure` CC unit may declare.
|
|
32
|
+
*/
|
|
33
|
+
export type ClaudeCodeReadOnlyTool = Exclude<ClaudeCodeBuiltinTool, ClaudeCodeSideEffectingTool>;
|
|
34
|
+
/**
|
|
35
|
+
* Tools a `bounded` CC unit may declare: read-only ∪ the decidable
|
|
36
|
+
* side-effecting tools (Write/Edit/MultiEdit/NotebookEdit) ∪ `Bash` (its
|
|
37
|
+
* command is decided at RUNTIME by the gate). Bars MCP / unknown / wildcard —
|
|
38
|
+
* those are simply not in the built-in union, so listing one is a `tsc` error.
|
|
39
|
+
*
|
|
40
|
+
* NOTE: `BashOutput`/`KillBash` are read-only-ish helpers tied to a running
|
|
41
|
+
* Bash; `Bash` is the admitting tool, so they're included via the read-only
|
|
42
|
+
* exclusion path only if read-only — here they stay side-effecting, hence the
|
|
43
|
+
* explicit add of the bounded-decidable set plus `Bash`.
|
|
44
|
+
*/
|
|
45
|
+
export type ClaudeCodeBoundedTool = ClaudeCodeReadOnlyTool | "Write" | "Edit" | "MultiEdit" | "NotebookEdit" | "Bash";
|
|
12
46
|
export type { HarnessDialect } from "../../core/dialect.js";
|
|
13
47
|
//# sourceMappingURL=dialect.d.ts.map
|
|
@@ -1,28 +1,54 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.claudeCodeDialect = void 0;
|
|
3
|
+
exports.claudeCodeDialect = exports.claudeCodeSideEffectingTools = exports.claudeCodeBuiltinAgentTools = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The Claude Code built-in subagent tool catalog as a `const` tuple, so a typed
|
|
6
|
+
* authoring surface can derive a LITERAL union (`ClaudeCodeBuiltinTool`) from it.
|
|
7
|
+
* `claudeCodeDialect.builtinAgentTools` references this same array — one source
|
|
8
|
+
* of truth for the runtime catalog AND the compile-time tool vocabulary.
|
|
9
|
+
*/
|
|
10
|
+
exports.claudeCodeBuiltinAgentTools = [
|
|
11
|
+
"Read",
|
|
12
|
+
"Write",
|
|
13
|
+
"Edit",
|
|
14
|
+
"MultiEdit",
|
|
15
|
+
"Bash",
|
|
16
|
+
"BashOutput",
|
|
17
|
+
"KillBash",
|
|
18
|
+
"Grep",
|
|
19
|
+
"Glob",
|
|
20
|
+
"LS",
|
|
21
|
+
"WebSearch",
|
|
22
|
+
"WebFetch",
|
|
23
|
+
"NotebookEdit",
|
|
24
|
+
"TodoWrite",
|
|
25
|
+
"Task",
|
|
26
|
+
"Skill",
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* The Claude Code side-effecting tools as a `const` tuple (the complement of
|
|
30
|
+
* read-only within `builtinAgentTools`). `claudeCodeDialect.sideEffectingTools`
|
|
31
|
+
* references this; the typed vocabulary derives the read-only / bounded splits.
|
|
32
|
+
*/
|
|
33
|
+
exports.claudeCodeSideEffectingTools = [
|
|
34
|
+
"Bash",
|
|
35
|
+
"BashOutput",
|
|
36
|
+
"KillBash",
|
|
37
|
+
"Edit",
|
|
38
|
+
"MultiEdit",
|
|
39
|
+
"Write",
|
|
40
|
+
"NotebookEdit",
|
|
41
|
+
"WebFetch",
|
|
42
|
+
"WebSearch",
|
|
43
|
+
"Skill",
|
|
44
|
+
"Task",
|
|
45
|
+
"TodoWrite",
|
|
46
|
+
];
|
|
4
47
|
exports.claudeCodeDialect = {
|
|
5
48
|
name: "claude-code",
|
|
6
49
|
// The tool contract a subagent may declare — the rails it runs on. Anything
|
|
7
50
|
// else must be an MCP tool, else it's a typo / nonexistent tool.
|
|
8
|
-
builtinAgentTools:
|
|
9
|
-
"Read",
|
|
10
|
-
"Write",
|
|
11
|
-
"Edit",
|
|
12
|
-
"MultiEdit",
|
|
13
|
-
"Bash",
|
|
14
|
-
"BashOutput",
|
|
15
|
-
"KillBash",
|
|
16
|
-
"Grep",
|
|
17
|
-
"Glob",
|
|
18
|
-
"LS",
|
|
19
|
-
"WebSearch",
|
|
20
|
-
"WebFetch",
|
|
21
|
-
"NotebookEdit",
|
|
22
|
-
"TodoWrite",
|
|
23
|
-
"Task",
|
|
24
|
-
"Skill",
|
|
25
|
-
],
|
|
51
|
+
builtinAgentTools: exports.claudeCodeBuiltinAgentTools,
|
|
26
52
|
// Tools the platform never exposes to a subagent, whatever the list says — so
|
|
27
53
|
// a subagent listing one is a guaranteed-dead reference only a compiler catches.
|
|
28
54
|
neverAvailableTools: [
|
|
@@ -63,5 +89,11 @@ exports.claudeCodeDialect = {
|
|
|
63
89
|
// Claude Code reads the full SKILL.md frontmatter set (description,
|
|
64
90
|
// disable-model-invocation, argument-hint, …).
|
|
65
91
|
skillFrontmatter: "claude-code",
|
|
92
|
+
// Tools that produce side effects in Claude Code. The complement — the
|
|
93
|
+
// read-only tools — are: Read, Grep, Glob, LS, ToolSearch (and LSP/Agent
|
|
94
|
+
// which are not in the subagent catalog). Bash is side-effecting because
|
|
95
|
+
// `cat` and `rm -rf` are the same tool at the tool-name level — the
|
|
96
|
+
// sandbox is the only closure for subprocess effects.
|
|
97
|
+
sideEffectingTools: exports.claudeCodeSideEffectingTools,
|
|
66
98
|
};
|
|
67
99
|
//# sourceMappingURL=dialect.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Record that the agent has entered an effect boundary. */
|
|
2
|
+
export declare function setEffectActive(cwd: string): void;
|
|
3
|
+
/** Clear the effect-active marker (the agent exited the effect boundary). */
|
|
4
|
+
export declare function clearEffectActive(cwd: string): void;
|
|
5
|
+
/** True iff the agent is currently inside an effect boundary. Tolerates malformed file. */
|
|
6
|
+
export declare function readEffectActive(cwd: string): boolean;
|
|
7
|
+
/** True iff the compiled markdown declares an effect boundary (`<!-- vigiles:effect -->`). */
|
|
8
|
+
export declare function hasEffectBoundary(markdown: string): boolean;
|
|
9
|
+
//# sourceMappingURL=effect-region.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setEffectActive = setEffectActive;
|
|
4
|
+
exports.clearEffectActive = clearEffectActive;
|
|
5
|
+
exports.readEffectActive = readEffectActive;
|
|
6
|
+
exports.hasEffectBoundary = hasEffectBoundary;
|
|
7
|
+
/**
|
|
8
|
+
* Effect-boundary position-aware state tracking.
|
|
9
|
+
* Mirrors active-unit tracking (.vigiles/active-agent.json / .vigiles/active-skill.json).
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
const node_fs_1 = require("node:fs");
|
|
14
|
+
const node_path_1 = require("node:path");
|
|
15
|
+
const EFFECT_ACTIVE_PATH = ".vigiles/effect-active.json";
|
|
16
|
+
/** Record that the agent has entered an effect boundary. */
|
|
17
|
+
function setEffectActive(cwd) {
|
|
18
|
+
const p = (0, node_path_1.resolve)(cwd, EFFECT_ACTIVE_PATH);
|
|
19
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(p), { recursive: true });
|
|
20
|
+
(0, node_fs_1.writeFileSync)(p, JSON.stringify({ active: true }) + "\n");
|
|
21
|
+
}
|
|
22
|
+
/** Clear the effect-active marker (the agent exited the effect boundary). */
|
|
23
|
+
function clearEffectActive(cwd) {
|
|
24
|
+
const p = (0, node_path_1.resolve)(cwd, EFFECT_ACTIVE_PATH);
|
|
25
|
+
if ((0, node_fs_1.existsSync)(p))
|
|
26
|
+
(0, node_fs_1.rmSync)(p);
|
|
27
|
+
}
|
|
28
|
+
/** True iff the agent is currently inside an effect boundary. Tolerates malformed file. */
|
|
29
|
+
function readEffectActive(cwd) {
|
|
30
|
+
const p = (0, node_path_1.resolve)(cwd, EFFECT_ACTIVE_PATH);
|
|
31
|
+
if (!(0, node_fs_1.existsSync)(p))
|
|
32
|
+
return false;
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(p, "utf-8"));
|
|
35
|
+
return parsed.active === true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** True iff the compiled markdown declares an effect boundary (`<!-- vigiles:effect -->`). */
|
|
42
|
+
function hasEffectBoundary(markdown) {
|
|
43
|
+
return markdown.includes("<!-- vigiles:effect -->");
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=effect-region.js.map
|
|
@@ -9,6 +9,9 @@ exports.claudeCodeLayout = {
|
|
|
9
9
|
settingsFormat: "json",
|
|
10
10
|
instructionFile: "CLAUDE.md",
|
|
11
11
|
surfaceDirs: ["skills", "agents", "commands"],
|
|
12
|
+
skillDir: "skills",
|
|
13
|
+
agentDir: "agents",
|
|
14
|
+
commandDir: "commands",
|
|
12
15
|
materializeRoot: ".claude",
|
|
13
16
|
pluginRootToken: "${CLAUDE_PLUGIN_ROOT}",
|
|
14
17
|
mcpConfigFile: ".mcp.json",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* (`vigiles run-skill`). It is not a silent hook and runs nothing the spec
|
|
21
21
|
* didn't declare as a gate.
|
|
22
22
|
*/
|
|
23
|
+
import type { PurityLevel } from "../../core/effects.js";
|
|
23
24
|
export type RuntimeGate = {
|
|
24
25
|
readonly kind: "cmd";
|
|
25
26
|
readonly command: string;
|
|
@@ -86,6 +87,30 @@ export declare function setActiveSkill(cwd: string, skillPath: string): void;
|
|
|
86
87
|
export declare function clearActiveSkill(cwd: string): void;
|
|
87
88
|
/** The path of the active skill, or null when none is in progress. */
|
|
88
89
|
export declare function readActiveSkill(cwd: string): string | null;
|
|
90
|
+
/**
|
|
91
|
+
* Parse the declared purity floor from a compiled SKILL.md — the
|
|
92
|
+
* `<!-- vigiles:purity:LEVEL -->` marker `compileSkill` emits (see
|
|
93
|
+
* `purityMarker` in compile.ts). Returns null when no marker is present (the
|
|
94
|
+
* skill declared no floor, so the purity gate imposes no constraint). Mirrors
|
|
95
|
+
* `parseAgentPurity` in agent-runtime.ts.
|
|
96
|
+
*/
|
|
97
|
+
export declare function parseSkillPurity(markdown: string): PurityLevel | null;
|
|
98
|
+
/** A runtime allow/deny decision (mirrors PreToolDecision in agent-runtime.ts). */
|
|
99
|
+
export interface SkillPreToolDecision {
|
|
100
|
+
/** Whether the tool call is allowed (true) or blocked (false). */
|
|
101
|
+
readonly allow: boolean;
|
|
102
|
+
/** Message fed back to the model on a block; empty on allow. */
|
|
103
|
+
readonly message: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* PreToolUse purity gate for skills. Mirrors `evaluatePreToolUse` in
|
|
107
|
+
* agent-runtime.ts, but enforces ONLY the purity floor — skills have no
|
|
108
|
+
* tools-allowlist rail (that's a separate, future concern). If a skill is
|
|
109
|
+
* active and declares a `vigiles:purity:` marker, `decidePurityGate` checks
|
|
110
|
+
* the live call (refining `Bash` by the concrete command via `isReadOnlyBash`).
|
|
111
|
+
* With no active skill, a missing `.md`, or no purity marker, always allows.
|
|
112
|
+
*/
|
|
113
|
+
export declare function evaluateSkillPreToolUse(cwd: string, tool: string, command?: string): SkillPreToolDecision;
|
|
89
114
|
export interface StopDecision {
|
|
90
115
|
/** Whether the agent may stop (true) or must keep working (false). */
|
|
91
116
|
readonly allow: boolean;
|
|
@@ -30,10 +30,14 @@ exports.runSkillGates = runSkillGates;
|
|
|
30
30
|
exports.setActiveSkill = setActiveSkill;
|
|
31
31
|
exports.clearActiveSkill = clearActiveSkill;
|
|
32
32
|
exports.readActiveSkill = readActiveSkill;
|
|
33
|
+
exports.parseSkillPurity = parseSkillPurity;
|
|
34
|
+
exports.evaluateSkillPreToolUse = evaluateSkillPreToolUse;
|
|
33
35
|
exports.evaluateStopHook = evaluateStopHook;
|
|
34
36
|
const node_child_process_1 = require("node:child_process");
|
|
35
37
|
const node_fs_1 = require("node:fs");
|
|
36
38
|
const node_path_1 = require("node:path");
|
|
39
|
+
const effects_js_1 = require("../../core/effects.js");
|
|
40
|
+
const dialect_js_1 = require("./dialect.js");
|
|
37
41
|
const STEP_RE = /^###\s+Step\s+(\d+)/;
|
|
38
42
|
const GATE_CMD_RE = /<!--\s*vigiles:gate\s+"([^"]*)"(?:\s+retry:(\d+))?\s*-->/;
|
|
39
43
|
const GATE_FILE_RE = /<!--\s*vigiles:gate\s+file:(\S+)\s*-->/;
|
|
@@ -257,6 +261,42 @@ function readActiveSkill(cwd) {
|
|
|
257
261
|
return null;
|
|
258
262
|
}
|
|
259
263
|
}
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Skill purity gate (mirrors agent-runtime.ts parseAgentPurity / evaluatePreToolUse)
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
const PURITY_RE = /<!--\s*vigiles:purity:(pure|bounded|unrestricted)\s*-->/;
|
|
268
|
+
/**
|
|
269
|
+
* Parse the declared purity floor from a compiled SKILL.md — the
|
|
270
|
+
* `<!-- vigiles:purity:LEVEL -->` marker `compileSkill` emits (see
|
|
271
|
+
* `purityMarker` in compile.ts). Returns null when no marker is present (the
|
|
272
|
+
* skill declared no floor, so the purity gate imposes no constraint). Mirrors
|
|
273
|
+
* `parseAgentPurity` in agent-runtime.ts.
|
|
274
|
+
*/
|
|
275
|
+
function parseSkillPurity(markdown) {
|
|
276
|
+
const m = PURITY_RE.exec(markdown);
|
|
277
|
+
return m ? m[1] : null;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* PreToolUse purity gate for skills. Mirrors `evaluatePreToolUse` in
|
|
281
|
+
* agent-runtime.ts, but enforces ONLY the purity floor — skills have no
|
|
282
|
+
* tools-allowlist rail (that's a separate, future concern). If a skill is
|
|
283
|
+
* active and declares a `vigiles:purity:` marker, `decidePurityGate` checks
|
|
284
|
+
* the live call (refining `Bash` by the concrete command via `isReadOnlyBash`).
|
|
285
|
+
* With no active skill, a missing `.md`, or no purity marker, always allows.
|
|
286
|
+
*/
|
|
287
|
+
function evaluateSkillPreToolUse(cwd, tool, command) {
|
|
288
|
+
const skillPath = readActiveSkill(cwd);
|
|
289
|
+
if (!skillPath)
|
|
290
|
+
return { allow: true, message: "" };
|
|
291
|
+
const full = (0, node_path_1.resolve)(cwd, skillPath);
|
|
292
|
+
if (!(0, node_fs_1.existsSync)(full))
|
|
293
|
+
return { allow: true, message: "" };
|
|
294
|
+
const md = (0, node_fs_1.readFileSync)(full, "utf-8");
|
|
295
|
+
const purity = parseSkillPurity(md);
|
|
296
|
+
if (!purity)
|
|
297
|
+
return { allow: true, message: "" };
|
|
298
|
+
return (0, effects_js_1.decidePurityGate)(purity, tool, command, dialect_js_1.claudeCodeDialect);
|
|
299
|
+
}
|
|
260
300
|
/**
|
|
261
301
|
* Stop-hook decision. If a skill is active and declares a result gate, run it:
|
|
262
302
|
* allow the stop only when the gate passes; otherwise block and tell the model
|