vigiles 2.4.0 → 2.6.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 +72 -327
- package/dist/agent-result.d.ts +40 -0
- package/dist/agent-result.js +97 -0
- package/dist/agent-runtime.d.ts +64 -0
- package/dist/agent-runtime.js +147 -0
- package/dist/claude-code.d.ts +10 -0
- package/dist/claude-code.js +26 -0
- package/dist/cli.js +106 -0
- package/dist/compile.d.ts +32 -3
- package/dist/compile.js +268 -0
- package/dist/egress-proxy.d.ts +2 -0
- package/dist/egress-proxy.js +60 -0
- package/dist/eval-baseline.d.ts +68 -0
- package/dist/eval-baseline.js +173 -0
- package/dist/eval-cache.d.ts +33 -0
- package/dist/eval-cache.js +94 -0
- package/dist/eval.d.ts +172 -9
- package/dist/eval.js +319 -58
- package/dist/harness-assert.d.ts +175 -13
- package/dist/harness-assert.js +358 -25
- package/dist/harness-test.d.ts +97 -11
- package/dist/harness-test.js +147 -37
- package/dist/judge.js +2 -0
- package/dist/linters.d.ts +6 -0
- package/dist/linters.js +1 -0
- package/dist/linting.d.ts +9 -0
- package/dist/linting.js +25 -0
- package/dist/mock-entry.d.ts +2 -0
- package/dist/mock-entry.js +36 -0
- package/dist/mock-model.d.ts +29 -0
- package/dist/mock-model.js +40 -0
- package/dist/plugin-loader.js +51 -17
- package/dist/run-hook.d.ts +81 -1
- package/dist/run-hook.js +189 -11
- package/dist/sandbox.d.ts +107 -0
- package/dist/sandbox.js +307 -0
- package/dist/spec.d.ts +130 -0
- package/dist/spec.js +55 -0
- package/dist/stats.d.ts +49 -0
- package/dist/stats.js +109 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +28 -0
- package/package.json +10 -4
package/dist/eval.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runEval = runEval;
|
|
4
|
+
exports.parseUsage = parseUsage;
|
|
3
5
|
exports.aggregate = aggregate;
|
|
4
6
|
exports.aggregateStats = aggregateStats;
|
|
5
|
-
exports.
|
|
7
|
+
exports.aggregateUsage = aggregateUsage;
|
|
8
|
+
exports.isRateLimited = isRateLimited;
|
|
9
|
+
exports.runPool = runPool;
|
|
10
|
+
exports.runEvalWith = runEvalWith;
|
|
6
11
|
exports.formatEvalReport = formatEvalReport;
|
|
12
|
+
exports.measureTriggerRateWith = measureTriggerRateWith;
|
|
13
|
+
exports.measureTriggerRate = measureTriggerRate;
|
|
14
|
+
exports.formatTriggerRateReport = formatTriggerRateReport;
|
|
7
15
|
/**
|
|
8
16
|
* vigiles — Claude Code harness *evals*.
|
|
9
17
|
*
|
|
@@ -32,6 +40,8 @@ const node_fs_1 = require("node:fs");
|
|
|
32
40
|
const node_os_1 = require("node:os");
|
|
33
41
|
const node_path_1 = require("node:path");
|
|
34
42
|
const plugin_loader_js_1 = require("./plugin-loader.js");
|
|
43
|
+
const harness_test_js_1 = require("./harness-test.js");
|
|
44
|
+
const eval_cache_js_1 = require("./eval-cache.js");
|
|
35
45
|
function writeFiles(cwd, files) {
|
|
36
46
|
for (const [p, content] of Object.entries(files)) {
|
|
37
47
|
const full = (0, node_path_1.resolve)(cwd, p);
|
|
@@ -39,50 +49,87 @@ function writeFiles(cwd, files) {
|
|
|
39
49
|
(0, node_fs_1.writeFileSync)(full, content);
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
|
-
|
|
52
|
+
/* v8 ignore start -- real claude subprocess; exercised by bench/, not the unit gate */
|
|
53
|
+
function spawnAgent(a) {
|
|
43
54
|
return new Promise((resolvePromise) => {
|
|
44
55
|
const args = [
|
|
45
56
|
"-p",
|
|
46
|
-
task,
|
|
57
|
+
a.task,
|
|
58
|
+
// stream-json (+ --verbose, required with -p) so the per-turn tool_use
|
|
59
|
+
// events survive into `ctx.toolCalls` — the unified Trace, same as the
|
|
60
|
+
// harness tier. The terminal `result` event still carries num_turns/output.
|
|
47
61
|
"--output-format",
|
|
48
|
-
"json",
|
|
62
|
+
"stream-json",
|
|
63
|
+
"--verbose",
|
|
49
64
|
"--model",
|
|
50
|
-
model,
|
|
65
|
+
a.model,
|
|
51
66
|
"--permission-mode",
|
|
52
67
|
"acceptEdits",
|
|
53
|
-
...(pluginDir !== undefined
|
|
54
|
-
|
|
68
|
+
...(a.pluginDir !== undefined
|
|
69
|
+
? ["--plugin-dir", (0, node_path_1.resolve)(a.pluginDir)]
|
|
70
|
+
: []),
|
|
71
|
+
...(a.hasSettings ? ["--settings", "settings.json"] : []),
|
|
55
72
|
"--allowedTools",
|
|
56
|
-
...tools,
|
|
73
|
+
...a.tools,
|
|
57
74
|
];
|
|
58
75
|
const child = (0, node_child_process_1.spawn)("claude", args, {
|
|
59
|
-
cwd,
|
|
76
|
+
cwd: a.cwd,
|
|
60
77
|
env: process.env,
|
|
61
78
|
stdio: ["ignore", "pipe", "pipe"],
|
|
62
79
|
});
|
|
63
80
|
let stdout = "";
|
|
81
|
+
let stderr = "";
|
|
64
82
|
child.stdout.on("data", (d) => (stdout += d.toString()));
|
|
65
|
-
|
|
83
|
+
child.stderr.on("data", (d) => (stderr += d.toString()));
|
|
84
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), a.timeoutMs);
|
|
66
85
|
child.on("close", (code) => {
|
|
67
86
|
clearTimeout(timer);
|
|
68
|
-
resolvePromise({ code: code ?? 0, stdout });
|
|
87
|
+
resolvePromise({ code: code ?? 0, stdout, stderr });
|
|
69
88
|
});
|
|
70
89
|
});
|
|
71
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Run the eval: every arm × every trial against the real `claude` CLI, with the
|
|
93
|
+
* metric computed per run and aggregated per arm. Requires `claude` on PATH and
|
|
94
|
+
* working model auth (e.g. `ANTHROPIC_API_KEY`). Thin wrapper over
|
|
95
|
+
* {@link runEvalWith} with the real agent runner.
|
|
96
|
+
*/
|
|
97
|
+
async function runEval(spec) {
|
|
98
|
+
return runEvalWith(spec, spawnAgent);
|
|
99
|
+
}
|
|
100
|
+
/* v8 ignore stop */
|
|
72
101
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
102
|
+
/** Pull cost / latency / tokens out of a parsed `result` event (0 when absent). */
|
|
103
|
+
function usageFrom(result) {
|
|
104
|
+
const num = (v) => (typeof v === "number" ? v : 0);
|
|
105
|
+
const usage = (result?.usage ?? {});
|
|
106
|
+
return {
|
|
107
|
+
costUsd: num(result?.total_cost_usd),
|
|
108
|
+
durationMs: num(result?.duration_ms),
|
|
109
|
+
inputTokens: num(usage.input_tokens),
|
|
110
|
+
outputTokens: num(usage.output_tokens),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Parse per-run cost/latency/tokens from a stream — pure, model-free. */
|
|
114
|
+
function parseUsage(stdout) {
|
|
115
|
+
return usageFrom((0, harness_test_js_1.parseResultEvent)(stdout));
|
|
116
|
+
}
|
|
73
117
|
function makeContext(cwd, out) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
/* non-JSON output */
|
|
80
|
-
}
|
|
118
|
+
const result = (0, harness_test_js_1.parseResultEvent)(out.stdout);
|
|
119
|
+
const turns = typeof result?.num_turns === "number" ? result.num_turns : 0;
|
|
120
|
+
const output = typeof result?.result === "string" ? result.result : "";
|
|
81
121
|
return {
|
|
82
122
|
cwd,
|
|
83
123
|
exitCode: out.code,
|
|
84
124
|
stdout: out.stdout,
|
|
85
125
|
turns,
|
|
126
|
+
toolCalls: (0, harness_test_js_1.parseToolCalls)(out.stdout),
|
|
127
|
+
hooks: (0, harness_test_js_1.parseHooks)(out.stdout),
|
|
128
|
+
output,
|
|
129
|
+
usage: usageFrom(result),
|
|
130
|
+
// The eval tier drives the real API (no mock between claude and the model),
|
|
131
|
+
// so the requests can't be captured here — modelRequests is harness-tier only.
|
|
132
|
+
modelRequests: [],
|
|
86
133
|
file: (p) => {
|
|
87
134
|
const f = (0, node_path_1.resolve)(cwd, p);
|
|
88
135
|
return (0, node_fs_1.existsSync)(f) ? (0, node_fs_1.readFileSync)(f, "utf-8") : null;
|
|
@@ -143,66 +190,280 @@ function aggregateStats(rows) {
|
|
|
143
190
|
const std = n > 1
|
|
144
191
|
? Math.sqrt(values.reduce((a, b) => a + (b - mean) ** 2, 0) / (n - 1))
|
|
145
192
|
: 0;
|
|
146
|
-
|
|
193
|
+
const passK = n > 0 && values.every((v) => v > 0) ? 1 : 0;
|
|
194
|
+
out[k] = { mean, std, se: n > 0 ? std / Math.sqrt(n) : 0, n, passK };
|
|
147
195
|
}
|
|
148
196
|
return out;
|
|
149
197
|
}
|
|
198
|
+
/** Aggregate per-run usage into an arm's cost / latency / token totals + means. */
|
|
199
|
+
function aggregateUsage(usages) {
|
|
200
|
+
const n = usages.length;
|
|
201
|
+
const sum = (f) => usages.reduce((a, u) => a + f(u), 0);
|
|
202
|
+
const totalCostUsd = sum((u) => u.costUsd);
|
|
203
|
+
return {
|
|
204
|
+
totalCostUsd,
|
|
205
|
+
meanCostUsd: n > 0 ? totalCostUsd / n : 0,
|
|
206
|
+
meanDurationMs: n > 0 ? sum((u) => u.durationMs) / n : 0,
|
|
207
|
+
totalInputTokens: sum((u) => u.inputTokens),
|
|
208
|
+
totalOutputTokens: sum((u) => u.outputTokens),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
150
211
|
/**
|
|
151
|
-
* Run
|
|
152
|
-
*
|
|
153
|
-
*
|
|
212
|
+
* Run one trial through the cache: on a hit, restore the recorded post-run
|
|
213
|
+
* filesystem into `cwd` and return the recorded output (no model call); on a
|
|
214
|
+
* miss, run the agent and (in `readwrite`) record output + cwd snapshot. The
|
|
215
|
+
* cache key excludes `measure`, so editing the metric still replays.
|
|
154
216
|
*/
|
|
155
|
-
async function
|
|
217
|
+
async function runWithCache(runArgs, keyParts, runner, cfg) {
|
|
218
|
+
if (cfg.cache === "off")
|
|
219
|
+
return runner(runArgs);
|
|
220
|
+
const key = (0, eval_cache_js_1.cacheKey)({
|
|
221
|
+
task: runArgs.task,
|
|
222
|
+
model: runArgs.model,
|
|
223
|
+
tools: runArgs.tools,
|
|
224
|
+
files: keyParts.files,
|
|
225
|
+
settings: keyParts.settings,
|
|
226
|
+
trialIndex: keyParts.trialIndex,
|
|
227
|
+
});
|
|
228
|
+
const hit = (0, eval_cache_js_1.readCache)(cfg.cacheDir, key);
|
|
229
|
+
if (hit) {
|
|
230
|
+
(0, eval_cache_js_1.restoreDir)(runArgs.cwd, hit.files);
|
|
231
|
+
return hit.out;
|
|
232
|
+
}
|
|
233
|
+
const out = await runner(runArgs);
|
|
234
|
+
if (cfg.cache === "readwrite") {
|
|
235
|
+
(0, eval_cache_js_1.writeCache)(cfg.cacheDir, key, { out, files: (0, eval_cache_js_1.snapshotDir)(runArgs.cwd) });
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
/** Execute one trial in a fresh sandbox; returns its metric row + usage. */
|
|
240
|
+
async function executeTrial(spec, arm, trialIndex, runner, cfg) {
|
|
241
|
+
const cwd = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "vigiles-eval-"));
|
|
242
|
+
try {
|
|
243
|
+
const { files, settings } = (0, plugin_loader_js_1.resolveHarness)({
|
|
244
|
+
plugin: arm.plugin,
|
|
245
|
+
settings: arm.settings,
|
|
246
|
+
files: { ...spec.fixture, ...arm.files },
|
|
247
|
+
});
|
|
248
|
+
writeFiles(cwd, files);
|
|
249
|
+
const hasSettings = settings !== undefined;
|
|
250
|
+
if (hasSettings) {
|
|
251
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(cwd, "settings.json"), JSON.stringify(settings, null, 2).replaceAll("{cwd}", cwd));
|
|
252
|
+
}
|
|
253
|
+
const out = await runWithCache({
|
|
254
|
+
task: spec.task,
|
|
255
|
+
cwd,
|
|
256
|
+
model: cfg.model,
|
|
257
|
+
tools: cfg.tools,
|
|
258
|
+
hasSettings,
|
|
259
|
+
pluginDir: arm.pluginDir,
|
|
260
|
+
timeoutMs: cfg.timeoutMs,
|
|
261
|
+
}, { files, settings, trialIndex }, runner, cfg);
|
|
262
|
+
const ctx = makeContext(cwd, out);
|
|
263
|
+
return { row: spec.measure(ctx), usage: ctx.usage };
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
(0, node_fs_1.rmSync)(cwd, { recursive: true, force: true });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// A signal in the captured streams that the model call was rate-limited /
|
|
270
|
+
// overloaded — worth a backoff + retry rather than counting as a real sample.
|
|
271
|
+
const RATE_LIMIT_RE = /rate.?limit|\b429\b|overloaded|too many requests/i;
|
|
272
|
+
/** Whether a run's captured output looks like a rate-limit / overload. Pure. */
|
|
273
|
+
function isRateLimited(out) {
|
|
274
|
+
return RATE_LIMIT_RE.test(`${out.stderr ?? ""}\n${out.stdout}`);
|
|
275
|
+
}
|
|
276
|
+
/** Call `runner`, retrying with exponential backoff while it looks rate-limited. */
|
|
277
|
+
async function runWithRetry(runArgs, runner, retries, baseMs) {
|
|
278
|
+
for (let attempt = 0;; attempt++) {
|
|
279
|
+
const out = await runner(runArgs);
|
|
280
|
+
if (!isRateLimited(out) || attempt >= retries)
|
|
281
|
+
return out;
|
|
282
|
+
await sleep(baseMs * 2 ** attempt);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** Map `worker` over `items` with at most `concurrency` in flight, order preserved. */
|
|
286
|
+
async function runPool(items, concurrency, worker) {
|
|
287
|
+
const results = new Array(items.length);
|
|
288
|
+
let next = 0;
|
|
289
|
+
const drain = async () => {
|
|
290
|
+
for (;;) {
|
|
291
|
+
const i = next++;
|
|
292
|
+
const item = items[i];
|
|
293
|
+
if (i >= items.length || item === undefined)
|
|
294
|
+
return;
|
|
295
|
+
results[i] = await worker(item);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const workers = Math.max(1, Math.min(concurrency, items.length || 1));
|
|
299
|
+
await Promise.all(Array.from({ length: workers }, drain));
|
|
300
|
+
return results;
|
|
301
|
+
}
|
|
302
|
+
/** Flatten arms × trials into a single work list (so concurrency spans both). */
|
|
303
|
+
function buildUnits(arms, trials) {
|
|
304
|
+
const units = [];
|
|
305
|
+
for (const [armName, arm] of Object.entries(arms)) {
|
|
306
|
+
for (let t = 0; t < trials; t++)
|
|
307
|
+
units.push({ armName, arm, trialIndex: t });
|
|
308
|
+
}
|
|
309
|
+
return units;
|
|
310
|
+
}
|
|
311
|
+
/** Group completed (non-skipped) unit results by arm and aggregate each. */
|
|
312
|
+
function aggregateArms(armNames, results) {
|
|
313
|
+
const arms = {};
|
|
314
|
+
let totalCostUsd = 0;
|
|
315
|
+
for (const armName of armNames) {
|
|
316
|
+
const done = results.filter((r) => !r.skipped && r.armName === armName);
|
|
317
|
+
const rows = done.map((d) => d.row);
|
|
318
|
+
const usage = aggregateUsage(done.map((d) => d.usage));
|
|
319
|
+
totalCostUsd += usage.totalCostUsd;
|
|
320
|
+
arms[armName] = {
|
|
321
|
+
runs: rows.length,
|
|
322
|
+
metrics: aggregate(rows),
|
|
323
|
+
stats: aggregateStats(rows),
|
|
324
|
+
usage,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
return { arms, totalCostUsd };
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* The eval orchestration — every arm × trial via `runner`, run through the cache
|
|
331
|
+
* and a rate-limit retry, with at most `concurrency` in flight and an optional
|
|
332
|
+
* `maxCostUsd` budget cap; metric + usage computed per run and aggregated per
|
|
333
|
+
* arm. Exported with an injectable `runner` so the loop, `measure` context,
|
|
334
|
+
* caching, pooling, and aggregation are unit-testable without spawning a model
|
|
335
|
+
* (pass a fake returning canned stream-json). `runEval` is this with the real
|
|
336
|
+
* agent runner.
|
|
337
|
+
*/
|
|
338
|
+
async function runEvalWith(spec, runner) {
|
|
156
339
|
const trials = spec.trials ?? 5;
|
|
340
|
+
const spacing = (spec.spacingSec ?? 4) * 1000;
|
|
341
|
+
const concurrency = spec.concurrency ?? 1;
|
|
342
|
+
const retries = spec.rateLimitRetries ?? 3;
|
|
343
|
+
const backoffMs = spec.retryBackoffMs ?? 1000;
|
|
344
|
+
const cfg = {
|
|
345
|
+
model: spec.model ?? "haiku",
|
|
346
|
+
tools: spec.allowedTools ?? ["Read", "Edit", "Write", "Bash"],
|
|
347
|
+
timeoutMs: spec.timeoutMs ?? 240000,
|
|
348
|
+
cache: spec.cache ?? "off",
|
|
349
|
+
cacheDir: spec.cacheDir ?? (0, node_path_1.resolve)(process.cwd(), ".vigiles", "eval-cache"),
|
|
350
|
+
};
|
|
351
|
+
const retrying = (a) => runWithRetry(a, runner, retries, backoffMs);
|
|
352
|
+
const units = buildUnits(spec.arms, trials);
|
|
353
|
+
let spent = 0;
|
|
354
|
+
let aborted = false;
|
|
355
|
+
const worker = async (unit) => {
|
|
356
|
+
if (aborted)
|
|
357
|
+
return { armName: unit.armName, skipped: true };
|
|
358
|
+
const { row, usage } = await executeTrial(spec, unit.arm, unit.trialIndex, retrying, cfg);
|
|
359
|
+
spent += usage.costUsd;
|
|
360
|
+
if (spec.maxCostUsd !== undefined && spent >= spec.maxCostUsd) {
|
|
361
|
+
aborted = true;
|
|
362
|
+
}
|
|
363
|
+
if (spacing > 0)
|
|
364
|
+
await sleep(spacing);
|
|
365
|
+
return { armName: unit.armName, skipped: false, row, usage };
|
|
366
|
+
};
|
|
367
|
+
const results = await runPool(units, concurrency, worker);
|
|
368
|
+
const { arms, totalCostUsd } = aggregateArms(Object.keys(spec.arms), results);
|
|
369
|
+
return { name: spec.name ?? "eval", trials, arms, totalCostUsd, aborted };
|
|
370
|
+
}
|
|
371
|
+
/** Render one metric: `name=mean±se pass^k=…` (se/pass^k shown when measured). */
|
|
372
|
+
function formatMetric(name, mean, stat) {
|
|
373
|
+
const base = stat && stat.se > 0
|
|
374
|
+
? `${name}=${mean.toFixed(2)}±${stat.se.toFixed(2)}`
|
|
375
|
+
: `${name}=${mean.toFixed(2)}`;
|
|
376
|
+
return stat && stat.n > 0 ? `${base} pass^k=${String(stat.passK)}` : base;
|
|
377
|
+
}
|
|
378
|
+
/** Compact tokens like `3.4k`; whole numbers under 1000 stay as-is. */
|
|
379
|
+
function fmtTokens(n) {
|
|
380
|
+
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
381
|
+
}
|
|
382
|
+
/** A `($0.0123 · 1.2s/run · 3.4k tok)` suffix, or "" when no usage was reported. */
|
|
383
|
+
function formatUsage(u) {
|
|
384
|
+
if (u.totalCostUsd === 0 && u.totalInputTokens + u.totalOutputTokens === 0) {
|
|
385
|
+
return "";
|
|
386
|
+
}
|
|
387
|
+
const tok = fmtTokens(u.totalInputTokens + u.totalOutputTokens);
|
|
388
|
+
return ` ($${u.totalCostUsd.toFixed(4)} · ${(u.meanDurationMs / 1000).toFixed(1)}s/run · ${tok} tok)`;
|
|
389
|
+
}
|
|
390
|
+
/** Format an eval report as a compact table for the console (mean ± se, pass^k). */
|
|
391
|
+
function formatEvalReport(report) {
|
|
392
|
+
const header = report.totalCostUsd > 0
|
|
393
|
+
? `${report.name} (${String(report.trials)} trials/arm) — $${report.totalCostUsd.toFixed(4)} total`
|
|
394
|
+
: `${report.name} (${String(report.trials)} trials/arm)`;
|
|
395
|
+
const lines = [header];
|
|
396
|
+
for (const [arm, r] of Object.entries(report.arms)) {
|
|
397
|
+
const parts = Object.entries(r.metrics)
|
|
398
|
+
.map(([k, v]) => formatMetric(k, v, r.stats[k]))
|
|
399
|
+
.join(" ");
|
|
400
|
+
lines.push(` ${arm.padEnd(10)} ${parts}${formatUsage(r.usage)}`);
|
|
401
|
+
}
|
|
402
|
+
return lines.join("\n");
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Trigger-rate orchestration — every prompt × trial via `runner`, the `fired`
|
|
406
|
+
* predicate evaluated per run and aggregated into an overall + per-prompt rate.
|
|
407
|
+
* Exported with an injectable `runner` so the loop is unit-testable without a
|
|
408
|
+
* model; `measureTriggerRate` is this with the real agent runner.
|
|
409
|
+
*/
|
|
410
|
+
async function measureTriggerRateWith(spec, runner) {
|
|
411
|
+
const trials = spec.trials ?? 1;
|
|
157
412
|
const model = spec.model ?? "haiku";
|
|
158
|
-
const tools = spec.allowedTools ?? ["Read", "Edit", "Write", "Bash"];
|
|
413
|
+
const tools = spec.allowedTools ?? ["Read", "Edit", "Write", "Bash", "Skill"];
|
|
159
414
|
const timeoutMs = spec.timeoutMs ?? 240000;
|
|
160
415
|
const spacing = (spec.spacingSec ?? 4) * 1000;
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
416
|
+
const perPrompt = [];
|
|
417
|
+
let firedTotal = 0;
|
|
418
|
+
let n = 0;
|
|
419
|
+
for (const prompt of spec.prompts) {
|
|
420
|
+
let fired = 0;
|
|
164
421
|
for (let t = 0; t < trials; t++) {
|
|
165
|
-
const cwd = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "vigiles-
|
|
422
|
+
const cwd = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "vigiles-trigger-"));
|
|
166
423
|
try {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
424
|
+
const out = await runner({
|
|
425
|
+
task: prompt,
|
|
426
|
+
cwd,
|
|
427
|
+
model,
|
|
428
|
+
tools,
|
|
429
|
+
hasSettings: false,
|
|
430
|
+
pluginDir: spec.pluginDir,
|
|
431
|
+
timeoutMs,
|
|
171
432
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (hasSettings) {
|
|
175
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(cwd, "settings.json"), JSON.stringify(settings, null, 2).replaceAll("{cwd}", cwd));
|
|
176
|
-
}
|
|
177
|
-
const out = await spawnAgent(spec.task, cwd, model, tools, hasSettings, arm.pluginDir, timeoutMs);
|
|
178
|
-
rows.push(spec.measure(makeContext(cwd, out)));
|
|
433
|
+
if (spec.fired(makeContext(cwd, out)))
|
|
434
|
+
fired++;
|
|
179
435
|
}
|
|
180
436
|
finally {
|
|
181
437
|
(0, node_fs_1.rmSync)(cwd, { recursive: true, force: true });
|
|
182
438
|
await sleep(spacing);
|
|
183
439
|
}
|
|
184
440
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
441
|
+
perPrompt.push({
|
|
442
|
+
prompt,
|
|
443
|
+
fired,
|
|
444
|
+
trials,
|
|
445
|
+
rate: trials > 0 ? fired / trials : 0,
|
|
446
|
+
});
|
|
447
|
+
firedTotal += fired;
|
|
448
|
+
n += trials;
|
|
190
449
|
}
|
|
191
|
-
return {
|
|
450
|
+
return { rate: n > 0 ? firedTotal / n : 0, n, perPrompt };
|
|
192
451
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
452
|
+
/* v8 ignore start -- real claude subprocess; thin wrapper over measureTriggerRateWith */
|
|
453
|
+
/**
|
|
454
|
+
* Measure a skill/behaviour's real trigger rate across prompts × trials against
|
|
455
|
+
* the real `claude` CLI. Requires `claude` + model auth.
|
|
456
|
+
*/
|
|
457
|
+
async function measureTriggerRate(spec) {
|
|
458
|
+
return measureTriggerRateWith(spec, spawnAgent);
|
|
459
|
+
}
|
|
460
|
+
/* v8 ignore stop */
|
|
461
|
+
/** Format a trigger-rate report: overall %, then each prompt's rate. */
|
|
462
|
+
function formatTriggerRateReport(report) {
|
|
463
|
+
const pct = (report.rate * 100).toFixed(0);
|
|
464
|
+
const lines = [`trigger-rate: ${pct}% (${String(report.n)} runs)`];
|
|
465
|
+
for (const p of report.perPrompt) {
|
|
466
|
+
lines.push(` ${p.rate.toFixed(2)} ${p.prompt.slice(0, 60)}`);
|
|
206
467
|
}
|
|
207
468
|
return lines.join("\n");
|
|
208
469
|
}
|