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,246 @@
1
+ "use strict";
2
+ /**
3
+ * vigiles — Validate vigiles-builder calls in markdown code blocks.
4
+ *
5
+ * Mirror of inline mode but inverted: inline mode skips fenced code blocks
6
+ * (so `<!-- vigiles:enforce -->` in prose doesn't accidentally match an
7
+ * example). This module enters fenced code blocks (ts/typescript/js/
8
+ * javascript) and validates the vigiles builder calls inside —
9
+ * `enforce("...")`, `file("...")`, `cmd("...")`, `ref("...")` — using the
10
+ * same engines that validate them in spec.ts.
11
+ *
12
+ * Default: validate every ref. Illustrative blocks opt out via
13
+ * `<!-- vigiles:ignore -->` immediately before the fence. Whole files
14
+ * opt out via `<!-- vigiles:ignore-file -->` anywhere in the file
15
+ * (intended for research/design docs that quote hypothetical refs).
16
+ *
17
+ * Scope: ONLY vigiles builder calls. Generic TS syntax / type checking
18
+ * in markdown is explicitly out of scope — use eslint-plugin-markdown or
19
+ * twoslash for that.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.extractDocRefs = extractDocRefs;
23
+ exports.findDocRefs = findDocRefs;
24
+ exports.formatDocRefReport = formatDocRefReport;
25
+ const node_fs_1 = require("node:fs");
26
+ const node_path_1 = require("node:path");
27
+ const glob_1 = require("glob");
28
+ const linters_js_1 = require("./linters.js");
29
+ const compile_js_1 = require("./compile.js");
30
+ // ---------------------------------------------------------------------------
31
+ // Constants
32
+ // ---------------------------------------------------------------------------
33
+ const DEFAULT_IGNORE = [
34
+ "node_modules/**",
35
+ "dist/**",
36
+ ".vigiles/**",
37
+ ".git/**",
38
+ ];
39
+ const FENCE_RE = /^(\s{0,3})(`{3,}|~{3,})(.*)$/;
40
+ const TS_LANGS = new Set(["ts", "typescript", "js", "javascript"]);
41
+ // Ignore markers must appear as standalone lines (whole line is the
42
+ // comment, modulo whitespace) so an inline-code mention like
43
+ // `` `<!-- vigiles:ignore-file -->` `` in prose documenting the syntax
44
+ // doesn't accidentally disable validation.
45
+ const IGNORE_BLOCK_RE = /^\s{0,3}<!--\s*vigiles:ignore\s*-->\s*$/;
46
+ const IGNORE_FILE_RE = /^\s{0,3}<!--\s*vigiles:ignore-file\s*-->\s*$/m;
47
+ const CALL_RE = /\b(enforce|file|cmd|ref)\(\s*["']([^"'\n]+)["']/g;
48
+ const PLACEHOLDER_RE = /[<>]/;
49
+ /**
50
+ * Error-message patterns that mean "tool not available in this env" rather
51
+ * than "ref is actually broken." We can't decide between valid and invalid
52
+ * when the underlying linter or CLI isn't installed, so count these
53
+ * separately from real errors.
54
+ */
55
+ const UNVERIFIABLE_PATTERNS = [
56
+ /Unknown linter:/i,
57
+ /not found on PATH/i,
58
+ /No Cedar policies found/i,
59
+ ];
60
+ /** @internal */ function extractDocRefs(content, file) {
61
+ const lines = content.split("\n");
62
+ const refs = [];
63
+ let blocksIgnored = 0;
64
+ let fenceChar = null;
65
+ let fenceLen = 0;
66
+ let fenceLang = "";
67
+ let blockLines = [];
68
+ let nextBlockIgnored = false;
69
+ for (let i = 0; i < lines.length; i++) {
70
+ const line = lines[i];
71
+ const fm = FENCE_RE.exec(line);
72
+ if (fm) {
73
+ const marker = fm[2];
74
+ const ch = marker[0];
75
+ const len = marker.length;
76
+ const info = fm[3].trim();
77
+ if (fenceChar === null) {
78
+ fenceChar = ch;
79
+ fenceLen = len;
80
+ fenceLang = info.split(/\s+/)[0].toLowerCase();
81
+ blockLines = [];
82
+ continue;
83
+ }
84
+ else if (ch === fenceChar && len >= fenceLen && info === "") {
85
+ // Closing fence
86
+ if (TS_LANGS.has(fenceLang)) {
87
+ if (nextBlockIgnored) {
88
+ blocksIgnored++;
89
+ }
90
+ else {
91
+ for (const { lineNo, text } of blockLines) {
92
+ for (const m of text.matchAll(CALL_RE)) {
93
+ refs.push({
94
+ file,
95
+ line: lineNo,
96
+ kind: m[1],
97
+ value: m[2],
98
+ });
99
+ }
100
+ }
101
+ }
102
+ }
103
+ fenceChar = null;
104
+ fenceLen = 0;
105
+ fenceLang = "";
106
+ nextBlockIgnored = false;
107
+ blockLines = [];
108
+ continue;
109
+ }
110
+ }
111
+ if (fenceChar !== null) {
112
+ blockLines.push({ lineNo: i + 1, text: line });
113
+ continue;
114
+ }
115
+ if (IGNORE_BLOCK_RE.test(line)) {
116
+ nextBlockIgnored = true;
117
+ }
118
+ }
119
+ return { refs, blocksIgnored };
120
+ }
121
+ function isUnverifiable(message) {
122
+ return UNVERIFIABLE_PATTERNS.some((p) => p.test(message));
123
+ }
124
+ function validateRefs(refs, basePath) {
125
+ const errors = [];
126
+ let unverified = 0;
127
+ let placeholders = 0;
128
+ const scripts = (0, compile_js_1.readPackageScripts)(basePath) ?? {};
129
+ for (const r of refs) {
130
+ // Skip obvious placeholders like enforce("<linter>/<rule>") that
131
+ // appear in skill format documentation. They aren't typos — they're
132
+ // syntax templates. Real refs don't contain < or >.
133
+ if (PLACEHOLDER_RE.test(r.value)) {
134
+ placeholders++;
135
+ continue;
136
+ }
137
+ switch (r.kind) {
138
+ case "enforce": {
139
+ const result = (0, linters_js_1.checkLinterRule)(r.value, basePath, {
140
+ catalogOnly: true,
141
+ });
142
+ if (!result.exists) {
143
+ const msg = result.error ?? `Rule "${r.value}" not found`;
144
+ if (isUnverifiable(msg)) {
145
+ unverified++;
146
+ }
147
+ else {
148
+ errors.push({ ...r, message: msg });
149
+ }
150
+ }
151
+ break;
152
+ }
153
+ case "file":
154
+ case "ref": {
155
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.resolve)(basePath, r.value))) {
156
+ errors.push({ ...r, message: `File not found: "${r.value}"` });
157
+ }
158
+ break;
159
+ }
160
+ case "cmd": {
161
+ const npmRun = r.value.match(/^npm\s+run\s+(\S+)/);
162
+ const npmDirect = r.value.match(/^npm\s+(test|start|build|pretest)\b/);
163
+ const scriptName = npmRun?.[1] ?? npmDirect?.[1];
164
+ if (scriptName && !scripts[scriptName]) {
165
+ errors.push({
166
+ ...r,
167
+ message: `Script "${scriptName}" not found in package.json`,
168
+ });
169
+ }
170
+ break;
171
+ }
172
+ }
173
+ }
174
+ return { errors, unverified, placeholders };
175
+ }
176
+ // ---------------------------------------------------------------------------
177
+ // Public API
178
+ // ---------------------------------------------------------------------------
179
+ /**
180
+ * Walk every `.md` under `basePath`, extract vigiles builder calls from
181
+ * fenced TS/JS code blocks, validate against the same engines used for
182
+ * spec.ts. Honors `<!-- vigiles:ignore-file -->` (skip the whole file)
183
+ * and `<!-- vigiles:ignore -->` (skip the next code block).
184
+ */
185
+ function findDocRefs(options = {}) {
186
+ const basePath = options.basePath ?? process.cwd();
187
+ const ignore = [...DEFAULT_IGNORE, ...(options.ignore ?? [])];
188
+ const files = (0, glob_1.globSync)("**/*.md", { cwd: basePath, ignore });
189
+ const allRefs = [];
190
+ let filesIgnored = 0;
191
+ let blocksIgnored = 0;
192
+ for (const f of files) {
193
+ let content;
194
+ try {
195
+ content = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(basePath, f), "utf-8");
196
+ }
197
+ catch {
198
+ continue;
199
+ }
200
+ if (IGNORE_FILE_RE.test(content)) {
201
+ filesIgnored++;
202
+ continue;
203
+ }
204
+ const r = extractDocRefs(content, f);
205
+ allRefs.push(...r.refs);
206
+ blocksIgnored += r.blocksIgnored;
207
+ }
208
+ const outcome = validateRefs(allRefs, basePath);
209
+ return {
210
+ filesScanned: files.length,
211
+ filesIgnored,
212
+ blocksIgnored,
213
+ refs: allRefs,
214
+ errors: outcome.errors,
215
+ unverified: outcome.unverified,
216
+ placeholders: outcome.placeholders,
217
+ };
218
+ }
219
+ /** Format a DocRefReport as human-readable text. */
220
+ function formatDocRefReport(report) {
221
+ const lines = [];
222
+ const meta = [];
223
+ if (report.filesIgnored > 0)
224
+ meta.push(`${String(report.filesIgnored)} via vigiles:ignore-file`);
225
+ if (report.blocksIgnored > 0)
226
+ meta.push(`${String(report.blocksIgnored)} blocks via vigiles:ignore`);
227
+ if (report.placeholders > 0)
228
+ meta.push(`${String(report.placeholders)} placeholders skipped`);
229
+ if (report.unverified > 0)
230
+ meta.push(`${String(report.unverified)} unverified (tool unavailable)`);
231
+ const metaStr = meta.length > 0 ? ` (${meta.join(", ")})` : "";
232
+ lines.push(`scanned ${String(report.filesScanned)} files${metaStr}`);
233
+ lines.push(`${String(report.refs.length)} vigiles refs in code blocks${report.errors.length === 0 ? " — all valid" : ""}`);
234
+ if (report.errors.length === 0)
235
+ return lines.join("\n");
236
+ lines.push(`✗ ${String(report.errors.length)} broken ref(s):`);
237
+ for (const e of report.errors.slice(0, 12)) {
238
+ const trunc = e.message.length > 80 ? `${e.message.slice(0, 80)}…` : e.message;
239
+ lines.push(` ${e.file}:${String(e.line)} ${e.kind}("${e.value}") — ${trunc}`);
240
+ }
241
+ if (report.errors.length > 12) {
242
+ lines.push(` ... +${String(report.errors.length - 12)} more`);
243
+ }
244
+ return lines.join("\n");
245
+ }
246
+ //# sourceMappingURL=doc-refs.js.map
package/dist/eval.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ /** One arm of the comparison: fixture overrides + settings (hooks) for this arm. */
2
+ export interface EvalArm {
3
+ /** Files written on top of the base fixture for this arm. */
4
+ readonly files?: Record<string, string>;
5
+ /** `.claude/settings.json` (hooks/permissions) for this arm; omit for none. */
6
+ readonly settings?: unknown;
7
+ }
8
+ /** Context handed to `measure` after a run, to compute that run's metrics. */
9
+ export interface RunContext {
10
+ readonly cwd: string;
11
+ readonly exitCode: number;
12
+ readonly stdout: string;
13
+ /** `num_turns` reported by claude, or 0. */
14
+ readonly turns: number;
15
+ /** Contents of a file under the working dir, or null if absent. */
16
+ file(path: string): string | null;
17
+ /** Run a shell command in the working dir; returns trimmed stdout ("" on error). */
18
+ sh(command: string): string;
19
+ }
20
+ export type Metrics = Record<string, number | boolean>;
21
+ export interface EvalSpec<M extends Metrics> {
22
+ readonly name?: string;
23
+ /** Base fixture files (path → contents), written fresh for every run. */
24
+ readonly fixture?: Record<string, string>;
25
+ /** The arms to compare, by name. */
26
+ readonly arms: Record<string, EvalArm>;
27
+ /** The task prompt given to the agent. */
28
+ readonly task: string;
29
+ /** Compute this run's metrics from its outcome. */
30
+ readonly measure: (ctx: RunContext) => M;
31
+ /** Trials per arm. Default 5. */
32
+ readonly trials?: number;
33
+ /** Model alias. Default "haiku". */
34
+ readonly model?: string;
35
+ /** Tools the agent may use. Default: Read Edit Write Bash. */
36
+ readonly allowedTools?: readonly string[];
37
+ /** Per-run timeout ms. Default 240000. */
38
+ readonly timeoutMs?: number;
39
+ /** Seconds to wait between runs (avoid rate-limit bursts). Default 4. */
40
+ readonly spacingSec?: number;
41
+ }
42
+ export interface ArmReport {
43
+ readonly runs: number;
44
+ /** Aggregated metrics: mean for numbers, fraction-true (0..1) for booleans. */
45
+ readonly metrics: Record<string, number>;
46
+ }
47
+ export interface EvalReport {
48
+ readonly name: string;
49
+ readonly trials: number;
50
+ readonly arms: Record<string, ArmReport>;
51
+ }
52
+ /** Aggregate per-run metrics: mean for numbers, fraction-true (0..1) for booleans. */
53
+ export declare function aggregate(rows: readonly Metrics[]): Record<string, number>;
54
+ /**
55
+ * Run the eval: every arm × every trial against the real `claude` CLI, with the
56
+ * metric computed per run and aggregated per arm. Requires `claude` on PATH and
57
+ * working model auth (e.g. `ANTHROPIC_API_KEY`).
58
+ */
59
+ export declare function runEval<M extends Metrics>(spec: EvalSpec<M>): Promise<EvalReport>;
60
+ /** Format an eval report as a compact table for the console. */
61
+ export declare function formatEvalReport(report: EvalReport): string;
62
+ //# sourceMappingURL=eval.d.ts.map
package/dist/eval.js ADDED
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.aggregate = aggregate;
4
+ exports.runEval = runEval;
5
+ exports.formatEvalReport = formatEvalReport;
6
+ /**
7
+ * vigiles — Claude Code harness *evals*.
8
+ *
9
+ * Measure whether a harness change actually changes agent behaviour. Define a
10
+ * fixture, a set of **arms** (e.g. a hook on vs off, with/without a CLAUDE.md
11
+ * rule), a task prompt, and a **metric**; `runEval` drives the real `claude` CLI
12
+ * N trials per arm and aggregates. This is the generalized form of the
13
+ * benchmark harness under `bench/` — the empirical half of testing your harness.
14
+ *
15
+ * const report = await runEval({
16
+ * fixture: { "src/billing.ts": "export function chargeCard(){}" },
17
+ * arms: {
18
+ * vanilla: {},
19
+ * gated: { settings: { hooks: { PostToolUse: [refsHook] } } },
20
+ * },
21
+ * task: "document chargeCard in SKILL.md, referencing it by name",
22
+ * measure: (ctx) => ({ marked: ctx.sh("grep -c vigiles:symbol SKILL.md") > 0 }),
23
+ * trials: 6,
24
+ * });
25
+ *
26
+ * Real model → real cost + statistical, not deterministic. For fast, free,
27
+ * deterministic checks of hook *logic*, see `harness-test.ts`.
28
+ */
29
+ const node_child_process_1 = require("node:child_process");
30
+ const node_fs_1 = require("node:fs");
31
+ const node_os_1 = require("node:os");
32
+ const node_path_1 = require("node:path");
33
+ function writeFiles(cwd, files) {
34
+ for (const [p, content] of Object.entries(files)) {
35
+ const full = (0, node_path_1.resolve)(cwd, p);
36
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(full), { recursive: true });
37
+ (0, node_fs_1.writeFileSync)(full, content);
38
+ }
39
+ }
40
+ function spawnAgent(task, cwd, model, tools, hasSettings, timeoutMs) {
41
+ return new Promise((resolvePromise) => {
42
+ const args = [
43
+ "-p",
44
+ task,
45
+ "--output-format",
46
+ "json",
47
+ "--model",
48
+ model,
49
+ "--permission-mode",
50
+ "acceptEdits",
51
+ ...(hasSettings ? ["--settings", "settings.json"] : []),
52
+ "--allowedTools",
53
+ ...tools,
54
+ ];
55
+ const child = (0, node_child_process_1.spawn)("claude", args, {
56
+ cwd,
57
+ env: process.env,
58
+ stdio: ["ignore", "pipe", "pipe"],
59
+ });
60
+ let stdout = "";
61
+ child.stdout.on("data", (d) => (stdout += d.toString()));
62
+ const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
63
+ child.on("close", (code) => {
64
+ clearTimeout(timer);
65
+ resolvePromise({ code: code ?? 0, stdout });
66
+ });
67
+ });
68
+ }
69
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
70
+ function makeContext(cwd, out) {
71
+ let turns = 0;
72
+ try {
73
+ turns = JSON.parse(out.stdout).num_turns ?? 0;
74
+ }
75
+ catch {
76
+ /* non-JSON output */
77
+ }
78
+ return {
79
+ cwd,
80
+ exitCode: out.code,
81
+ stdout: out.stdout,
82
+ turns,
83
+ file: (p) => {
84
+ const f = (0, node_path_1.resolve)(cwd, p);
85
+ return (0, node_fs_1.existsSync)(f) ? (0, node_fs_1.readFileSync)(f, "utf-8") : null;
86
+ },
87
+ sh: (command) => {
88
+ try {
89
+ return (0, node_child_process_1.execSync)(command, {
90
+ cwd,
91
+ encoding: "utf-8",
92
+ stdio: ["ignore", "pipe", "ignore"],
93
+ }).trim();
94
+ }
95
+ catch (e) {
96
+ // Return captured stdout even on a non-zero exit (e.g. `audit` exits 2
97
+ // but still prints its findings), rather than swallowing it.
98
+ const out = e.stdout;
99
+ return typeof out === "string" ? out.trim() : "";
100
+ }
101
+ },
102
+ };
103
+ }
104
+ /** Aggregate per-run metrics: mean for numbers, fraction-true (0..1) for booleans. */
105
+ function aggregate(rows) {
106
+ const keys = new Set();
107
+ for (const r of rows)
108
+ for (const k of Object.keys(r))
109
+ keys.add(k);
110
+ const out = {};
111
+ for (const k of keys) {
112
+ let sum = 0;
113
+ let n = 0;
114
+ for (const r of rows) {
115
+ const v = r[k];
116
+ if (typeof v === "number") {
117
+ sum += v;
118
+ n++;
119
+ }
120
+ else if (typeof v === "boolean") {
121
+ sum += v ? 1 : 0;
122
+ n++;
123
+ }
124
+ }
125
+ out[k] = n > 0 ? sum / n : 0;
126
+ }
127
+ return out;
128
+ }
129
+ /**
130
+ * Run the eval: every arm × every trial against the real `claude` CLI, with the
131
+ * metric computed per run and aggregated per arm. Requires `claude` on PATH and
132
+ * working model auth (e.g. `ANTHROPIC_API_KEY`).
133
+ */
134
+ async function runEval(spec) {
135
+ const trials = spec.trials ?? 5;
136
+ const model = spec.model ?? "haiku";
137
+ const tools = spec.allowedTools ?? ["Read", "Edit", "Write", "Bash"];
138
+ const timeoutMs = spec.timeoutMs ?? 240000;
139
+ const spacing = (spec.spacingSec ?? 4) * 1000;
140
+ const arms = {};
141
+ for (const [armName, arm] of Object.entries(spec.arms)) {
142
+ const rows = [];
143
+ for (let t = 0; t < trials; t++) {
144
+ const cwd = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "vigiles-eval-"));
145
+ try {
146
+ writeFiles(cwd, { ...spec.fixture, ...arm.files });
147
+ const hasSettings = arm.settings !== undefined;
148
+ if (hasSettings) {
149
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(cwd, "settings.json"), JSON.stringify(arm.settings, null, 2).replaceAll("{cwd}", cwd));
150
+ }
151
+ const out = await spawnAgent(spec.task, cwd, model, tools, hasSettings, timeoutMs);
152
+ rows.push(spec.measure(makeContext(cwd, out)));
153
+ }
154
+ finally {
155
+ (0, node_fs_1.rmSync)(cwd, { recursive: true, force: true });
156
+ await sleep(spacing);
157
+ }
158
+ }
159
+ arms[armName] = { runs: rows.length, metrics: aggregate(rows) };
160
+ }
161
+ return { name: spec.name ?? "eval", trials, arms };
162
+ }
163
+ /** Format an eval report as a compact table for the console. */
164
+ function formatEvalReport(report) {
165
+ const lines = [`${report.name} (${String(report.trials)} trials/arm)`];
166
+ for (const [arm, r] of Object.entries(report.arms)) {
167
+ const parts = Object.entries(r.metrics)
168
+ .map(([k, v]) => `${k}=${v.toFixed(2)}`)
169
+ .join(" ");
170
+ lines.push(` ${arm.padEnd(10)} ${parts}`);
171
+ }
172
+ return lines.join("\n");
173
+ }
174
+ //# sourceMappingURL=eval.js.map