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,125 @@
1
+ "use strict";
2
+ /**
3
+ * vigiles generate-schema — emit a JSON Schema for `vigiles:` frontmatter.
4
+ *
5
+ * Sibling of `generate-types` (which emits a `.d.ts` for the spec.ts path).
6
+ * This emits `.vigiles/schema.json` so a YAML LSP can autocomplete rule
7
+ * names and squiggle typos in markdown frontmatter:
8
+ *
9
+ * ---
10
+ * # yaml-language-server: $schema=./.vigiles/schema.json
11
+ * vigiles:
12
+ * enforce:
13
+ * - rule: eslint/no-consolee # red squiggle in VS Code
14
+ * ---
15
+ *
16
+ * The `rule` enum is populated from the project's ACTUAL enabled linter
17
+ * rules — the same discovery `generate-types` runs — so the schema reflects
18
+ * what is really enforceable, not a static catalog.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.generateSchema = generateSchema;
22
+ const node_fs_1 = require("node:fs");
23
+ const node_path_1 = require("node:path");
24
+ const glob_1 = require("glob");
25
+ const generate_types_js_1 = require("./generate-types.js");
26
+ /**
27
+ * Rule references for config-declared custom linters. Mirrors
28
+ * `checkLinterRule`'s rulesDir lookup (any file `<rule>.*` is a rule) so the
29
+ * enum matches what `vigiles audit` accepts for these linters.
30
+ */
31
+ function customRuleRefs(basePath, linters) {
32
+ const refs = [];
33
+ for (const [name, cfg] of Object.entries(linters ?? {})) {
34
+ const dirs = Array.isArray(cfg.rulesDir) ? cfg.rulesDir : [cfg.rulesDir];
35
+ for (const dir of dirs) {
36
+ if (!dir)
37
+ continue;
38
+ const full = (0, node_path_1.resolve)(basePath, dir);
39
+ if (!(0, node_fs_1.existsSync)(full))
40
+ continue;
41
+ for (const f of (0, glob_1.globSync)("*.*", { cwd: full, nodir: true })) {
42
+ refs.push(`${name}/${f.replace(/\.[^.]+$/, "")}`);
43
+ }
44
+ }
45
+ }
46
+ return refs;
47
+ }
48
+ /**
49
+ * Build a JSON Schema for the `vigiles:` frontmatter block from the
50
+ * project's discovered linter rules. When no rules are discoverable the
51
+ * `rule` field falls back to a freeform string so the schema never
52
+ * false-flags a valid reference.
53
+ */
54
+ function generateSchema(options = {}) {
55
+ const basePath = options.basePath ?? process.cwd();
56
+ const { linters } = (0, generate_types_js_1.generateTypes)({ basePath });
57
+ const ruleNames = new Set();
58
+ for (const { linter, rules } of linters) {
59
+ for (const rule of rules) {
60
+ ruleNames.add(`${linter}/${rule}`);
61
+ // ESLint also accepts scoped-plugin rules in bare form (the scope is
62
+ // treated as the linter, e.g. "@typescript-eslint/no-explicit-any").
63
+ // Emit both so neither form produces a false squiggle.
64
+ if (linter === "eslint" && rule.includes("/")) {
65
+ ruleNames.add(rule);
66
+ }
67
+ }
68
+ }
69
+ for (const ref of customRuleRefs(basePath, options.linters)) {
70
+ ruleNames.add(ref);
71
+ }
72
+ const sorted = [...ruleNames].sort();
73
+ const ruleSchema = sorted.length > 0 ? { enum: sorted } : { type: "string" };
74
+ const schema = {
75
+ $schema: "http://json-schema.org/draft-07/schema#",
76
+ $id: "https://vigiles.dev/frontmatter.schema.json",
77
+ title: "vigiles frontmatter",
78
+ description: "vigiles enforce rules declared in markdown YAML frontmatter (Level 1).",
79
+ type: "object",
80
+ properties: {
81
+ vigiles: {
82
+ type: "object",
83
+ additionalProperties: false,
84
+ properties: {
85
+ enforce: {
86
+ type: "array",
87
+ description: "Linter rules to enforce, verified by `vigiles audit`.",
88
+ items: {
89
+ type: "object",
90
+ additionalProperties: false,
91
+ required: ["rule", "why"],
92
+ properties: {
93
+ rule: {
94
+ description: "Linter rule reference, e.g. eslint/no-console. Verified to exist AND be enabled.",
95
+ ...ruleSchema,
96
+ },
97
+ why: {
98
+ type: "string",
99
+ description: "Why this rule is enforced — shown to the agent as context.",
100
+ },
101
+ },
102
+ },
103
+ },
104
+ files: {
105
+ type: "array",
106
+ description: "File paths referenced by this instruction file, verified to exist by `vigiles audit`.",
107
+ items: { type: "string" },
108
+ },
109
+ commands: {
110
+ type: "array",
111
+ description: "Commands (npm scripts / script-runner invocations) referenced here, verified by `vigiles audit`.",
112
+ items: { type: "string" },
113
+ },
114
+ },
115
+ },
116
+ },
117
+ };
118
+ return {
119
+ schema,
120
+ json: JSON.stringify(schema, null, 2) + "\n",
121
+ ruleNames: sorted,
122
+ linters: linters.map((l) => ({ linter: l.linter, count: l.rules.length })),
123
+ };
124
+ }
125
+ //# sourceMappingURL=generate-schema.js.map
@@ -73,7 +73,7 @@ function discoverEslintRules(basePath) {
73
73
  const rules = JSON.parse(output.trim() || "[]");
74
74
  if (rules.length === 0)
75
75
  return null;
76
- return { linter: "eslint", rules, via: "flat config" };
76
+ return { linter: "eslint", rules, via: "flat config (v9+/v10)" };
77
77
  }
78
78
  catch {
79
79
  return null;
@@ -199,6 +199,41 @@ function discoverRubocopRules(basePath) {
199
199
  return null;
200
200
  }
201
201
  }
202
+ function discoverCedarPolicies(basePath) {
203
+ const ID_RE = /@id\("([^"]+)"\)/g;
204
+ const STATEMENT_RE = /\b(?:permit|forbid)\s*\(/;
205
+ const policies = new Set();
206
+ for (const dir of [".cedar", "cedar"]) {
207
+ const fullDir = (0, node_path_1.resolve)(basePath, dir);
208
+ if (!(0, node_fs_1.existsSync)(fullDir))
209
+ continue;
210
+ const files = (0, glob_1.globSync)("**/*.cedar", { cwd: fullDir, nodir: true });
211
+ for (const file of files) {
212
+ let content;
213
+ try {
214
+ content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(fullDir, file), "utf-8");
215
+ }
216
+ catch {
217
+ continue;
218
+ }
219
+ const annotated = [...content.matchAll(ID_RE)].map((m) => m[1]);
220
+ if (annotated.length > 0) {
221
+ for (const id of annotated)
222
+ policies.add(id);
223
+ }
224
+ else if (STATEMENT_RE.test(content)) {
225
+ policies.add(file.replace(/\.cedar$/, "").replace(/\\/g, "/"));
226
+ }
227
+ }
228
+ }
229
+ if (policies.size === 0)
230
+ return null;
231
+ return {
232
+ linter: "cedar",
233
+ rules: [...policies].sort(),
234
+ via: ".cedar files",
235
+ };
236
+ }
202
237
  function discoverClippyRules(basePath) {
203
238
  try {
204
239
  const cargoPath = (0, node_path_1.resolve)(basePath, "Cargo.toml");
@@ -300,6 +335,9 @@ function generateTypes(options = {}) {
300
335
  const clippy = discoverClippyRules(basePath);
301
336
  if (clippy)
302
337
  linters.push(clippy);
338
+ const cedar = discoverCedarPolicies(basePath);
339
+ if (cedar)
340
+ linters.push(cedar);
303
341
  const scripts = discoverNpmScripts(basePath);
304
342
  const files = discoverProjectFiles(basePath, fileGlobs);
305
343
  // Generate .d.ts
@@ -0,0 +1,38 @@
1
+ import { type ModelTurn } from "./mock-model.js";
2
+ export { scriptModel, type ModelTurn } from "./mock-model.js";
3
+ export interface HarnessTestSpec {
4
+ /** Fixture files to write in a fresh temp working dir (path → contents). */
5
+ readonly files?: Record<string, string>;
6
+ /** `.claude/settings.json` contents — the hooks/permissions under test. */
7
+ readonly settings?: unknown;
8
+ /** The scripted model turns the agent will take. */
9
+ readonly model: readonly ModelTurn[];
10
+ /** The user prompt. Default: "go". */
11
+ readonly prompt?: string;
12
+ /** Tools the agent may use. Default: Read Edit Write Bash. */
13
+ readonly allowedTools?: readonly string[];
14
+ /** Per-run wall-clock timeout in ms. Default 60000. */
15
+ readonly timeoutMs?: number;
16
+ }
17
+ export interface HarnessTestResult {
18
+ readonly exitCode: number;
19
+ readonly stdout: string;
20
+ /** Hook block messages and diagnostics land here. */
21
+ readonly stderr: string;
22
+ /** The temp working dir (inspect or clean it up). */
23
+ readonly cwd: string;
24
+ /** Number of model turns the agent took (mock turns served). */
25
+ readonly turns: number;
26
+ /** Final contents of a file under the working dir, or null if absent. */
27
+ file(path: string): string | null;
28
+ /** Remove the temp working dir. */
29
+ cleanup(): void;
30
+ }
31
+ /** Whether the `claude` CLI is available — harness tests need it. */
32
+ export declare function claudeAvailable(): boolean;
33
+ /**
34
+ * Run the real `claude` CLI against a scripted mock model, with the given
35
+ * fixture and settings (hooks). Deterministic — same script, same result.
36
+ */
37
+ export declare function runHarnessTest(spec: HarnessTestSpec): Promise<HarnessTestResult>;
38
+ //# sourceMappingURL=harness-test.d.ts.map
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scriptModel = void 0;
4
+ exports.claudeAvailable = claudeAvailable;
5
+ exports.runHarnessTest = runHarnessTest;
6
+ /**
7
+ * vigiles — deterministic Claude Code harness testing.
8
+ *
9
+ * Test what your *harness* does — hooks, settings, skills, instruction files —
10
+ * without paying for or depending on a real model. `runHarnessTest` spins up the
11
+ * real `claude` CLI (so your real hooks and settings fire exactly as in
12
+ * production) but points it at a scripted mock model (`src/mock-model.ts`), so
13
+ * the agent's turns are fixed and the outcome is reproducible. No API key, no
14
+ * cost, CI-friendly.
15
+ *
16
+ * const r = await runHarnessTest({
17
+ * settings: { hooks: { Stop: [{ hooks: [{ type: "command",
18
+ * command: "test -f DONE || { echo 'not done' >&2; exit 2; }" }] }] } },
19
+ * model: scriptModel([
20
+ * { text: "I'm done" }, // tries to stop → blocked
21
+ * { tool: "Bash", input: { command: "touch DONE" } },
22
+ * { text: "now done" },
23
+ * ]),
24
+ * });
25
+ * assert(JSON.parse(r.stdout).num_turns > 1); // the Stop hook fired
26
+ *
27
+ * The "steps" are the scripted model turns — their real home is deterministic
28
+ * harness testing, not production enforcement.
29
+ *
30
+ * Note: the simple mock drives the Bash tool and Stop hooks reliably; the
31
+ * Edit/Write tools are gated in headless mode and don't fire via the mock —
32
+ * drive file actions through Bash, or use the real-model eval tier (`eval.ts`)
33
+ * for Edit/Write hooks.
34
+ */
35
+ const node_child_process_1 = require("node:child_process");
36
+ const node_fs_1 = require("node:fs");
37
+ const node_os_1 = require("node:os");
38
+ const node_path_1 = require("node:path");
39
+ const mock_model_js_1 = require("./mock-model.js");
40
+ var mock_model_js_2 = require("./mock-model.js");
41
+ Object.defineProperty(exports, "scriptModel", { enumerable: true, get: function () { return mock_model_js_2.scriptModel; } });
42
+ /** Whether the `claude` CLI is available — harness tests need it. */
43
+ function claudeAvailable() {
44
+ try {
45
+ return (0, node_child_process_1.spawnSync)("claude", ["--version"], { stdio: "ignore" }).status === 0;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ function writeFixture(cwd, spec) {
52
+ for (const [p, content] of Object.entries(spec.files ?? {})) {
53
+ const full = (0, node_path_1.resolve)(cwd, p);
54
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(full), { recursive: true });
55
+ (0, node_fs_1.writeFileSync)(full, content);
56
+ }
57
+ if (spec.settings !== undefined) {
58
+ // `{cwd}` in any hook command is substituted with the working dir, so a
59
+ // hook can reference an absolute path inside it (hooks don't run with the
60
+ // project dir as cwd).
61
+ const json = JSON.stringify(spec.settings, null, 2).replaceAll("{cwd}", cwd);
62
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(cwd, "settings.json"), json);
63
+ }
64
+ }
65
+ function spawnClaude(args, cwd, baseUrl, timeoutMs) {
66
+ return new Promise((resolvePromise) => {
67
+ const child = (0, node_child_process_1.spawn)("claude", args, {
68
+ cwd,
69
+ env: {
70
+ ...process.env,
71
+ ANTHROPIC_BASE_URL: baseUrl,
72
+ // Any value works — the mock ignores auth; this avoids needing a real key.
73
+ ANTHROPIC_API_KEY: "sk-vigiles-mock",
74
+ },
75
+ stdio: ["ignore", "pipe", "pipe"],
76
+ });
77
+ let stdout = "";
78
+ let stderr = "";
79
+ child.stdout.on("data", (d) => (stdout += d.toString()));
80
+ child.stderr.on("data", (d) => (stderr += d.toString()));
81
+ const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
82
+ child.on("close", (code) => {
83
+ clearTimeout(timer);
84
+ resolvePromise({ code: code ?? 0, stdout, stderr });
85
+ });
86
+ });
87
+ }
88
+ /**
89
+ * Run the real `claude` CLI against a scripted mock model, with the given
90
+ * fixture and settings (hooks). Deterministic — same script, same result.
91
+ */
92
+ async function runHarnessTest(spec) {
93
+ const cwd = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "vigiles-harness-"));
94
+ writeFixture(cwd, spec);
95
+ const mock = await (0, mock_model_js_1.startMock)(spec.model);
96
+ try {
97
+ const tools = spec.allowedTools ?? ["Read", "Edit", "Write", "Bash"];
98
+ const args = [
99
+ "-p",
100
+ spec.prompt ?? "go",
101
+ "--output-format",
102
+ "json",
103
+ "--model",
104
+ "claude-sonnet-4-5",
105
+ ...(spec.settings !== undefined ? ["--settings", "settings.json"] : []),
106
+ "--allowedTools",
107
+ ...tools,
108
+ ];
109
+ const out = await spawnClaude(args, cwd, mock.url, spec.timeoutMs ?? 60000);
110
+ return {
111
+ exitCode: out.code,
112
+ stdout: out.stdout,
113
+ stderr: out.stderr,
114
+ cwd,
115
+ turns: mock.count,
116
+ file: (p) => {
117
+ const f = (0, node_path_1.resolve)(cwd, p);
118
+ return (0, node_fs_1.existsSync)(f) ? (0, node_fs_1.readFileSync)(f, "utf-8") : null;
119
+ },
120
+ cleanup: () => {
121
+ (0, node_fs_1.rmSync)(cwd, { recursive: true, force: true });
122
+ },
123
+ };
124
+ }
125
+ finally {
126
+ mock.close();
127
+ }
128
+ }
129
+ //# sourceMappingURL=harness-test.js.map
package/dist/hash.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ declare const __brand: unique symbol;
2
+ export type SHA256Hash = string & {
3
+ readonly [__brand]: "SHA256Hash";
4
+ };
5
+ export declare function sha256short(data: string | Buffer): SHA256Hash;
6
+ export declare function assertNever(x: never): never;
7
+ export {};
8
+ //# sourceMappingURL=hash.d.ts.map
package/dist/hash.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sha256short = sha256short;
4
+ exports.assertNever = assertNever;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const HASH_LENGTH = 16;
7
+ function sha256short(data) {
8
+ return (0, node_crypto_1.createHash)("sha256")
9
+ .update(data)
10
+ .digest("hex")
11
+ .slice(0, HASH_LENGTH);
12
+ }
13
+ function assertNever(x) {
14
+ throw new Error(`Unexpected value: ${String(x)}`);
15
+ }
16
+ //# sourceMappingURL=hash.js.map
package/dist/inline.d.ts CHANGED
@@ -25,8 +25,24 @@ export interface InlineRule {
25
25
  /** 1-based line number of the comment in the source file. */
26
26
  line: number;
27
27
  }
28
+ /** A `<!-- vigiles:file <path> -->` reference (verified to exist). */
29
+ export interface InlineFileRef {
30
+ /** Project-relative path to verify exists. */
31
+ path: string;
32
+ /** 1-based line number of the comment in the source file. */
33
+ line: number;
34
+ }
35
+ /** A `<!-- vigiles:cmd "<command>" -->` reference (npm scripts verified). */
36
+ export interface InlineCmdRef {
37
+ /** Command to verify (npm scripts checked against package.json). */
38
+ command: string;
39
+ /** 1-based line number of the comment in the source file. */
40
+ line: number;
41
+ }
28
42
  export interface InlineParseResult {
29
43
  rules: InlineRule[];
44
+ files: InlineFileRef[];
45
+ commands: InlineCmdRef[];
30
46
  /** Lines that look like vigiles: markers but failed to parse. */
31
47
  errors: {
32
48
  line: number;
@@ -46,13 +62,15 @@ export interface InlineParseResult {
46
62
  */
47
63
  export declare function parseInlineRules(content: string): InlineParseResult;
48
64
  /**
49
- * True if the content contains at least one parseable vigiles:enforce
50
- * rule (ignoring fenced code blocks and malformed markers). Used by
51
- * `require-spec` validation to treat inline mode as spec-equivalent.
65
+ * True if the content contains at least one parseable vigiles inline marker —
66
+ * an `enforce` rule, a `file` reference, or a `cmd` reference (ignoring fenced
67
+ * code blocks and malformed markers). Used by `require-spec` validation to
68
+ * treat inline mode as spec-equivalent: a file that pins even a single path is
69
+ * meaningfully managed.
52
70
  *
53
71
  * Deliberately delegates to `parseInlineRules` so a loose prefix regex
54
72
  * can't satisfy require-spec with a malformed marker that produces no
55
- * real enforceable rule.
73
+ * real reference.
56
74
  */
57
75
  export declare function hasInlineRules(content: string): boolean;
58
76
  //# sourceMappingURL=inline.d.ts.map
package/dist/inline.js CHANGED
@@ -29,6 +29,17 @@ exports.hasInlineRules = hasInlineRules;
29
29
  * embedded quotes, they can move to spec mode.
30
30
  */
31
31
  const ENFORCE_RE = /<!--\s*vigiles:enforce\s+([@A-Za-z0-9_/:.-]+)\s+"([^"\n]*)"\s*-->/;
32
+ /**
33
+ * Match `<!-- vigiles:file <path> -->`. The path is a single whitespace-free
34
+ * token (project-relative); spaces in paths are vanishingly rare and would be
35
+ * ambiguous against the closing `-->`.
36
+ */
37
+ const FILE_RE = /<!--\s*vigiles:file\s+(\S+)\s*-->/;
38
+ /**
39
+ * Match `<!-- vigiles:cmd "<command>" -->`. The command is quoted because
40
+ * commands contain spaces (e.g. `npm run build`).
41
+ */
42
+ const CMD_RE = /<!--\s*vigiles:cmd\s+"([^"\n]*)"\s*-->/;
32
43
  /**
33
44
  * Detects any `<!-- vigiles:<kind> -->` comment (valid or not) so we can
34
45
  * surface errors for typos and reserved-but-unrecognized kinds. Uses a
@@ -36,6 +47,16 @@ const ENFORCE_RE = /<!--\s*vigiles:enforce\s+([@A-Za-z0-9_/:.-]+)\s+"([^"\n]*)"\
36
47
  * circuit the pattern.
37
48
  */
38
49
  const MARKER_RE = /<!--\s*vigiles:([A-Za-z_-]+)[^]*?-->/;
50
+ // vigiles markers handled by other subsystems (skill runtime gates, opt-outs).
51
+ // The inline *rule* parser skips them rather than flagging them as unknown.
52
+ const KNOWN_NON_RULE_MARKERS = new Set([
53
+ "disable",
54
+ "ignore",
55
+ "ignore-file",
56
+ "gate", // skill step gate (src/skill-runtime.ts)
57
+ "result", // skill result gate
58
+ "symbol", // symbol reference mark (src/refs.ts)
59
+ ]);
39
60
  /**
40
61
  * Parse inline vigiles rules out of a markdown file's contents.
41
62
  * Does not touch the filesystem and does not verify the rules against
@@ -48,6 +69,8 @@ const MARKER_RE = /<!--\s*vigiles:([A-Za-z_-]+)[^]*?-->/;
48
69
  */
49
70
  function parseInlineRules(content) {
50
71
  const rules = [];
72
+ const files = [];
73
+ const commands = [];
51
74
  const errors = [];
52
75
  const lines = content.split("\n");
53
76
  let fenceChar = null;
@@ -97,15 +120,24 @@ function parseInlineRules(content) {
97
120
  });
98
121
  continue;
99
122
  }
123
+ const fileMatch = FILE_RE.exec(scannable);
124
+ if (fileMatch) {
125
+ files.push({ path: fileMatch[1], line: i + 1 });
126
+ continue;
127
+ }
128
+ const cmdMatch = CMD_RE.exec(scannable);
129
+ if (cmdMatch) {
130
+ commands.push({ command: cmdMatch[1], line: i + 1 });
131
+ continue;
132
+ }
100
133
  // Skip the compiled-file hash header (`<!-- vigiles:sha256:... -->`)
101
134
  // entirely — it's not a rule marker and should not be reported.
102
135
  if (/<!--\s*vigiles:sha\d+:/.test(scannable))
103
136
  continue;
104
137
  const markerMatch = MARKER_RE.exec(scannable);
105
138
  if (markerMatch) {
106
- // Looks like a vigiles marker but didn't parse as enforce
107
- // surface it so users catch typos like "vigile:enforce" or
108
- // unquoted why.
139
+ // Looks like a vigiles marker but didn't parse surface it so users
140
+ // catch typos like "vigile:enforce" or an unquoted why/command.
109
141
  const kind = markerMatch[1];
110
142
  if (kind === "enforce") {
111
143
  errors.push({
@@ -114,29 +146,44 @@ function parseInlineRules(content) {
114
146
  raw: line.trim(),
115
147
  });
116
148
  }
117
- else if (kind !== "disable" && kind !== "ignore") {
118
- // `vigiles:disable ...` / `vigiles:ignore ...` are reserved for
119
- // future disable-comment support; don't complain about them.
149
+ else if (kind === "file") {
150
+ errors.push({
151
+ line: i + 1,
152
+ message: "Malformed vigiles:file — expected `<!-- vigiles:file <path> -->`",
153
+ raw: line.trim(),
154
+ });
155
+ }
156
+ else if (kind === "cmd") {
157
+ errors.push({
158
+ line: i + 1,
159
+ message: 'Malformed vigiles:cmd — expected `<!-- vigiles:cmd "<command>" -->`',
160
+ raw: line.trim(),
161
+ });
162
+ }
163
+ else if (!KNOWN_NON_RULE_MARKERS.has(kind)) {
120
164
  errors.push({
121
165
  line: i + 1,
122
- message: `Unknown vigiles marker "${kind}". Only \`vigiles:enforce\` is supported.`,
166
+ message: `Unknown vigiles marker "${kind}". Only \`vigiles:enforce\`, \`vigiles:file\`, and \`vigiles:cmd\` are supported.`,
123
167
  raw: line.trim(),
124
168
  });
125
169
  }
126
170
  }
127
171
  }
128
- return { rules, errors };
172
+ return { rules, files, commands, errors };
129
173
  }
130
174
  /**
131
- * True if the content contains at least one parseable vigiles:enforce
132
- * rule (ignoring fenced code blocks and malformed markers). Used by
133
- * `require-spec` validation to treat inline mode as spec-equivalent.
175
+ * True if the content contains at least one parseable vigiles inline marker —
176
+ * an `enforce` rule, a `file` reference, or a `cmd` reference (ignoring fenced
177
+ * code blocks and malformed markers). Used by `require-spec` validation to
178
+ * treat inline mode as spec-equivalent: a file that pins even a single path is
179
+ * meaningfully managed.
134
180
  *
135
181
  * Deliberately delegates to `parseInlineRules` so a loose prefix regex
136
182
  * can't satisfy require-spec with a malformed marker that produces no
137
- * real enforceable rule.
183
+ * real reference.
138
184
  */
139
185
  function hasInlineRules(content) {
140
- return parseInlineRules(content).rules.length > 0;
186
+ const r = parseInlineRules(content);
187
+ return r.rules.length + r.files.length + r.commands.length > 0;
141
188
  }
142
189
  //# sourceMappingURL=inline.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Integrity check: verify a compiled markdown file hasn't been hand-edited.
3
+ *
4
+ * Every compiled file has a SHA-256 hash in its first line:
5
+ * <!-- vigiles:sha256:<hash> compiled from CLAUDE.md.spec.ts -->
6
+ *
7
+ * If the body content's hash no longer matches what's recorded, someone
8
+ * edited the compiled output directly. The check is one-pass and ~free.
9
+ *
10
+ * This is the entire freshness story now: no input fingerprinting, no
11
+ * recompile diffing. Those responsibilities belong elsewhere:
12
+ *
13
+ * - Hand-edit detection → this module
14
+ * - "Did the spec change?" → guard() rules emitting compile hooks
15
+ * - "Are referenced linter rules / files / scripts still valid?"
16
+ * → enforce() / file() / cmd() catch this at compile time
17
+ * - "Are committed compiled files actually fresh?"
18
+ * → CI runs `vigiles compile` then `git diff --exit-code`
19
+ */
20
+ export interface IntegrityResult {
21
+ intact: boolean;
22
+ reason?: string;
23
+ }
24
+ /**
25
+ * Check whether the compiled markdown's SHA-256 hash matches its body.
26
+ * Files without a hash header are treated as hand-written (intact).
27
+ */
28
+ export declare function checkIntegrity(content: string): IntegrityResult;
29
+ //# sourceMappingURL=integrity.d.ts.map
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ /**
3
+ * Integrity check: verify a compiled markdown file hasn't been hand-edited.
4
+ *
5
+ * Every compiled file has a SHA-256 hash in its first line:
6
+ * <!-- vigiles:sha256:<hash> compiled from CLAUDE.md.spec.ts -->
7
+ *
8
+ * If the body content's hash no longer matches what's recorded, someone
9
+ * edited the compiled output directly. The check is one-pass and ~free.
10
+ *
11
+ * This is the entire freshness story now: no input fingerprinting, no
12
+ * recompile diffing. Those responsibilities belong elsewhere:
13
+ *
14
+ * - Hand-edit detection → this module
15
+ * - "Did the spec change?" → guard() rules emitting compile hooks
16
+ * - "Are referenced linter rules / files / scripts still valid?"
17
+ * → enforce() / file() / cmd() catch this at compile time
18
+ * - "Are committed compiled files actually fresh?"
19
+ * → CI runs `vigiles compile` then `git diff --exit-code`
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.checkIntegrity = checkIntegrity;
23
+ const hash_js_1 = require("./hash.js");
24
+ const HASH_LINE_RE = /^<!-- vigiles:sha256:([a-f0-9]+) compiled from (.+) -->\r?\n\r?\n?/;
25
+ /**
26
+ * Check whether the compiled markdown's SHA-256 hash matches its body.
27
+ * Files without a hash header are treated as hand-written (intact).
28
+ */
29
+ function checkIntegrity(content) {
30
+ const match = content.match(HASH_LINE_RE);
31
+ if (!match) {
32
+ return { intact: true, reason: "No hash header (hand-written file)" };
33
+ }
34
+ const expectedHash = match[1];
35
+ const body = content.replace(HASH_LINE_RE, "");
36
+ if ((0, hash_js_1.sha256short)(body) !== expectedHash) {
37
+ return {
38
+ intact: false,
39
+ reason: "Compiled file was modified directly — edit the .spec.ts source and recompile",
40
+ };
41
+ }
42
+ return { intact: true };
43
+ }
44
+ //# sourceMappingURL=integrity.js.map
package/dist/linters.d.ts CHANGED
@@ -3,10 +3,11 @@
3
3
  *
4
4
  * Verifies that linter rule references (e.g., "eslint/no-console") point to
5
5
  * real rules that exist and are enabled in project config. Supports:
6
- * ESLint, Stylelint (Node API), Ruff, Clippy, Pylint, RuboCop (CLI).
6
+ * ESLint, Stylelint (Node API), Ruff, Clippy, Pylint, RuboCop (CLI),
7
+ * Cedar (filesystem policies for AWS Bedrock AgentCore / Vectimus).
7
8
  *
8
- * This is the core moat — no other tool resolves rules against 6 linter APIs
9
- * and checks config-enabled status.
9
+ * This is the core moat — no other tool resolves rules across 7 catalog APIs
10
+ * (6 linters + Cedar policy language) and checks config-enabled status.
10
11
  */
11
12
  export type ConfigEnabledStatus = "enabled" | "disabled" | "unknown";
12
13
  export interface LinterCheckResult {
@@ -23,6 +24,7 @@ export interface DetectedLinter {
23
24
  }
24
25
  /** @internal */ export declare function extractLinterName(enforcedBy: string): string;
25
26
  /** @internal */ export declare function extractRuleName(enforcedBy: string): string | null;
27
+ /** @internal */ export declare function clearCedarCache(): void;
26
28
  /**
27
29
  * Check a single linter rule reference (e.g., "eslint/no-console").
28
30
  *