vigiles 2.1.1 → 2.3.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.
Files changed (49) hide show
  1. package/README.md +127 -5
  2. package/dist/action-gate.d.ts +28 -0
  3. package/dist/action-gate.js +73 -0
  4. package/dist/cli.js +450 -75
  5. package/dist/community-skills.d.ts +22 -0
  6. package/dist/community-skills.js +86 -0
  7. package/dist/compile-generator.d.ts +48 -0
  8. package/dist/compile-generator.js +322 -0
  9. package/dist/compile.d.ts +3 -0
  10. package/dist/compile.js +217 -26
  11. package/dist/eval.d.ts +87 -0
  12. package/dist/eval.js +208 -0
  13. package/dist/frontmatter.d.ts +24 -6
  14. package/dist/frontmatter.js +103 -30
  15. package/dist/generate-schema.js +10 -0
  16. package/dist/harness-assert.d.ts +68 -0
  17. package/dist/harness-assert.js +127 -0
  18. package/dist/harness-test.d.ts +45 -0
  19. package/dist/harness-test.js +138 -0
  20. package/dist/inline.d.ts +22 -4
  21. package/dist/inline.js +60 -13
  22. package/dist/jest.d.ts +9 -0
  23. package/dist/jest.js +23 -0
  24. package/dist/judge.d.ts +29 -0
  25. package/dist/judge.js +88 -0
  26. package/dist/linters.js +28 -0
  27. package/dist/mock-model.d.ts +31 -0
  28. package/dist/mock-model.js +189 -0
  29. package/dist/plugin-loader.d.ts +37 -0
  30. package/dist/plugin-loader.js +195 -0
  31. package/dist/refs.d.ts +44 -0
  32. package/dist/refs.js +144 -0
  33. package/dist/run-hook.d.ts +77 -0
  34. package/dist/run-hook.js +80 -0
  35. package/dist/run-scripts.d.ts +20 -0
  36. package/dist/run-scripts.js +70 -0
  37. package/dist/skill-driver.d.ts +77 -0
  38. package/dist/skill-driver.js +76 -0
  39. package/dist/skill-runtime.d.ts +101 -0
  40. package/dist/skill-runtime.js +289 -0
  41. package/dist/skill-test.d.ts +47 -0
  42. package/dist/skill-test.js +77 -0
  43. package/dist/spec.d.ts +90 -4
  44. package/dist/spec.js +29 -0
  45. package/dist/symbols.d.ts +30 -0
  46. package/dist/symbols.js +142 -0
  47. package/dist/vitest.d.mts +9 -0
  48. package/dist/vitest.mjs +22 -0
  49. package/package.json +45 -6
@@ -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
@@ -0,0 +1,289 @@
1
+ "use strict";
2
+ /**
3
+ * vigiles — Skill runtime: the deterministic gate ladder.
4
+ *
5
+ * Parses the `vigiles:gate` / `vigiles:result` markers a compiled SKILL.md
6
+ * carries (emitted by compileSkill) and *executes* them: run each step's gate
7
+ * in document order, short-circuit on the first failure (Railway error track),
8
+ * then run the terminal result gate. This is the deterministic spine of the
9
+ * skill driver — the part that turns the markers from documentation into
10
+ * enforcement.
11
+ *
12
+ * Scope (v0): this runs the GATES only. It does not drive the LLM through the
13
+ * prose steps one at a time — that needs a live harness and is a later phase.
14
+ * `retry:N` is parsed and surfaced but executed once here: re-running a
15
+ * deterministic gate without a model in the loop to fix the step yields the
16
+ * same result. Retry is meaningful only once the model re-does the step
17
+ * between attempts.
18
+ *
19
+ * Safety: this executes the gate commands the skill author declared (e.g.
20
+ * `npm test`, `validate.py`) via an explicit, user-invoked command
21
+ * (`vigiles run-skill`). It is not a silent hook and runs nothing the spec
22
+ * didn't declare as a gate.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.parseSkillGates = parseSkillGates;
26
+ exports.detectProjectCommand = detectProjectCommand;
27
+ exports.runGate = runGate;
28
+ exports.gateLabel = gateLabel;
29
+ exports.runSkillGates = runSkillGates;
30
+ exports.setActiveSkill = setActiveSkill;
31
+ exports.clearActiveSkill = clearActiveSkill;
32
+ exports.readActiveSkill = readActiveSkill;
33
+ exports.evaluateStopHook = evaluateStopHook;
34
+ const node_child_process_1 = require("node:child_process");
35
+ const node_fs_1 = require("node:fs");
36
+ const node_path_1 = require("node:path");
37
+ const STEP_RE = /^###\s+Step\s+(\d+)/;
38
+ const GATE_CMD_RE = /<!--\s*vigiles:gate\s+"([^"]*)"(?:\s+retry:(\d+))?\s*-->/;
39
+ const GATE_FILE_RE = /<!--\s*vigiles:gate\s+file:(\S+)\s*-->/;
40
+ const GATE_ROLE_RE = /<!--\s*vigiles:gate\s+role:(\w+)(?:\s+retry:(\d+))?\s*-->/;
41
+ const RESULT_CMD_RE = /<!--\s*vigiles:result\s+"([^"]*)"\s*-->/;
42
+ const RESULT_FILE_RE = /<!--\s*vigiles:result\s+file:(\S+)\s*-->/;
43
+ const RESULT_ROLE_RE = /<!--\s*vigiles:result\s+role:(\w+)\s*-->/;
44
+ /** Parse a single line into a gate, or null if it carries none. */
45
+ function parseGateLine(line) {
46
+ const cmd = GATE_CMD_RE.exec(line);
47
+ if (cmd) {
48
+ return { kind: "cmd", command: cmd[1], retry: cmd[2] ? Number(cmd[2]) : 1 };
49
+ }
50
+ const role = GATE_ROLE_RE.exec(line);
51
+ if (role) {
52
+ return {
53
+ kind: "role",
54
+ role: role[1],
55
+ retry: role[2] ? Number(role[2]) : 1,
56
+ };
57
+ }
58
+ const fileM = GATE_FILE_RE.exec(line);
59
+ if (fileM)
60
+ return { kind: "file", path: fileM[1], retry: 1 };
61
+ return null;
62
+ }
63
+ /** Parse the terminal result gate from a line, or null. */
64
+ function parseResultLine(line) {
65
+ const cmd = RESULT_CMD_RE.exec(line);
66
+ if (cmd)
67
+ return { kind: "cmd", command: cmd[1], retry: 1 };
68
+ const role = RESULT_ROLE_RE.exec(line);
69
+ if (role)
70
+ return { kind: "role", role: role[1], retry: 1 };
71
+ const fileM = RESULT_FILE_RE.exec(line);
72
+ if (fileM)
73
+ return { kind: "file", path: fileM[1], retry: 1 };
74
+ return null;
75
+ }
76
+ /** Extract the step gates and result gate from a compiled SKILL.md. */
77
+ function parseSkillGates(markdown) {
78
+ const steps = [];
79
+ let result;
80
+ let currentStep = 0;
81
+ for (const line of markdown.split("\n")) {
82
+ const stepMatch = STEP_RE.exec(line);
83
+ if (stepMatch) {
84
+ currentStep = Number(stepMatch[1]);
85
+ continue;
86
+ }
87
+ const resultGate = parseResultLine(line);
88
+ if (resultGate) {
89
+ result = resultGate;
90
+ continue;
91
+ }
92
+ const gate = parseGateLine(line);
93
+ if (gate)
94
+ steps.push({ step: currentStep, gate });
95
+ }
96
+ return { steps, result };
97
+ }
98
+ /** Execute a shell command in `cwd`; exit 0 = pass, capturing output. */
99
+ function execCommand(command, cwd) {
100
+ try {
101
+ const out = (0, node_child_process_1.execSync)(command, {
102
+ cwd,
103
+ encoding: "utf-8",
104
+ stdio: ["ignore", "pipe", "pipe"],
105
+ });
106
+ return { ok: true, output: out.trim() };
107
+ }
108
+ catch (e) {
109
+ const err = e;
110
+ const output = [err.stdout, err.stderr].filter(Boolean).join("\n").trim();
111
+ return { ok: false, output };
112
+ }
113
+ }
114
+ const NPM_FOR_ROLE = {
115
+ test: "npm test",
116
+ build: "npm run build",
117
+ lint: "npm run lint",
118
+ };
119
+ /** Detect the npm command for a role from package.json scripts, or null. */
120
+ function detectNpmCommand(role, cwd) {
121
+ const pkg = (0, node_path_1.resolve)(cwd, "package.json");
122
+ if (!(0, node_fs_1.existsSync)(pkg))
123
+ return null;
124
+ try {
125
+ const scripts = JSON.parse((0, node_fs_1.readFileSync)(pkg, "utf-8")).scripts ?? {};
126
+ return scripts[role] ? (NPM_FOR_ROLE[role] ?? null) : null;
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ /** Non-npm ecosystems: marker files + the command for each role. */
133
+ const ECOSYSTEMS = [
134
+ {
135
+ markers: ["pyproject.toml", "setup.cfg"],
136
+ commands: { test: "pytest", lint: "ruff check ." },
137
+ },
138
+ {
139
+ markers: ["Cargo.toml"],
140
+ commands: {
141
+ test: "cargo test",
142
+ build: "cargo build",
143
+ lint: "cargo clippy",
144
+ },
145
+ },
146
+ {
147
+ markers: ["go.mod"],
148
+ commands: { test: "go test ./...", build: "go build ./..." },
149
+ },
150
+ ];
151
+ /**
152
+ * Resolve a project role (test/build/lint) to the host project's real command,
153
+ * detected from its ecosystem. Returns null when no command can be found —
154
+ * which the caller surfaces as a failed gate rather than a silent pass.
155
+ */
156
+ function detectProjectCommand(role, cwd) {
157
+ const npm = detectNpmCommand(role, cwd);
158
+ if (npm)
159
+ return npm;
160
+ for (const eco of ECOSYSTEMS) {
161
+ const present = eco.markers.some((m) => (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, m)));
162
+ const command = eco.commands[role];
163
+ if (present && command)
164
+ return command;
165
+ }
166
+ return null;
167
+ }
168
+ /**
169
+ * Run one gate against `cwd`: a command (exit 0 = pass), a file existence, or
170
+ * a project role resolved to the host project's command.
171
+ */
172
+ function runGate(gate, cwd) {
173
+ if (gate.kind === "file") {
174
+ const there = (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, gate.path));
175
+ return { ok: there, output: there ? "" : `${gate.path} not found` };
176
+ }
177
+ if (gate.kind === "role") {
178
+ const command = detectProjectCommand(gate.role, cwd);
179
+ if (!command) {
180
+ return {
181
+ ok: false,
182
+ output: `No ${gate.role} command detected for this project`,
183
+ };
184
+ }
185
+ return execCommand(command, cwd);
186
+ }
187
+ return execCommand(gate.command, cwd);
188
+ }
189
+ /** Human-readable label for a gate (for reports and hook messages). */
190
+ function gateLabel(gate) {
191
+ if (gate.kind === "cmd")
192
+ return `\`${gate.command}\``;
193
+ if (gate.kind === "role")
194
+ return `the project's ${gate.role} command`;
195
+ return `${gate.path} exists`;
196
+ }
197
+ /**
198
+ * Run the gate ladder: step gates in order (short-circuiting on the first
199
+ * failure), then the result gate. Mirrors a Sequence behavior tree —
200
+ * ordered AND with short-circuit on FAILURE.
201
+ */
202
+ function runSkillGates(gates, cwd) {
203
+ const results = [];
204
+ for (const { step, gate } of gates.steps) {
205
+ const r = runGate(gate, cwd);
206
+ results.push({ at: step, gate, ok: r.ok, output: r.output });
207
+ if (!r.ok)
208
+ return { results, blockedAt: step, ok: false };
209
+ }
210
+ if (gates.result) {
211
+ const r = runGate(gates.result, cwd);
212
+ results.push({
213
+ at: "result",
214
+ gate: gates.result,
215
+ ok: r.ok,
216
+ output: r.output,
217
+ });
218
+ if (!r.ok)
219
+ return { results, blockedAt: "result", ok: false };
220
+ }
221
+ return { results, blockedAt: null, ok: true };
222
+ }
223
+ // ---------------------------------------------------------------------------
224
+ // Stop-hook enforcement
225
+ // ---------------------------------------------------------------------------
226
+ //
227
+ // A skill is "active" while the agent is executing it. The Stop hook then runs
228
+ // the active skill's result gate and blocks completion until it passes — so the
229
+ // agent cannot declare a skill done until its result is deterministically
230
+ // proven. Which skill is active is tracked in `.vigiles/active-skill.json`
231
+ // (Claude Code hooks don't surface the active skill, so we record it). Wiring
232
+ // `skill-start` to fire automatically is the integration step; the decision
233
+ // logic below is harness-agnostic and fully testable.
234
+ const ACTIVE_PATH = ".vigiles/active-skill.json";
235
+ /** Record the skill the agent is currently executing. */
236
+ function setActiveSkill(cwd, skillPath) {
237
+ const p = (0, node_path_1.resolve)(cwd, ACTIVE_PATH);
238
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(p), { recursive: true });
239
+ (0, node_fs_1.writeFileSync)(p, JSON.stringify({ skill: skillPath }) + "\n");
240
+ }
241
+ /** Clear the active-skill marker (the skill finished). */
242
+ function clearActiveSkill(cwd) {
243
+ const p = (0, node_path_1.resolve)(cwd, ACTIVE_PATH);
244
+ if ((0, node_fs_1.existsSync)(p))
245
+ (0, node_fs_1.rmSync)(p);
246
+ }
247
+ /** The path of the active skill, or null when none is in progress. */
248
+ function readActiveSkill(cwd) {
249
+ const p = (0, node_path_1.resolve)(cwd, ACTIVE_PATH);
250
+ if (!(0, node_fs_1.existsSync)(p))
251
+ return null;
252
+ try {
253
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(p, "utf-8"));
254
+ return typeof parsed.skill === "string" ? parsed.skill : null;
255
+ }
256
+ catch {
257
+ return null;
258
+ }
259
+ }
260
+ /**
261
+ * Stop-hook decision. If a skill is active and declares a result gate, run it:
262
+ * allow the stop only when the gate passes; otherwise block and tell the model
263
+ * what to fix. With no active skill (or no result gate), always allow.
264
+ */
265
+ function evaluateStopHook(cwd) {
266
+ const skillPath = readActiveSkill(cwd);
267
+ if (!skillPath)
268
+ return { allow: true, message: "" };
269
+ const full = (0, node_path_1.resolve)(cwd, skillPath);
270
+ if (!(0, node_fs_1.existsSync)(full))
271
+ return { allow: true, message: "" };
272
+ const gates = parseSkillGates((0, node_fs_1.readFileSync)(full, "utf-8"));
273
+ if (!gates.result)
274
+ return { allow: true, message: "" };
275
+ const outcome = runGate(gates.result, cwd);
276
+ const desc = gateLabel(gates.result);
277
+ if (outcome.ok) {
278
+ return {
279
+ allow: true,
280
+ message: `✓ ${skillPath}: result gate ${desc} passed.`,
281
+ };
282
+ }
283
+ const tail = outcome.output ? `\n${outcome.output}` : "";
284
+ return {
285
+ allow: false,
286
+ message: `Skill "${skillPath}" is not done: result gate ${desc} failed. Fix it, then finish.${tail}`,
287
+ };
288
+ }
289
+ //# sourceMappingURL=skill-runtime.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * vigiles — Skill testing: deterministic tests for a skill's action sequence.
3
+ *
4
+ * A thin wrapper over the generator driver (`driveSkill`) for use inside an
5
+ * ordinary `node:test` / Vitest `test()` — no custom runner, no DSL. You script
6
+ * the *model* (the non-deterministic part) and assert the deterministic spine:
7
+ * which gates ran, in what order, which branch was taken, whether the result
8
+ * gate blocked. The model's prose quality is never asserted — that's the
9
+ * probabilistic boundary; everything else is deterministic and checkable.
10
+ */
11
+ import { type SkillProgram, type GeneratorSkill } from "./skill-driver.js";
12
+ export type ModelFn = (prose: string) => string;
13
+ /**
14
+ * Script the mocked model. Pass:
15
+ * - an array — answers consumed in order across all acts; or
16
+ * - a map keyed by a case-insensitive substring of the act's prose, where each
17
+ * value is a single answer, or an array consumed in order *per key* (so a
18
+ * loop's prompt can return different answers on successive iterations).
19
+ * Unmatched prose yields "".
20
+ */
21
+ export declare function scriptModel(spec: readonly string[] | Readonly<Record<string, string | readonly string[]>>): ModelFn;
22
+ export interface SkillRunResult {
23
+ /** Every gate passed and the skill reached its end. */
24
+ readonly ok: boolean;
25
+ /** Trace index of the gate that blocked, or null when all passed. */
26
+ readonly blockedAt: number | null;
27
+ /** Prose steps the model executed, with the scripted answer fed back in. */
28
+ readonly acts: readonly {
29
+ prose: string;
30
+ answer: string;
31
+ }[];
32
+ /** Gates that ran, in order, with their label and outcome. */
33
+ readonly gates: readonly {
34
+ label: string;
35
+ terminal: boolean;
36
+ ok: boolean;
37
+ }[];
38
+ }
39
+ /**
40
+ * Drive a skill to completion (or first failed gate) with a scripted model and
41
+ * return a friendly summary to assert on with plain `assert`.
42
+ */
43
+ export declare function runSkill(skill: SkillProgram | GeneratorSkill, opts?: {
44
+ cwd?: string;
45
+ model?: ModelFn;
46
+ }): SkillRunResult;
47
+ //# sourceMappingURL=skill-test.d.ts.map
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ /**
3
+ * vigiles — Skill testing: deterministic tests for a skill's action sequence.
4
+ *
5
+ * A thin wrapper over the generator driver (`driveSkill`) for use inside an
6
+ * ordinary `node:test` / Vitest `test()` — no custom runner, no DSL. You script
7
+ * the *model* (the non-deterministic part) and assert the deterministic spine:
8
+ * which gates ran, in what order, which branch was taken, whether the result
9
+ * gate blocked. The model's prose quality is never asserted — that's the
10
+ * probabilistic boundary; everything else is deterministic and checkable.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.scriptModel = scriptModel;
14
+ exports.runSkill = runSkill;
15
+ const skill_driver_js_1 = require("./skill-driver.js");
16
+ function gateLabel(g) {
17
+ if (g._ref === "cmd")
18
+ return g.command;
19
+ if (g._ref === "role")
20
+ return `project:${g.role}`;
21
+ return g.path;
22
+ }
23
+ /**
24
+ * Script the mocked model. Pass:
25
+ * - an array — answers consumed in order across all acts; or
26
+ * - a map keyed by a case-insensitive substring of the act's prose, where each
27
+ * value is a single answer, or an array consumed in order *per key* (so a
28
+ * loop's prompt can return different answers on successive iterations).
29
+ * Unmatched prose yields "".
30
+ */
31
+ function scriptModel(spec) {
32
+ if (Array.isArray(spec)) {
33
+ const answers = spec;
34
+ let i = 0;
35
+ return () => answers[Math.min(i++, answers.length - 1)] ?? "";
36
+ }
37
+ const map = spec;
38
+ const idx = {};
39
+ return (prose) => {
40
+ const p = prose.toLowerCase();
41
+ for (const k of Object.keys(map)) {
42
+ if (!p.includes(k.toLowerCase()))
43
+ continue;
44
+ const v = map[k];
45
+ if (typeof v === "string")
46
+ return v;
47
+ const i = idx[k] ?? 0;
48
+ idx[k] = i + 1;
49
+ return v[Math.min(i, v.length - 1)] ?? "";
50
+ }
51
+ return "";
52
+ };
53
+ }
54
+ /**
55
+ * Drive a skill to completion (or first failed gate) with a scripted model and
56
+ * return a friendly summary to assert on with plain `assert`.
57
+ */
58
+ function runSkill(skill, opts = {}) {
59
+ const program = typeof skill === "function" ? skill : skill.program;
60
+ const report = (0, skill_driver_js_1.driveSkill)(program, opts.cwd ?? process.cwd(), opts.model ?? (() => ""));
61
+ const acts = [];
62
+ const gates = [];
63
+ for (const t of report.trace) {
64
+ if (t.effect.kind === "act") {
65
+ acts.push({ prose: t.effect.prose, answer: t.answer ?? "" });
66
+ }
67
+ else {
68
+ gates.push({
69
+ label: gateLabel(t.effect.gate),
70
+ terminal: t.effect.kind === "result",
71
+ ok: t.outcome?.ok ?? false,
72
+ });
73
+ }
74
+ }
75
+ return { ok: report.ok, blockedAt: report.blockedAt, acts, gates };
76
+ }
77
+ //# sourceMappingURL=skill-test.js.map