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.
- package/README.md +182 -134
- package/dist/action-gate.d.ts +28 -0
- package/dist/action-gate.js +73 -0
- package/dist/cli.js +705 -230
- package/dist/community-skills.d.ts +22 -0
- package/dist/community-skills.js +86 -0
- package/dist/compile-generator.d.ts +48 -0
- package/dist/compile-generator.js +322 -0
- package/dist/compile.d.ts +4 -0
- package/dist/compile.js +239 -45
- package/dist/coverage.d.ts +56 -0
- package/dist/coverage.js +178 -0
- package/dist/doc-refs.d.ts +60 -0
- package/dist/doc-refs.js +246 -0
- package/dist/eval.d.ts +62 -0
- package/dist/eval.js +174 -0
- package/dist/evolve.js +60 -125
- package/dist/frontmatter.d.ts +75 -0
- package/dist/frontmatter.js +263 -0
- package/dist/generate-schema.d.ts +51 -0
- package/dist/generate-schema.js +125 -0
- package/dist/generate-types.js +39 -1
- package/dist/harness-test.d.ts +38 -0
- package/dist/harness-test.js +129 -0
- package/dist/hash.d.ts +8 -0
- package/dist/hash.js +16 -0
- package/dist/inline.d.ts +22 -4
- package/dist/inline.js +60 -13
- package/dist/integrity.d.ts +29 -0
- package/dist/integrity.js +44 -0
- package/dist/linters.d.ts +5 -3
- package/dist/linters.js +144 -4
- package/dist/mock-model.d.ts +31 -0
- package/dist/mock-model.js +189 -0
- package/dist/orphans.d.ts +52 -0
- package/dist/orphans.js +124 -0
- package/dist/proofs.js +22 -16
- package/dist/refs.d.ts +44 -0
- package/dist/refs.js +144 -0
- package/dist/session.d.ts +97 -0
- package/dist/session.js +306 -0
- package/dist/sidecar.d.ts +35 -0
- package/dist/sidecar.js +102 -0
- package/dist/skill-driver.d.ts +77 -0
- package/dist/skill-driver.js +76 -0
- package/dist/skill-runtime.d.ts +101 -0
- package/dist/skill-runtime.js +289 -0
- package/dist/skill-test.d.ts +47 -0
- package/dist/skill-test.js +77 -0
- package/dist/spec.d.ts +119 -13
- package/dist/spec.js +51 -4
- package/dist/symbols.d.ts +30 -0
- package/dist/symbols.js +142 -0
- package/dist/test-utils.d.ts +8 -0
- package/dist/test-utils.js +41 -0
- package/dist/types.d.ts +34 -8
- package/dist/types.js +19 -0
- package/dist/validate.js +5 -3
- package/package.json +26 -5
- package/.claude/settings.json +0 -46
- package/.claude/settings.local.json +0 -8
- package/.github/workflows/ci.yml +0 -81
- package/.prettierignore +0 -1
- package/.vigiles/generated.d.ts +0 -205
- package/CLAUDE.md +0 -95
- package/CLAUDE.md.spec.ts +0 -142
- package/CONTRIBUTING.md +0 -121
- package/dist/action.d.ts.map +0 -1
- package/dist/action.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/cli.test.d.ts +0 -2
- package/dist/cli.test.d.ts.map +0 -1
- package/dist/cli.test.js +0 -650
- package/dist/cli.test.js.map +0 -1
- package/dist/compile.d.ts.map +0 -1
- package/dist/compile.js.map +0 -1
- package/dist/evolve.d.ts.map +0 -1
- package/dist/evolve.js.map +0 -1
- package/dist/freshness.d.ts +0 -67
- package/dist/freshness.d.ts.map +0 -1
- package/dist/freshness.js +0 -244
- package/dist/freshness.js.map +0 -1
- package/dist/freshness.test.d.ts +0 -2
- package/dist/freshness.test.d.ts.map +0 -1
- package/dist/freshness.test.js +0 -356
- package/dist/freshness.test.js.map +0 -1
- package/dist/generate-types.d.ts.map +0 -1
- package/dist/generate-types.js.map +0 -1
- package/dist/inline.d.ts.map +0 -1
- package/dist/inline.js.map +0 -1
- package/dist/inline.test.d.ts +0 -5
- package/dist/inline.test.d.ts.map +0 -1
- package/dist/inline.test.js +0 -152
- package/dist/inline.test.js.map +0 -1
- package/dist/linters.d.ts.map +0 -1
- package/dist/linters.js.map +0 -1
- package/dist/proofs.d.ts.map +0 -1
- package/dist/proofs.js.map +0 -1
- package/dist/proofs.test.d.ts +0 -9
- package/dist/proofs.test.d.ts.map +0 -1
- package/dist/proofs.test.js +0 -952
- package/dist/proofs.test.js.map +0 -1
- package/dist/spec.d.ts.map +0 -1
- package/dist/spec.js.map +0 -1
- package/dist/spec.test.d.ts +0 -2
- package/dist/spec.test.d.ts.map +0 -1
- package/dist/spec.test.js +0 -1222
- package/dist/spec.test.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js.map +0 -1
- package/dist/validate.test.d.ts +0 -2
- package/dist/validate.test.d.ts.map +0 -1
- package/dist/validate.test.js +0 -531
- package/dist/validate.test.js.map +0 -1
- package/docs/agent-setup.md +0 -85
- package/docs/agent-workflows.md +0 -103
- package/docs/comparison.md +0 -71
- package/docs/freshness.md +0 -124
- package/docs/inline-mode.md +0 -119
- package/docs/linter-support.md +0 -166
- package/docs/spec-format.md +0 -194
- package/eslint.config.mjs +0 -79
- package/examples/CLAUDE.md +0 -54
- package/examples/CLAUDE.md.spec.ts +0 -65
- package/examples/SKILL.md +0 -50
- package/examples/SKILL.md.spec.ts +0 -57
- package/fixtures/example-project/CLAUDE.md +0 -11
- package/fixtures/example-project/package.json +0 -9
- package/fixtures/example-project/src/index.ts +0 -3
- package/fixtures/example-project/src/utils.test.ts +0 -2
- package/fixtures/example-project/src/utils.ts +0 -3
- package/logo.png +0 -0
- package/research/adoption-strategy.md +0 -111
- package/research/agent-integration.md +0 -145
- package/research/ai-code-quality.md +0 -197
- package/research/code-search-for-agents.md +0 -313
- package/research/competitive-landscape.md +0 -163
- package/research/doc-freshness.md +0 -516
- package/research/executable-specs.md +0 -368
- package/research/feature-ideas.md +0 -464
- package/research/formal-proofs-for-agents.md +0 -338
- package/research/fp-for-agent-harness.md +0 -150
- package/research/fp-for-deterministic-ai.md +0 -131
- package/research/self-evolving-specs.md +0 -298
- package/schemas/claude-md-strict.yml +0 -18
- package/schemas/claude-md.yml +0 -6
- package/schemas/skill-strict.yml +0 -12
- package/schemas/skill.yml +0 -5
- package/skills/audit-feedback-loop/SKILL.md +0 -76
- package/skills/edit-spec/SKILL.md +0 -131
- package/skills/enforce-rules-format/SKILL.md +0 -71
- package/skills/generate-logo/SKILL.md +0 -102
- package/skills/generate-rule/SKILL.md +0 -90
- package/skills/linter-docs/clippy.md +0 -241
- package/skills/linter-docs/eslint.md +0 -384
- package/skills/linter-docs/pylint.md +0 -288
- package/skills/linter-docs/rubocop.md +0 -277
- package/skills/linter-docs/ruff.md +0 -187
- package/skills/linter-docs/stylelint.md +0 -247
- package/skills/migrate-to-spec/SKILL.md +0 -124
- package/skills/pr-to-lint-rule/SKILL.md +0 -97
- package/skills/strengthen/SKILL.md +0 -168
- package/src/action.ts +0 -214
- package/src/cli.test.ts +0 -914
- package/src/cli.ts +0 -1631
- package/src/compile.ts +0 -691
- package/src/evolve.ts +0 -781
- package/src/freshness.test.ts +0 -449
- package/src/freshness.ts +0 -299
- package/src/generate-types.ts +0 -448
- package/src/inline.test.ts +0 -206
- package/src/inline.ts +0 -164
- package/src/linters.ts +0 -739
- package/src/proofs.test.ts +0 -1314
- package/src/proofs.ts +0 -849
- package/src/spec.test.ts +0 -1471
- package/src/spec.ts +0 -427
- package/src/types.ts +0 -117
- package/src/validate.test.ts +0 -701
- package/src/validate.ts +0 -381
- package/tsconfig.json +0 -23
package/dist/doc-refs.js
ADDED
|
@@ -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
|