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.
@@ -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