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,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
|
package/dist/spec.d.ts
CHANGED
|
@@ -135,7 +135,13 @@ export interface SkillRef {
|
|
|
135
135
|
readonly _ref: "skill";
|
|
136
136
|
readonly path: VerifiedRef;
|
|
137
137
|
}
|
|
138
|
-
|
|
138
|
+
/** A typed symbol reference — the named file must define the named symbol. */
|
|
139
|
+
export interface SymbolRef {
|
|
140
|
+
readonly _ref: "symbol";
|
|
141
|
+
readonly file: VerifiedPath;
|
|
142
|
+
readonly symbol: string;
|
|
143
|
+
}
|
|
144
|
+
export type Ref = FileRef | CmdRef | SkillRef | SymbolRef;
|
|
139
145
|
/**
|
|
140
146
|
* Reference a file path — verified to exist at compile time.
|
|
141
147
|
* When generated types are present, narrowed to known project files.
|
|
@@ -146,6 +152,13 @@ export declare function file(path: NoInfer<StrictFile>): FileRef;
|
|
|
146
152
|
* When generated types are present, narrowed to known npm scripts.
|
|
147
153
|
*/
|
|
148
154
|
export declare function cmd(command: NoInfer<StrictCmd>): CmdRef;
|
|
155
|
+
/**
|
|
156
|
+
* Reference a symbol defined in a file — verified at compile time that the
|
|
157
|
+
* named file exists AND defines the named symbol (via ast-grep, cross-language).
|
|
158
|
+
* Compiles to the file-qualified inline form `` `file#symbol` `` so the markdown
|
|
159
|
+
* `audit` / `refs-hook` re-verify the same reference.
|
|
160
|
+
*/
|
|
161
|
+
export declare function symbol(file: NoInfer<StrictFile>, name: string): SymbolRef;
|
|
149
162
|
/**
|
|
150
163
|
* Reference another skill or instruction file — verified to exist.
|
|
151
164
|
* Compiles to a markdown link: [skill name](path)
|
|
@@ -214,18 +227,91 @@ type ClaudeSpecInput = ClaudeSpecBase & ClaudeSpecSections;
|
|
|
214
227
|
* export default claude({ commands: {...}, rules: {...} });
|
|
215
228
|
*/
|
|
216
229
|
export declare function claude(spec: ClaudeSpecInput): ClaudeSpec;
|
|
230
|
+
/**
|
|
231
|
+
* A deterministic gate on a skill step or its final result. A gate is one of:
|
|
232
|
+
* a command (exit 0), a file (must exist), or a *project role* that resolves to
|
|
233
|
+
* the host project's real command at run time. cmd/file gates are verified
|
|
234
|
+
* against the repo at author time; role gates are portable — a skill that runs
|
|
235
|
+
* in other repos should prefer `project("test")` over a hard-coded `npm test`.
|
|
236
|
+
*/
|
|
237
|
+
export type Gate = CmdRef | FileRef | RoleGate;
|
|
238
|
+
/** Project command roles, resolved per host project at run time. */
|
|
239
|
+
export type ProjectRole = "test" | "build" | "lint";
|
|
240
|
+
/** A portable gate that resolves to the host project's command for a role. */
|
|
241
|
+
export interface RoleGate {
|
|
242
|
+
readonly _ref: "role";
|
|
243
|
+
readonly role: ProjectRole;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* A portable gate that resolves to the host project's command for a role
|
|
247
|
+
* (e.g. `project("test")` → `npm test` / `pytest` / `cargo test`). Use this
|
|
248
|
+
* in skills meant to run across projects, instead of hard-coding a command.
|
|
249
|
+
*/
|
|
250
|
+
export declare function project(role: ProjectRole): RoleGate;
|
|
251
|
+
/**
|
|
252
|
+
* A declared skill input. Compiles to the `argument-hint` frontmatter and a
|
|
253
|
+
* `## Arguments` section; referenced as `$1`/`$2`/`$ARGUMENTS` in the body.
|
|
254
|
+
*/
|
|
255
|
+
export interface SkillInput {
|
|
256
|
+
/** Argument name, e.g. "pattern". */
|
|
257
|
+
readonly name: string;
|
|
258
|
+
/** Human-readable hint shown in argument-hint and the Arguments section. */
|
|
259
|
+
readonly hint: string;
|
|
260
|
+
/** Required by default; set false to render as optional (`[<name>]`). */
|
|
261
|
+
readonly required?: boolean;
|
|
262
|
+
}
|
|
263
|
+
/** One step of a gated skill pipeline. */
|
|
264
|
+
export interface SkillStep {
|
|
265
|
+
/** What the model should do — prose, optionally with typed refs. */
|
|
266
|
+
readonly do: string | InstructionFragment[];
|
|
267
|
+
/** Deterministic check that must pass before advancing to the next step. */
|
|
268
|
+
readonly gate?: Gate;
|
|
269
|
+
/** Max attempts to satisfy the gate before the step fails (default 1). */
|
|
270
|
+
readonly retry?: number;
|
|
271
|
+
}
|
|
272
|
+
/** Declare a skill input (compiles to argument-hint + an Arguments entry). */
|
|
273
|
+
export declare function input(name: string, hint: string, opts?: {
|
|
274
|
+
required?: boolean;
|
|
275
|
+
}): SkillInput;
|
|
276
|
+
/** Declare a gated pipeline step. */
|
|
277
|
+
export declare function step(instr: string | InstructionFragment[], opts?: {
|
|
278
|
+
gate?: Gate;
|
|
279
|
+
retry?: number;
|
|
280
|
+
}): SkillStep;
|
|
217
281
|
export interface SkillSpec {
|
|
218
282
|
readonly _specType: "skill";
|
|
219
283
|
/** Skill name (used in frontmatter). */
|
|
220
284
|
readonly name: string;
|
|
221
285
|
/** Short description (used in frontmatter). */
|
|
222
286
|
readonly description: string;
|
|
223
|
-
/**
|
|
287
|
+
/**
|
|
288
|
+
* Hint for the argument (frontmatter). Ignored when `inputs` is set —
|
|
289
|
+
* `inputs` derive the argument-hint instead.
|
|
290
|
+
*/
|
|
224
291
|
readonly argumentHint?: string;
|
|
292
|
+
/** Typed inputs — compile to argument-hint + a `## Arguments` section. */
|
|
293
|
+
readonly inputs?: readonly SkillInput[];
|
|
225
294
|
/** Whether to disable model invocation (frontmatter flag). */
|
|
226
295
|
readonly disableModelInvocation?: boolean;
|
|
227
|
-
/**
|
|
228
|
-
|
|
296
|
+
/**
|
|
297
|
+
* Gated pipeline steps. When set, the skill compiles to a `## Steps`
|
|
298
|
+
* checklist with a deterministic gate per step. Use this OR `body`.
|
|
299
|
+
*/
|
|
300
|
+
readonly steps?: readonly SkillStep[];
|
|
301
|
+
/**
|
|
302
|
+
* Terminal postcondition — the skill is "done" only when this gate passes.
|
|
303
|
+
* Compiles to a `## Result` section + a `vigiles:result` marker.
|
|
304
|
+
*/
|
|
305
|
+
readonly result?: Gate;
|
|
306
|
+
/** Freeform instruction body (linear/unstructured skills). Use this OR `steps`. */
|
|
307
|
+
readonly body?: string | InstructionFragment[];
|
|
308
|
+
/**
|
|
309
|
+
* Max lines for an inline fenced code block before compilation errors,
|
|
310
|
+
* forcing the script into a file referenced via `file()` (default 20).
|
|
311
|
+
* Keeps big scripts out of the skill body (token budget + progressive
|
|
312
|
+
* disclosure). Set 0 to disable.
|
|
313
|
+
*/
|
|
314
|
+
readonly maxInlineCodeLines?: number;
|
|
229
315
|
}
|
|
230
316
|
/**
|
|
231
317
|
* Define a SKILL.md specification.
|
package/dist/spec.js
CHANGED
|
@@ -15,9 +15,13 @@ exports.guidance = guidance;
|
|
|
15
15
|
exports.guard = guard;
|
|
16
16
|
exports.file = file;
|
|
17
17
|
exports.cmd = cmd;
|
|
18
|
+
exports.symbol = symbol;
|
|
18
19
|
exports.ref = ref;
|
|
19
20
|
exports.instructions = instructions;
|
|
20
21
|
exports.claude = claude;
|
|
22
|
+
exports.project = project;
|
|
23
|
+
exports.input = input;
|
|
24
|
+
exports.step = step;
|
|
21
25
|
exports.skill = skill;
|
|
22
26
|
exports.defineConfig = defineConfig;
|
|
23
27
|
// ---------------------------------------------------------------------------
|
|
@@ -81,6 +85,15 @@ function file(path) {
|
|
|
81
85
|
function cmd(command) {
|
|
82
86
|
return { _ref: "cmd", command: command };
|
|
83
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Reference a symbol defined in a file — verified at compile time that the
|
|
90
|
+
* named file exists AND defines the named symbol (via ast-grep, cross-language).
|
|
91
|
+
* Compiles to the file-qualified inline form `` `file#symbol` `` so the markdown
|
|
92
|
+
* `audit` / `refs-hook` re-verify the same reference.
|
|
93
|
+
*/
|
|
94
|
+
function symbol(file, name) {
|
|
95
|
+
return { _ref: "symbol", file: file, symbol: name };
|
|
96
|
+
}
|
|
84
97
|
/**
|
|
85
98
|
* Reference another skill or instruction file — verified to exist.
|
|
86
99
|
* Compiles to a markdown link: [skill name](path)
|
|
@@ -116,6 +129,22 @@ function instructions(strings, ...values) {
|
|
|
116
129
|
function claude(spec) {
|
|
117
130
|
return { _specType: "claude", ...spec };
|
|
118
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* A portable gate that resolves to the host project's command for a role
|
|
134
|
+
* (e.g. `project("test")` → `npm test` / `pytest` / `cargo test`). Use this
|
|
135
|
+
* in skills meant to run across projects, instead of hard-coding a command.
|
|
136
|
+
*/
|
|
137
|
+
function project(role) {
|
|
138
|
+
return { _ref: "role", role };
|
|
139
|
+
}
|
|
140
|
+
/** Declare a skill input (compiles to argument-hint + an Arguments entry). */
|
|
141
|
+
function input(name, hint, opts = {}) {
|
|
142
|
+
return { name, hint, required: opts.required };
|
|
143
|
+
}
|
|
144
|
+
/** Declare a gated pipeline step. */
|
|
145
|
+
function step(instr, opts = {}) {
|
|
146
|
+
return { do: instr, gate: opts.gate, retry: opts.retry };
|
|
147
|
+
}
|
|
119
148
|
/**
|
|
120
149
|
* Define a SKILL.md specification.
|
|
121
150
|
*
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Lang } from "@ast-grep/napi";
|
|
2
|
+
/** A language key accepted by ast-grep's `parse` (core enum or registered id). */
|
|
3
|
+
type LangKey = Lang | string;
|
|
4
|
+
/** The ast-grep language for a file, or null if unsupported (graceful skip). */
|
|
5
|
+
export declare function langForFile(file: string): LangKey | null;
|
|
6
|
+
/** A symbol definition found in a file. */
|
|
7
|
+
export interface SymbolDef {
|
|
8
|
+
/** The defined identifier, e.g. "parseConfig". */
|
|
9
|
+
readonly name: string;
|
|
10
|
+
/** The tree-sitter node kind, e.g. "function_declaration" (raw, per-grammar). */
|
|
11
|
+
readonly kind: string;
|
|
12
|
+
/** Enclosing class/module name, or "" at top level. */
|
|
13
|
+
readonly scope: string;
|
|
14
|
+
/** 1-based line of the definition. */
|
|
15
|
+
readonly line: number;
|
|
16
|
+
}
|
|
17
|
+
/** Extract the symbols defined in a single file's source. */
|
|
18
|
+
export declare function definedSymbols(code: string, lang: LangKey): SymbolDef[];
|
|
19
|
+
/** Defined symbols for a file on disk, or [] if unreadable/unsupported. */
|
|
20
|
+
export declare function definedSymbolsInFile(file: string): SymbolDef[];
|
|
21
|
+
/**
|
|
22
|
+
* Whether `file` defines a top-level (or scoped) symbol named `name`. This is
|
|
23
|
+
* the whole check for a file-qualified reference (`path#symbol`): we parse the
|
|
24
|
+
* one named file — no project-wide index, no resolution across files. As a
|
|
25
|
+
* fallback we also consult a co-located declaration file (`.rbi` / `.d.ts`), so
|
|
26
|
+
* typed dynamic symbols resolve without running Sorbet / the TS compiler.
|
|
27
|
+
*/
|
|
28
|
+
export declare function fileDefinesSymbol(file: string, name: string): boolean;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=symbols.d.ts.map
|