vigiles 2.0.0 → 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.
Files changed (184) hide show
  1. package/README.md +182 -134
  2. package/dist/action-gate.d.ts +28 -0
  3. package/dist/action-gate.js +73 -0
  4. package/dist/cli.js +705 -230
  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 +4 -0
  10. package/dist/compile.js +239 -45
  11. package/dist/coverage.d.ts +56 -0
  12. package/dist/coverage.js +178 -0
  13. package/dist/doc-refs.d.ts +60 -0
  14. package/dist/doc-refs.js +246 -0
  15. package/dist/eval.d.ts +62 -0
  16. package/dist/eval.js +174 -0
  17. package/dist/evolve.js +60 -125
  18. package/dist/frontmatter.d.ts +75 -0
  19. package/dist/frontmatter.js +263 -0
  20. package/dist/generate-schema.d.ts +51 -0
  21. package/dist/generate-schema.js +125 -0
  22. package/dist/generate-types.js +39 -1
  23. package/dist/harness-test.d.ts +38 -0
  24. package/dist/harness-test.js +129 -0
  25. package/dist/hash.d.ts +8 -0
  26. package/dist/hash.js +16 -0
  27. package/dist/inline.d.ts +22 -4
  28. package/dist/inline.js +60 -13
  29. package/dist/integrity.d.ts +29 -0
  30. package/dist/integrity.js +44 -0
  31. package/dist/linters.d.ts +5 -3
  32. package/dist/linters.js +144 -4
  33. package/dist/mock-model.d.ts +31 -0
  34. package/dist/mock-model.js +189 -0
  35. package/dist/orphans.d.ts +52 -0
  36. package/dist/orphans.js +124 -0
  37. package/dist/proofs.js +22 -16
  38. package/dist/refs.d.ts +44 -0
  39. package/dist/refs.js +144 -0
  40. package/dist/session.d.ts +97 -0
  41. package/dist/session.js +306 -0
  42. package/dist/sidecar.d.ts +35 -0
  43. package/dist/sidecar.js +102 -0
  44. package/dist/skill-driver.d.ts +77 -0
  45. package/dist/skill-driver.js +76 -0
  46. package/dist/skill-runtime.d.ts +101 -0
  47. package/dist/skill-runtime.js +289 -0
  48. package/dist/skill-test.d.ts +47 -0
  49. package/dist/skill-test.js +77 -0
  50. package/dist/spec.d.ts +119 -13
  51. package/dist/spec.js +51 -4
  52. package/dist/symbols.d.ts +30 -0
  53. package/dist/symbols.js +142 -0
  54. package/dist/test-utils.d.ts +8 -0
  55. package/dist/test-utils.js +41 -0
  56. package/dist/types.d.ts +34 -8
  57. package/dist/types.js +19 -0
  58. package/dist/validate.js +5 -3
  59. package/package.json +26 -5
  60. package/.claude/settings.json +0 -46
  61. package/.claude/settings.local.json +0 -8
  62. package/.github/workflows/ci.yml +0 -81
  63. package/.prettierignore +0 -1
  64. package/.vigiles/generated.d.ts +0 -205
  65. package/CLAUDE.md +0 -95
  66. package/CLAUDE.md.spec.ts +0 -142
  67. package/CONTRIBUTING.md +0 -121
  68. package/dist/action.d.ts.map +0 -1
  69. package/dist/action.js.map +0 -1
  70. package/dist/cli.d.ts.map +0 -1
  71. package/dist/cli.js.map +0 -1
  72. package/dist/cli.test.d.ts +0 -2
  73. package/dist/cli.test.d.ts.map +0 -1
  74. package/dist/cli.test.js +0 -650
  75. package/dist/cli.test.js.map +0 -1
  76. package/dist/compile.d.ts.map +0 -1
  77. package/dist/compile.js.map +0 -1
  78. package/dist/evolve.d.ts.map +0 -1
  79. package/dist/evolve.js.map +0 -1
  80. package/dist/freshness.d.ts +0 -67
  81. package/dist/freshness.d.ts.map +0 -1
  82. package/dist/freshness.js +0 -244
  83. package/dist/freshness.js.map +0 -1
  84. package/dist/freshness.test.d.ts +0 -2
  85. package/dist/freshness.test.d.ts.map +0 -1
  86. package/dist/freshness.test.js +0 -356
  87. package/dist/freshness.test.js.map +0 -1
  88. package/dist/generate-types.d.ts.map +0 -1
  89. package/dist/generate-types.js.map +0 -1
  90. package/dist/inline.d.ts.map +0 -1
  91. package/dist/inline.js.map +0 -1
  92. package/dist/inline.test.d.ts +0 -5
  93. package/dist/inline.test.d.ts.map +0 -1
  94. package/dist/inline.test.js +0 -152
  95. package/dist/inline.test.js.map +0 -1
  96. package/dist/linters.d.ts.map +0 -1
  97. package/dist/linters.js.map +0 -1
  98. package/dist/proofs.d.ts.map +0 -1
  99. package/dist/proofs.js.map +0 -1
  100. package/dist/proofs.test.d.ts +0 -9
  101. package/dist/proofs.test.d.ts.map +0 -1
  102. package/dist/proofs.test.js +0 -952
  103. package/dist/proofs.test.js.map +0 -1
  104. package/dist/spec.d.ts.map +0 -1
  105. package/dist/spec.js.map +0 -1
  106. package/dist/spec.test.d.ts +0 -2
  107. package/dist/spec.test.d.ts.map +0 -1
  108. package/dist/spec.test.js +0 -1222
  109. package/dist/spec.test.js.map +0 -1
  110. package/dist/types.d.ts.map +0 -1
  111. package/dist/types.js.map +0 -1
  112. package/dist/validate.d.ts.map +0 -1
  113. package/dist/validate.js.map +0 -1
  114. package/dist/validate.test.d.ts +0 -2
  115. package/dist/validate.test.d.ts.map +0 -1
  116. package/dist/validate.test.js +0 -531
  117. package/dist/validate.test.js.map +0 -1
  118. package/docs/agent-setup.md +0 -85
  119. package/docs/agent-workflows.md +0 -103
  120. package/docs/comparison.md +0 -71
  121. package/docs/freshness.md +0 -124
  122. package/docs/inline-mode.md +0 -119
  123. package/docs/linter-support.md +0 -166
  124. package/docs/spec-format.md +0 -194
  125. package/eslint.config.mjs +0 -79
  126. package/examples/CLAUDE.md +0 -54
  127. package/examples/CLAUDE.md.spec.ts +0 -65
  128. package/examples/SKILL.md +0 -50
  129. package/examples/SKILL.md.spec.ts +0 -57
  130. package/fixtures/example-project/CLAUDE.md +0 -11
  131. package/fixtures/example-project/package.json +0 -9
  132. package/fixtures/example-project/src/index.ts +0 -3
  133. package/fixtures/example-project/src/utils.test.ts +0 -2
  134. package/fixtures/example-project/src/utils.ts +0 -3
  135. package/logo.png +0 -0
  136. package/research/adoption-strategy.md +0 -111
  137. package/research/agent-integration.md +0 -145
  138. package/research/ai-code-quality.md +0 -197
  139. package/research/code-search-for-agents.md +0 -313
  140. package/research/competitive-landscape.md +0 -163
  141. package/research/doc-freshness.md +0 -516
  142. package/research/executable-specs.md +0 -368
  143. package/research/feature-ideas.md +0 -464
  144. package/research/formal-proofs-for-agents.md +0 -338
  145. package/research/fp-for-agent-harness.md +0 -150
  146. package/research/fp-for-deterministic-ai.md +0 -131
  147. package/research/self-evolving-specs.md +0 -298
  148. package/schemas/claude-md-strict.yml +0 -18
  149. package/schemas/claude-md.yml +0 -6
  150. package/schemas/skill-strict.yml +0 -12
  151. package/schemas/skill.yml +0 -5
  152. package/skills/audit-feedback-loop/SKILL.md +0 -76
  153. package/skills/edit-spec/SKILL.md +0 -131
  154. package/skills/enforce-rules-format/SKILL.md +0 -71
  155. package/skills/generate-logo/SKILL.md +0 -102
  156. package/skills/generate-rule/SKILL.md +0 -90
  157. package/skills/linter-docs/clippy.md +0 -241
  158. package/skills/linter-docs/eslint.md +0 -384
  159. package/skills/linter-docs/pylint.md +0 -288
  160. package/skills/linter-docs/rubocop.md +0 -277
  161. package/skills/linter-docs/ruff.md +0 -187
  162. package/skills/linter-docs/stylelint.md +0 -247
  163. package/skills/migrate-to-spec/SKILL.md +0 -124
  164. package/skills/pr-to-lint-rule/SKILL.md +0 -97
  165. package/skills/strengthen/SKILL.md +0 -168
  166. package/src/action.ts +0 -214
  167. package/src/cli.test.ts +0 -914
  168. package/src/cli.ts +0 -1631
  169. package/src/compile.ts +0 -691
  170. package/src/evolve.ts +0 -781
  171. package/src/freshness.test.ts +0 -449
  172. package/src/freshness.ts +0 -299
  173. package/src/generate-types.ts +0 -448
  174. package/src/inline.test.ts +0 -206
  175. package/src/inline.ts +0 -164
  176. package/src/linters.ts +0 -739
  177. package/src/proofs.test.ts +0 -1314
  178. package/src/proofs.ts +0 -849
  179. package/src/spec.test.ts +0 -1471
  180. package/src/spec.ts +0 -427
  181. package/src/types.ts +0 -117
  182. package/src/validate.test.ts +0 -701
  183. package/src/validate.ts +0 -381
  184. package/tsconfig.json +0 -23
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /**
3
+ * Per-spec sidecar manifests stored at `.vigiles/<target>.inputs.json`.
4
+ *
5
+ * Used by the post-session audit to know which targets exist and which
6
+ * spec source / inputs each one tracks. The compile pipeline writes
7
+ * these whenever a spec is built; readers (currently only session.ts)
8
+ * consume them at audit time.
9
+ *
10
+ * This module is the ONLY place sidecars live now — the freshness rule
11
+ * doesn't depend on them anymore.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.sidecarPath = sidecarPath;
15
+ exports.writeSidecarManifest = writeSidecarManifest;
16
+ exports.readSidecarManifest = readSidecarManifest;
17
+ exports.computePerFileHashes = computePerFileHashes;
18
+ exports.iterateSidecars = iterateSidecars;
19
+ const node_fs_1 = require("node:fs");
20
+ const node_path_1 = require("node:path");
21
+ const hash_js_1 = require("./hash.js");
22
+ const SIDECAR_ROOT = ".vigiles";
23
+ const MANIFEST_SUFFIX = ".inputs.json";
24
+ function sidecarPath(basePath, target) {
25
+ return (0, node_path_1.resolve)(basePath, SIDECAR_ROOT, `${target}${MANIFEST_SUFFIX}`);
26
+ }
27
+ function writeSidecarManifest(basePath, manifest) {
28
+ // Targets can be nested (e.g. ".github/copilot-instructions.md"), so
29
+ // ensure every intermediate directory under .vigiles/ exists before
30
+ // writing — otherwise writeFileSync throws ENOENT.
31
+ const filePath = sidecarPath(basePath, manifest.target);
32
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(filePath), { recursive: true });
33
+ (0, node_fs_1.writeFileSync)(filePath, JSON.stringify(manifest, null, 2) + "\n");
34
+ }
35
+ function readSidecarManifest(basePath, target) {
36
+ const filePath = sidecarPath(basePath, target);
37
+ if (!(0, node_fs_1.existsSync)(filePath))
38
+ return null;
39
+ try {
40
+ return JSON.parse((0, node_fs_1.readFileSync)(filePath, "utf-8"));
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ function computePerFileHashes(inputFiles, basePath) {
47
+ const result = {};
48
+ for (const f of inputFiles) {
49
+ const fullPath = (0, node_path_1.resolve)(basePath, f);
50
+ result[f] = (0, node_fs_1.existsSync)(fullPath)
51
+ ? (0, hash_js_1.sha256short)((0, node_fs_1.readFileSync)(fullPath))
52
+ : "MISSING";
53
+ }
54
+ return result;
55
+ }
56
+ /**
57
+ * Iterate all sidecar manifests under `.vigiles/`, including nested
58
+ * directories. Handles directory-not-found and read errors gracefully.
59
+ *
60
+ * Recurses so that nested targets (e.g. `.github/copilot-instructions.md`,
61
+ * which lives at `.vigiles/.github/copilot-instructions.md.inputs.json`)
62
+ * are not silently skipped.
63
+ */
64
+ function iterateSidecars(basePath, fn) {
65
+ const root = (0, node_path_1.resolve)(basePath, SIDECAR_ROOT);
66
+ if (!(0, node_fs_1.existsSync)(root))
67
+ return;
68
+ walk(root, root, basePath, fn);
69
+ }
70
+ function walk(dir, root, basePath, fn) {
71
+ let entries;
72
+ try {
73
+ entries = (0, node_fs_1.readdirSync)(dir);
74
+ }
75
+ catch {
76
+ return;
77
+ }
78
+ for (const entry of entries) {
79
+ const fullPath = (0, node_path_1.resolve)(dir, entry);
80
+ let isDir;
81
+ try {
82
+ isDir = (0, node_fs_1.statSync)(fullPath).isDirectory();
83
+ }
84
+ catch {
85
+ continue;
86
+ }
87
+ if (isDir) {
88
+ walk(fullPath, root, basePath, fn);
89
+ continue;
90
+ }
91
+ if (!entry.endsWith(MANIFEST_SUFFIX))
92
+ continue;
93
+ // Reconstruct the target name from the path relative to .vigiles/
94
+ const rel = (0, node_path_1.relative)(root, fullPath);
95
+ const target = rel.slice(0, -MANIFEST_SUFFIX.length);
96
+ const manifest = readSidecarManifest(basePath, target);
97
+ if (!manifest)
98
+ continue;
99
+ fn(target, manifest);
100
+ }
101
+ }
102
+ //# sourceMappingURL=sidecar.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
@@ -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