vigiles 2.3.0 → 2.5.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 +214 -190
- package/dist/agent-result.d.ts +40 -0
- package/dist/agent-result.js +97 -0
- package/dist/agent-runtime.d.ts +64 -0
- package/dist/agent-runtime.js +147 -0
- package/dist/cli.js +155 -1
- package/dist/compile.d.ts +32 -3
- package/dist/compile.js +268 -0
- package/dist/eval-cache.d.ts +33 -0
- package/dist/eval-cache.js +94 -0
- package/dist/eval.d.ts +180 -9
- package/dist/eval.js +319 -57
- package/dist/harness-assert.d.ts +175 -6
- package/dist/harness-assert.js +355 -4
- package/dist/harness-test.d.ts +130 -5
- package/dist/harness-test.js +205 -32
- package/dist/judge.js +2 -0
- package/dist/linters.d.ts +6 -0
- package/dist/linters.js +1 -0
- package/dist/mcp.d.ts +48 -0
- package/dist/mcp.js +247 -0
- package/dist/mock-entry.d.ts +2 -0
- package/dist/mock-entry.js +36 -0
- package/dist/mock-model.d.ts +29 -0
- package/dist/mock-model.js +40 -0
- package/dist/plugin-loader.js +51 -17
- package/dist/sandbox.d.ts +76 -0
- package/dist/sandbox.js +241 -0
- package/dist/spec.d.ts +130 -0
- package/dist/spec.js +55 -0
- package/dist/stats.d.ts +49 -0
- package/dist/stats.js +109 -0
- package/package.json +7 -3
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vigiles — parse a subagent's railway result.
|
|
4
|
+
*
|
|
5
|
+
* A subagent with a `result()` contract is told (in its compiled system prompt)
|
|
6
|
+
* to end its turn with exactly one fenced block:
|
|
7
|
+
*
|
|
8
|
+
* ```vigiles:ok
|
|
9
|
+
* { "files": ["a.ts"], "summary": "done" }
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* or `vigiles:err` for the error track. This module extracts and validates that
|
|
13
|
+
* block — the single primitive the railway orchestrator and the harness-test
|
|
14
|
+
* assertions (`assertAgentOk`/`assertAgentErr`) both build on. Pure and
|
|
15
|
+
* model-free: hand it the worker's text, get back a discriminated outcome.
|
|
16
|
+
*
|
|
17
|
+
* "Railway-oriented" is literal here: the parse is `text -> Result<S, E>` with a
|
|
18
|
+
* third `malformed` track for a worker that didn't honor its contract (no block,
|
|
19
|
+
* bad JSON, or a shape that doesn't match the declared schema).
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.parseAgentResult = parseAgentResult;
|
|
23
|
+
// Capture every vigiles:ok / vigiles:err fenced block; the LAST one is the
|
|
24
|
+
// worker's final answer (earlier ones may be illustrative in its reasoning).
|
|
25
|
+
const BLOCK_RE = /```vigiles:(ok|err)[ \t]*\r?\n([\s\S]*?)```/g;
|
|
26
|
+
/** Does a runtime value match a declared field type? */
|
|
27
|
+
function fieldMatches(value, type) {
|
|
28
|
+
switch (type) {
|
|
29
|
+
case "string":
|
|
30
|
+
return typeof value === "string";
|
|
31
|
+
case "number":
|
|
32
|
+
return typeof value === "number";
|
|
33
|
+
case "boolean":
|
|
34
|
+
return typeof value === "boolean";
|
|
35
|
+
case "string[]":
|
|
36
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Validate a parsed object against a contract track; null when it conforms. */
|
|
40
|
+
function shapeError(obj, shape) {
|
|
41
|
+
for (const [field, type] of Object.entries(shape)) {
|
|
42
|
+
if (!(field in obj))
|
|
43
|
+
return `missing field "${field}"`;
|
|
44
|
+
if (!fieldMatches(obj[field], type)) {
|
|
45
|
+
return `field "${field}" should be ${type}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse the last `vigiles:ok` / `vigiles:err` block from a worker's output.
|
|
52
|
+
*
|
|
53
|
+
* With a `contract`, the parsed object is validated against the matching track's
|
|
54
|
+
* shape — a worker that emits the wrong shape is `malformed`, not a silent pass.
|
|
55
|
+
* Without one, any well-formed JSON block is accepted.
|
|
56
|
+
*/
|
|
57
|
+
function parseAgentResult(text, contract) {
|
|
58
|
+
BLOCK_RE.lastIndex = 0;
|
|
59
|
+
let last = null;
|
|
60
|
+
for (let m = BLOCK_RE.exec(text); m !== null; m = BLOCK_RE.exec(text)) {
|
|
61
|
+
last = { track: m[1], body: m[2] };
|
|
62
|
+
}
|
|
63
|
+
if (!last) {
|
|
64
|
+
return {
|
|
65
|
+
kind: "malformed",
|
|
66
|
+
reason: "no vigiles:ok/vigiles:err block found",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(last.body);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return {
|
|
75
|
+
kind: "malformed",
|
|
76
|
+
reason: `invalid JSON in vigiles:${last.track} block`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
80
|
+
return {
|
|
81
|
+
kind: "malformed",
|
|
82
|
+
reason: `vigiles:${last.track} block must be a JSON object`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const obj = parsed;
|
|
86
|
+
if (contract) {
|
|
87
|
+
const shape = last.track === "ok" ? contract.ok : contract.err;
|
|
88
|
+
const err = shapeError(obj, shape);
|
|
89
|
+
if (err) {
|
|
90
|
+
return { kind: "malformed", reason: `${last.track} block: ${err}` };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return last.track === "ok"
|
|
94
|
+
? { kind: "ok", value: obj }
|
|
95
|
+
: { kind: "err", error: obj };
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=agent-result.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vigiles — Agent runtime: the PreToolUse tool-contract rail.
|
|
3
|
+
*
|
|
4
|
+
* A subagent declares an allowed-tools contract in its frontmatter (`tools:`).
|
|
5
|
+
* But that field is documentation, not a hard runtime boundary (Claude Code
|
|
6
|
+
* issue #54898): permissions are session-wide, a subagent inherits the parent
|
|
7
|
+
* session's grants, and `tools:` only filters what's *offered* — it can't deny
|
|
8
|
+
* what the session allows. The deterministic layer that actually closes the gap
|
|
9
|
+
* is a **PreToolUse hook** that blocks any tool the active agent's contract
|
|
10
|
+
* doesn't list.
|
|
11
|
+
*
|
|
12
|
+
* This is the same emit-a-hook pattern the skill runtime already ships
|
|
13
|
+
* (`src/skill-runtime.ts`): there a `Stop` hook reads the active skill's
|
|
14
|
+
* compiled SKILL.md and runs its result gate; here a `PreToolUse` hook reads
|
|
15
|
+
* the active agent's compiled `.md`, parses its `tools:` allowlist, and
|
|
16
|
+
* allows/denies the tool call. The compiled markdown's frontmatter is the
|
|
17
|
+
* single source of truth — the same list that documents intent IS the list the
|
|
18
|
+
* hook enforces, so the two agree by construction (see `enforcedTools`).
|
|
19
|
+
*
|
|
20
|
+
* Which agent is active is recorded in `.vigiles/active-agent.json` — Claude
|
|
21
|
+
* Code hooks don't surface the dispatched subagent, so vigiles records it
|
|
22
|
+
* (mirrors `.vigiles/active-skill.json`). The decision logic below is
|
|
23
|
+
* harness-agnostic and fully testable.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Parse an agent's allowed-tools contract from its compiled markdown.
|
|
27
|
+
*
|
|
28
|
+
* Returns the list of allowed tool names, or `null` when the agent declares no
|
|
29
|
+
* `tools:` line at all — which in Claude Code means it inherits EVERY tool (the
|
|
30
|
+
* #1 footgun). `null` is the "no restriction" signal the decision logic honors;
|
|
31
|
+
* an empty list (`tools:` with nothing after it) means "no tools allowed".
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseAgentTools(markdown: string): string[] | null;
|
|
34
|
+
export interface PreToolDecision {
|
|
35
|
+
/** Whether the tool call is allowed (true) or blocked (false). */
|
|
36
|
+
readonly allow: boolean;
|
|
37
|
+
/** Message fed back to the model on a block; empty on allow. */
|
|
38
|
+
readonly message: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Decide whether `tool` is allowed under an agent's tool contract. Pure, so the
|
|
42
|
+
* rail is unit-testable without spawning anything.
|
|
43
|
+
*
|
|
44
|
+
* - `allowed === null` → the agent declared no `tools:` line, so it inherits
|
|
45
|
+
* everything and the rail imposes no restriction (allow).
|
|
46
|
+
* - otherwise → allow iff the tool is in the allowlist; deny anything else,
|
|
47
|
+
* feeding the contract back to the model so it self-corrects.
|
|
48
|
+
*/
|
|
49
|
+
export declare function decidePreToolUse(allowed: readonly string[] | null, tool: string): PreToolDecision;
|
|
50
|
+
/** Record the subagent currently dispatched, so PreToolUse enforces its contract. */
|
|
51
|
+
export declare function setActiveAgent(cwd: string, agentPath: string): void;
|
|
52
|
+
/** Clear the active-agent marker (the subagent finished). */
|
|
53
|
+
export declare function clearActiveAgent(cwd: string): void;
|
|
54
|
+
/** The path of the active agent's compiled `.md`, or null when none is active. */
|
|
55
|
+
export declare function readActiveAgent(cwd: string): string | null;
|
|
56
|
+
/**
|
|
57
|
+
* PreToolUse-hook decision. If an agent is active, parse its compiled `.md`
|
|
58
|
+
* tool contract and allow the call only when the tool is in the allowlist;
|
|
59
|
+
* otherwise block and tell the model which tools it may use. With no active
|
|
60
|
+
* agent (or an agent that inherits all tools), always allow — the rail only
|
|
61
|
+
* constrains agents that declared a contract.
|
|
62
|
+
*/
|
|
63
|
+
export declare function evaluatePreToolUse(cwd: string, tool: string): PreToolDecision;
|
|
64
|
+
//# sourceMappingURL=agent-runtime.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vigiles — Agent runtime: the PreToolUse tool-contract rail.
|
|
4
|
+
*
|
|
5
|
+
* A subagent declares an allowed-tools contract in its frontmatter (`tools:`).
|
|
6
|
+
* But that field is documentation, not a hard runtime boundary (Claude Code
|
|
7
|
+
* issue #54898): permissions are session-wide, a subagent inherits the parent
|
|
8
|
+
* session's grants, and `tools:` only filters what's *offered* — it can't deny
|
|
9
|
+
* what the session allows. The deterministic layer that actually closes the gap
|
|
10
|
+
* is a **PreToolUse hook** that blocks any tool the active agent's contract
|
|
11
|
+
* doesn't list.
|
|
12
|
+
*
|
|
13
|
+
* This is the same emit-a-hook pattern the skill runtime already ships
|
|
14
|
+
* (`src/skill-runtime.ts`): there a `Stop` hook reads the active skill's
|
|
15
|
+
* compiled SKILL.md and runs its result gate; here a `PreToolUse` hook reads
|
|
16
|
+
* the active agent's compiled `.md`, parses its `tools:` allowlist, and
|
|
17
|
+
* allows/denies the tool call. The compiled markdown's frontmatter is the
|
|
18
|
+
* single source of truth — the same list that documents intent IS the list the
|
|
19
|
+
* hook enforces, so the two agree by construction (see `enforcedTools`).
|
|
20
|
+
*
|
|
21
|
+
* Which agent is active is recorded in `.vigiles/active-agent.json` — Claude
|
|
22
|
+
* Code hooks don't surface the dispatched subagent, so vigiles records it
|
|
23
|
+
* (mirrors `.vigiles/active-skill.json`). The decision logic below is
|
|
24
|
+
* harness-agnostic and fully testable.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.parseAgentTools = parseAgentTools;
|
|
28
|
+
exports.decidePreToolUse = decidePreToolUse;
|
|
29
|
+
exports.setActiveAgent = setActiveAgent;
|
|
30
|
+
exports.clearActiveAgent = clearActiveAgent;
|
|
31
|
+
exports.readActiveAgent = readActiveAgent;
|
|
32
|
+
exports.evaluatePreToolUse = evaluatePreToolUse;
|
|
33
|
+
const node_fs_1 = require("node:fs");
|
|
34
|
+
const node_path_1 = require("node:path");
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Parse the tool contract from a compiled agent .md
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
/** Extract the YAML frontmatter block (between the first pair of `---` fences). */
|
|
39
|
+
function extractFrontmatter(markdown) {
|
|
40
|
+
const lines = markdown.split("\n");
|
|
41
|
+
let start = -1;
|
|
42
|
+
for (let i = 0; i < lines.length; i++) {
|
|
43
|
+
if (lines[i].trim() === "---") {
|
|
44
|
+
start = i;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (start === -1)
|
|
49
|
+
return null;
|
|
50
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
51
|
+
if (lines[i].trim() === "---") {
|
|
52
|
+
return lines.slice(start + 1, i).join("\n");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Parse an agent's allowed-tools contract from its compiled markdown.
|
|
59
|
+
*
|
|
60
|
+
* Returns the list of allowed tool names, or `null` when the agent declares no
|
|
61
|
+
* `tools:` line at all — which in Claude Code means it inherits EVERY tool (the
|
|
62
|
+
* #1 footgun). `null` is the "no restriction" signal the decision logic honors;
|
|
63
|
+
* an empty list (`tools:` with nothing after it) means "no tools allowed".
|
|
64
|
+
*/
|
|
65
|
+
function parseAgentTools(markdown) {
|
|
66
|
+
const fm = extractFrontmatter(markdown);
|
|
67
|
+
if (fm === null)
|
|
68
|
+
return null;
|
|
69
|
+
const match = /^tools:[ \t]*(.*)$/m.exec(fm);
|
|
70
|
+
if (!match)
|
|
71
|
+
return null;
|
|
72
|
+
return match[1]
|
|
73
|
+
.split(",")
|
|
74
|
+
.map((t) => t.trim())
|
|
75
|
+
.filter((t) => t.length > 0);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Decide whether `tool` is allowed under an agent's tool contract. Pure, so the
|
|
79
|
+
* rail is unit-testable without spawning anything.
|
|
80
|
+
*
|
|
81
|
+
* - `allowed === null` → the agent declared no `tools:` line, so it inherits
|
|
82
|
+
* everything and the rail imposes no restriction (allow).
|
|
83
|
+
* - otherwise → allow iff the tool is in the allowlist; deny anything else,
|
|
84
|
+
* feeding the contract back to the model so it self-corrects.
|
|
85
|
+
*/
|
|
86
|
+
function decidePreToolUse(allowed, tool) {
|
|
87
|
+
if (allowed === null)
|
|
88
|
+
return { allow: true, message: "" };
|
|
89
|
+
if (allowed.includes(tool))
|
|
90
|
+
return { allow: true, message: "" };
|
|
91
|
+
const list = allowed.length > 0 ? allowed.join(", ") : "(none)";
|
|
92
|
+
return {
|
|
93
|
+
allow: false,
|
|
94
|
+
message: `Tool "${tool}" is not in this subagent's allowed-tools contract ` +
|
|
95
|
+
`(${list}). Use only the listed tools, or widen the agent's \`tools\`.`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Active-agent tracking (mirrors .vigiles/active-skill.json)
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
const ACTIVE_PATH = ".vigiles/active-agent.json";
|
|
102
|
+
/** Record the subagent currently dispatched, so PreToolUse enforces its contract. */
|
|
103
|
+
function setActiveAgent(cwd, agentPath) {
|
|
104
|
+
const p = (0, node_path_1.resolve)(cwd, ACTIVE_PATH);
|
|
105
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(p), { recursive: true });
|
|
106
|
+
(0, node_fs_1.writeFileSync)(p, JSON.stringify({ agent: agentPath }) + "\n");
|
|
107
|
+
}
|
|
108
|
+
/** Clear the active-agent marker (the subagent finished). */
|
|
109
|
+
function clearActiveAgent(cwd) {
|
|
110
|
+
const p = (0, node_path_1.resolve)(cwd, ACTIVE_PATH);
|
|
111
|
+
if ((0, node_fs_1.existsSync)(p))
|
|
112
|
+
(0, node_fs_1.rmSync)(p);
|
|
113
|
+
}
|
|
114
|
+
/** The path of the active agent's compiled `.md`, or null when none is active. */
|
|
115
|
+
function readActiveAgent(cwd) {
|
|
116
|
+
const p = (0, node_path_1.resolve)(cwd, ACTIVE_PATH);
|
|
117
|
+
if (!(0, node_fs_1.existsSync)(p))
|
|
118
|
+
return null;
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse((0, node_fs_1.readFileSync)(p, "utf-8"));
|
|
121
|
+
return typeof parsed.agent === "string" ? parsed.agent : null;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// PreToolUse-hook decision
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
/**
|
|
131
|
+
* PreToolUse-hook decision. If an agent is active, parse its compiled `.md`
|
|
132
|
+
* tool contract and allow the call only when the tool is in the allowlist;
|
|
133
|
+
* otherwise block and tell the model which tools it may use. With no active
|
|
134
|
+
* agent (or an agent that inherits all tools), always allow — the rail only
|
|
135
|
+
* constrains agents that declared a contract.
|
|
136
|
+
*/
|
|
137
|
+
function evaluatePreToolUse(cwd, tool) {
|
|
138
|
+
const agentPath = readActiveAgent(cwd);
|
|
139
|
+
if (!agentPath)
|
|
140
|
+
return { allow: true, message: "" };
|
|
141
|
+
const full = (0, node_path_1.resolve)(cwd, agentPath);
|
|
142
|
+
if (!(0, node_fs_1.existsSync)(full))
|
|
143
|
+
return { allow: true, message: "" };
|
|
144
|
+
const allowed = parseAgentTools((0, node_fs_1.readFileSync)(full, "utf-8"));
|
|
145
|
+
return decidePreToolUse(allowed, tool);
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=agent-runtime.js.map
|
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,9 @@ const frontmatter_js_1 = require("./frontmatter.js");
|
|
|
23
23
|
const generate_schema_js_1 = require("./generate-schema.js");
|
|
24
24
|
const compile_generator_js_1 = require("./compile-generator.js");
|
|
25
25
|
const action_gate_js_1 = require("./action-gate.js");
|
|
26
|
+
const agent_runtime_js_1 = require("./agent-runtime.js");
|
|
26
27
|
const refs_js_1 = require("./refs.js");
|
|
28
|
+
const mcp_js_1 = require("./mcp.js");
|
|
27
29
|
const skill_runtime_js_1 = require("./skill-runtime.js");
|
|
28
30
|
const linters_js_1 = require("./linters.js");
|
|
29
31
|
const harness_test_js_1 = require("./harness-test.js");
|
|
@@ -176,8 +178,57 @@ function compileSkillToFile(spec, specPath) {
|
|
|
176
178
|
printErrors(specPath, errors);
|
|
177
179
|
return false;
|
|
178
180
|
}
|
|
181
|
+
/** Compile a subagent spec → agents/<name>.md (with its result-contract section). */
|
|
182
|
+
function compileAgentToFile(spec, specPath) {
|
|
183
|
+
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
184
|
+
const { markdown, errors } = (0, compile_js_1.compileAgent)(spec, {
|
|
185
|
+
basePath: process.cwd(),
|
|
186
|
+
specFile: specPath,
|
|
187
|
+
});
|
|
188
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
|
|
189
|
+
if (errors.length === 0) {
|
|
190
|
+
console.log(`\n✓ ${specPath} → ${outputPath}`);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
|
|
194
|
+
printErrors(specPath, errors);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Compile a railway spec → the orchestrator command markdown. `knownAgents` is
|
|
199
|
+
* the set of compiled agent names in the project, so every `delegate()` target
|
|
200
|
+
* is resolved at compile time (an unknown target is a stale-ref error).
|
|
201
|
+
*/
|
|
202
|
+
function compileRailwayToFile(spec, specPath, knownAgents) {
|
|
203
|
+
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
204
|
+
const { markdown, errors } = (0, compile_js_1.compileRailway)(spec, {
|
|
205
|
+
specFile: specPath,
|
|
206
|
+
knownAgents,
|
|
207
|
+
});
|
|
208
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
|
|
209
|
+
if (errors.length === 0) {
|
|
210
|
+
console.log(`\n✓ ${specPath} → ${outputPath}`);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
console.log(`\n✗ ${specPath} — ${String(errors.length)} error(s)`);
|
|
214
|
+
printErrors(specPath, errors);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
/** Names of every compiled agent spec in the project — resolves delegate() targets. */
|
|
218
|
+
async function collectAgentNames() {
|
|
219
|
+
const names = [];
|
|
220
|
+
for (const p of findSpecs()) {
|
|
221
|
+
const s = await loadSpec(p);
|
|
222
|
+
if (s && s._specType === "agent")
|
|
223
|
+
names.push(s.name);
|
|
224
|
+
}
|
|
225
|
+
return names;
|
|
226
|
+
}
|
|
179
227
|
async function compile(specPaths, config) {
|
|
180
228
|
let allValid = true;
|
|
229
|
+
// Resolved lazily on the first railway spec — every delegate() target is
|
|
230
|
+
// checked against the agents defined anywhere in the project.
|
|
231
|
+
let knownAgents = null;
|
|
181
232
|
for (const specPath of specPaths) {
|
|
182
233
|
// Generator skills can't be executed to markdown — compile from source.
|
|
183
234
|
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), specPath), "utf-8");
|
|
@@ -201,6 +252,15 @@ async function compile(specPaths, config) {
|
|
|
201
252
|
if (!compileSkillToFile(spec, specPath))
|
|
202
253
|
allValid = false;
|
|
203
254
|
}
|
|
255
|
+
else if (spec._specType === "agent") {
|
|
256
|
+
if (!compileAgentToFile(spec, specPath))
|
|
257
|
+
allValid = false;
|
|
258
|
+
}
|
|
259
|
+
else if (spec._specType === "railway") {
|
|
260
|
+
knownAgents ??= await collectAgentNames();
|
|
261
|
+
if (!compileRailwayToFile(spec, specPath, knownAgents))
|
|
262
|
+
allValid = false;
|
|
263
|
+
}
|
|
204
264
|
}
|
|
205
265
|
return allValid;
|
|
206
266
|
}
|
|
@@ -433,6 +493,46 @@ function verifyMarkdownSymbols(files, silent) {
|
|
|
433
493
|
}
|
|
434
494
|
return errors;
|
|
435
495
|
}
|
|
496
|
+
/**
|
|
497
|
+
* Verify `vigiles:mcp server#tool` marks in instruction files against the live
|
|
498
|
+
* MCP servers declared in `.mcp.json` — the referenced tool must exist on the
|
|
499
|
+
* server (it gets started for the check). No `.mcp.json` ⇒ skipped; a server is
|
|
500
|
+
* only started if a mark actually references it. Returns the count of broken
|
|
501
|
+
* references. Async because it speaks to real servers.
|
|
502
|
+
*/
|
|
503
|
+
async function verifyMarkdownMcpRefs(files, silent) {
|
|
504
|
+
const cwd = process.cwd();
|
|
505
|
+
const servers = (0, mcp_js_1.loadMcpServers)(cwd);
|
|
506
|
+
if (files.length === 0 || Object.keys(servers).length === 0)
|
|
507
|
+
return 0;
|
|
508
|
+
let printedHeader = false;
|
|
509
|
+
let errors = 0;
|
|
510
|
+
for (const f of files) {
|
|
511
|
+
let markdown;
|
|
512
|
+
try {
|
|
513
|
+
markdown = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(cwd, f), "utf-8");
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const broken = await (0, mcp_js_1.verifyMcpRefs)(markdown, servers);
|
|
519
|
+
if (broken.length === 0)
|
|
520
|
+
continue;
|
|
521
|
+
if (!silent) {
|
|
522
|
+
if (!printedHeader) {
|
|
523
|
+
console.log("\nMCP reference check:\n");
|
|
524
|
+
printedHeader = true;
|
|
525
|
+
}
|
|
526
|
+
for (const b of broken) {
|
|
527
|
+
const msg = (0, mcp_js_1.mcpRefMessage)(b);
|
|
528
|
+
console.log(` ✗ ${f}:${String(b.line)} ${msg}`);
|
|
529
|
+
ghAnnotate("error", msg, f, b.line);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
errors += broken.length;
|
|
533
|
+
}
|
|
534
|
+
return errors;
|
|
535
|
+
}
|
|
436
536
|
/** Exit codes: 0 clean, 1 warnings only, 2 hard errors. */
|
|
437
537
|
function auditExitCode(report) {
|
|
438
538
|
if (report.hashErrors > 0 ||
|
|
@@ -441,7 +541,8 @@ function auditExitCode(report) {
|
|
|
441
541
|
report.frontmatterErrors > 0 ||
|
|
442
542
|
report.integrityErrors > 0 ||
|
|
443
543
|
report.coverageErrors > 0 ||
|
|
444
|
-
report.symbolRefErrors > 0
|
|
544
|
+
report.symbolRefErrors > 0 ||
|
|
545
|
+
report.mcpRefErrors > 0)
|
|
445
546
|
return 2;
|
|
446
547
|
if (report.duplicatePairs > 0 ||
|
|
447
548
|
report.orphanCount > 0 ||
|
|
@@ -730,6 +831,9 @@ async function audit(restArgs, flags, config) {
|
|
|
730
831
|
}
|
|
731
832
|
// 9. Verify code-shaped symbol references live (see src/refs.ts).
|
|
732
833
|
const symbolRefErrors = verifyMarkdownSymbols(files, silent);
|
|
834
|
+
// 10. Verify `vigiles:mcp server#tool` marks against live MCP servers
|
|
835
|
+
// (only when a .mcp.json declares them). See src/mcp.ts.
|
|
836
|
+
const mcpRefErrors = await verifyMarkdownMcpRefs(files, silent);
|
|
733
837
|
const report = {
|
|
734
838
|
hashErrors: hashResult.hashErrors,
|
|
735
839
|
validationErrors: hashResult.validationErrors,
|
|
@@ -746,6 +850,7 @@ async function audit(restArgs, flags, config) {
|
|
|
746
850
|
orphanCount: orphanReport.orphans.length,
|
|
747
851
|
docRefErrors: docRefReport.errors.length,
|
|
748
852
|
symbolRefErrors,
|
|
853
|
+
mcpRefErrors,
|
|
749
854
|
files,
|
|
750
855
|
};
|
|
751
856
|
if (summary) {
|
|
@@ -775,6 +880,8 @@ function printAuditSummary(report) {
|
|
|
775
880
|
parts.push(`${String(report.docRefErrors)} broken doc refs`);
|
|
776
881
|
if (report.symbolRefErrors > 0)
|
|
777
882
|
parts.push(`${String(report.symbolRefErrors)} broken symbol refs`);
|
|
883
|
+
if (report.mcpRefErrors > 0)
|
|
884
|
+
parts.push(`${String(report.mcpRefErrors)} broken MCP refs`);
|
|
778
885
|
const undocumented = report.coverageEnabled - report.coverageDocumented;
|
|
779
886
|
if (undocumented > 0)
|
|
780
887
|
parts.push(`${String(undocumented)} undocumented rules`);
|
|
@@ -1564,6 +1671,44 @@ function skillStartCommand(target) {
|
|
|
1564
1671
|
(0, skill_runtime_js_1.setActiveSkill)(process.cwd(), target);
|
|
1565
1672
|
console.log(`Active skill: ${target}`);
|
|
1566
1673
|
}
|
|
1674
|
+
/**
|
|
1675
|
+
* PreToolUse-hook entrypoint: enforce the active subagent's allowed-tools
|
|
1676
|
+
* contract. Reads the tool event on stdin, parses the active agent's compiled
|
|
1677
|
+
* `.md` tool rail, and blocks (exit 2 + reason on stderr) any tool outside it —
|
|
1678
|
+
* the deterministic boundary `tools:` alone can't provide (Claude Code #54898).
|
|
1679
|
+
*/
|
|
1680
|
+
function agentHookCommand() {
|
|
1681
|
+
let raw = "";
|
|
1682
|
+
try {
|
|
1683
|
+
raw = (0, node_fs_1.readFileSync)(0, "utf-8");
|
|
1684
|
+
}
|
|
1685
|
+
catch {
|
|
1686
|
+
/* no stdin */
|
|
1687
|
+
}
|
|
1688
|
+
let tool = "";
|
|
1689
|
+
try {
|
|
1690
|
+
tool = JSON.parse(raw).tool_name ?? "";
|
|
1691
|
+
}
|
|
1692
|
+
catch {
|
|
1693
|
+
/* malformed input → no tool, allow */
|
|
1694
|
+
}
|
|
1695
|
+
if (!tool)
|
|
1696
|
+
return;
|
|
1697
|
+
const decision = (0, agent_runtime_js_1.evaluatePreToolUse)(process.cwd(), tool);
|
|
1698
|
+
if (!decision.allow) {
|
|
1699
|
+
console.error(decision.message);
|
|
1700
|
+
process.exit(2);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
/** Mark a subagent active so the PreToolUse hook enforces its tool contract. */
|
|
1704
|
+
function agentStartCommand(target) {
|
|
1705
|
+
if (!target) {
|
|
1706
|
+
console.error("Usage: vigiles agent-start <agents/<name>.md>");
|
|
1707
|
+
process.exit(2);
|
|
1708
|
+
}
|
|
1709
|
+
(0, agent_runtime_js_1.setActiveAgent)(process.cwd(), target);
|
|
1710
|
+
console.log(`Active agent: ${target}`);
|
|
1711
|
+
}
|
|
1567
1712
|
/** Dispatch the skill-runtime subcommands. Returns false if unrecognized. */
|
|
1568
1713
|
function handleSkillCommand(command, restArgs) {
|
|
1569
1714
|
switch (command) {
|
|
@@ -1579,6 +1724,15 @@ function handleSkillCommand(command, restArgs) {
|
|
|
1579
1724
|
case "skill-hook":
|
|
1580
1725
|
skillHookCommand();
|
|
1581
1726
|
return true;
|
|
1727
|
+
case "agent-start":
|
|
1728
|
+
agentStartCommand(restArgs[0]);
|
|
1729
|
+
return true;
|
|
1730
|
+
case "agent-done":
|
|
1731
|
+
(0, agent_runtime_js_1.clearActiveAgent)(process.cwd());
|
|
1732
|
+
return true;
|
|
1733
|
+
case "agent-hook":
|
|
1734
|
+
agentHookCommand();
|
|
1735
|
+
return true;
|
|
1582
1736
|
case "action-hook":
|
|
1583
1737
|
actionHookCommand();
|
|
1584
1738
|
return true;
|
package/dist/compile.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Reads .spec.ts files, validates references, and produces
|
|
5
5
|
* markdown instruction files with integrity hashes.
|
|
6
6
|
*/
|
|
7
|
-
import type { ClaudeSpec, SkillSpec } from "./spec.js";
|
|
7
|
+
import type { ClaudeSpec, SkillSpec, AgentSpec, Railway } from "./spec.js";
|
|
8
8
|
import type { LinterCheckResult } from "./linters.js";
|
|
9
9
|
/** @internal Compute SHA-256 hash of content (excluding any existing hash line). */
|
|
10
10
|
export declare function computeHash(content: string): string;
|
|
@@ -24,7 +24,7 @@ export declare function verifyHash(content: string): {
|
|
|
24
24
|
*/
|
|
25
25
|
/** @internal */ export declare function estimateTokens(text: string): number;
|
|
26
26
|
export interface CompileError {
|
|
27
|
-
type: "stale-file" | "stale-command" | "stale-ref" | "invalid-rule" | "budget-exceeded" | "section-too-long" | "section-has-header" | "reserved-section-key" | "spec-name-mismatch";
|
|
27
|
+
type: "stale-file" | "stale-command" | "stale-ref" | "invalid-rule" | "budget-exceeded" | "section-too-long" | "section-has-header" | "reserved-section-key" | "spec-name-mismatch" | "unknown-tool" | "invalid-railway";
|
|
28
28
|
message: string;
|
|
29
29
|
path?: string;
|
|
30
30
|
}
|
|
@@ -79,6 +79,35 @@ export declare function compileSkill(spec: SkillSpec, options?: {
|
|
|
79
79
|
basePath?: string;
|
|
80
80
|
specFile?: string;
|
|
81
81
|
}): CompileSkillResult;
|
|
82
|
+
export interface CompileAgentResult {
|
|
83
|
+
markdown: string;
|
|
84
|
+
errors: CompileError[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Compile an AgentSpec into a subagent markdown file with YAML frontmatter.
|
|
88
|
+
* Verifies the tool contract and the body's references; the marks the body
|
|
89
|
+
* carries (`vigiles:symbol`, file/cmd refs) are the same ones `audit` re-checks.
|
|
90
|
+
*/
|
|
91
|
+
export declare function compileAgent(spec: AgentSpec, options?: {
|
|
92
|
+
basePath?: string;
|
|
93
|
+
specFile?: string;
|
|
94
|
+
}): CompileAgentResult;
|
|
95
|
+
export interface CompileRailwayOptions {
|
|
96
|
+
/** Names of compiled agents, to resolve `delegate` targets. Skipped if omitted. */
|
|
97
|
+
knownAgents?: readonly string[];
|
|
98
|
+
specFile?: string;
|
|
99
|
+
}
|
|
100
|
+
export interface CompileRailwayResult {
|
|
101
|
+
markdown: string;
|
|
102
|
+
errors: CompileError[];
|
|
103
|
+
}
|
|
104
|
+
/** Verify a railway: non-empty, bounded recovery, every delegate target real. */
|
|
105
|
+
export declare function validateRailway(rw: Railway, knownAgents?: readonly string[]): CompileError[];
|
|
106
|
+
/**
|
|
107
|
+
* Compile a railway into an orchestrator command markdown (with integrity hash),
|
|
108
|
+
* resolving every delegate target against `knownAgents` when provided.
|
|
109
|
+
*/
|
|
110
|
+
export declare function compileRailway(rw: Railway, options?: CompileRailwayOptions): CompileRailwayResult;
|
|
82
111
|
export interface HashCheckResult {
|
|
83
112
|
hasHash: boolean;
|
|
84
113
|
valid: boolean;
|
|
@@ -101,5 +130,5 @@ export interface AdoptResult {
|
|
|
101
130
|
* Compare a generated file against what the spec would produce.
|
|
102
131
|
* Returns the diff so users can see what was manually changed.
|
|
103
132
|
*/
|
|
104
|
-
export declare function adoptDiff(filePath: string, spec: ClaudeSpec | SkillSpec, basePath: string): AdoptResult;
|
|
133
|
+
export declare function adoptDiff(filePath: string, spec: ClaudeSpec | SkillSpec | AgentSpec, basePath: string): AdoptResult;
|
|
105
134
|
//# sourceMappingURL=compile.d.ts.map
|