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,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
@@ -8,8 +8,8 @@
8
8
  * enforce() — delegated to an external linter (ESLint, Ruff, Clippy, etc.)
9
9
  * guidance() — prose only, no mechanical enforcement
10
10
  */
11
- /** Linters vigiles can cross-reference. */
12
- type BuiltinLinter = "eslint" | "stylelint" | "ruff" | "clippy" | "pylint" | "rubocop";
11
+ /** Linters and policy catalogs vigiles can cross-reference. */
12
+ type BuiltinLinter = "eslint" | "stylelint" | "ruff" | "clippy" | "pylint" | "rubocop" | "cedar";
13
13
  /** Scoped ESLint plugin prefix (e.g., @typescript-eslint). */
14
14
  type ScopedPlugin = `@${string}/${string}`;
15
15
  /** A linter/rule reference: "eslint/no-console", "ruff/T201", "@typescript-eslint/no-explicit-any". */
@@ -52,12 +52,12 @@ export type StrictFile = [keyof KnownProjectFiles] extends [never] ? string : Kn
52
52
  export type StrictCmd = [keyof KnownNpmScripts] extends [never] ? string : `npm run ${KnownNpmScripts[keyof KnownNpmScripts] & string}` | `npm ${KnownNpmScripts[keyof KnownNpmScripts] & string}` | (string & {});
53
53
  export type ClaudeTool = "Read" | "Write" | "Edit" | "Bash" | "Grep" | "Glob" | "Agent" | "TodoWrite" | "WebSearch" | "WebFetch" | "NotebookEdit";
54
54
  export type HookEvent = "PreToolUse" | "PostToolUse" | "PreSession" | "PostSession" | "Notification";
55
- /** A rule delegated to an external tool (linter, ast-grep, dependency-cruiser, etc.). */
55
+ /** A rule delegated to an external tool (linter, ast-grep, dependency-cruiser, etc.) or to a vigiles-internal check. */
56
56
  export interface EnforceRule {
57
57
  readonly _kind: "enforce";
58
- readonly linterRule: LinterRule;
58
+ readonly linterRule: LinterRule | VigilesRef;
59
59
  readonly why: string;
60
- /** Skip linter verification for this rule. Default: true (verify). */
60
+ /** Skip verification for this rule. Default: true (verify). */
61
61
  readonly verify: boolean;
62
62
  }
63
63
  /** A guidance-only rule (prose, no enforcement). */
@@ -65,18 +65,28 @@ export interface GuidanceRule {
65
65
  readonly _kind: "guidance";
66
66
  readonly text: string;
67
67
  }
68
- export type Rule = EnforceRule | GuidanceRule;
68
+ /** A reactive rule: runs a command when watched files change. */
69
+ export interface GuardRule {
70
+ readonly _kind: "guard";
71
+ readonly watch: string | readonly string[];
72
+ readonly run: string;
73
+ readonly description: string;
74
+ }
75
+ export type Rule = EnforceRule | GuidanceRule | GuardRule;
69
76
  /**
70
- * Declare a rule enforced by an external tool.
77
+ * Declare a rule enforced by an external tool or vigiles itself.
71
78
  *
72
79
  * When generated types are present, the `linterRule` argument is narrowed
73
- * to only accept rules that exist in your linter configs.
80
+ * to only accept rules that exist in your linter configs. Vigiles-internal
81
+ * checks use the `vigiles/<assertion-id>` namespace and are dispatched to
82
+ * built-in mechanical validators (e.g. `vigiles/orphan-docs`).
74
83
  *
75
84
  * enforce("eslint/no-console", "Use structured logger.")
76
85
  * enforce("@typescript-eslint/no-floating-promises", "Always await.")
77
86
  * enforce("ruff/T201", "Use logging module.")
87
+ * enforce("vigiles/orphan-docs", "No docs without spec references.")
78
88
  */
79
- export declare function enforce(linterRule: NoInfer<StrictLinterRule>, why: string, options?: {
89
+ export declare function enforce(ref: NoInfer<StrictLinterRule> | VigilesRef, why: string, options?: {
80
90
  verify?: boolean;
81
91
  }): EnforceRule;
82
92
  /**
@@ -85,6 +95,16 @@ export declare function enforce(linterRule: NoInfer<StrictLinterRule>, why: stri
85
95
  * guidance("Google unfamiliar APIs before implementing.")
86
96
  */
87
97
  export declare function guidance(text: string): GuidanceRule;
98
+ /**
99
+ * Declare a reactive guard: runs a command when watched files change.
100
+ *
101
+ * guard({ watch: "*.spec.ts", run: "npx vigiles compile" }, "Recompile on spec change")
102
+ * guard({ watch: ["eslint.config.*", "package.json"], run: "npx vigiles generate-types" }, "Regen types")
103
+ */
104
+ export declare function guard(options: {
105
+ watch: string | readonly string[];
106
+ run: string;
107
+ }, description: string): GuardRule;
88
108
  /**
89
109
  * Branded string types — these prove a reference has gone through
90
110
  * vigiles's verification. The compiler only accepts branded refs,
@@ -115,7 +135,13 @@ export interface SkillRef {
115
135
  readonly _ref: "skill";
116
136
  readonly path: VerifiedRef;
117
137
  }
118
- 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;
119
145
  /**
120
146
  * Reference a file path — verified to exist at compile time.
121
147
  * When generated types are present, narrowed to known project files.
@@ -126,6 +152,13 @@ export declare function file(path: NoInfer<StrictFile>): FileRef;
126
152
  * When generated types are present, narrowed to known npm scripts.
127
153
  */
128
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;
129
162
  /**
130
163
  * Reference another skill or instruction file — verified to exist.
131
164
  * Compiles to a markdown link: [skill name](path)
@@ -194,18 +227,91 @@ type ClaudeSpecInput = ClaudeSpecBase & ClaudeSpecSections;
194
227
  * export default claude({ commands: {...}, rules: {...} });
195
228
  */
196
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;
197
281
  export interface SkillSpec {
198
282
  readonly _specType: "skill";
199
283
  /** Skill name (used in frontmatter). */
200
284
  readonly name: string;
201
285
  /** Short description (used in frontmatter). */
202
286
  readonly description: string;
203
- /** 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
+ */
204
291
  readonly argumentHint?: string;
292
+ /** Typed inputs — compile to argument-hint + a `## Arguments` section. */
293
+ readonly inputs?: readonly SkillInput[];
205
294
  /** Whether to disable model invocation (frontmatter flag). */
206
295
  readonly disableModelInvocation?: boolean;
207
- /** Instruction body — string or tagged template with typed refs. */
208
- 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;
209
315
  }
210
316
  /**
211
317
  * Define a SKILL.md specification.
package/dist/spec.js CHANGED
@@ -12,31 +12,39 @@
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.enforce = enforce;
14
14
  exports.guidance = guidance;
15
+ exports.guard = guard;
15
16
  exports.file = file;
16
17
  exports.cmd = cmd;
18
+ exports.symbol = symbol;
17
19
  exports.ref = ref;
18
20
  exports.instructions = instructions;
19
21
  exports.claude = claude;
22
+ exports.project = project;
23
+ exports.input = input;
24
+ exports.step = step;
20
25
  exports.skill = skill;
21
26
  exports.defineConfig = defineConfig;
22
27
  // ---------------------------------------------------------------------------
23
28
  // Builder functions
24
29
  // ---------------------------------------------------------------------------
25
30
  /**
26
- * Declare a rule enforced by an external tool.
31
+ * Declare a rule enforced by an external tool or vigiles itself.
27
32
  *
28
33
  * When generated types are present, the `linterRule` argument is narrowed
29
- * to only accept rules that exist in your linter configs.
34
+ * to only accept rules that exist in your linter configs. Vigiles-internal
35
+ * checks use the `vigiles/<assertion-id>` namespace and are dispatched to
36
+ * built-in mechanical validators (e.g. `vigiles/orphan-docs`).
30
37
  *
31
38
  * enforce("eslint/no-console", "Use structured logger.")
32
39
  * enforce("@typescript-eslint/no-floating-promises", "Always await.")
33
40
  * enforce("ruff/T201", "Use logging module.")
41
+ * enforce("vigiles/orphan-docs", "No docs without spec references.")
34
42
  */
35
- function enforce(linterRule, why, options) {
43
+ function enforce(ref, why, options) {
36
44
  return {
37
45
  _kind: "enforce",
38
46
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
39
- linterRule: linterRule,
47
+ linterRule: ref,
40
48
  why,
41
49
  verify: options?.verify ?? true,
42
50
  };
@@ -49,6 +57,20 @@ function enforce(linterRule, why, options) {
49
57
  function guidance(text) {
50
58
  return { _kind: "guidance", text };
51
59
  }
60
+ /**
61
+ * Declare a reactive guard: runs a command when watched files change.
62
+ *
63
+ * guard({ watch: "*.spec.ts", run: "npx vigiles compile" }, "Recompile on spec change")
64
+ * guard({ watch: ["eslint.config.*", "package.json"], run: "npx vigiles generate-types" }, "Regen types")
65
+ */
66
+ function guard(options, description) {
67
+ return {
68
+ _kind: "guard",
69
+ watch: options.watch,
70
+ run: options.run,
71
+ description,
72
+ };
73
+ }
52
74
  /**
53
75
  * Reference a file path — verified to exist at compile time.
54
76
  * When generated types are present, narrowed to known project files.
@@ -63,6 +85,15 @@ function file(path) {
63
85
  function cmd(command) {
64
86
  return { _ref: "cmd", command: command };
65
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
+ }
66
97
  /**
67
98
  * Reference another skill or instruction file — verified to exist.
68
99
  * Compiles to a markdown link: [skill name](path)
@@ -98,6 +129,22 @@ function instructions(strings, ...values) {
98
129
  function claude(spec) {
99
130
  return { _specType: "claude", ...spec };
100
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
+ }
101
148
  /**
102
149
  * Define a SKILL.md specification.
103
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
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.langForFile = langForFile;
7
+ exports.definedSymbols = definedSymbols;
8
+ exports.definedSymbolsInFile = definedSymbolsInFile;
9
+ exports.fileDefinesSymbol = fileDefinesSymbol;
10
+ /**
11
+ * vigiles — cross-language symbol index (ast-grep / tree-sitter).
12
+ *
13
+ * The kernel of harness-pinned reference verification. Instruction files
14
+ * reference project symbols in prose (`parseConfig`, `User`, `create_docx.py`);
15
+ * authors never write a verifiable `file#symbol` form (corpus: ~0%). So instead
16
+ * of asking authors to annotate, the *harness* resolves a bare reference against
17
+ * the live code at write time and pins it. This module is the resolver: extract
18
+ * the symbols a file defines, and build a project-wide name → locations index.
19
+ *
20
+ * Boundary (see research/symbol-verification.md): we answer "does a definition
21
+ * with this name (in this scope) exist", NOT "does this reference resolve through
22
+ * imports / Zeitwerk / tsconfig". Resolution is per-language and architectural —
23
+ * delegated. Ambiguity (a name defined in several files) is reported, not guessed.
24
+ */
25
+ const node_fs_1 = require("node:fs");
26
+ const node_path_1 = require("node:path");
27
+ const napi_1 = require("@ast-grep/napi");
28
+ const lang_python_1 = __importDefault(require("@ast-grep/lang-python"));
29
+ const lang_rust_1 = __importDefault(require("@ast-grep/lang-rust"));
30
+ const lang_ruby_1 = __importDefault(require("@ast-grep/lang-ruby"));
31
+ let registered = false;
32
+ function ensureRegistered() {
33
+ if (registered)
34
+ return;
35
+ // Non-web grammars ship as separate packages registered at runtime.
36
+ (0, napi_1.registerDynamicLanguage)({ python: lang_python_1.default, rust: lang_rust_1.default, ruby: lang_ruby_1.default });
37
+ registered = true;
38
+ }
39
+ const EXT_LANG = {
40
+ ".ts": napi_1.Lang.TypeScript,
41
+ ".tsx": napi_1.Lang.Tsx,
42
+ ".mts": napi_1.Lang.TypeScript,
43
+ ".cts": napi_1.Lang.TypeScript,
44
+ ".d.ts": napi_1.Lang.TypeScript,
45
+ ".js": napi_1.Lang.JavaScript,
46
+ ".jsx": napi_1.Lang.JavaScript,
47
+ ".mjs": napi_1.Lang.JavaScript,
48
+ ".cjs": napi_1.Lang.JavaScript,
49
+ ".css": napi_1.Lang.Css,
50
+ ".py": "python",
51
+ ".pyi": "python",
52
+ ".rs": "rust",
53
+ ".rb": "ruby",
54
+ ".rbi": "ruby",
55
+ };
56
+ /** The ast-grep language for a file, or null if unsupported (graceful skip). */
57
+ function langForFile(file) {
58
+ if (file.endsWith(".d.ts"))
59
+ return napi_1.Lang.TypeScript;
60
+ return EXT_LANG[(0, node_path_1.extname)(file).toLowerCase()] ?? null;
61
+ }
62
+ const ID_KINDS = new Set(["identifier", "constant", "type_identifier"]);
63
+ const SCOPE_KINDS = new Set([
64
+ "class_declaration",
65
+ "class_definition",
66
+ "class",
67
+ "module",
68
+ "interface_declaration",
69
+ "enum_declaration",
70
+ ]);
71
+ function recordNode(node, scope, out) {
72
+ const line = node.range().start.line + 1;
73
+ const nameNode = node.field("name");
74
+ if (nameNode) {
75
+ out.push({ name: nameNode.text(), kind: node.kind(), scope, line });
76
+ }
77
+ // Assignment-style constants (Python/Ruby `X = ...`): the identifier is the
78
+ // `left` field, not `name`.
79
+ const left = node.field("left");
80
+ if (left && ID_KINDS.has(left.kind())) {
81
+ out.push({ name: left.text(), kind: node.kind(), scope, line });
82
+ }
83
+ }
84
+ /** Extract the symbols defined in a single file's source. */
85
+ function definedSymbols(code, lang) {
86
+ ensureRegistered();
87
+ const out = [];
88
+ const walk = (node, scope) => {
89
+ recordNode(node, scope, out);
90
+ const nameNode = node.field("name");
91
+ const nextScope = SCOPE_KINDS.has(node.kind()) && nameNode ? nameNode.text() : scope;
92
+ for (const child of node.children())
93
+ walk(child, nextScope);
94
+ };
95
+ walk((0, napi_1.parse)(lang, code).root(), "");
96
+ return out;
97
+ }
98
+ /** Defined symbols for a file on disk, or [] if unreadable/unsupported. */
99
+ function definedSymbolsInFile(file) {
100
+ const lang = langForFile(file);
101
+ if (!lang)
102
+ return [];
103
+ try {
104
+ return definedSymbols((0, node_fs_1.readFileSync)(file, "utf-8"), lang);
105
+ }
106
+ catch {
107
+ return [];
108
+ }
109
+ }
110
+ // A co-located declaration file that may declare symbols the source defines
111
+ // dynamically (Sorbet `.rbi`, TypeScript `.d.ts`) — checked as a fallback so a
112
+ // metaprogrammed `define_method` / ambient declaration still resolves.
113
+ const DECL_SIBLING = {
114
+ ".ts": ".d.ts",
115
+ ".tsx": ".d.ts",
116
+ ".js": ".d.ts",
117
+ ".jsx": ".d.ts",
118
+ ".mjs": ".d.ts",
119
+ ".rb": ".rbi",
120
+ };
121
+ /**
122
+ * Whether `file` defines a top-level (or scoped) symbol named `name`. This is
123
+ * the whole check for a file-qualified reference (`path#symbol`): we parse the
124
+ * one named file — no project-wide index, no resolution across files. As a
125
+ * fallback we also consult a co-located declaration file (`.rbi` / `.d.ts`), so
126
+ * typed dynamic symbols resolve without running Sorbet / the TS compiler.
127
+ */
128
+ function fileDefinesSymbol(file, name) {
129
+ if (definedSymbolsInFile(file).some((d) => d.name === name))
130
+ return true;
131
+ const ext = (0, node_path_1.extname)(file);
132
+ const decl = DECL_SIBLING[ext];
133
+ if (decl && !file.endsWith(decl)) {
134
+ const sibling = file.slice(0, -ext.length) + decl;
135
+ if ((0, node_fs_1.existsSync)(sibling) &&
136
+ definedSymbolsInFile(sibling).some((d) => d.name === name)) {
137
+ return true;
138
+ }
139
+ }
140
+ return false;
141
+ }
142
+ //# sourceMappingURL=symbols.js.map
@@ -0,0 +1,8 @@
1
+ import type { ClaudeSpec } from "./spec.js";
2
+ export declare function makeTmpDir(suffix?: string): string;
3
+ export declare function cleanupTmpDir(dir: string): void;
4
+ export declare function makeSpec(overrides?: Partial<ClaudeSpec>): ClaudeSpec;
5
+ declare function git(cwd: string, cmd: string): string;
6
+ export declare function initGitRepo(dir: string): void;
7
+ export { git };
8
+ //# sourceMappingURL=test-utils.d.ts.map
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeTmpDir = makeTmpDir;
4
+ exports.cleanupTmpDir = cleanupTmpDir;
5
+ exports.makeSpec = makeSpec;
6
+ exports.initGitRepo = initGitRepo;
7
+ exports.git = git;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = require("node:path");
10
+ const node_os_1 = require("node:os");
11
+ const node_child_process_1 = require("node:child_process");
12
+ function makeTmpDir(suffix = "test") {
13
+ return (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), `vigiles-${suffix}-`));
14
+ }
15
+ function cleanupTmpDir(dir) {
16
+ (0, node_fs_1.rmSync)(dir, { recursive: true, force: true });
17
+ }
18
+ function makeSpec(overrides) {
19
+ return {
20
+ _specType: "claude",
21
+ rules: {},
22
+ ...overrides,
23
+ };
24
+ }
25
+ function git(cwd, cmd) {
26
+ return (0, node_child_process_1.execSync)(`git ${cmd}`, {
27
+ cwd,
28
+ encoding: "utf-8",
29
+ stdio: ["pipe", "pipe", "pipe"],
30
+ }).trim();
31
+ }
32
+ function initGitRepo(dir) {
33
+ git(dir, "init");
34
+ git(dir, "config user.email test@test.com");
35
+ git(dir, "config user.name Test");
36
+ git(dir, "config commit.gpgsign false");
37
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, "README.md"), "# test");
38
+ git(dir, "add .");
39
+ git(dir, 'commit -m "init"');
40
+ }
41
+ //# sourceMappingURL=test-utils.js.map