vigiles 5.1.0 → 5.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.
- package/README.md +2 -2
- package/dist/adapters/claude-code/adapter.js +1 -0
- package/dist/adapters/claude-code/agent-runtime.d.ts +20 -6
- package/dist/adapters/claude-code/agent-runtime.js +51 -8
- package/dist/adapters/claude-code/dialect.js +19 -0
- 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 +48 -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/cli.js +323 -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 +162 -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/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 +142 -3
- package/dist/core/spec.js +48 -0
- 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 +30 -0
- package/dist/scaffold-test.js +158 -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
|
@@ -138,8 +138,8 @@ That's why you can eval your harness on every change, not just once.
|
|
|
138
138
|
## More
|
|
139
139
|
|
|
140
140
|
- **[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; `
|
|
141
|
+
- **[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.
|
|
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; `adopt-spec` and `linter-docs` are user-invoked.
|
|
143
143
|
- **[Docs index →](docs/README.md)** · **[Research →](research/README.md)** · **[Related tools →](docs/related-tools.md)** (ast-grep, Dependency Cruiser, Ruler, rulesync).
|
|
144
144
|
- Companion to [Feedback Loop Is All You Need](https://zernie.com/blog/feedback-loop-is-all-you-need).
|
|
145
145
|
|
|
@@ -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,16 @@ 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
|
-
* PreToolUse-hook decision. If an agent is active,
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
76
|
+
* PreToolUse-hook decision. If an agent is active, enforce BOTH deterministic
|
|
77
|
+
* rails its compiled `.md` declares, in order:
|
|
78
|
+
*
|
|
79
|
+
* 1. the tool-contract rail (`tools:`) — allow only listed tools;
|
|
80
|
+
* 2. the purity gate (`vigiles:purity:`) — allow only calls within the declared
|
|
81
|
+
* effect floor, refining `Bash` by the live `command` (`decidePurityGate`).
|
|
82
|
+
*
|
|
83
|
+
* The first to deny wins, feeding its reason back to the model. With no active
|
|
84
|
+
* agent (or one that declared neither contract), always allow — the rails only
|
|
85
|
+
* constrain agents that opted in.
|
|
72
86
|
*/
|
|
73
|
-
export declare function evaluatePreToolUse(cwd: string, tool: string): PreToolDecision;
|
|
87
|
+
export declare function evaluatePreToolUse(cwd: string, tool: string, command?: string): PreToolDecision;
|
|
74
88
|
//# sourceMappingURL=agent-runtime.d.ts.map
|
|
@@ -26,6 +26,7 @@
|
|
|
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;
|
|
@@ -34,6 +35,9 @@ exports.evaluatePreToolUse = evaluatePreToolUse;
|
|
|
34
35
|
const node_fs_1 = require("node:fs");
|
|
35
36
|
const node_path_1 = require("node:path");
|
|
36
37
|
const frontmatter_read_js_1 = require("../../core/frontmatter-read.js");
|
|
38
|
+
const effects_js_1 = require("../../core/effects.js");
|
|
39
|
+
const dialect_js_1 = require("./dialect.js");
|
|
40
|
+
const effect_region_js_1 = require("./effect-region.js");
|
|
37
41
|
// ---------------------------------------------------------------------------
|
|
38
42
|
// Parse the tool contract from a compiled agent .md
|
|
39
43
|
// ---------------------------------------------------------------------------
|
|
@@ -60,6 +64,18 @@ function parseAgentTools(markdown) {
|
|
|
60
64
|
function parseAgentToolList(markdown, key) {
|
|
61
65
|
return (0, frontmatter_read_js_1.frontmatterList)((0, frontmatter_read_js_1.readFrontmatter)(markdown), key);
|
|
62
66
|
}
|
|
67
|
+
const PURITY_RE = /<!--\s*vigiles:purity:(pure|bounded|unrestricted)\s*-->/;
|
|
68
|
+
/**
|
|
69
|
+
* Parse the declared purity floor from a compiled agent's `.md` — the
|
|
70
|
+
* `<!-- vigiles:purity:LEVEL -->` marker `compile` emits (see `purityMarker`).
|
|
71
|
+
* Returns null when no marker is present (the unit declared no floor, so the
|
|
72
|
+
* purity gate imposes no constraint). The single source of truth the runtime
|
|
73
|
+
* gate reads, exactly like `tools:` for the tool-contract rail.
|
|
74
|
+
*/
|
|
75
|
+
function parseAgentPurity(markdown) {
|
|
76
|
+
const m = PURITY_RE.exec(markdown);
|
|
77
|
+
return m ? m[1] : null;
|
|
78
|
+
}
|
|
63
79
|
/**
|
|
64
80
|
* Decide whether `tool` is allowed under an agent's tool contract. Pure, so the
|
|
65
81
|
* rail is unit-testable without spawning anything.
|
|
@@ -114,20 +130,47 @@ function readActiveAgent(cwd) {
|
|
|
114
130
|
// PreToolUse-hook decision
|
|
115
131
|
// ---------------------------------------------------------------------------
|
|
116
132
|
/**
|
|
117
|
-
* PreToolUse-hook decision. If an agent is active,
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
133
|
+
* PreToolUse-hook decision. If an agent is active, enforce BOTH deterministic
|
|
134
|
+
* rails its compiled `.md` declares, in order:
|
|
135
|
+
*
|
|
136
|
+
* 1. the tool-contract rail (`tools:`) — allow only listed tools;
|
|
137
|
+
* 2. the purity gate (`vigiles:purity:`) — allow only calls within the declared
|
|
138
|
+
* effect floor, refining `Bash` by the live `command` (`decidePurityGate`).
|
|
139
|
+
*
|
|
140
|
+
* The first to deny wins, feeding its reason back to the model. With no active
|
|
141
|
+
* agent (or one that declared neither contract), always allow — the rails only
|
|
142
|
+
* constrain agents that opted in.
|
|
122
143
|
*/
|
|
123
|
-
function evaluatePreToolUse(cwd, tool) {
|
|
144
|
+
function evaluatePreToolUse(cwd, tool, command) {
|
|
124
145
|
const agentPath = readActiveAgent(cwd);
|
|
125
146
|
if (!agentPath)
|
|
126
147
|
return { allow: true, message: "" };
|
|
127
148
|
const full = (0, node_path_1.resolve)(cwd, agentPath);
|
|
128
149
|
if (!(0, node_fs_1.existsSync)(full))
|
|
129
150
|
return { allow: true, message: "" };
|
|
130
|
-
const
|
|
131
|
-
|
|
151
|
+
const md = (0, node_fs_1.readFileSync)(full, "utf-8");
|
|
152
|
+
// 1) Tool-contract rail — the declared allowlist.
|
|
153
|
+
const rail = decidePreToolUse(parseAgentTools(md), tool);
|
|
154
|
+
if (!rail.allow)
|
|
155
|
+
return rail;
|
|
156
|
+
// 2) Purity gate — the declared effect floor, refined by the live command.
|
|
157
|
+
// If an effect boundary is declared, tighten to "pure" outside it and apply
|
|
158
|
+
// the declared purity (or "unrestricted") inside.
|
|
159
|
+
const purity = parseAgentPurity(md);
|
|
160
|
+
const boundary = (0, effect_region_js_1.hasEffectBoundary)(md);
|
|
161
|
+
if (boundary) {
|
|
162
|
+
const effective = (0, effect_region_js_1.readEffectActive)(cwd)
|
|
163
|
+
? (purity ?? "unrestricted")
|
|
164
|
+
: "pure";
|
|
165
|
+
const gate = (0, effects_js_1.decidePurityGate)(effective, tool, command, dialect_js_1.claudeCodeDialect);
|
|
166
|
+
if (!gate.allow)
|
|
167
|
+
return gate;
|
|
168
|
+
}
|
|
169
|
+
else if (purity) {
|
|
170
|
+
const gate = (0, effects_js_1.decidePurityGate)(purity, tool, command, dialect_js_1.claudeCodeDialect);
|
|
171
|
+
if (!gate.allow)
|
|
172
|
+
return gate;
|
|
173
|
+
}
|
|
174
|
+
return { allow: true, message: "" };
|
|
132
175
|
}
|
|
133
176
|
//# sourceMappingURL=agent-runtime.js.map
|
|
@@ -63,5 +63,24 @@ exports.claudeCodeDialect = {
|
|
|
63
63
|
// Claude Code reads the full SKILL.md frontmatter set (description,
|
|
64
64
|
// disable-model-invocation, argument-hint, …).
|
|
65
65
|
skillFrontmatter: "claude-code",
|
|
66
|
+
// Tools that produce side effects in Claude Code. The complement — the
|
|
67
|
+
// read-only tools — are: Read, Grep, Glob, LS, ToolSearch (and LSP/Agent
|
|
68
|
+
// which are not in the subagent catalog). Bash is side-effecting because
|
|
69
|
+
// `cat` and `rm -rf` are the same tool at the tool-name level — the
|
|
70
|
+
// sandbox is the only closure for subprocess effects.
|
|
71
|
+
sideEffectingTools: [
|
|
72
|
+
"Bash",
|
|
73
|
+
"BashOutput",
|
|
74
|
+
"KillBash",
|
|
75
|
+
"Edit",
|
|
76
|
+
"MultiEdit",
|
|
77
|
+
"Write",
|
|
78
|
+
"NotebookEdit",
|
|
79
|
+
"WebFetch",
|
|
80
|
+
"WebSearch",
|
|
81
|
+
"Skill",
|
|
82
|
+
"Task",
|
|
83
|
+
"TodoWrite",
|
|
84
|
+
],
|
|
66
85
|
};
|
|
67
86
|
//# 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,15 @@ 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");
|
|
41
|
+
const effect_region_js_1 = require("./effect-region.js");
|
|
37
42
|
const STEP_RE = /^###\s+Step\s+(\d+)/;
|
|
38
43
|
const GATE_CMD_RE = /<!--\s*vigiles:gate\s+"([^"]*)"(?:\s+retry:(\d+))?\s*-->/;
|
|
39
44
|
const GATE_FILE_RE = /<!--\s*vigiles:gate\s+file:(\S+)\s*-->/;
|
|
@@ -257,6 +262,49 @@ function readActiveSkill(cwd) {
|
|
|
257
262
|
return null;
|
|
258
263
|
}
|
|
259
264
|
}
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Skill purity gate (mirrors agent-runtime.ts parseAgentPurity / evaluatePreToolUse)
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
const PURITY_RE = /<!--\s*vigiles:purity:(pure|bounded|unrestricted)\s*-->/;
|
|
269
|
+
/**
|
|
270
|
+
* Parse the declared purity floor from a compiled SKILL.md — the
|
|
271
|
+
* `<!-- vigiles:purity:LEVEL -->` marker `compileSkill` emits (see
|
|
272
|
+
* `purityMarker` in compile.ts). Returns null when no marker is present (the
|
|
273
|
+
* skill declared no floor, so the purity gate imposes no constraint). Mirrors
|
|
274
|
+
* `parseAgentPurity` in agent-runtime.ts.
|
|
275
|
+
*/
|
|
276
|
+
function parseSkillPurity(markdown) {
|
|
277
|
+
const m = PURITY_RE.exec(markdown);
|
|
278
|
+
return m ? m[1] : null;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* PreToolUse purity gate for skills. Mirrors `evaluatePreToolUse` in
|
|
282
|
+
* agent-runtime.ts, but enforces ONLY the purity floor — skills have no
|
|
283
|
+
* tools-allowlist rail (that's a separate, future concern). If a skill is
|
|
284
|
+
* active and declares a `vigiles:purity:` marker, `decidePurityGate` checks
|
|
285
|
+
* the live call (refining `Bash` by the concrete command via `isReadOnlyBash`).
|
|
286
|
+
* With no active skill, a missing `.md`, or no purity marker, always allows.
|
|
287
|
+
*/
|
|
288
|
+
function evaluateSkillPreToolUse(cwd, tool, command) {
|
|
289
|
+
const skillPath = readActiveSkill(cwd);
|
|
290
|
+
if (!skillPath)
|
|
291
|
+
return { allow: true, message: "" };
|
|
292
|
+
const full = (0, node_path_1.resolve)(cwd, skillPath);
|
|
293
|
+
if (!(0, node_fs_1.existsSync)(full))
|
|
294
|
+
return { allow: true, message: "" };
|
|
295
|
+
const md = (0, node_fs_1.readFileSync)(full, "utf-8");
|
|
296
|
+
const purity = parseSkillPurity(md);
|
|
297
|
+
const boundary = (0, effect_region_js_1.hasEffectBoundary)(md);
|
|
298
|
+
if (boundary) {
|
|
299
|
+
const effective = (0, effect_region_js_1.readEffectActive)(cwd)
|
|
300
|
+
? (purity ?? "unrestricted")
|
|
301
|
+
: "pure";
|
|
302
|
+
return (0, effects_js_1.decidePurityGate)(effective, tool, command, dialect_js_1.claudeCodeDialect);
|
|
303
|
+
}
|
|
304
|
+
if (!purity)
|
|
305
|
+
return { allow: true, message: "" };
|
|
306
|
+
return (0, effects_js_1.decidePurityGate)(purity, tool, command, dialect_js_1.claudeCodeDialect);
|
|
307
|
+
}
|
|
260
308
|
/**
|
|
261
309
|
* Stop-hook decision. If a skill is active and declares a result gate, run it:
|
|
262
310
|
* allow the stop only when the gate passes; otherwise block and tell the model
|
|
@@ -29,6 +29,9 @@ exports.codexAdapter = {
|
|
|
29
29
|
referenceVerification: true,
|
|
30
30
|
harnessTesting: true,
|
|
31
31
|
shellHooks: true,
|
|
32
|
+
// Codex `[agents]` is a concurrency table, not a subagent tool-contract file
|
|
33
|
+
// — the subagent-surface rules report n/a here (a deliberate non-goal).
|
|
34
|
+
subagents: false,
|
|
32
35
|
},
|
|
33
36
|
dialect: dialect_js_1.codexDialect,
|
|
34
37
|
layout: layout_js_1.codexLayout,
|
|
@@ -9,6 +9,9 @@ exports.codexLayout = {
|
|
|
9
9
|
settingsFormat: "toml",
|
|
10
10
|
instructionFile: "AGENTS.md",
|
|
11
11
|
surfaceDirs: ["skills", "prompts"],
|
|
12
|
+
skillDir: "skills",
|
|
13
|
+
agentDir: "", // Codex `[agents]` is a TOML concurrency table, not a subagent dir
|
|
14
|
+
commandDir: "prompts",
|
|
12
15
|
materializeRoot: ".codex",
|
|
13
16
|
pluginRootToken: "${PLUGIN_ROOT}",
|
|
14
17
|
mcpConfigFile: ".mcp.json",
|
|
@@ -16,6 +16,9 @@ exports.opencodeLayout = {
|
|
|
16
16
|
// `.opencode/` segment. (Contrast Claude Code: root-level `skills/` surfaces
|
|
17
17
|
// relocated under `.claude`.)
|
|
18
18
|
surfaceDirs: [".opencode/agent", ".opencode/command"],
|
|
19
|
+
skillDir: ".opencode/skill",
|
|
20
|
+
agentDir: ".opencode/agent",
|
|
21
|
+
commandDir: ".opencode/command",
|
|
19
22
|
materializeRoot: "",
|
|
20
23
|
pluginRootToken: "${OPENCODE_PLUGIN_ROOT}",
|
|
21
24
|
mcpConfigFile: "opencode.json",
|
package/dist/check.d.ts
CHANGED
|
@@ -104,6 +104,14 @@ export declare function turns(opts: {
|
|
|
104
104
|
}): Check<Trace>;
|
|
105
105
|
/** The agent wrote (or left) a file at this path in the work dir. */
|
|
106
106
|
export declare function wrote(path: string): Check<Trace>;
|
|
107
|
+
/**
|
|
108
|
+
* The agent did NOT leave a file at this path — the **side-effect boundary**
|
|
109
|
+
* negative: a skill that declares it writes only `out.txt` should leave nothing
|
|
110
|
+
* at `secrets.env`. The symmetric sibling of `wrote()`; pairs with
|
|
111
|
+
* `notTool(...)` to assert a unit stayed inside its declared write surface
|
|
112
|
+
* deterministically (no model judge).
|
|
113
|
+
*/
|
|
114
|
+
export declare function didNotWrite(path: string): Check<Trace>;
|
|
107
115
|
/** The named subagent (`Task` `subagent_type`) ran and passed every nested check. */
|
|
108
116
|
export declare function subagent(name: string, checks: readonly Check<Trace>[]): Check<Trace>;
|
|
109
117
|
/** The hook blocked the event (exit 2 / deny / block). */
|
package/dist/check.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.hookFired = hookFired;
|
|
|
11
11
|
exports.received = received;
|
|
12
12
|
exports.turns = turns;
|
|
13
13
|
exports.wrote = wrote;
|
|
14
|
+
exports.didNotWrite = didNotWrite;
|
|
14
15
|
exports.subagent = subagent;
|
|
15
16
|
exports.blocked = blocked;
|
|
16
17
|
exports.allowed = allowed;
|
|
@@ -236,16 +237,34 @@ function wrote(path) {
|
|
|
236
237
|
toJSON: () => ({ kind: "wrote", path }),
|
|
237
238
|
};
|
|
238
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* The agent did NOT leave a file at this path — the **side-effect boundary**
|
|
242
|
+
* negative: a skill that declares it writes only `out.txt` should leave nothing
|
|
243
|
+
* at `secrets.env`. The symmetric sibling of `wrote()`; pairs with
|
|
244
|
+
* `notTool(...)` to assert a unit stayed inside its declared write surface
|
|
245
|
+
* deterministically (no model judge).
|
|
246
|
+
*/
|
|
247
|
+
function didNotWrite(path) {
|
|
248
|
+
return {
|
|
249
|
+
kind: "didNotWrite",
|
|
250
|
+
eval: (t) => t.file(path) === null
|
|
251
|
+
? ok(`file "${path}" was not created`)
|
|
252
|
+
: no(`expected the agent NOT to create "${path}", but it exists`),
|
|
253
|
+
toJSON: () => ({ kind: "didNotWrite", path }),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
239
256
|
// ---------------------------------------------------------------------------
|
|
240
257
|
// Subagent — a `Task` run as a nested trace. Run checks over what the SUBAGENT
|
|
241
258
|
// did, not just that `Task` fired. Composes the whole vocabulary recursively.
|
|
242
259
|
// ---------------------------------------------------------------------------
|
|
243
|
-
/** Wrap a subagent's tool calls as a minimal `Trace` so checks
|
|
260
|
+
/** Wrap a subagent's tool calls + returned text as a minimal `Trace` so checks
|
|
261
|
+
* (incl. `output()` over the sub's RETURN — where a result() vigiles:ok/err block
|
|
262
|
+
* lands) run over it. */
|
|
244
263
|
function subTrace(sub) {
|
|
245
264
|
return {
|
|
246
265
|
toolCalls: sub.toolCalls,
|
|
247
266
|
hooks: [],
|
|
248
|
-
output:
|
|
267
|
+
output: sub.output,
|
|
249
268
|
modelRequests: [],
|
|
250
269
|
turns: 0,
|
|
251
270
|
subagents: [],
|
|
@@ -258,7 +277,12 @@ function subagent(name, checks) {
|
|
|
258
277
|
kind: "subagent",
|
|
259
278
|
eval: (t) => {
|
|
260
279
|
const subs = t.subagents ?? [];
|
|
261
|
-
|
|
280
|
+
// A `--plugin-dir` agent's `subagent_type` is namespaced `plugin:agent`
|
|
281
|
+
// (e.g. "reviewer-spec:code-reviewer"), but callers pass the bare agent name
|
|
282
|
+
// — so match the full id OR its last `:`-segment. Non-namespaced (harness
|
|
283
|
+
// mock) names match exactly as before.
|
|
284
|
+
const bare = (n) => n.includes(":") ? n.slice(n.lastIndexOf(":") + 1) : n;
|
|
285
|
+
const sub = subs.find((s) => s.name === name || bare(s.name) === name);
|
|
262
286
|
if (!sub) {
|
|
263
287
|
return no(`expected subagent "${name}" to run; subagents that ran: ${subs.length > 0 ? `[${subs.map((s) => s.name).join(", ")}]` : "none"}`);
|
|
264
288
|
}
|