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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyToolEffect = classifyToolEffect;
|
|
4
|
+
exports.effectSurface = effectSurface;
|
|
5
|
+
exports.purityViolations = purityViolations;
|
|
6
|
+
exports.pureContractViolations = pureContractViolations;
|
|
7
|
+
exports.decidePurityGate = decidePurityGate;
|
|
8
|
+
const hash_js_1 = require("./hash.js");
|
|
9
|
+
const bash_effects_js_1 = require("./bash-effects.js");
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Internal helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/** Strips a `Tool(restriction)` suffix and returns the base tool name. */
|
|
14
|
+
function baseTool(raw) {
|
|
15
|
+
return raw.split("(")[0].trim();
|
|
16
|
+
}
|
|
17
|
+
/** Returns true for the wildcard sentinels that mean "inherits-all". */
|
|
18
|
+
function isWildcard(tool) {
|
|
19
|
+
return tool === "" || tool === "*";
|
|
20
|
+
}
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Public API
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* Classify the effect of ONE tool name against a dialect's known catalogs.
|
|
26
|
+
*
|
|
27
|
+
* A `Tool(restriction)` suffix (e.g. `Bash(git:*)`) is stripped first — the
|
|
28
|
+
* restriction narrows what the tool can DO but doesn't change its effect class
|
|
29
|
+
* (Bash with any restriction is still conservatively side-effecting).
|
|
30
|
+
*
|
|
31
|
+
* Classification rules (in priority order):
|
|
32
|
+
* 1. In `dialect.sideEffectingTools` → `"side-effecting"`
|
|
33
|
+
* 2. In `dialect.builtinAgentTools` (and NOT side-effecting) → `"read-only"`
|
|
34
|
+
* 3. Matches `dialect.mcpToolPattern` → `"unknown"` (MCP tools are not
|
|
35
|
+
* classifiable from the name alone — treated as unknown-effect)
|
|
36
|
+
* 4. Otherwise → `"unknown"` (unrecognized tool; may be a plugin tool or a typo)
|
|
37
|
+
*/
|
|
38
|
+
function classifyToolEffect(tool, dialect) {
|
|
39
|
+
const base = baseTool(tool);
|
|
40
|
+
const sideEffecting = dialect.sideEffectingTools ?? [];
|
|
41
|
+
if (sideEffecting.includes(base))
|
|
42
|
+
return "side-effecting";
|
|
43
|
+
if (dialect.builtinAgentTools.includes(base))
|
|
44
|
+
return "read-only";
|
|
45
|
+
if (dialect.mcpToolPattern.test(base))
|
|
46
|
+
return "unknown";
|
|
47
|
+
return "unknown";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Compute the static effect surface of a declared `tools:` contract.
|
|
51
|
+
*
|
|
52
|
+
* `"*"` / `""` (inherits-all) entries make purity `"unrestricted"` because the
|
|
53
|
+
* contract grants access to all tools including every side-effecting one — the
|
|
54
|
+
* full surface is unknowable statically. They are NOT listed in any bucket
|
|
55
|
+
* (they represent a wildcard, not a named tool).
|
|
56
|
+
*
|
|
57
|
+
* De-duplication: a tool name that appears more than once in `tools` is counted
|
|
58
|
+
* once in its bucket (base tool after restriction stripping).
|
|
59
|
+
*/
|
|
60
|
+
function effectSurface(tools, dialect) {
|
|
61
|
+
const readOnly = new Set();
|
|
62
|
+
const sideEffecting = new Set();
|
|
63
|
+
const unknown = new Set();
|
|
64
|
+
let hasWildcard = false;
|
|
65
|
+
let hasBash = false;
|
|
66
|
+
for (const raw of tools) {
|
|
67
|
+
const base = baseTool(raw);
|
|
68
|
+
if (isWildcard(base)) {
|
|
69
|
+
hasWildcard = true;
|
|
70
|
+
continue; // wildcards don't go into any named bucket
|
|
71
|
+
}
|
|
72
|
+
const effect = classifyToolEffect(raw, dialect);
|
|
73
|
+
switch (effect) {
|
|
74
|
+
case "read-only":
|
|
75
|
+
readOnly.add(base);
|
|
76
|
+
break;
|
|
77
|
+
case "side-effecting":
|
|
78
|
+
sideEffecting.add(base);
|
|
79
|
+
if (base === "Bash")
|
|
80
|
+
hasBash = true;
|
|
81
|
+
break;
|
|
82
|
+
case "unknown":
|
|
83
|
+
unknown.add(base);
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
(0, hash_js_1.assertNever)(effect);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const purity = hasWildcard || hasBash || unknown.size > 0
|
|
90
|
+
? "unrestricted"
|
|
91
|
+
: sideEffecting.size > 0
|
|
92
|
+
? "bounded"
|
|
93
|
+
: "pure";
|
|
94
|
+
return {
|
|
95
|
+
readOnly: [...readOnly],
|
|
96
|
+
sideEffecting: [...sideEffecting],
|
|
97
|
+
unknown: [...unknown],
|
|
98
|
+
purity,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Returns the violations of a DECLARED purity floor — the tools that make the
|
|
103
|
+
* actual effect surface LOOSER than the declared level. Empty ⇒ the contract
|
|
104
|
+
* honours the declared level. The `message` on each is actionable (names the
|
|
105
|
+
* tool, the effect class, and what to do).
|
|
106
|
+
*
|
|
107
|
+
* What counts as a violation depends on `declared`:
|
|
108
|
+
* - `"pure"`: every side-effecting tool (incl. `Bash`), every
|
|
109
|
+
* unknown-effect tool, and any wildcard (a pure unit may
|
|
110
|
+
* only observe — no `Bash`, no effects, fully static).
|
|
111
|
+
* - `"bounded"`: only the truly UNBOUNDED tools — unknown-effect (MCP /
|
|
112
|
+
* unrecognized) and wildcards. Every decidable side-effecting
|
|
113
|
+
* tool is ALLOWED: Write/Edit confine to the boundary, and
|
|
114
|
+
* `Bash` is admitted because the RUNTIME gate
|
|
115
|
+
* (`decidePurityGate`) refines it by command (read-only Bash
|
|
116
|
+
* is an observation; a mutating command is denied).
|
|
117
|
+
* - `"unrestricted"`: never a violation (the rung carries no constraint).
|
|
118
|
+
*
|
|
119
|
+
* A wildcard (`"*"` / `""`) contract is a violation at every constrained level:
|
|
120
|
+
* "inherits-all" grants every tool, so neither `pure` nor `bounded` can hold.
|
|
121
|
+
*/
|
|
122
|
+
function purityViolations(tools, dialect, declared) {
|
|
123
|
+
if (declared === "unrestricted")
|
|
124
|
+
return []; // no constraint to violate
|
|
125
|
+
const violations = [];
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
for (const raw of tools) {
|
|
128
|
+
const base = baseTool(raw);
|
|
129
|
+
if (isWildcard(base)) {
|
|
130
|
+
const key = base === "" ? '""' : '"*"';
|
|
131
|
+
if (!seen.has(key)) {
|
|
132
|
+
seen.add(key);
|
|
133
|
+
violations.push({
|
|
134
|
+
tool: base,
|
|
135
|
+
effect: "side-effecting",
|
|
136
|
+
message: `${key} (inherits-all) is not allowed in a ${declared} contract — it grants access to every tool, including side-effecting ones. Declare explicit tools instead.`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (seen.has(base))
|
|
142
|
+
continue;
|
|
143
|
+
const effect = classifyToolEffect(raw, dialect);
|
|
144
|
+
switch (effect) {
|
|
145
|
+
case "read-only":
|
|
146
|
+
break; // allowed at every level
|
|
147
|
+
case "unknown":
|
|
148
|
+
seen.add(base);
|
|
149
|
+
violations.push({
|
|
150
|
+
tool: base,
|
|
151
|
+
effect,
|
|
152
|
+
message: `"${base}" has unknown effect class (MCP or unrecognized tool); a ${declared} contract cannot declare it — its effects are unbounded from static analysis. Remove it or declare the unit dangerously-unrestricted.`,
|
|
153
|
+
});
|
|
154
|
+
break;
|
|
155
|
+
case "side-effecting":
|
|
156
|
+
// In a BOUNDED unit every decidable side-effecting tool is allowed:
|
|
157
|
+
// Write/Edit confine to the boundary, and `Bash` is admitted because the
|
|
158
|
+
// RUNTIME gate (`decidePurityGate`) refines it by command — a read-only
|
|
159
|
+
// Bash is an observation, a mutating command is denied. Only `pure` bars
|
|
160
|
+
// them (a pure unit may only observe; no `Bash`, no effects).
|
|
161
|
+
if (declared === "bounded")
|
|
162
|
+
break;
|
|
163
|
+
seen.add(base);
|
|
164
|
+
violations.push({
|
|
165
|
+
tool: base,
|
|
166
|
+
effect,
|
|
167
|
+
message: base === "Bash"
|
|
168
|
+
? `"Bash" is undecidable at the tool-name level; a pure unit cannot declare it. A read-only Bash belongs in a bounded unit (the runtime gate confines it by command) — declare the unit bounded or dangerously-unrestricted.`
|
|
169
|
+
: `"${base}" is side-effecting; a pure unit cannot declare it. Remove it or declare the unit bounded.`,
|
|
170
|
+
});
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
(0, hash_js_1.assertNever)(effect);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return violations;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The violations of a `purity: "pure"` contract — every side-effecting,
|
|
180
|
+
* unknown-effect, or wildcard tool. A thin alias for `purityViolations(…,
|
|
181
|
+
* "pure")` kept for the common pure case.
|
|
182
|
+
*/
|
|
183
|
+
function pureContractViolations(tools, dialect) {
|
|
184
|
+
return purityViolations(tools, dialect, "pure");
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The RUNTIME half of the purity contract: decide whether a single LIVE tool
|
|
188
|
+
* call is allowed under the active unit's declared purity floor.
|
|
189
|
+
*
|
|
190
|
+
* Unlike `purityViolations` (which checks the DECLARED tools contract
|
|
191
|
+
* statically), this sees the ACTUAL call — including the `Bash` command string —
|
|
192
|
+
* so it refines `Bash` by effect via `isReadOnlyBash`. That command is the whole
|
|
193
|
+
* reason the gate's home is the runtime hook: only here is the concrete command
|
|
194
|
+
* visible (the static surface sees a tool name + a `Bash(git:*)` pattern, never
|
|
195
|
+
* the command).
|
|
196
|
+
*
|
|
197
|
+
* Rules (the ladder, command-refined):
|
|
198
|
+
* - `unrestricted` → always allow (no constraint).
|
|
199
|
+
* - read-only tool → allow at every level.
|
|
200
|
+
* - `Bash` → allow iff the command is provably read-only (an observation);
|
|
201
|
+
* otherwise deny — a mutating/undecidable command's effect must move to a
|
|
202
|
+
* marked boundary. Same at `pure` and `bounded`.
|
|
203
|
+
* - other side-effecting tool (Write, Edit, …) → allow under `bounded`
|
|
204
|
+
* (a decidable, boundary-confined effect), deny under `pure` (observe-only).
|
|
205
|
+
* - unknown-effect (MCP / unrecognized) → deny under `pure`/`bounded`
|
|
206
|
+
* (unbounded from static analysis).
|
|
207
|
+
*
|
|
208
|
+
* Dialect injected (core ⊄ adapter). Reuses `classifyToolEffect` +
|
|
209
|
+
* `isReadOnlyBash` — one-detector-no-drift with compile + scan.
|
|
210
|
+
*/
|
|
211
|
+
function decidePurityGate(declared, tool, command, dialect) {
|
|
212
|
+
if (declared === "unrestricted")
|
|
213
|
+
return { allow: true, message: "" };
|
|
214
|
+
const base = baseTool(tool);
|
|
215
|
+
const effect = classifyToolEffect(tool, dialect);
|
|
216
|
+
if (effect === "read-only")
|
|
217
|
+
return { allow: true, message: "" };
|
|
218
|
+
if (base === "Bash") {
|
|
219
|
+
if (command !== undefined && (0, bash_effects_js_1.isReadOnlyBash)(command)) {
|
|
220
|
+
return { allow: true, message: "" };
|
|
221
|
+
}
|
|
222
|
+
const shown = command ? `"${command}" ` : "";
|
|
223
|
+
return {
|
|
224
|
+
allow: false,
|
|
225
|
+
message: `Bash command ${shown}is not provably read-only; a ${declared} unit may run only read-only Bash ` +
|
|
226
|
+
`(observation). Move the side effect into a marked boundary, or declare the unit dangerously-unrestricted.`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (effect === "side-effecting") {
|
|
230
|
+
if (declared === "bounded")
|
|
231
|
+
return { allow: true, message: "" };
|
|
232
|
+
return {
|
|
233
|
+
allow: false,
|
|
234
|
+
message: `"${base}" is side-effecting; a pure unit may only observe. ` +
|
|
235
|
+
`Declare the unit bounded (or dangerously-unrestricted) to use it.`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
// unknown — MCP / unrecognized tool
|
|
239
|
+
return {
|
|
240
|
+
allow: false,
|
|
241
|
+
message: `"${base}" has unknown effect class; a ${declared} unit cannot use it — its effects are unbounded. ` +
|
|
242
|
+
`Declare the unit dangerously-unrestricted to allow it.`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=effects.js.map
|
package/dist/core/layout.d.ts
CHANGED
|
@@ -29,6 +29,12 @@ export interface PluginLayout {
|
|
|
29
29
|
readonly instructionFile: string;
|
|
30
30
|
/** Surface dirs materialized into the sandbox, e.g. skills/agents/commands. */
|
|
31
31
|
readonly surfaceDirs: readonly string[];
|
|
32
|
+
/** Skills dir, holding the nested `<dir>/<name>/SKILL.md`, e.g. `skills`. */
|
|
33
|
+
readonly skillDir: string;
|
|
34
|
+
/** Subagents dir, holding flat `<dir>/<name>.md`, e.g. `agents` (`""` = none). */
|
|
35
|
+
readonly agentDir: string;
|
|
36
|
+
/** Slash-commands dir, holding flat `<dir>/<name>.md`, e.g. `commands`. */
|
|
37
|
+
readonly commandDir: string;
|
|
32
38
|
/** Dir the surfaces are materialized under, e.g. `.claude`. */
|
|
33
39
|
readonly materializeRoot: string;
|
|
34
40
|
/** Env token expanded to the plugin's absolute root in hook commands. */
|
package/dist/core/mcp-tool.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* names a server `linear`; if the plugin declares its own MCP servers (a
|
|
5
5
|
* `.mcp.json` / manifest `mcpServers` block) and `linear` isn't among them, the
|
|
6
6
|
* tool can't resolve — a dead contract entry. This completes the tool moat:
|
|
7
|
-
* `
|
|
7
|
+
* `subagent-tool-contract` (tool-contract.ts) verifies BUILT-IN tools but passes
|
|
8
8
|
* ANY `mcp__*` token unchecked; this verifies the MCP half.
|
|
9
9
|
*
|
|
10
10
|
* Calibrated HIGH-PRECISION — three guards, each learned from a real plugin in
|
package/dist/core/orphans.js
CHANGED
|
@@ -35,6 +35,25 @@ const DEFAULT_IGNORE = [
|
|
|
35
35
|
* that nothing else links to but is not rot.
|
|
36
36
|
*/
|
|
37
37
|
const DISABLE_RE = /<!--\s*vigiles-disable\s+orphan-docs\s*-->/;
|
|
38
|
+
/**
|
|
39
|
+
* Files the HARNESS loads directly — an instruction file (`CLAUDE.md` /
|
|
40
|
+
* `AGENTS.md`), a skill (`SKILL.md`), a subagent (`agents/*.md`), or a slash
|
|
41
|
+
* command (`commands/*.md`) — are load-bearing by their NAME/LOCATION, not
|
|
42
|
+
* because another `.md` links to them. They are categorically NOT docs, so they
|
|
43
|
+
* are never orphans, even if a project broadens `orphans.include` to scan the
|
|
44
|
+
* whole repo. (They are still scanned as REFERENCERS, so a real doc that only
|
|
45
|
+
* a CLAUDE.md links to is still credited — this exemption only removes them from
|
|
46
|
+
* the orphan-CANDIDATE set.)
|
|
47
|
+
*/
|
|
48
|
+
function isHarnessLoadedFile(path) {
|
|
49
|
+
const norm = normalizePath(path);
|
|
50
|
+
const base = norm.slice(norm.lastIndexOf("/") + 1);
|
|
51
|
+
if (base === "CLAUDE.md" || base === "AGENTS.md" || base === "SKILL.md") {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
// Subagent / slash-command surfaces the harness enumerates by directory.
|
|
55
|
+
return /(^|\/)(agents|commands)\//.test(norm);
|
|
56
|
+
}
|
|
38
57
|
// Match markdown links ](path.md) or ](path.md#anchor)
|
|
39
58
|
const LINK_RE = /\]\(([^)\s]+\.md)(?:#[^)]*)?\)/g;
|
|
40
59
|
// Match backtick code spans wrapping a path ending in .md
|
|
@@ -56,6 +75,8 @@ function collectDocs(basePath, include, ignore) {
|
|
|
56
75
|
const docs = new Set();
|
|
57
76
|
for (const pattern of include) {
|
|
58
77
|
for (const p of (0, glob_1.globSync)(pattern, { cwd: basePath, ignore: [...ignore] })) {
|
|
78
|
+
if (isHarnessLoadedFile(p))
|
|
79
|
+
continue; // instruction files are never orphans
|
|
59
80
|
if (isOrphanExempt((0, node_path_1.resolve)(basePath, p)))
|
|
60
81
|
continue;
|
|
61
82
|
docs.add(normalizePath(p));
|
package/dist/core/spec.d.ts
CHANGED
|
@@ -120,6 +120,12 @@ export type VerifiedCmd = string & {
|
|
|
120
120
|
export type VerifiedRef = string & {
|
|
121
121
|
readonly [__brand]: "VerifiedRef";
|
|
122
122
|
};
|
|
123
|
+
export type VerifiedDir = string & {
|
|
124
|
+
readonly [__brand]: "VerifiedDir";
|
|
125
|
+
};
|
|
126
|
+
export type VerifiedGlob = string & {
|
|
127
|
+
readonly [__brand]: "VerifiedGlob";
|
|
128
|
+
};
|
|
123
129
|
/** A typed file reference — verified at compile time. */
|
|
124
130
|
export interface FileRef {
|
|
125
131
|
readonly _ref: "file";
|
|
@@ -141,7 +147,17 @@ export interface SymbolRef {
|
|
|
141
147
|
readonly file: VerifiedPath;
|
|
142
148
|
readonly symbol: string;
|
|
143
149
|
}
|
|
144
|
-
|
|
150
|
+
/** A typed directory reference — verified to exist AND be a directory. */
|
|
151
|
+
export interface DirRef {
|
|
152
|
+
readonly _ref: "dir";
|
|
153
|
+
readonly path: VerifiedDir;
|
|
154
|
+
}
|
|
155
|
+
/** A typed glob reference — verified to match at least one path. */
|
|
156
|
+
export interface GlobRef {
|
|
157
|
+
readonly _ref: "glob";
|
|
158
|
+
readonly pattern: VerifiedGlob;
|
|
159
|
+
}
|
|
160
|
+
export type Ref = FileRef | CmdRef | SkillRef | SymbolRef | DirRef | GlobRef;
|
|
145
161
|
/**
|
|
146
162
|
* Reference a file path — verified to exist at compile time.
|
|
147
163
|
* When generated types are present, narrowed to known project files.
|
|
@@ -164,7 +180,33 @@ export declare function symbol(file: NoInfer<StrictFile>, name: string): SymbolR
|
|
|
164
180
|
* Compiles to a markdown link: [skill name](path)
|
|
165
181
|
*/
|
|
166
182
|
export declare function ref(path: string): SkillRef;
|
|
167
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Reference a directory — verified at compile time to exist AND be a directory
|
|
185
|
+
* (not a file). The "architecture floats free" fix: a spec that names `src/core/`
|
|
186
|
+
* proves the directory is really there, where a plain string in prose rots
|
|
187
|
+
* silently. Compiles to the inline form `` `path` ``.
|
|
188
|
+
*/
|
|
189
|
+
export declare function dir(path: string): DirRef;
|
|
190
|
+
/**
|
|
191
|
+
* Reference a glob pattern — verified at compile time to match at least one path,
|
|
192
|
+
* so `glob("src/*.test.ts")` proves tests actually exist where the instructions
|
|
193
|
+
* claim (the pattern supports the usual `*` / `**` syntax). Compiles to the
|
|
194
|
+
* inline form `` `pattern` ``.
|
|
195
|
+
*/
|
|
196
|
+
export declare function glob(pattern: string): GlobRef;
|
|
197
|
+
/**
|
|
198
|
+
* A marked side-effect BOUNDARY inside a skill/agent body — "side effects are
|
|
199
|
+
* allowed ONLY inside this block." Compiles to `<!-- vigiles:effect -->` …
|
|
200
|
+
* `<!-- /vigiles:effect -->` markers the runtime PreToolUse gate keys on: outside
|
|
201
|
+
* the region the unit is treated as read-only (the `"pure"` effective floor),
|
|
202
|
+
* inside it the declared purity floor applies. The position-aware companion to
|
|
203
|
+
* the per-call `purity` floor. See `research/effect-boundary-design.md`.
|
|
204
|
+
*/
|
|
205
|
+
export interface EffectRegion {
|
|
206
|
+
readonly _ref: "effect";
|
|
207
|
+
readonly body: InstructionFragment[];
|
|
208
|
+
}
|
|
209
|
+
export type InstructionFragment = string | Ref | EffectRegion;
|
|
168
210
|
/**
|
|
169
211
|
* Tagged template literal for skill instructions with typed references.
|
|
170
212
|
*
|
|
@@ -175,6 +217,39 @@ export type InstructionFragment = string | Ref;
|
|
|
175
217
|
* `
|
|
176
218
|
*/
|
|
177
219
|
export declare function instructions(strings: TemplateStringsArray, ...values: InstructionFragment[]): InstructionFragment[];
|
|
220
|
+
/**
|
|
221
|
+
* Tagged template literal marking a side-effect boundary — usable as an
|
|
222
|
+
* interpolated fragment inside a body / `instructions\`\``:
|
|
223
|
+
*
|
|
224
|
+
* instructions`
|
|
225
|
+
* ## Apply
|
|
226
|
+
* ${effect`
|
|
227
|
+
* Side effects are allowed ONLY here:
|
|
228
|
+
* - write ${file("CHANGELOG.md")}
|
|
229
|
+
* - ${cmd("npm publish")}
|
|
230
|
+
* `}
|
|
231
|
+
* `
|
|
232
|
+
*
|
|
233
|
+
* Returns an `EffectRegion` fragment; `compile` wraps its rendered body in
|
|
234
|
+
* `<!-- vigiles:effect -->` markers. Independent of the `doc()` authoring
|
|
235
|
+
* surface — it does not block on it.
|
|
236
|
+
*/
|
|
237
|
+
export declare function effect(strings: TemplateStringsArray, ...values: InstructionFragment[]): EffectRegion;
|
|
238
|
+
/**
|
|
239
|
+
* The purity an author DECLARES for a skill/agent — the floor `compile`
|
|
240
|
+
* enforces against the tool contract (see `purityViolations` in
|
|
241
|
+
* `core/effects.ts`). Mirrors the analysis `PurityLevel` for the two meaningful
|
|
242
|
+
* rungs, so what you DECLARE and what `scan` REPORTS share one vocabulary:
|
|
243
|
+
* - `"pure"`: only read-only tools — no side effects at all.
|
|
244
|
+
* - `"bounded"`: decidable side-effecting tools (Write, Edit, …) are allowed,
|
|
245
|
+
* but not `Bash` / unknown-effect / inherits-all (the unbounded cells).
|
|
246
|
+
* - `"dangerously-unrestricted"`: the explicit escape hatch — no enforcement.
|
|
247
|
+
* Deliberately loud (cf. React's `dangerouslySetInnerHTML`) so opting OUT of
|
|
248
|
+
* the guardrail stands out in review. Omitting `purity` is the same
|
|
249
|
+
* (unenforced) default WITHOUT typing the loud word — you write it only when
|
|
250
|
+
* you mean to override a stricter level.
|
|
251
|
+
*/
|
|
252
|
+
export type AuthoredPurity = "pure" | "bounded" | "dangerously-unrestricted";
|
|
178
253
|
/** Known markdown instruction file targets. */
|
|
179
254
|
export type InstructionTarget = "CLAUDE.md" | "AGENTS.md" | (string & {});
|
|
180
255
|
export interface ClaudeSpec {
|
|
@@ -190,7 +265,12 @@ export interface ClaudeSpec {
|
|
|
190
265
|
readonly keyFiles?: Record<string, string>;
|
|
191
266
|
/** Named prose sections — plain strings or tagged templates with file()/cmd()/ref(). */
|
|
192
267
|
readonly sections?: Record<string, string | InstructionFragment[]>;
|
|
193
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* Maximum lines for a single named prose section. Overrides the generous
|
|
270
|
+
* compile-time default (200 lines) that guards every section + agent section
|
|
271
|
+
* against an egregious content dump — set a tighter number to enforce your own
|
|
272
|
+
* house limit, or a larger one for an intentionally long section.
|
|
273
|
+
*/
|
|
194
274
|
readonly maxSectionLines?: number;
|
|
195
275
|
/**
|
|
196
276
|
* Maximum estimated tokens for the compiled output (~4 chars per token).
|
|
@@ -293,6 +373,31 @@ export interface SkillSpec {
|
|
|
293
373
|
readonly inputs?: readonly SkillInput[];
|
|
294
374
|
/** Whether to disable model invocation (frontmatter flag). */
|
|
295
375
|
readonly disableModelInvocation?: boolean;
|
|
376
|
+
/**
|
|
377
|
+
* Execution context. `"fork"` runs the skill's body as the task inside a
|
|
378
|
+
* forked SUBAGENT (its own context window) instead of inline in the main
|
|
379
|
+
* conversation (Anthropic's `context: fork` frontmatter). This is the ONLY
|
|
380
|
+
* setting under which a skill gains a real call→return boundary — so it's the
|
|
381
|
+
* prerequisite for declaring an `output` Result contract (see `output`). Omit
|
|
382
|
+
* for the default inline execution.
|
|
383
|
+
*/
|
|
384
|
+
readonly context?: "fork";
|
|
385
|
+
/**
|
|
386
|
+
* The allowed-tools contract for this skill. Each entry must be a known
|
|
387
|
+
* built-in tool or an MCP tool (`mcp__server__tool`). Omit to inherit all
|
|
388
|
+
* tools. When `purity` is `"pure"`/`"bounded"`, the declared tools are checked
|
|
389
|
+
* against that floor — compile rejects a tool looser than the declared level.
|
|
390
|
+
*/
|
|
391
|
+
readonly tools?: readonly string[];
|
|
392
|
+
/**
|
|
393
|
+
* Declare this skill's purity floor — compile rejects a tool contract looser
|
|
394
|
+
* than it. `"pure"` allows only read-only tools; `"bounded"` also allows
|
|
395
|
+
* decidable side-effecting tools (Write, Edit, …) but bars `Bash` /
|
|
396
|
+
* unknown-effect / inherits-all; `"dangerously-unrestricted"` (or omitting it)
|
|
397
|
+
* enforces nothing. NOTE: `"pure"`/`"bounded"` require an explicit read-only
|
|
398
|
+
* `tools` list — an absent list inherits ALL tools and is a violation.
|
|
399
|
+
*/
|
|
400
|
+
readonly purity?: AuthoredPurity;
|
|
296
401
|
/**
|
|
297
402
|
* Gated pipeline steps. When set, the skill compiles to a `## Steps`
|
|
298
403
|
* checklist with a deterministic gate per step. Use this OR `body`.
|
|
@@ -303,6 +408,18 @@ export interface SkillSpec {
|
|
|
303
408
|
* Compiles to a `## Result` section + a `vigiles:result` marker.
|
|
304
409
|
*/
|
|
305
410
|
readonly result?: Gate;
|
|
411
|
+
/**
|
|
412
|
+
* The skill's typed railway outcome — the SAME `Result<ok, err>` contract a
|
|
413
|
+
* subagent declares with `result(okShape, errShape)`. Valid ONLY with
|
|
414
|
+
* `context: "fork"`: a forked skill runs as a subagent, so it has the
|
|
415
|
+
* call→return boundary a typed outcome needs (compile errors if `output` is set
|
|
416
|
+
* without `context: "fork"`). When valid, compiles to a `## Output contract`
|
|
417
|
+
* with a `vigiles:ok` / `vigiles:err` block — parseable (`parseAgentResult`) and
|
|
418
|
+
* testable (`assertAgentOk`) via the existing subagent rail. An INLINE skill has
|
|
419
|
+
* no return, so a typed outcome there is a category error — hence the gate. See
|
|
420
|
+
* `research/spec-syntax-and-railway-scope.md`.
|
|
421
|
+
*/
|
|
422
|
+
readonly output?: OutputContract;
|
|
306
423
|
/** Freeform instruction body (linear/unstructured skills). Use this OR `steps`. */
|
|
307
424
|
readonly body?: string | InstructionFragment[];
|
|
308
425
|
/**
|
|
@@ -338,6 +455,8 @@ export interface AgentSpec {
|
|
|
338
455
|
readonly description: string;
|
|
339
456
|
/** Model alias (e.g. "sonnet", "opus", "haiku", "inherit"). Optional. */
|
|
340
457
|
readonly model?: string;
|
|
458
|
+
/** Subagent UI colour (Claude Code frontmatter, e.g. "pink", "blue"). Optional. */
|
|
459
|
+
readonly color?: string;
|
|
341
460
|
/**
|
|
342
461
|
* The allowed-tools contract — the rails the worker runs on. Each entry must be
|
|
343
462
|
* a known built-in tool (Read/Write/Edit/Bash/Grep/Glob/WebSearch/WebFetch/
|
|
@@ -345,6 +464,17 @@ export interface AgentSpec {
|
|
|
345
464
|
* Omit to inherit all tools. Verified at compile time.
|
|
346
465
|
*/
|
|
347
466
|
readonly tools?: readonly string[];
|
|
467
|
+
/**
|
|
468
|
+
* The DENY-side contract — tools the worker may NOT use. Use this INSTEAD OF
|
|
469
|
+
* `tools`, not with it: `tools` is an allowlist (only these), so a tool not
|
|
470
|
+
* listed is already unavailable and a `disallowedTools` entry would be
|
|
471
|
+
* redundant. `disallowedTools` earns its place only when there's NO allowlist
|
|
472
|
+
* (the agent inherits ALL tools) and you want to subtract a few — e.g.
|
|
473
|
+
* `disallowedTools: ["Bash"]` on an otherwise-unrestricted worker. Rendered to
|
|
474
|
+
* the `disallowedTools:` frontmatter; close-typos are flagged (a typo'd entry
|
|
475
|
+
* blocks nothing). For a read-only floor prefer a tight `tools` list + `purity`.
|
|
476
|
+
*/
|
|
477
|
+
readonly disallowedTools?: readonly string[];
|
|
348
478
|
/**
|
|
349
479
|
* The lead/intro prose of the system prompt (the "You are…" opener), before any
|
|
350
480
|
* sections. Carries verified `file()`/`cmd()`/`symbol()`/`ref()` marks. No
|
|
@@ -367,6 +497,15 @@ export interface AgentSpec {
|
|
|
367
497
|
* and testable (see `result()`, `parseAgentResult`, `assertAgentOk`).
|
|
368
498
|
*/
|
|
369
499
|
readonly output?: OutputContract;
|
|
500
|
+
/**
|
|
501
|
+
* Declare this agent's purity floor — compile rejects a tool contract looser
|
|
502
|
+
* than it. `"pure"` allows only read-only tools; `"bounded"` also allows
|
|
503
|
+
* decidable side-effecting tools (Write, Edit, …) but bars `Bash` /
|
|
504
|
+
* unknown-effect / inherits-all; `"dangerously-unrestricted"` (or omitting it)
|
|
505
|
+
* enforces nothing. `"pure"`/`"bounded"` require an explicit `tools` list — a
|
|
506
|
+
* wildcard or absent-tools (inherits-all) is always a violation.
|
|
507
|
+
*/
|
|
508
|
+
readonly purity?: AuthoredPurity;
|
|
370
509
|
}
|
|
371
510
|
/**
|
|
372
511
|
* Define a subagent specification (compiles to `agents/<name>.md`).
|
package/dist/core/spec.js
CHANGED
|
@@ -17,7 +17,10 @@ exports.file = file;
|
|
|
17
17
|
exports.cmd = cmd;
|
|
18
18
|
exports.symbol = symbol;
|
|
19
19
|
exports.ref = ref;
|
|
20
|
+
exports.dir = dir;
|
|
21
|
+
exports.glob = glob;
|
|
20
22
|
exports.instructions = instructions;
|
|
23
|
+
exports.effect = effect;
|
|
21
24
|
exports.claude = claude;
|
|
22
25
|
exports.project = project;
|
|
23
26
|
exports.input = input;
|
|
@@ -105,6 +108,24 @@ function symbol(file, name) {
|
|
|
105
108
|
function ref(path) {
|
|
106
109
|
return { _ref: "skill", path: path };
|
|
107
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Reference a directory — verified at compile time to exist AND be a directory
|
|
113
|
+
* (not a file). The "architecture floats free" fix: a spec that names `src/core/`
|
|
114
|
+
* proves the directory is really there, where a plain string in prose rots
|
|
115
|
+
* silently. Compiles to the inline form `` `path` ``.
|
|
116
|
+
*/
|
|
117
|
+
function dir(path) {
|
|
118
|
+
return { _ref: "dir", path: path };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Reference a glob pattern — verified at compile time to match at least one path,
|
|
122
|
+
* so `glob("src/*.test.ts")` proves tests actually exist where the instructions
|
|
123
|
+
* claim (the pattern supports the usual `*` / `**` syntax). Compiles to the
|
|
124
|
+
* inline form `` `pattern` ``.
|
|
125
|
+
*/
|
|
126
|
+
function glob(pattern) {
|
|
127
|
+
return { _ref: "glob", pattern: pattern };
|
|
128
|
+
}
|
|
108
129
|
/**
|
|
109
130
|
* Tagged template literal for skill instructions with typed references.
|
|
110
131
|
*
|
|
@@ -124,6 +145,33 @@ function instructions(strings, ...values) {
|
|
|
124
145
|
}
|
|
125
146
|
return result;
|
|
126
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Tagged template literal marking a side-effect boundary — usable as an
|
|
150
|
+
* interpolated fragment inside a body / `instructions\`\``:
|
|
151
|
+
*
|
|
152
|
+
* instructions`
|
|
153
|
+
* ## Apply
|
|
154
|
+
* ${effect`
|
|
155
|
+
* Side effects are allowed ONLY here:
|
|
156
|
+
* - write ${file("CHANGELOG.md")}
|
|
157
|
+
* - ${cmd("npm publish")}
|
|
158
|
+
* `}
|
|
159
|
+
* `
|
|
160
|
+
*
|
|
161
|
+
* Returns an `EffectRegion` fragment; `compile` wraps its rendered body in
|
|
162
|
+
* `<!-- vigiles:effect -->` markers. Independent of the `doc()` authoring
|
|
163
|
+
* surface — it does not block on it.
|
|
164
|
+
*/
|
|
165
|
+
function effect(strings, ...values) {
|
|
166
|
+
const body = [];
|
|
167
|
+
for (let i = 0; i < strings.length; i++) {
|
|
168
|
+
if (strings[i])
|
|
169
|
+
body.push(strings[i]);
|
|
170
|
+
if (i < values.length)
|
|
171
|
+
body.push(values[i]);
|
|
172
|
+
}
|
|
173
|
+
return { _ref: "effect", body };
|
|
174
|
+
}
|
|
127
175
|
/**
|
|
128
176
|
* Define a CLAUDE.md specification.
|
|
129
177
|
*
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* ONE pure detector (`one-detector-no-drift`), reused by THREE callers so they
|
|
9
9
|
* can't disagree: `compileAgent` (spec authoring), `scan` (read-only audit of a
|
|
10
|
-
* shipped plugin), and the `
|
|
10
|
+
* shipped plugin), and the `subagent-tool-contract` lint rule (the severity-gated
|
|
11
11
|
* commit gate). The dialect is injected (core ⊄ adapter) — the composition root
|
|
12
12
|
* passes `claudeCodeDialect` / `codexDialect`.
|
|
13
13
|
*
|
package/dist/core/types.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ export interface OrphansConfig {
|
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
73
|
* Shared options for the per-kind untested-* rules (`untested-skill` /
|
|
74
|
-
* `untested-
|
|
74
|
+
* `untested-subagent` / `untested-hook`). Which kinds are scanned is controlled by
|
|
75
75
|
* each rule's severity (set a rule to `false` to skip that kind), so only the
|
|
76
76
|
* test-discovery knobs live here.
|
|
77
77
|
*/
|
|
@@ -97,7 +97,7 @@ export interface RulesConfig {
|
|
|
97
97
|
/** Flag a skill (SKILL.md) that ships with no test or eval. Default: "warn". */
|
|
98
98
|
"untested-skill"?: RuleWithOptions<TestCoverageConfig>;
|
|
99
99
|
/** Flag a subagent (agents/*.md) that ships with no test or eval. Default: "warn". */
|
|
100
|
-
"untested-
|
|
100
|
+
"untested-subagent"?: RuleWithOptions<TestCoverageConfig>;
|
|
101
101
|
/** Flag a hook script that ships with no test or eval. Default: "warn". */
|
|
102
102
|
"untested-hook"?: RuleWithOptions<TestCoverageConfig>;
|
|
103
103
|
/**
|
|
@@ -115,7 +115,7 @@ export interface RulesConfig {
|
|
|
115
115
|
* plugin/MCP-provided, never flagged). Off unless set; "warn" surfaces,
|
|
116
116
|
* "error" gates CI. Same detector as `scan` + `compileAgent`.
|
|
117
117
|
*/
|
|
118
|
-
"
|
|
118
|
+
"subagent-tool-contract"?: RuleSeverity;
|
|
119
119
|
/**
|
|
120
120
|
* Flag a hook registered under an event name the harness doesn't define (a
|
|
121
121
|
* typo → the hook never fires). High-precision: close typos only, never a
|
|
@@ -128,7 +128,7 @@ export interface RulesConfig {
|
|
|
128
128
|
* `name` (to load), an agent needs `name` + `description`. A broken surface
|
|
129
129
|
* that won't register. Default "warn"; "error" gates CI. Same detector as `scan`.
|
|
130
130
|
*/
|
|
131
|
-
"
|
|
131
|
+
"subagent-frontmatter"?: RuleSeverity;
|
|
132
132
|
/**
|
|
133
133
|
* Flag a declared MCP server that can't start — neither a `command` (stdio)
|
|
134
134
|
* nor a `url` (http/sse). Default "warn"; "error" gates CI. Same detector as
|
|
@@ -146,7 +146,7 @@ export interface RulesConfig {
|
|
|
146
146
|
/**
|
|
147
147
|
* Cross-reference an `mcp__server__tool` in a subagent's contract against the
|
|
148
148
|
* plugin's declared `mcpServers` — flag a server the plugin doesn't declare
|
|
149
|
-
* (the MCP half of the tool moat; `
|
|
149
|
+
* (the MCP half of the tool moat; `subagent-tool-contract` checks the built-in
|
|
150
150
|
* half). High-precision: only flags when the plugin SHIPS a declared set,
|
|
151
151
|
* allowlists harness built-ins (`ide`), and skips the plugin-namespaced
|
|
152
152
|
* `mcp__plugin_…` form. Default "warn"; "error" gates CI. Same detector as
|
|
@@ -163,7 +163,7 @@ export interface RulesConfig {
|
|
|
163
163
|
"hook-script-exists"?: RuleSeverity;
|
|
164
164
|
/**
|
|
165
165
|
* Cross-reference a subagent's `disallowedTools:` block-list against the
|
|
166
|
-
* catalog — the deny-side mirror of `
|
|
166
|
+
* catalog — the deny-side mirror of `subagent-tool-contract`. A close typo there
|
|
167
167
|
* blocks NOTHING (you meant to deny `Bash`, wrote `Bsh`), leaving the tool
|
|
168
168
|
* available. High-precision: close-typo only (a never-available tool is
|
|
169
169
|
* harmless to list, a bare unknown is likely a plugin tool). Default "warn";
|