vigiles 2.1.1 → 2.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 +76 -5
- package/dist/action-gate.d.ts +28 -0
- package/dist/action-gate.js +73 -0
- package/dist/cli.js +408 -75
- package/dist/community-skills.d.ts +22 -0
- package/dist/community-skills.js +86 -0
- package/dist/compile-generator.d.ts +48 -0
- package/dist/compile-generator.js +322 -0
- package/dist/compile.d.ts +3 -0
- package/dist/compile.js +217 -26
- package/dist/eval.d.ts +62 -0
- package/dist/eval.js +174 -0
- package/dist/frontmatter.d.ts +24 -6
- package/dist/frontmatter.js +103 -30
- package/dist/generate-schema.js +10 -0
- package/dist/harness-test.d.ts +38 -0
- package/dist/harness-test.js +129 -0
- package/dist/inline.d.ts +22 -4
- package/dist/inline.js +60 -13
- package/dist/linters.js +28 -0
- package/dist/mock-model.d.ts +31 -0
- package/dist/mock-model.js +189 -0
- package/dist/refs.d.ts +44 -0
- package/dist/refs.js +144 -0
- package/dist/skill-driver.d.ts +77 -0
- package/dist/skill-driver.js +76 -0
- package/dist/skill-runtime.d.ts +101 -0
- package/dist/skill-runtime.js +289 -0
- package/dist/skill-test.d.ts +47 -0
- package/dist/skill-test.js +77 -0
- package/dist/spec.d.ts +90 -4
- package/dist/spec.js +29 -0
- package/dist/symbols.d.ts +30 -0
- package/dist/symbols.js +142 -0
- package/package.json +14 -5
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.scriptModel = scriptModel;
|
|
7
|
+
exports.startMock = startMock;
|
|
8
|
+
/**
|
|
9
|
+
* vigiles — a scripted, deterministic Anthropic Messages API mock.
|
|
10
|
+
*
|
|
11
|
+
* Point a Claude Code client at it with `ANTHROPIC_BASE_URL` (and any dummy
|
|
12
|
+
* `ANTHROPIC_API_KEY`) and it serves a fixed *script* of model turns in order —
|
|
13
|
+
* each `POST /v1/messages` returns the next. This is the seam that makes harness
|
|
14
|
+
* testing deterministic: the real `claude` CLI runs your real hooks/settings,
|
|
15
|
+
* but the model's turns are scripted, so the outcome is reproducible and free.
|
|
16
|
+
*
|
|
17
|
+
* scriptModel([
|
|
18
|
+
* { tool: "Write", input: { file_path: "SKILL.md", content: "..." } },
|
|
19
|
+
* { text: "done" },
|
|
20
|
+
* ])
|
|
21
|
+
*
|
|
22
|
+
* Implements the parts a real client needs: SSE streaming (flushed per event),
|
|
23
|
+
* `/v1/messages/count_tokens` (else Claude Code hangs), HEAD/health tolerance,
|
|
24
|
+
* and echoing the requested model.
|
|
25
|
+
*/
|
|
26
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
27
|
+
/** Build a scripted model from an ordered list of turns. */
|
|
28
|
+
function scriptModel(turns) {
|
|
29
|
+
return [...turns];
|
|
30
|
+
}
|
|
31
|
+
function writeEvent(res, event, data) {
|
|
32
|
+
res.write(`event: ${event}\n`);
|
|
33
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
34
|
+
}
|
|
35
|
+
const rid = (p) => `${p}${Math.random().toString(36).slice(2, 10)}`;
|
|
36
|
+
function streamTurn(res, turn, model) {
|
|
37
|
+
res.writeHead(200, {
|
|
38
|
+
"content-type": "text/event-stream",
|
|
39
|
+
"cache-control": "no-cache",
|
|
40
|
+
connection: "keep-alive",
|
|
41
|
+
});
|
|
42
|
+
writeEvent(res, "message_start", {
|
|
43
|
+
type: "message_start",
|
|
44
|
+
message: {
|
|
45
|
+
id: rid("msg_"),
|
|
46
|
+
type: "message",
|
|
47
|
+
role: "assistant",
|
|
48
|
+
model,
|
|
49
|
+
content: [],
|
|
50
|
+
stop_reason: null,
|
|
51
|
+
usage: { input_tokens: 10, output_tokens: 1 },
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
if (turn.tool) {
|
|
55
|
+
writeEvent(res, "content_block_start", {
|
|
56
|
+
type: "content_block_start",
|
|
57
|
+
index: 0,
|
|
58
|
+
content_block: {
|
|
59
|
+
type: "tool_use",
|
|
60
|
+
id: rid("toolu_"),
|
|
61
|
+
name: turn.tool,
|
|
62
|
+
input: {},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
writeEvent(res, "content_block_delta", {
|
|
66
|
+
type: "content_block_delta",
|
|
67
|
+
index: 0,
|
|
68
|
+
delta: {
|
|
69
|
+
type: "input_json_delta",
|
|
70
|
+
partial_json: JSON.stringify(turn.input ?? {}),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
writeEvent(res, "content_block_stop", {
|
|
74
|
+
type: "content_block_stop",
|
|
75
|
+
index: 0,
|
|
76
|
+
});
|
|
77
|
+
writeEvent(res, "message_delta", {
|
|
78
|
+
type: "message_delta",
|
|
79
|
+
delta: { stop_reason: "tool_use", stop_sequence: null },
|
|
80
|
+
usage: { output_tokens: 5 },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
writeEvent(res, "content_block_start", {
|
|
85
|
+
type: "content_block_start",
|
|
86
|
+
index: 0,
|
|
87
|
+
content_block: { type: "text", text: "" },
|
|
88
|
+
});
|
|
89
|
+
writeEvent(res, "content_block_delta", {
|
|
90
|
+
type: "content_block_delta",
|
|
91
|
+
index: 0,
|
|
92
|
+
delta: { type: "text_delta", text: turn.text ?? "" },
|
|
93
|
+
});
|
|
94
|
+
writeEvent(res, "content_block_stop", {
|
|
95
|
+
type: "content_block_stop",
|
|
96
|
+
index: 0,
|
|
97
|
+
});
|
|
98
|
+
writeEvent(res, "message_delta", {
|
|
99
|
+
type: "message_delta",
|
|
100
|
+
delta: { stop_reason: "end_turn", stop_sequence: null },
|
|
101
|
+
usage: { output_tokens: 5 },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
writeEvent(res, "message_stop", { type: "message_stop" });
|
|
105
|
+
res.end();
|
|
106
|
+
}
|
|
107
|
+
function jsonTurn(res, turn, model) {
|
|
108
|
+
const content = turn.tool
|
|
109
|
+
? [
|
|
110
|
+
{
|
|
111
|
+
type: "tool_use",
|
|
112
|
+
id: rid("toolu_"),
|
|
113
|
+
name: turn.tool,
|
|
114
|
+
input: turn.input ?? {},
|
|
115
|
+
},
|
|
116
|
+
]
|
|
117
|
+
: [{ type: "text", text: turn.text ?? "" }];
|
|
118
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
119
|
+
res.end(JSON.stringify({
|
|
120
|
+
id: rid("msg_"),
|
|
121
|
+
type: "message",
|
|
122
|
+
role: "assistant",
|
|
123
|
+
model,
|
|
124
|
+
stop_reason: turn.tool ? "tool_use" : "end_turn",
|
|
125
|
+
stop_sequence: null,
|
|
126
|
+
content,
|
|
127
|
+
usage: { input_tokens: 10, output_tokens: 5 },
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Start the scripted mock on a free port. Each `/v1/messages` POST consumes the
|
|
132
|
+
* next turn (the last turn repeats if the client asks for more). Resolves to a
|
|
133
|
+
* handle with the base `url` and a `close()`.
|
|
134
|
+
*/
|
|
135
|
+
function startMock(script, opts = {}) {
|
|
136
|
+
let i = 0;
|
|
137
|
+
const server = node_http_1.default.createServer((req, res) => {
|
|
138
|
+
let body = "";
|
|
139
|
+
req.on("data", (c) => (body += c));
|
|
140
|
+
req.on("end", () => {
|
|
141
|
+
const url = req.url ?? "";
|
|
142
|
+
const isCount = url.includes("count_tokens");
|
|
143
|
+
const isMessages = url.includes("/v1/messages") && !isCount;
|
|
144
|
+
let reqBody = {};
|
|
145
|
+
try {
|
|
146
|
+
reqBody = JSON.parse(body);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
/* HEAD / health checks have no JSON body */
|
|
150
|
+
}
|
|
151
|
+
if (req.method === "HEAD" || (!isMessages && !isCount)) {
|
|
152
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
153
|
+
res.end("{}");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (isCount) {
|
|
157
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
158
|
+
res.end(JSON.stringify({ input_tokens: 10 }));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const last = JSON.stringify(reqBody.messages?.at(-1)?.content ?? "");
|
|
162
|
+
opts.onTurn?.({
|
|
163
|
+
n: i,
|
|
164
|
+
stream: reqBody.stream === true,
|
|
165
|
+
hasToolResult: last.includes('"tool_result"'),
|
|
166
|
+
});
|
|
167
|
+
const turn = script[Math.min(i, script.length - 1)] ?? { text: "" };
|
|
168
|
+
i++;
|
|
169
|
+
const model = reqBody.model ?? "claude-mock";
|
|
170
|
+
if (reqBody.stream === true)
|
|
171
|
+
streamTurn(res, turn, model);
|
|
172
|
+
else
|
|
173
|
+
jsonTurn(res, turn, model);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
server.listen(0, "127.0.0.1", () => {
|
|
178
|
+
const { port } = server.address();
|
|
179
|
+
resolve({
|
|
180
|
+
url: `http://127.0.0.1:${String(port)}`,
|
|
181
|
+
close: () => server.close(),
|
|
182
|
+
get count() {
|
|
183
|
+
return i;
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=mock-model.js.map
|
package/dist/refs.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** An inline code span with its 1-based source line. */
|
|
2
|
+
export interface Span {
|
|
3
|
+
readonly text: string;
|
|
4
|
+
readonly line: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Extract inline code spans, skipping fenced code blocks (R1). Returns each
|
|
8
|
+
* span's trimmed text and 1-based line.
|
|
9
|
+
*/
|
|
10
|
+
export declare function inlineSpans(markdown: string): Span[];
|
|
11
|
+
/** A parsed file-qualified reference. */
|
|
12
|
+
export interface SymbolRef {
|
|
13
|
+
readonly file: string;
|
|
14
|
+
readonly symbol: string;
|
|
15
|
+
readonly line: number;
|
|
16
|
+
}
|
|
17
|
+
/** A reference that failed verification. */
|
|
18
|
+
export interface SymbolRefError extends SymbolRef {
|
|
19
|
+
readonly reason: string;
|
|
20
|
+
}
|
|
21
|
+
/** Extract the `vigiles:symbol` references from a markdown file. */
|
|
22
|
+
export declare function symbolRefs(markdown: string): SymbolRef[];
|
|
23
|
+
/**
|
|
24
|
+
* Verify the file-qualified symbol references in a markdown file: the named
|
|
25
|
+
* file must exist and define the named symbol. `basePath` is the directory the
|
|
26
|
+
* paths resolve against (the instruction file's own directory).
|
|
27
|
+
*/
|
|
28
|
+
export declare function verifySymbolRefs(markdown: string, basePath: string): SymbolRefError[];
|
|
29
|
+
/**
|
|
30
|
+
* Whether a span looks like a *code reference* that ought to carry a
|
|
31
|
+
* file-qualified mark — a scoped name, or an identifier that isn't a bare
|
|
32
|
+
* lowercase prose word. A function-call form `` `foo(args)` `` is treated as a
|
|
33
|
+
* reference to its callee `foo`. Paths/filenames are excluded (they are `file`
|
|
34
|
+
* refs).
|
|
35
|
+
*/
|
|
36
|
+
export declare function isCodeShaped(text: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Code-shaped inline references that are NOT yet marked — the spans the
|
|
39
|
+
* enforcement hook makes the agent mark as `` `vigiles:symbol path.ext#symbol` ``
|
|
40
|
+
* or opt out of with `<!-- vigiles:ignore -->` (or `<!-- vigiles:ignore-file -->`
|
|
41
|
+
* for the whole file).
|
|
42
|
+
*/
|
|
43
|
+
export declare function unmarkedCodeRefs(markdown: string): Span[];
|
|
44
|
+
//# sourceMappingURL=refs.d.ts.map
|
package/dist/refs.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inlineSpans = inlineSpans;
|
|
4
|
+
exports.symbolRefs = symbolRefs;
|
|
5
|
+
exports.verifySymbolRefs = verifySymbolRefs;
|
|
6
|
+
exports.isCodeShaped = isCodeShaped;
|
|
7
|
+
exports.unmarkedCodeRefs = unmarkedCodeRefs;
|
|
8
|
+
/**
|
|
9
|
+
* vigiles — file-qualified symbol reference verification (variant A).
|
|
10
|
+
*
|
|
11
|
+
* A reference names both the file and the symbol, as an inline code span:
|
|
12
|
+
*
|
|
13
|
+
* See `src/config.ts#parseConfig` for the loader.
|
|
14
|
+
* Render with `app/models/user.rb#full_name`.
|
|
15
|
+
*
|
|
16
|
+
* We parse *that one named file* and check it defines the symbol. No
|
|
17
|
+
* project-wide index, no cross-file resolution, no autoloader chasing, no
|
|
18
|
+
* ambiguity (the file disambiguates). Because the `path.ext#symbol` shape is
|
|
19
|
+
* unmistakable and deliberately written, it is a *declared* reference — like
|
|
20
|
+
* `vigiles:file` / `vigiles:cmd` — so a broken one is an **error**, not an
|
|
21
|
+
* inferred-prose warning. The author names the file; vigiles proves the symbol.
|
|
22
|
+
*
|
|
23
|
+
* R1 (cross-compat): only inline code spans are read. Fenced code blocks are
|
|
24
|
+
* never touched, so rustdoc doctests / typescript-docs-verifier keep working.
|
|
25
|
+
*/
|
|
26
|
+
const node_fs_1 = require("node:fs");
|
|
27
|
+
const node_path_1 = require("node:path");
|
|
28
|
+
const symbols_js_1 = require("./symbols.js");
|
|
29
|
+
const FENCE = /^\s*```/;
|
|
30
|
+
const SPAN = /`([^`\n]+)`/g;
|
|
31
|
+
/**
|
|
32
|
+
* Extract inline code spans, skipping fenced code blocks (R1). Returns each
|
|
33
|
+
* span's trimmed text and 1-based line.
|
|
34
|
+
*/
|
|
35
|
+
function inlineSpans(markdown) {
|
|
36
|
+
const spans = [];
|
|
37
|
+
let inFence = false;
|
|
38
|
+
const lines = markdown.split("\n");
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
if (FENCE.test(lines[i])) {
|
|
41
|
+
inFence = !inFence;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (inFence)
|
|
45
|
+
continue;
|
|
46
|
+
for (const m of lines[i].matchAll(SPAN)) {
|
|
47
|
+
spans.push({ text: m[1].trim(), line: i + 1 });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return spans;
|
|
51
|
+
}
|
|
52
|
+
// A file-qualified symbol reference: `<path>.<ext>` then `#`/`::` then a symbol.
|
|
53
|
+
// Requires a real file extension before the separator, so a bare scoped symbol
|
|
54
|
+
// An explicit `vigiles:symbol <path>.<ext>#<symbol>` directive inside a code
|
|
55
|
+
// span. The literal `vigiles:symbol` prefix means zero detection heuristic — a
|
|
56
|
+
// span either carries it or it does not — consistent with the rest of vigiles'
|
|
57
|
+
// markers. The mark is self-contained (file + symbol in one inline token), so
|
|
58
|
+
// it binds unambiguously even in a long line with several references.
|
|
59
|
+
const SYMBOL_MARK = /^vigiles:symbol\s+([\w@./-]+\.[A-Za-z0-9]+)(?:#|::)([A-Za-z_]\w*[?!]?)$/;
|
|
60
|
+
/** Extract the `vigiles:symbol` references from a markdown file. */
|
|
61
|
+
function symbolRefs(markdown) {
|
|
62
|
+
const refs = [];
|
|
63
|
+
for (const span of inlineSpans(markdown)) {
|
|
64
|
+
const m = SYMBOL_MARK.exec(span.text);
|
|
65
|
+
if (m)
|
|
66
|
+
refs.push({ file: m[1], symbol: m[2], line: span.line });
|
|
67
|
+
}
|
|
68
|
+
return refs;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Verify the file-qualified symbol references in a markdown file: the named
|
|
72
|
+
* file must exist and define the named symbol. `basePath` is the directory the
|
|
73
|
+
* paths resolve against (the instruction file's own directory).
|
|
74
|
+
*/
|
|
75
|
+
function verifySymbolRefs(markdown, basePath) {
|
|
76
|
+
const errors = [];
|
|
77
|
+
for (const ref of symbolRefs(markdown)) {
|
|
78
|
+
const full = (0, node_path_1.resolve)(basePath, ref.file);
|
|
79
|
+
if (!(0, node_fs_1.existsSync)(full)) {
|
|
80
|
+
errors.push({ ...ref, reason: `File not found: "${ref.file}"` });
|
|
81
|
+
}
|
|
82
|
+
else if ((0, symbols_js_1.langForFile)(ref.file) === null) {
|
|
83
|
+
errors.push({
|
|
84
|
+
...ref,
|
|
85
|
+
reason: `Unsupported language for symbol check: "${ref.file}"`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else if (!(0, symbols_js_1.fileDefinesSymbol)(full, ref.symbol)) {
|
|
89
|
+
errors.push({
|
|
90
|
+
...ref,
|
|
91
|
+
reason: `"${ref.symbol}" is not defined in ${ref.file}`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return errors;
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Enforcement: force code references to carry the file-qualified mark
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
const PATH_LIKE = /[/\\]|\.[A-Za-z0-9]+$/; // a path or a bare filename
|
|
101
|
+
const PLAIN_ID = /^[A-Za-z_]\w*$/;
|
|
102
|
+
const SCOPED = /^[A-Za-z_]\w*(?:#|::)[\w?!]+$/;
|
|
103
|
+
const IGNORE_FILE = /<!--\s*vigiles:ignore-file\s*-->/;
|
|
104
|
+
const IGNORE_LINE = /<!--\s*vigiles:ignore\s*-->/;
|
|
105
|
+
/**
|
|
106
|
+
* Whether a span looks like a *code reference* that ought to carry a
|
|
107
|
+
* file-qualified mark — a scoped name, or an identifier that isn't a bare
|
|
108
|
+
* lowercase prose word. A function-call form `` `foo(args)` `` is treated as a
|
|
109
|
+
* reference to its callee `foo`. Paths/filenames are excluded (they are `file`
|
|
110
|
+
* refs).
|
|
111
|
+
*/
|
|
112
|
+
function isCodeShaped(text) {
|
|
113
|
+
const callee = text.replace(/\s*\([^)]*\)\s*$/, ""); // `foo(args)` → `foo`
|
|
114
|
+
if (SCOPED.test(callee))
|
|
115
|
+
return true;
|
|
116
|
+
if (!PLAIN_ID.test(callee))
|
|
117
|
+
return false;
|
|
118
|
+
const hasUnderscore = callee.includes("_");
|
|
119
|
+
const hasCamel = /[a-z][A-Z]/.test(callee);
|
|
120
|
+
const isPascal = /^[A-Z][a-z]/.test(callee);
|
|
121
|
+
const isScreaming = /^[A-Z][A-Z0-9_]+$/.test(callee);
|
|
122
|
+
return hasUnderscore || hasCamel || isPascal || isScreaming;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Code-shaped inline references that are NOT yet marked — the spans the
|
|
126
|
+
* enforcement hook makes the agent mark as `` `vigiles:symbol path.ext#symbol` ``
|
|
127
|
+
* or opt out of with `<!-- vigiles:ignore -->` (or `<!-- vigiles:ignore-file -->`
|
|
128
|
+
* for the whole file).
|
|
129
|
+
*/
|
|
130
|
+
function unmarkedCodeRefs(markdown) {
|
|
131
|
+
if (IGNORE_FILE.test(markdown))
|
|
132
|
+
return [];
|
|
133
|
+
const lines = markdown.split("\n");
|
|
134
|
+
return inlineSpans(markdown).filter((span) => {
|
|
135
|
+
if (IGNORE_LINE.test(lines[span.line - 1] ?? ""))
|
|
136
|
+
return false;
|
|
137
|
+
if (SYMBOL_MARK.test(span.text))
|
|
138
|
+
return false; // already a vigiles:symbol mark
|
|
139
|
+
if (PATH_LIKE.test(span.text))
|
|
140
|
+
return false; // a path/filename → file ref
|
|
141
|
+
return isCodeShaped(span.text);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=refs.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vigiles — Skill driver: the generator (durable-imperative) form of a skill.
|
|
3
|
+
*
|
|
4
|
+
* The declarative `skill({ steps })` form compiles to a *static* SKILL.md — it
|
|
5
|
+
* can't express branching/looping, because that control flow depends on runtime
|
|
6
|
+
* values. The generator form does: a skill is a generator that `yield`s
|
|
7
|
+
* effects, and the harness drives it with `.next()`. Branches are real `if`,
|
|
8
|
+
* loops are real `for`/`while`; the harness owns the loop and runs the
|
|
9
|
+
* deterministic gates between yields, short-circuiting on failure (Railway).
|
|
10
|
+
*
|
|
11
|
+
* The model is an injected seam: `runStep(prose)` is what executes a prose step
|
|
12
|
+
* (in production it calls the LLM; in tests it's a scripted mock and the answer
|
|
13
|
+
* is fed back into the generator). Gates are deterministic and reuse the gate
|
|
14
|
+
* runtime. This module is the mechanics — fully testable without a live model.
|
|
15
|
+
*/
|
|
16
|
+
import type { Gate } from "./spec.js";
|
|
17
|
+
import { type GateOutcome } from "./skill-runtime.js";
|
|
18
|
+
export type SkillEffect = {
|
|
19
|
+
readonly kind: "act";
|
|
20
|
+
readonly prose: string;
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: "gate";
|
|
23
|
+
readonly gate: Gate;
|
|
24
|
+
readonly retry: number;
|
|
25
|
+
} | {
|
|
26
|
+
readonly kind: "result";
|
|
27
|
+
readonly gate: Gate;
|
|
28
|
+
};
|
|
29
|
+
/** A prose step for the model to perform. Its answer is yielded back in. */
|
|
30
|
+
export declare function act(prose: string): SkillEffect;
|
|
31
|
+
/** A deterministic checkpoint gate the harness runs after the prior step(s). */
|
|
32
|
+
export declare function checkpoint(gate: Gate, retry?: number): SkillEffect;
|
|
33
|
+
/** The terminal result gate — the skill is done only when it passes. */
|
|
34
|
+
export declare function finish(gate: Gate): SkillEffect;
|
|
35
|
+
/**
|
|
36
|
+
* A skill program: a generator that yields effects and receives, for each
|
|
37
|
+
* `act`, the model's answer back in (`const x = yield act(...)`).
|
|
38
|
+
*/
|
|
39
|
+
export type SkillProgram = () => Generator<SkillEffect, void, string>;
|
|
40
|
+
/** Frontmatter metadata for a generator skill. */
|
|
41
|
+
export interface GeneratorSkillMeta {
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly description: string;
|
|
44
|
+
readonly disableModelInvocation?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A generator skill = metadata + a generator program. Authored as
|
|
48
|
+
* `export default genSkill({ name, description }, function* () { … })`.
|
|
49
|
+
* The CLI compiles it to SKILL.md by parsing the source (it can't execute a
|
|
50
|
+
* generator to markdown); `runSkill`/`driveSkill` execute `program` directly.
|
|
51
|
+
*/
|
|
52
|
+
export interface GeneratorSkill extends GeneratorSkillMeta {
|
|
53
|
+
readonly _specType: "skill-generator";
|
|
54
|
+
readonly program: SkillProgram;
|
|
55
|
+
}
|
|
56
|
+
/** Define a generator skill (metadata + program). */
|
|
57
|
+
export declare function genSkill(meta: GeneratorSkillMeta, program: SkillProgram): GeneratorSkill;
|
|
58
|
+
export interface DriveStep {
|
|
59
|
+
readonly effect: SkillEffect;
|
|
60
|
+
/** The model's answer, for an `act` effect. */
|
|
61
|
+
readonly answer?: string;
|
|
62
|
+
/** The gate outcome, for a `gate`/`result` effect. */
|
|
63
|
+
readonly outcome?: GateOutcome;
|
|
64
|
+
}
|
|
65
|
+
export interface DriveReport {
|
|
66
|
+
readonly ok: boolean;
|
|
67
|
+
/** Index (in the trace) of the gate that blocked, or null if all passed. */
|
|
68
|
+
readonly blockedAt: number | null;
|
|
69
|
+
readonly trace: readonly DriveStep[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Drive a skill program to completion (or to the first failed gate). `runStep`
|
|
73
|
+
* is the model seam: it executes a prose step and returns the model's answer,
|
|
74
|
+
* which is fed back into the generator so branches can switch on it.
|
|
75
|
+
*/
|
|
76
|
+
export declare function driveSkill(program: SkillProgram, cwd: string, runStep: (prose: string) => string): DriveReport;
|
|
77
|
+
//# sourceMappingURL=skill-driver.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vigiles — Skill driver: the generator (durable-imperative) form of a skill.
|
|
4
|
+
*
|
|
5
|
+
* The declarative `skill({ steps })` form compiles to a *static* SKILL.md — it
|
|
6
|
+
* can't express branching/looping, because that control flow depends on runtime
|
|
7
|
+
* values. The generator form does: a skill is a generator that `yield`s
|
|
8
|
+
* effects, and the harness drives it with `.next()`. Branches are real `if`,
|
|
9
|
+
* loops are real `for`/`while`; the harness owns the loop and runs the
|
|
10
|
+
* deterministic gates between yields, short-circuiting on failure (Railway).
|
|
11
|
+
*
|
|
12
|
+
* The model is an injected seam: `runStep(prose)` is what executes a prose step
|
|
13
|
+
* (in production it calls the LLM; in tests it's a scripted mock and the answer
|
|
14
|
+
* is fed back into the generator). Gates are deterministic and reuse the gate
|
|
15
|
+
* runtime. This module is the mechanics — fully testable without a live model.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.act = act;
|
|
19
|
+
exports.checkpoint = checkpoint;
|
|
20
|
+
exports.finish = finish;
|
|
21
|
+
exports.genSkill = genSkill;
|
|
22
|
+
exports.driveSkill = driveSkill;
|
|
23
|
+
const skill_runtime_js_1 = require("./skill-runtime.js");
|
|
24
|
+
/** A prose step for the model to perform. Its answer is yielded back in. */
|
|
25
|
+
function act(prose) {
|
|
26
|
+
return { kind: "act", prose };
|
|
27
|
+
}
|
|
28
|
+
/** A deterministic checkpoint gate the harness runs after the prior step(s). */
|
|
29
|
+
function checkpoint(gate, retry = 1) {
|
|
30
|
+
return { kind: "gate", gate, retry };
|
|
31
|
+
}
|
|
32
|
+
/** The terminal result gate — the skill is done only when it passes. */
|
|
33
|
+
function finish(gate) {
|
|
34
|
+
return { kind: "result", gate };
|
|
35
|
+
}
|
|
36
|
+
/** Define a generator skill (metadata + program). */
|
|
37
|
+
function genSkill(meta, program) {
|
|
38
|
+
return { _specType: "skill-generator", ...meta, program };
|
|
39
|
+
}
|
|
40
|
+
/** Convert a spec-level Gate to the runtime gate the executor understands. */
|
|
41
|
+
function toRuntimeGate(gate, retry) {
|
|
42
|
+
if (gate._ref === "cmd")
|
|
43
|
+
return { kind: "cmd", command: gate.command, retry };
|
|
44
|
+
if (gate._ref === "role")
|
|
45
|
+
return { kind: "role", role: gate.role, retry };
|
|
46
|
+
return { kind: "file", path: gate.path, retry };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Drive a skill program to completion (or to the first failed gate). `runStep`
|
|
50
|
+
* is the model seam: it executes a prose step and returns the model's answer,
|
|
51
|
+
* which is fed back into the generator so branches can switch on it.
|
|
52
|
+
*/
|
|
53
|
+
function driveSkill(program, cwd, runStep) {
|
|
54
|
+
const gen = program();
|
|
55
|
+
const trace = [];
|
|
56
|
+
let input = "";
|
|
57
|
+
for (;;) {
|
|
58
|
+
const { value: effect, done } = gen.next(input);
|
|
59
|
+
if (done)
|
|
60
|
+
break;
|
|
61
|
+
if (effect.kind === "act") {
|
|
62
|
+
const answer = runStep(effect.prose);
|
|
63
|
+
trace.push({ effect, answer });
|
|
64
|
+
input = answer;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const retry = effect.kind === "gate" ? effect.retry : 1;
|
|
68
|
+
const outcome = (0, skill_runtime_js_1.runGate)(toRuntimeGate(effect.gate, retry), cwd);
|
|
69
|
+
trace.push({ effect, outcome });
|
|
70
|
+
if (!outcome.ok)
|
|
71
|
+
return { ok: false, blockedAt: trace.length - 1, trace };
|
|
72
|
+
input = "";
|
|
73
|
+
}
|
|
74
|
+
return { ok: true, blockedAt: null, trace };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=skill-driver.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vigiles — Skill runtime: the deterministic gate ladder.
|
|
3
|
+
*
|
|
4
|
+
* Parses the `vigiles:gate` / `vigiles:result` markers a compiled SKILL.md
|
|
5
|
+
* carries (emitted by compileSkill) and *executes* them: run each step's gate
|
|
6
|
+
* in document order, short-circuit on the first failure (Railway error track),
|
|
7
|
+
* then run the terminal result gate. This is the deterministic spine of the
|
|
8
|
+
* skill driver — the part that turns the markers from documentation into
|
|
9
|
+
* enforcement.
|
|
10
|
+
*
|
|
11
|
+
* Scope (v0): this runs the GATES only. It does not drive the LLM through the
|
|
12
|
+
* prose steps one at a time — that needs a live harness and is a later phase.
|
|
13
|
+
* `retry:N` is parsed and surfaced but executed once here: re-running a
|
|
14
|
+
* deterministic gate without a model in the loop to fix the step yields the
|
|
15
|
+
* same result. Retry is meaningful only once the model re-does the step
|
|
16
|
+
* between attempts.
|
|
17
|
+
*
|
|
18
|
+
* Safety: this executes the gate commands the skill author declared (e.g.
|
|
19
|
+
* `npm test`, `validate.py`) via an explicit, user-invoked command
|
|
20
|
+
* (`vigiles run-skill`). It is not a silent hook and runs nothing the spec
|
|
21
|
+
* didn't declare as a gate.
|
|
22
|
+
*/
|
|
23
|
+
export type RuntimeGate = {
|
|
24
|
+
readonly kind: "cmd";
|
|
25
|
+
readonly command: string;
|
|
26
|
+
readonly retry: number;
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: "file";
|
|
29
|
+
readonly path: string;
|
|
30
|
+
readonly retry: number;
|
|
31
|
+
} | {
|
|
32
|
+
readonly kind: "role";
|
|
33
|
+
readonly role: string;
|
|
34
|
+
readonly retry: number;
|
|
35
|
+
};
|
|
36
|
+
export interface SkillGates {
|
|
37
|
+
/** Per-step gates, in document order. */
|
|
38
|
+
readonly steps: readonly {
|
|
39
|
+
readonly step: number;
|
|
40
|
+
readonly gate: RuntimeGate;
|
|
41
|
+
}[];
|
|
42
|
+
/** Terminal postcondition gate, if any. */
|
|
43
|
+
readonly result?: RuntimeGate;
|
|
44
|
+
}
|
|
45
|
+
/** Extract the step gates and result gate from a compiled SKILL.md. */
|
|
46
|
+
export declare function parseSkillGates(markdown: string): SkillGates;
|
|
47
|
+
export interface GateOutcome {
|
|
48
|
+
readonly ok: boolean;
|
|
49
|
+
readonly output: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a project role (test/build/lint) to the host project's real command,
|
|
53
|
+
* detected from its ecosystem. Returns null when no command can be found —
|
|
54
|
+
* which the caller surfaces as a failed gate rather than a silent pass.
|
|
55
|
+
*/
|
|
56
|
+
export declare function detectProjectCommand(role: string, cwd: string): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Run one gate against `cwd`: a command (exit 0 = pass), a file existence, or
|
|
59
|
+
* a project role resolved to the host project's command.
|
|
60
|
+
*/
|
|
61
|
+
export declare function runGate(gate: RuntimeGate, cwd: string): GateOutcome;
|
|
62
|
+
/** Human-readable label for a gate (for reports and hook messages). */
|
|
63
|
+
export declare function gateLabel(gate: RuntimeGate): string;
|
|
64
|
+
export interface GateRunResult {
|
|
65
|
+
/** Step number, or "result" for the terminal gate. */
|
|
66
|
+
readonly at: number | "result";
|
|
67
|
+
readonly gate: RuntimeGate;
|
|
68
|
+
readonly ok: boolean;
|
|
69
|
+
readonly output: string;
|
|
70
|
+
}
|
|
71
|
+
export interface SkillRunReport {
|
|
72
|
+
readonly results: readonly GateRunResult[];
|
|
73
|
+
/** Where the ladder short-circuited, or null when every gate passed. */
|
|
74
|
+
readonly blockedAt: number | "result" | null;
|
|
75
|
+
readonly ok: boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Run the gate ladder: step gates in order (short-circuiting on the first
|
|
79
|
+
* failure), then the result gate. Mirrors a Sequence behavior tree —
|
|
80
|
+
* ordered AND with short-circuit on FAILURE.
|
|
81
|
+
*/
|
|
82
|
+
export declare function runSkillGates(gates: SkillGates, cwd: string): SkillRunReport;
|
|
83
|
+
/** Record the skill the agent is currently executing. */
|
|
84
|
+
export declare function setActiveSkill(cwd: string, skillPath: string): void;
|
|
85
|
+
/** Clear the active-skill marker (the skill finished). */
|
|
86
|
+
export declare function clearActiveSkill(cwd: string): void;
|
|
87
|
+
/** The path of the active skill, or null when none is in progress. */
|
|
88
|
+
export declare function readActiveSkill(cwd: string): string | null;
|
|
89
|
+
export interface StopDecision {
|
|
90
|
+
/** Whether the agent may stop (true) or must keep working (false). */
|
|
91
|
+
readonly allow: boolean;
|
|
92
|
+
/** Message for the user (allow) or fed back to the model (block). */
|
|
93
|
+
readonly message: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Stop-hook decision. If a skill is active and declares a result gate, run it:
|
|
97
|
+
* allow the stop only when the gate passes; otherwise block and tell the model
|
|
98
|
+
* what to fix. With no active skill (or no result gate), always allow.
|
|
99
|
+
*/
|
|
100
|
+
export declare function evaluateStopHook(cwd: string): StopDecision;
|
|
101
|
+
//# sourceMappingURL=skill-runtime.d.ts.map
|