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,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
- export type Ref = FileRef | CmdRef | SkillRef;
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
- /** Hint for the argument (used in frontmatter). */
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
- /** Instruction body — string or tagged template with typed refs. */
228
- readonly body: string | InstructionFragment[];
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