cyber-aced 0.3.0 → 0.4.1

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.
@@ -0,0 +1,1256 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { accessSync, constants, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
5
+ import { homedir, tmpdir } from "node:os";
6
+ import { delimiter, dirname, isAbsolute, join, normalize, resolve } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { parseArgs } from "node:util";
9
+ import { gzipSync } from "node:zlib";
10
+ import { headlessCommand, headlessInvocation } from "@cyberuni/agent-harness";
11
+ //#region skills/bench/scripts/bench-claude-code.mts
12
+ /** Reads `--output-format stream-json`: tool calls from assistant messages, the rest from `result`. */
13
+ function parseStreamJson(text) {
14
+ const out = {
15
+ inputTokens: 0,
16
+ outputTokens: 0,
17
+ cacheReadTokens: 0,
18
+ cacheCreationTokens: 0,
19
+ turns: 0,
20
+ toolCalls: 0,
21
+ costUsd: 0,
22
+ capped: true
23
+ };
24
+ for (const line of text.split("\n")) {
25
+ if (!line.startsWith("{")) continue;
26
+ let event;
27
+ try {
28
+ event = JSON.parse(line);
29
+ } catch {
30
+ continue;
31
+ }
32
+ if (event.type === "system" && typeof event.model === "string" && event.model && out.model === void 0) out.model = event.model;
33
+ else if (event.type === "assistant") {
34
+ const content = Array.isArray(event.message?.content) ? event.message.content : [];
35
+ out.toolCalls += content.filter((c) => c?.type === "tool_use").length;
36
+ if (out.model === void 0 && typeof event.message?.model === "string" && event.message.model) out.model = event.message.model;
37
+ } else if (event.type === "result") {
38
+ const u = event.usage ?? {};
39
+ out.inputTokens = u.input_tokens ?? 0;
40
+ out.outputTokens = u.output_tokens ?? 0;
41
+ out.cacheReadTokens = u.cache_read_input_tokens ?? 0;
42
+ out.cacheCreationTokens = u.cache_creation_input_tokens ?? 0;
43
+ out.turns = event.num_turns ?? 0;
44
+ out.costUsd = event.total_cost_usd ?? 0;
45
+ out.capped = event.subtype !== "success" || event.is_error === true;
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+ const HARNESS = "claude-code";
51
+ function launchArgs(o) {
52
+ const facts = headlessInvocation(HARNESS);
53
+ const cmd = headlessCommand(HARNESS, {
54
+ prompt: o.prompt,
55
+ model: o.model,
56
+ permission: o.permissionMode,
57
+ outputFormat: "stream-json"
58
+ });
59
+ if (!cmd || facts.supported !== true) throw new Error("@cyberuni/agent-harness reports no headless mode for claude-code");
60
+ const args = [...cmd.args];
61
+ const disable = facts.transcript?.disable ?? [];
62
+ const extra = [
63
+ "--max-budget-usd",
64
+ String(o.maxBudgetUsd),
65
+ ...disable,
66
+ ...disable.includes("--no-session-persistence") ? [] : ["--no-session-persistence"],
67
+ "--setting-sources",
68
+ "project",
69
+ "--strict-mcp-config"
70
+ ];
71
+ if (o.pluginDir) extra.push("--plugin-dir", o.pluginDir);
72
+ if (existsSync(join(o.checkout, ".mcp.json"))) extra.push("--mcp-config", ".mcp.json");
73
+ return [...args, ...extra];
74
+ }
75
+ const claudeCode = {
76
+ harness: HARNESS,
77
+ name: "aced.claude-code",
78
+ runner: "print",
79
+ executable: (() => {
80
+ const facts = headlessInvocation(HARNESS);
81
+ return facts.supported === true ? facts.executable : "claude";
82
+ })(),
83
+ launch(o) {
84
+ const r = spawnSync(this.executable, launchArgs(o), {
85
+ cwd: o.checkout,
86
+ encoding: "utf8",
87
+ timeout: o.timeoutMs,
88
+ killSignal: "SIGKILL",
89
+ maxBuffer: 536870912,
90
+ stdio: [
91
+ "ignore",
92
+ "pipe",
93
+ "pipe"
94
+ ],
95
+ env: {
96
+ ...process.env,
97
+ ...o.env
98
+ }
99
+ });
100
+ const code = r.error?.code;
101
+ if (r.error && code !== "ETIMEDOUT" && code !== "ENOBUFS") throw new Error(`could not launch ${this.executable}: ${r.error.message}`);
102
+ const transcript = r.stdout ?? "";
103
+ const parsed = parseStreamJson(transcript);
104
+ if (r.error || r.signal) parsed.capped = true;
105
+ return {
106
+ ...parsed,
107
+ transcript
108
+ };
109
+ },
110
+ isolatedConfig(runTemp) {
111
+ const dir = join(runTemp, "claude-config");
112
+ mkdirSync(dir, { recursive: true });
113
+ const operator = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
114
+ const credentials = join(operator, ".credentials.json");
115
+ if (existsSync(credentials)) copyFileSync(credentials, join(dir, ".credentials.json"));
116
+ return { CLAUDE_CONFIG_DIR: dir };
117
+ }
118
+ };
119
+ const ADAPTERS = { [HARNESS]: claudeCode };
120
+ function adapterFor(harness) {
121
+ return Object.hasOwn(ADAPTERS, harness) ? ADAPTERS[harness] : void 0;
122
+ }
123
+ const ADAPTER_HARNESSES = Object.keys(ADAPTERS);
124
+ //#endregion
125
+ //#region skills/bench/scripts/bench-record.mts
126
+ /** `^[a-z0-9]+(?:[-.][a-z0-9]+)*$` — lowercase segments joined by single `-` or `.`. */
127
+ const SUITE_NAME_RULE = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/;
128
+ const BENCH_ROOT = join(".agents", "aced", "bench");
129
+ const RESULTS_ROOT = join(".agents", "aced", "results", "bench");
130
+ function suiteDir(suite) {
131
+ return join(BENCH_ROOT, suite);
132
+ }
133
+ function resultsDir(suite) {
134
+ return join(RESULTS_ROOT, suite);
135
+ }
136
+ /** An ISO timestamp safe in a file name: `:` and `.` become `-`. */
137
+ function stamp(createdAt) {
138
+ return createdAt.replace(/[:.]/g, "-");
139
+ }
140
+ /** The compared metrics. `pass` is 0/1 and has no pooled row. */
141
+ const METRICS = [
142
+ "pass",
143
+ "inputTokens",
144
+ "outputTokens",
145
+ "cacheReadTokens",
146
+ "turns",
147
+ "toolCalls",
148
+ "wallMs",
149
+ "costUsd"
150
+ ];
151
+ /** Metrics that can make a verdict `regressed`. Cost and cache reads are compared but never gate. */
152
+ const GATED = /* @__PURE__ */ new Set([
153
+ "pass",
154
+ "turns",
155
+ "toolCalls",
156
+ "inputTokens",
157
+ "outputTokens",
158
+ "wallMs"
159
+ ]);
160
+ function median(values) {
161
+ if (values.length === 0) return 0;
162
+ const sorted = [...values].sort((a, b) => a - b);
163
+ const mid = Math.floor(sorted.length / 2);
164
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
165
+ }
166
+ function round(n, places = 4) {
167
+ const f = 10 ** places;
168
+ return Math.round(n * f) / f;
169
+ }
170
+ //#endregion
171
+ //#region skills/bench/scripts/bench-compare.mts
172
+ const ALPHA = .05;
173
+ const EXACT_LIMIT = 2e5;
174
+ const SAMPLES = 2e4;
175
+ /** mulberry32: a small seeded generator, so a sampled p-value repeats exactly. */
176
+ function rng(seed) {
177
+ let s = seed >>> 0;
178
+ return () => {
179
+ s = s + 1831565813 >>> 0;
180
+ let t = s;
181
+ t = Math.imul(t ^ t >>> 15, t | 1);
182
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
183
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
184
+ };
185
+ }
186
+ /** FNV-1a, so each row draws from its own seed and one row's sampling never shifts another's. */
187
+ function seedOf(key) {
188
+ let h = 2166136261;
189
+ for (let i = 0; i < key.length; i++) {
190
+ h ^= key.charCodeAt(i);
191
+ h = Math.imul(h, 16777619);
192
+ }
193
+ return h >>> 0;
194
+ }
195
+ function choose(n, k) {
196
+ let c = 1;
197
+ for (let i = 1; i <= k; i++) c = c * (n - k + i) / i;
198
+ return Math.round(c);
199
+ }
200
+ /**
201
+ * The smallest p any data could reach with these group sizes, one `[before, after]` pair per task.
202
+ * One pair is a per-task row; several are a pooled row. Exact when every task enumerates and the
203
+ * product of their relabellings stays within the limit: only the real split and, with equal
204
+ * groups, its mirror can be the most extreme. Sampled otherwise: one hit out of N + 1.
205
+ */
206
+ function minAttainableP(sizes) {
207
+ let product = 1;
208
+ let exact = true;
209
+ for (const [na, nb] of sizes) {
210
+ const c = choose(na + nb, na);
211
+ if (c > EXACT_LIMIT) exact = false;
212
+ product *= c;
213
+ }
214
+ if (!exact || product > EXACT_LIMIT) return 1 / 20001;
215
+ return (sizes.every(([na, nb]) => na === nb) ? 2 : 1) / product;
216
+ }
217
+ /**
218
+ * Every split of the pooled runs into a "before" group of `a.length` and an "after" group of the
219
+ * rest, as `stat(beforeMean, afterMean)`. Enumerated when few enough, sampled otherwise.
220
+ */
221
+ function relabellings(a, b, stat, random) {
222
+ const all = [...a, ...b];
223
+ const na = a.length;
224
+ const nb = b.length;
225
+ const total = all.reduce((s, x) => s + x, 0);
226
+ const stats = [];
227
+ const split = (before) => stat(before / na, (total - before) / nb);
228
+ if (choose(all.length, na) <= EXACT_LIMIT) {
229
+ const walk = (from, left, acc) => {
230
+ if (left === 0) {
231
+ stats.push(split(acc));
232
+ return;
233
+ }
234
+ for (let i = from; i <= all.length - left; i++) walk(i + 1, left - 1, acc + all[i]);
235
+ };
236
+ walk(0, na, 0);
237
+ return {
238
+ stats,
239
+ exact: true
240
+ };
241
+ }
242
+ const idx = all.map((_, i) => i);
243
+ for (let n = 0; n < SAMPLES; n++) {
244
+ let acc = 0;
245
+ for (let i = 0; i < na; i++) {
246
+ const j = i + Math.floor(random() * (idx.length - i));
247
+ const tmp = idx[i];
248
+ idx[i] = idx[j];
249
+ idx[j] = tmp;
250
+ acc += all[idx[i]];
251
+ }
252
+ stats.push(split(acc));
253
+ }
254
+ return {
255
+ stats,
256
+ exact: false
257
+ };
258
+ }
259
+ /** Two-sided: the share of relabellings whose |statistic| reaches the observed one. */
260
+ function pValue(observed, { stats, exact }, minP) {
261
+ const tol = 1e-12 * Math.max(1, Math.abs(observed));
262
+ let hits = 0;
263
+ for (const x of stats) if (Math.abs(x) >= Math.abs(observed) - tol) hits++;
264
+ return {
265
+ p: exact ? hits / stats.length : (hits + 1) / (stats.length + 1),
266
+ exact,
267
+ minP,
268
+ tooFew: minP > ALPHA
269
+ };
270
+ }
271
+ const difference = (before, after) => after - before;
272
+ const logRatio = (before, after) => Math.log(after / before);
273
+ const mean = (xs) => xs.reduce((s, x) => s + x, 0) / xs.length;
274
+ const value = (r, m) => m === "pass" ? r.pass ? 1 : 0 : r[m];
275
+ function sideStats(xs) {
276
+ if (xs.length === 0) return null;
277
+ return {
278
+ n: xs.length,
279
+ mean: mean(xs),
280
+ median: median(xs),
281
+ min: Math.min(...xs),
282
+ max: Math.max(...xs)
283
+ };
284
+ }
285
+ /** Worse in the metric's direction: lower pass is worse; higher is worse for every other metric. */
286
+ function worse(metric, before, after) {
287
+ return metric === "pass" ? after < before : after > before;
288
+ }
289
+ function taskRow(task, metric, a, b, tags) {
290
+ const before = sideStats(a);
291
+ const after = sideStats(b);
292
+ const row = {
293
+ scope: "task",
294
+ task,
295
+ metric,
296
+ before,
297
+ after,
298
+ change: before && after && before.mean !== 0 ? after.mean / before.mean - 1 : null,
299
+ wrongWay: false,
300
+ significant: false,
301
+ tags
302
+ };
303
+ if (!before || !after) return row;
304
+ const test = pValue(difference(before.mean, after.mean), relabellings(a, b, difference, rng(seedOf(`${task}\0${metric}`))), minAttainableP([[a.length, b.length]]));
305
+ Object.assign(row, test);
306
+ row.wrongWay = worse(metric, before.mean, after.mean);
307
+ row.significant = test.p < ALPHA;
308
+ return row;
309
+ }
310
+ function pooledRow(metric, pairs) {
311
+ const usable = pairs.filter(([a, b]) => a.length > 0 && b.length > 0 && [...a, ...b].every((x) => x > 0));
312
+ if (usable.length === 0) return void 0;
313
+ const random = rng(seedOf(`pooled\0${metric}`));
314
+ const observed = mean(usable.map(([a, b]) => logRatio(mean(a), mean(b))));
315
+ const perTask = usable.map(([a, b]) => relabellings(a, b, logRatio, random));
316
+ const size = perTask.reduce((n, t) => n * t.stats.length, 1);
317
+ const exact = perTask.every((t) => t.exact) && size <= EXACT_LIMIT;
318
+ const stats = [];
319
+ if (exact) {
320
+ const walk = (i, acc) => {
321
+ if (i === perTask.length) {
322
+ stats.push(acc / perTask.length);
323
+ return;
324
+ }
325
+ for (const s of perTask[i].stats) walk(i + 1, acc + s);
326
+ };
327
+ walk(0, 0);
328
+ } else for (let n = 0; n < SAMPLES; n++) {
329
+ let acc = 0;
330
+ for (const t of perTask) acc += t.stats[Math.floor(random() * t.stats.length)];
331
+ stats.push(acc / perTask.length);
332
+ }
333
+ const test = pValue(observed, {
334
+ stats,
335
+ exact
336
+ }, minAttainableP(usable.map(([a, b]) => [a.length, b.length])));
337
+ const ratio = Math.exp(observed);
338
+ return {
339
+ scope: "pooled",
340
+ metric,
341
+ ratio,
342
+ change: ratio - 1,
343
+ tasks: usable.length,
344
+ ...test,
345
+ wrongWay: observed > 1e-12,
346
+ significant: test.p < ALPHA
347
+ };
348
+ }
349
+ const MATCHED_FIELDS = [
350
+ ["layer", (r) => String(r.layer)],
351
+ ["model", (r) => String(r.model)],
352
+ ["harness", (r) => String(r.harness)],
353
+ ["adapter", (r) => String(r.adapter)],
354
+ ["runner", (r) => String(r.runner)],
355
+ ["subject kind", (r) => String(r.subject?.kind)],
356
+ ["task-set hash", (r) => String(r.taskSetHash)]
357
+ ];
358
+ function incomparableReasons(a, b) {
359
+ const reasons = [];
360
+ for (const [field, get] of MATCHED_FIELDS) {
361
+ const x = get(a);
362
+ const y = get(b);
363
+ if (x !== y) reasons.push({
364
+ field,
365
+ before: x,
366
+ after: y,
367
+ message: `${field} differs: before "${x}", after "${y}"`
368
+ });
369
+ }
370
+ if (a.model === "unknown" && b.model === "unknown" || a.scoring_model === "unknown" && b.scoring_model === "unknown") reasons.push({
371
+ field: "model",
372
+ message: "the model is unknown on both sides"
373
+ });
374
+ return reasons;
375
+ }
376
+ function verdictOf(rows) {
377
+ const g = rows.filter((r) => GATED.has(r.metric));
378
+ if (g.some((r) => r.wrongWay && r.significant)) return "regressed";
379
+ if (g.some((r) => r.wrongWay)) return "inconclusive";
380
+ if (g.some((r) => r.significant)) return "improved";
381
+ return "unchanged";
382
+ }
383
+ /** Compares `after` against `before`. Never refuses: a mismatch is an `incomparable` verdict. */
384
+ function compareRecords(before, after) {
385
+ const incomparable = incomparableReasons(before, after);
386
+ const beforeTasks = [...new Set(before.runs.map((r) => r.task))];
387
+ const afterTasks = [...new Set(after.runs.map((r) => r.task))];
388
+ const unmatched = [...beforeTasks.filter((t) => !afterTasks.includes(t)), ...afterTasks.filter((t) => !beforeTasks.includes(t))];
389
+ const empty = {
390
+ verdict: "incomparable",
391
+ incomparable,
392
+ unmatched,
393
+ errorsExcluded: {
394
+ before: 0,
395
+ after: 0
396
+ },
397
+ rows: [],
398
+ footer: {
399
+ testCount: 0,
400
+ expectedByChance: 0,
401
+ alpha: ALPHA
402
+ }
403
+ };
404
+ if (incomparable.length > 0) return empty;
405
+ const tasks = beforeTasks.filter((t) => afterTasks.includes(t));
406
+ const errorsExcluded = {
407
+ before: 0,
408
+ after: 0
409
+ };
410
+ const kept = (record, task, side) => {
411
+ const rs = record.runs.filter((r) => r.task === task);
412
+ const ok = rs.filter((r) => !r.error);
413
+ errorsExcluded[side] += rs.length - ok.length;
414
+ return ok;
415
+ };
416
+ const rows = [];
417
+ const pairs = /* @__PURE__ */ new Map();
418
+ for (const task of tasks) {
419
+ const a = kept(before, task, "before");
420
+ const b = kept(after, task, "after");
421
+ const tags = after.tags?.[task] ?? before.tags?.[task] ?? {};
422
+ for (const metric of METRICS) {
423
+ const va = a.map((r) => value(r, metric));
424
+ const vb = b.map((r) => value(r, metric));
425
+ rows.push(taskRow(task, metric, va, vb, { ...tags }));
426
+ if (metric !== "pass") pairs.set(metric, [...pairs.get(metric) ?? [], [va, vb]]);
427
+ }
428
+ }
429
+ for (const metric of METRICS) {
430
+ if (metric === "pass") continue;
431
+ const row = pooledRow(metric, pairs.get(metric) ?? []);
432
+ if (row) rows.push(row);
433
+ }
434
+ const testCount = rows.filter((r) => r.p !== void 0).length;
435
+ return {
436
+ verdict: verdictOf(rows),
437
+ incomparable,
438
+ unmatched,
439
+ errorsExcluded,
440
+ rows,
441
+ footer: {
442
+ testCount,
443
+ expectedByChance: round(testCount * ALPHA),
444
+ alpha: ALPHA
445
+ }
446
+ };
447
+ }
448
+ //#endregion
449
+ //#region skills/bench/scripts/bench.mts
450
+ var BenchError = class extends Error {};
451
+ const DEFAULTS = {
452
+ model: "sonnet",
453
+ runs: 3,
454
+ maxBudgetUsd: .5,
455
+ permissionMode: "bypassPermissions",
456
+ timeoutMinutes: 20
457
+ };
458
+ const TEMPLATE = {
459
+ model: DEFAULTS.model,
460
+ runs: DEFAULTS.runs,
461
+ maxBudgetUsd: DEFAULTS.maxBudgetUsd,
462
+ permissionMode: DEFAULTS.permissionMode,
463
+ timeoutMinutes: DEFAULTS.timeoutMinutes,
464
+ tasks: [{
465
+ id: "example-task",
466
+ prompt: "Replace this with one small, self-contained task, then write a one-line summary of what you did to SUMMARY.md.",
467
+ check: "test -s SUMMARY.md",
468
+ tags: { area: "example" }
469
+ }]
470
+ };
471
+ function positive(raw, key, integer = false) {
472
+ const v = raw[key];
473
+ if (v === void 0) return DEFAULTS[key];
474
+ if (typeof v !== "number" || !(v > 0) || integer && !Number.isInteger(v)) throw new BenchError(`tasks.json: "${key}" must be a positive ${integer ? "whole " : ""}number`);
475
+ return v;
476
+ }
477
+ function parseTaskSet(text) {
478
+ let raw;
479
+ try {
480
+ raw = JSON.parse(text);
481
+ } catch (e) {
482
+ throw new BenchError(`tasks.json is not valid JSON: ${e.message}`);
483
+ }
484
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new BenchError("tasks.json must hold a JSON object");
485
+ if (!Array.isArray(raw.tasks)) throw new BenchError("tasks.json: \"tasks\" must be a list of tasks");
486
+ if (raw.tasks.length === 0) throw new BenchError("tasks.json: the tasks list is empty; list at least one task");
487
+ const ids = /* @__PURE__ */ new Set();
488
+ const tasks = raw.tasks.map((t, i) => {
489
+ if (t === null || typeof t !== "object") throw new BenchError(`tasks.json: tasks[${i}] must be an object`);
490
+ const label = typeof t.id === "string" && t.id ? `tasks[${i}] ("${t.id}")` : `tasks[${i}]`;
491
+ for (const key of [
492
+ "id",
493
+ "prompt",
494
+ "check"
495
+ ]) if (typeof t[key] !== "string" || t[key] === "") throw new BenchError(`tasks.json: ${label} has no "${key}"`);
496
+ if (t.setup !== void 0 && typeof t.setup !== "string") throw new BenchError(`tasks.json: ${label} "setup" must be a string`);
497
+ if (t.tags !== void 0 && (t.tags === null || typeof t.tags !== "object" || Array.isArray(t.tags) || Object.values(t.tags).some((v) => typeof v !== "string"))) throw new BenchError(`tasks.json: ${label} "tags" must map keys to string values`);
498
+ const id = t.id;
499
+ if (ids.has(id)) throw new BenchError(`tasks.json: the task id "${id}" appears twice`);
500
+ ids.add(id);
501
+ return {
502
+ id,
503
+ prompt: t.prompt,
504
+ check: t.check,
505
+ ...t.setup ? { setup: t.setup } : {},
506
+ ...t.tags ? { tags: t.tags } : {}
507
+ };
508
+ });
509
+ if (raw.setup !== void 0 && typeof raw.setup !== "string") throw new BenchError("tasks.json: \"setup\" must be a string");
510
+ for (const key of ["model", "permissionMode"]) if (raw[key] !== void 0 && (typeof raw[key] !== "string" || raw[key] === "")) throw new BenchError(`tasks.json: "${key}" must be a non-empty string`);
511
+ return {
512
+ model: raw.model ?? DEFAULTS.model,
513
+ runs: positive(raw, "runs", true),
514
+ maxBudgetUsd: positive(raw, "maxBudgetUsd"),
515
+ permissionMode: raw.permissionMode ?? DEFAULTS.permissionMode,
516
+ timeoutMinutes: positive(raw, "timeoutMinutes"),
517
+ ...raw.setup ? { setup: raw.setup } : {},
518
+ tasks
519
+ };
520
+ }
521
+ function git(cwd, args) {
522
+ return spawnSync("git", args, {
523
+ cwd,
524
+ encoding: "utf8",
525
+ maxBuffer: 268435456
526
+ });
527
+ }
528
+ function gitOk(cwd, args, what) {
529
+ const r = git(cwd, args);
530
+ if (r.status !== 0) throw new BenchError(`${what}: ${(r.stderr || r.error?.message || "").trim()}`);
531
+ return r.stdout;
532
+ }
533
+ function repoRoot(cwd) {
534
+ const r = git(cwd, ["rev-parse", "--show-toplevel"]);
535
+ if (r.status !== 0) throw new BenchError("not inside a git repository");
536
+ return r.stdout.trim();
537
+ }
538
+ function resolveCommit(root, ref) {
539
+ const r = git(root, [
540
+ "rev-parse",
541
+ "--verify",
542
+ "--quiet",
543
+ `${ref}^{commit}`
544
+ ]);
545
+ return r.status === 0 ? r.stdout.trim() : void 0;
546
+ }
547
+ function showAt(root, commit, path) {
548
+ const r = spawnSync("git", ["show", `${commit}:${path}`], {
549
+ cwd: root,
550
+ maxBuffer: 268435456
551
+ });
552
+ return r.status === 0 ? r.stdout : void 0;
553
+ }
554
+ /** The task set's committed files at `commit`: tasks.json and everything under checks/, path-sorted. */
555
+ function taskSetFiles(root, suite, commit) {
556
+ const dir = suiteDir(suite);
557
+ const listed = git(root, [
558
+ "ls-tree",
559
+ "-r",
560
+ "--name-only",
561
+ commit,
562
+ "--",
563
+ `${dir}/tasks.json`,
564
+ `${dir}/checks`
565
+ ]);
566
+ if (listed.status !== 0) return [];
567
+ return listed.stdout.split("\n").filter(Boolean).sort().map((path) => ({
568
+ path,
569
+ content: showAt(root, commit, path) ?? Buffer.alloc(0)
570
+ }));
571
+ }
572
+ function taskSetHash(files) {
573
+ const h = createHash("sha256");
574
+ for (const f of files) h.update(f.path).update("\0").update(f.content).update("\0");
575
+ return h.digest("hex");
576
+ }
577
+ const sha256 = (content) => createHash("sha256").update(content).digest("hex");
578
+ const ARM_LABEL = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
579
+ function parseArm(text) {
580
+ const eq = text.indexOf("=");
581
+ const label = text.slice(0, eq);
582
+ const subject = text.slice(eq + 1);
583
+ if (eq <= 0 || !ARM_LABEL.test(label)) throw new UsageError(`--arm "${text}" needs <label>=<subject>`);
584
+ if (subject.startsWith("git:") && subject.length > 4) return {
585
+ label,
586
+ kind: "git-ref",
587
+ ref: subject.slice(4)
588
+ };
589
+ if (subject.startsWith("package:")) {
590
+ const spec = subject.slice(8);
591
+ const at = spec.lastIndexOf("@");
592
+ if (at > 0 && at < spec.length - 1) return {
593
+ label,
594
+ kind: "package",
595
+ name: spec.slice(0, at),
596
+ version: spec.slice(at + 1)
597
+ };
598
+ }
599
+ const file = /^file:(.+?)=(ref:.+|path:.+|absent)$/.exec(subject);
600
+ if (file) return {
601
+ label,
602
+ kind: "file",
603
+ path: file[1],
604
+ from: file[2]
605
+ };
606
+ throw new UsageError(`--arm "${text}": the subject must be git:<ref>, package:<name>@<version>, or file:<path>=ref:<ref>|path:<source>|absent`);
607
+ }
608
+ function insideRepo(path) {
609
+ const n = normalize(path);
610
+ return !isAbsolute(n) && n !== ".." && !n.startsWith(`../`) && n !== ".";
611
+ }
612
+ function resolveArm(root, spec) {
613
+ if (spec.kind === "git-ref") {
614
+ const commit = resolveCommit(root, spec.ref);
615
+ if (!commit) throw new BenchError(`arm "${spec.label}": the git ref "${spec.ref}" names no commit in this repository`);
616
+ return {
617
+ label: spec.label,
618
+ subject: {
619
+ kind: "git-ref",
620
+ ref: spec.ref,
621
+ commit
622
+ }
623
+ };
624
+ }
625
+ if (spec.kind === "package") {
626
+ const r = spawnSync("npm", [
627
+ "view",
628
+ `${spec.name}@${spec.version}`,
629
+ "version",
630
+ "--json"
631
+ ], {
632
+ cwd: root,
633
+ encoding: "utf8"
634
+ });
635
+ if (!(r.status === 0 && r.stdout.trim() !== "" && r.stdout.trim() !== "[]")) throw new BenchError(`arm "${spec.label}": the registry has no package "${spec.name}" at version "${spec.version}"`);
636
+ return {
637
+ label: spec.label,
638
+ subject: {
639
+ kind: "package",
640
+ name: spec.name,
641
+ version: spec.version
642
+ }
643
+ };
644
+ }
645
+ if (!insideRepo(spec.path)) throw new BenchError(`arm "${spec.label}": the file path "${spec.path}" is outside the repository`);
646
+ const subject = {
647
+ kind: "file",
648
+ path: spec.path,
649
+ from: spec.from
650
+ };
651
+ if (spec.from.startsWith("ref:")) {
652
+ const ref = spec.from.slice(4);
653
+ const commit = resolveCommit(root, ref);
654
+ if (!commit) throw new BenchError(`arm "${spec.label}": the git ref "${ref}" names no commit in this repository`);
655
+ if (git(root, [
656
+ "cat-file",
657
+ "-e",
658
+ `${commit}:${spec.path}`
659
+ ]).status !== 0) throw new BenchError(`arm "${spec.label}": ${spec.path} does not exist at "${ref}"`);
660
+ subject.commit = commit;
661
+ } else if (spec.from.startsWith("path:")) {
662
+ const source = spec.from.slice(5);
663
+ if (!insideRepo(source) || !existsSync(join(root, source))) throw new BenchError(`arm "${spec.label}": the source file ${source} does not exist`);
664
+ }
665
+ return {
666
+ label: spec.label,
667
+ subject
668
+ };
669
+ }
670
+ function onPath(executable) {
671
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) {
672
+ if (!dir) continue;
673
+ try {
674
+ accessSync(join(dir, executable), constants.X_OK);
675
+ return true;
676
+ } catch {}
677
+ }
678
+ return false;
679
+ }
680
+ function checkSuiteName(suite) {
681
+ if (suite === void 0) throw new UsageError("--suite is required");
682
+ if (!SUITE_NAME_RULE.test(suite)) throw new BenchError(`suite name "${suite}" is outside the suite naming rule ${SUITE_NAME_RULE.source} (lowercase letters and digits, segments joined by a single "-" or ".")`);
683
+ return suite;
684
+ }
685
+ function loadCommittedTaskSet(root, suite, commit) {
686
+ const path = `${suiteDir(suite)}/tasks.json`;
687
+ const text = showAt(root, commit, path);
688
+ if (text === void 0) {
689
+ if (existsSync(join(root, path))) throw new BenchError(`${path} is not committed; the committed task set is what runs, so commit it first`);
690
+ throw new BenchError(`suite "${suite}" has no task set: ${path} does not exist (create one with \`aced-bench init --suite ${suite}\`)`);
691
+ }
692
+ return parseTaskSet(text.toString("utf8"));
693
+ }
694
+ /** Per task: the cost of each stored, error-free run on the same model, harness, and runner. */
695
+ function storedCosts(root, suite, model, harness, runner) {
696
+ const dir = join(root, resultsDir(suite));
697
+ const costs = /* @__PURE__ */ new Map();
698
+ if (!existsSync(dir)) return costs;
699
+ for (const file of readdirSync(dir)) {
700
+ if (!file.endsWith(".json") || file.startsWith("compare-")) continue;
701
+ let record;
702
+ try {
703
+ record = JSON.parse(readFileSync(join(dir, file), "utf8"));
704
+ } catch {
705
+ continue;
706
+ }
707
+ if (record?.schemaVersion !== 3 || record.layer !== "measured" || record.model !== model || record.harness !== harness || record.runner !== runner || !Array.isArray(record.runs)) continue;
708
+ for (const r of record.runs) {
709
+ if (r.error || typeof r.costUsd !== "number") continue;
710
+ costs.set(r.task, [...costs.get(r.task) ?? [], r.costUsd]);
711
+ }
712
+ }
713
+ return costs;
714
+ }
715
+ function porcelain(root, pathspecs) {
716
+ return gitOk(root, [
717
+ "status",
718
+ "--porcelain",
719
+ "--untracked-files=all",
720
+ "--",
721
+ ...pathspecs
722
+ ], "git status failed").trim();
723
+ }
724
+ function plan(root, args) {
725
+ const suite = checkSuiteName(args.suite);
726
+ const specs = (args.arm ?? []).map(parseArm);
727
+ if (specs.length === 0) throw new UsageError("plan needs at least one --arm <label>=<subject>");
728
+ if (new Set(specs.map((s) => s.label)).size !== specs.length) throw new UsageError("each --arm needs its own label");
729
+ if (!args.out) throw new UsageError("plan needs --out <file>");
730
+ let runsOverride;
731
+ if (args.runs !== void 0) {
732
+ runsOverride = Number(args.runs);
733
+ if (!Number.isInteger(runsOverride) || runsOverride < 1) throw new UsageError("--runs must be a positive whole number");
734
+ }
735
+ const harness = args.harness ?? "claude-code";
736
+ const taskSetCommit = resolveCommit(root, "HEAD");
737
+ if (!taskSetCommit) throw new BenchError("the repository has no commit; the task set must be committed");
738
+ const taskSet = loadCommittedTaskSet(root, suite, taskSetCommit);
739
+ let tasks = taskSet.tasks;
740
+ if (args.task !== void 0) {
741
+ tasks = tasks.filter((t) => t.id === args.task);
742
+ if (tasks.length === 0) throw new BenchError(`no task "${args.task}" in suite "${suite}" (tasks: ${taskSet.tasks.map((t) => t.id).join(", ")})`);
743
+ }
744
+ const adapter = adapterFor(harness);
745
+ if (!adapter) throw new BenchError(`the harness "${harness}" has no adapter (harnesses with one: ${ADAPTER_HARNESSES.join(", ")})`);
746
+ if (!onPath(adapter.executable)) throw new BenchError(`the ${harness} command "${adapter.executable}" is not on the PATH, so no run could start; install it or fix PATH before planning`);
747
+ if (args.baseline && specs.length > 1) throw new BenchError(`a suite records one baseline: --baseline takes exactly one --arm, not ${specs.length}`);
748
+ const arms = specs.map((s) => resolveArm(root, s));
749
+ const warnings = [];
750
+ const dir = suiteDir(suite);
751
+ if (porcelain(root, [`${dir}/tasks.json`, `${dir}/checks`]) !== "") warnings.push({
752
+ code: "suite-uncommitted",
753
+ message: `the suite has uncommitted changes; the committed suite (${taskSetCommit.slice(0, 12)}) is what runs`
754
+ });
755
+ if (arms.some((a) => a.subject.kind === "git-ref") && porcelain(root, [
756
+ ".",
757
+ `:(exclude)${dir}/tasks.json`,
758
+ `:(exclude)${dir}/checks`,
759
+ ":(exclude).agents/aced/results"
760
+ ]) !== "") warnings.push({
761
+ code: "uncommitted-not-benched",
762
+ message: "the working tree has uncommitted changes; they are not benched, since a git-ref arm runs its committed tree"
763
+ });
764
+ const runsPerArm = runsOverride ?? taskSet.runs;
765
+ const ceilingUsd = round(arms.length * tasks.length * runsPerArm * taskSet.maxBudgetUsd);
766
+ const stored = storedCosts(root, suite, taskSet.model, harness, adapter.runner);
767
+ const estimateTasks = {};
768
+ for (const t of tasks) {
769
+ const costs = stored.get(t.id) ?? [];
770
+ const perRunUsd = costs.length > 0 ? median(costs) : taskSet.maxBudgetUsd;
771
+ estimateTasks[t.id] = {
772
+ usd: round(arms.length * runsPerArm * perRunUsd),
773
+ perRunUsd: round(perRunUsd),
774
+ source: costs.length > 0 ? "stored" : "cap",
775
+ storedRuns: costs.length
776
+ };
777
+ }
778
+ const totalUsd = round(Object.values(estimateTasks).reduce((s, e) => s + e.usd, 0));
779
+ const perTaskMinP = minAttainableP([[runsPerArm, runsPerArm]]);
780
+ if (perTaskMinP > .05) {
781
+ const pooledMinP = minAttainableP(tasks.map(() => [runsPerArm, runsPerArm]));
782
+ const pooled = tasks.length >= 2 && pooledMinP <= .05 ? `a pooled result across the ${tasks.length} tasks still can (smallest attainable p ${round(pooledMinP, 6)})` : `a pooled result across ${tasks.length === 1 ? "one task" : `the ${tasks.length} tasks`} cannot either`;
783
+ warnings.push({
784
+ code: "too-few-to-call",
785
+ message: `too few to call: at ${runsPerArm} runs per arm no single task's result can be significant (smallest attainable p ${round(perTaskMinP, 6)} > 0.05); ${pooled}`
786
+ });
787
+ }
788
+ return {
789
+ planVersion: 1,
790
+ suite,
791
+ harness,
792
+ adapter: adapter.name,
793
+ runner: adapter.runner,
794
+ model: taskSet.model,
795
+ permissionMode: taskSet.permissionMode,
796
+ permissionScope: `the agent runs with --permission-mode ${taskSet.permissionMode}; it applies only inside the throwaway checkout each run gets, never to this working tree`,
797
+ maxBudgetUsd: taskSet.maxBudgetUsd,
798
+ timeoutMinutes: taskSet.timeoutMinutes,
799
+ runsPerArm,
800
+ baseline: Boolean(args.baseline),
801
+ arms,
802
+ tasks: tasks.map((t) => t.id),
803
+ counts: {
804
+ arms: arms.length,
805
+ tasks: tasks.length,
806
+ runsPerArm,
807
+ totalRuns: arms.length * tasks.length * runsPerArm
808
+ },
809
+ ceilingUsd,
810
+ estimate: {
811
+ totalUsd,
812
+ tasks: estimateTasks
813
+ },
814
+ warnings,
815
+ taskSetCommit,
816
+ taskSetHash: taskSetHash(taskSetFiles(root, suite, taskSetCommit))
817
+ };
818
+ }
819
+ const SHELL_TIMEOUT_MS = 9e5;
820
+ function shell(command, cwd) {
821
+ return spawnSync("sh", ["-c", command], {
822
+ cwd,
823
+ stdio: "ignore",
824
+ timeout: SHELL_TIMEOUT_MS
825
+ }).status;
826
+ }
827
+ function zeroMetrics() {
828
+ return {
829
+ wallMs: 0,
830
+ inputTokens: 0,
831
+ outputTokens: 0,
832
+ cacheReadTokens: 0,
833
+ cacheCreationTokens: 0,
834
+ turns: 0,
835
+ toolCalls: 0,
836
+ costUsd: 0,
837
+ capped: false
838
+ };
839
+ }
840
+ /** Applies the arm's subject to the checkout. Returns what the launch needs for a package arm. */
841
+ function applySubject(ctx, arm, checkout, runTemp) {
842
+ const s = arm.subject;
843
+ if (s.kind === "file") {
844
+ const target = join(checkout, s.path);
845
+ if (s.from === "absent") rmSync(target, {
846
+ force: true,
847
+ recursive: true
848
+ });
849
+ else {
850
+ const content = s.from.startsWith("ref:") ? showAt(ctx.root, s.commit ?? s.from.slice(4), s.path) : readFileSync(join(ctx.root, s.from.slice(5)));
851
+ if (content === void 0) throw new BenchError(`could not read ${s.path} at ${s.from}`);
852
+ mkdirSync(dirname(target), { recursive: true });
853
+ writeFileSync(target, content);
854
+ }
855
+ return {};
856
+ }
857
+ if (s.kind === "package") {
858
+ const packDir = join(runTemp, "package-pack");
859
+ mkdirSync(packDir, { recursive: true });
860
+ const pack = spawnSync("npm", [
861
+ "pack",
862
+ `${s.name}@${s.version}`,
863
+ "--pack-destination",
864
+ packDir
865
+ ], {
866
+ cwd: runTemp,
867
+ encoding: "utf8"
868
+ });
869
+ const tarball = readdirSync(packDir).find((f) => f.endsWith(".tgz"));
870
+ if (pack.status !== 0 || !tarball) throw new BenchError(`npm pack ${s.name}@${s.version} failed: ${(pack.stderr || "").trim()}`);
871
+ const unpacked = join(runTemp, "package");
872
+ mkdirSync(unpacked, { recursive: true });
873
+ const untar = spawnSync("tar", [
874
+ "-xzf",
875
+ join(packDir, tarball),
876
+ "-C",
877
+ unpacked
878
+ ], { encoding: "utf8" });
879
+ if (untar.status !== 0) throw new BenchError(`could not unpack ${tarball}: ${untar.stderr.trim()}`);
880
+ return {
881
+ pluginDir: join(unpacked, "package"),
882
+ env: ctx.adapter.isolatedConfig(runTemp)
883
+ };
884
+ }
885
+ return {};
886
+ }
887
+ /**
888
+ * Overlays the task-set commit's suite, stages the subject's change with it, and commits both with
889
+ * a throwaway identity, so the agent starts on a clean tree.
890
+ */
891
+ function commitCheckout(ctx, checkout) {
892
+ const dir = suiteDir(ctx.plan.suite);
893
+ gitOk(checkout, [
894
+ "rm",
895
+ "-r",
896
+ "-q",
897
+ "--ignore-unmatch",
898
+ "--",
899
+ dir
900
+ ], "could not clear the suite in the checkout");
901
+ gitOk(checkout, [
902
+ "checkout",
903
+ ctx.plan.taskSetCommit,
904
+ "--",
905
+ dir
906
+ ], "could not overlay the task-set commit");
907
+ gitOk(checkout, ["add", "-A"], "could not stage the checkout");
908
+ if (git(checkout, [
909
+ "diff",
910
+ "--cached",
911
+ "--quiet"
912
+ ]).status === 0) return;
913
+ gitOk(checkout, [
914
+ "-c",
915
+ "user.name=aced-bench",
916
+ "-c",
917
+ "user.email=aced-bench@localhost",
918
+ "-c",
919
+ "commit.gpgsign=false",
920
+ "-c",
921
+ "core.hooksPath=/dev/null",
922
+ "commit",
923
+ "--quiet",
924
+ "--no-verify",
925
+ "-m",
926
+ `bench: task set from ${ctx.plan.taskSetCommit}`
927
+ ], "could not commit the overlaid task set");
928
+ }
929
+ function runOne(ctx, arm, task, run) {
930
+ const runTemp = realpathSync(mkdtempSync(join(tmpdir(), "aced-bench-")));
931
+ const checkout = join(runTemp, "repo");
932
+ const base = {
933
+ task: task.id,
934
+ run
935
+ };
936
+ try {
937
+ const at = arm.subject.kind === "git-ref" ? arm.subject.commit : "HEAD";
938
+ gitOk(ctx.root, [
939
+ "worktree",
940
+ "add",
941
+ "--detach",
942
+ "--quiet",
943
+ checkout,
944
+ at
945
+ ], "git worktree add failed");
946
+ const launch = applySubject(ctx, arm, checkout, runTemp);
947
+ commitCheckout(ctx, checkout);
948
+ for (const [name, command] of [["suite setup", ctx.taskSet.setup], [`${task.id} setup`, task.setup]]) if (command && shell(command, checkout) !== 0) return {
949
+ ...base,
950
+ ...zeroMetrics(),
951
+ pass: false,
952
+ error: `${name} failed: ${command}`,
953
+ transcript: null
954
+ };
955
+ const start = performance.now();
956
+ const { transcript, model, ...metrics } = ctx.adapter.launch({
957
+ prompt: task.prompt,
958
+ model: ctx.plan.model,
959
+ maxBudgetUsd: ctx.plan.maxBudgetUsd,
960
+ permissionMode: ctx.plan.permissionMode,
961
+ checkout,
962
+ timeoutMs: ctx.plan.timeoutMinutes * 60 * 1e3,
963
+ ...launch
964
+ });
965
+ const wallMs = Math.round(performance.now() - start);
966
+ const pass = shell(task.check, checkout) === 0;
967
+ let kept = null;
968
+ if (transcript) {
969
+ kept = `${ctx.transcriptDir}/${task.id}-${run}.jsonl.gz`;
970
+ mkdirSync(dirname(join(ctx.root, kept)), { recursive: true });
971
+ writeFileSync(join(ctx.root, kept), gzipSync(transcript));
972
+ }
973
+ return {
974
+ ...base,
975
+ ...metrics,
976
+ wallMs,
977
+ pass,
978
+ error: null,
979
+ transcript: kept,
980
+ ...model ? { model } : {}
981
+ };
982
+ } finally {
983
+ git(ctx.root, [
984
+ "worktree",
985
+ "remove",
986
+ "--force",
987
+ checkout
988
+ ]);
989
+ rmSync(runTemp, {
990
+ recursive: true,
991
+ force: true
992
+ });
993
+ git(ctx.root, ["worktree", "prune"]);
994
+ }
995
+ }
996
+ function summarize(runs) {
997
+ const passes = runs.filter((r) => r.pass).length;
998
+ const ok = runs.filter((r) => !r.error);
999
+ const m = (f) => median(ok.map(f));
1000
+ const totalCostUsd = round(runs.reduce((s, r) => s + r.costUsd, 0));
1001
+ return {
1002
+ runs: runs.length,
1003
+ passes,
1004
+ passRate: runs.length === 0 ? 0 : round(passes / runs.length),
1005
+ capped: runs.filter((r) => r.capped).length,
1006
+ errors: runs.length - ok.length,
1007
+ medianInputTokens: m((r) => r.inputTokens),
1008
+ medianOutputTokens: m((r) => r.outputTokens),
1009
+ medianCacheReadTokens: m((r) => r.cacheReadTokens),
1010
+ medianTurns: m((r) => r.turns),
1011
+ medianToolCalls: m((r) => r.toolCalls),
1012
+ medianWallMs: m((r) => r.wallMs),
1013
+ totalCostUsd,
1014
+ ...passes > 0 ? { costPerSuccessUsd: round(totalCostUsd / passes) } : {}
1015
+ };
1016
+ }
1017
+ /** A createdAt whose record file and transcript folder are both still free. */
1018
+ function freshCreatedAt(dir, suffix, last) {
1019
+ let t = Math.max(Date.now(), last + 1);
1020
+ for (;;) {
1021
+ const createdAt = new Date(t).toISOString();
1022
+ if (!existsSync(join(dir, `${stamp(createdAt)}${suffix}`)) && !existsSync(join(dir, stamp(createdAt)))) return createdAt;
1023
+ t++;
1024
+ }
1025
+ }
1026
+ function readPlan(path) {
1027
+ let plan;
1028
+ try {
1029
+ plan = JSON.parse(readFileSync(path, "utf8"));
1030
+ } catch (e) {
1031
+ throw new BenchError(`cannot read the plan ${path}: ${e.message}`);
1032
+ }
1033
+ if (plan?.planVersion !== 1 || !Array.isArray(plan.arms) || !Array.isArray(plan.tasks)) throw new BenchError(`${path} is not an aced-bench plan`);
1034
+ return plan;
1035
+ }
1036
+ function runPlan(root, args) {
1037
+ if (!args.plan) throw new UsageError("run needs --plan <file>");
1038
+ if (!args.consent) throw new BenchError("run spends real money and needs explicit consent: re-run with --consent; nothing was launched");
1039
+ const plan = readPlan(resolve(args.plan));
1040
+ checkSuiteName(plan.suite);
1041
+ const adapter = adapterFor(plan.harness);
1042
+ if (!adapter) throw new BenchError(`the harness "${plan.harness}" has no adapter`);
1043
+ const head = resolveCommit(root, "HEAD");
1044
+ if ((head ? taskSetHash(taskSetFiles(root, plan.suite, head)) : "") !== plan.taskSetHash) throw new BenchError(`the suite "${plan.suite}" changed since the plan was written (task-set hash differs); the consent was given to a different plan, so plan again`);
1045
+ const taskSet = loadCommittedTaskSet(root, plan.suite, plan.taskSetCommit);
1046
+ const tasks = plan.tasks.map((id) => {
1047
+ const t = taskSet.tasks.find((x) => x.id === id);
1048
+ if (!t) throw new BenchError(`the plan's task "${id}" is not in the task-set commit`);
1049
+ return t;
1050
+ });
1051
+ const files = taskSetFiles(root, plan.suite, plan.taskSetCommit);
1052
+ const outDir = resultsDir(plan.suite);
1053
+ mkdirSync(join(root, outDir), { recursive: true });
1054
+ const written = [];
1055
+ let baselinePath;
1056
+ let last = 0;
1057
+ for (const arm of plan.arms) {
1058
+ const createdAt = freshCreatedAt(join(root, outDir), `.${arm.label}.json`, last);
1059
+ last = Date.parse(createdAt);
1060
+ const ctx = {
1061
+ root,
1062
+ plan,
1063
+ taskSet,
1064
+ adapter,
1065
+ transcriptDir: `${outDir}/${stamp(createdAt)}`
1066
+ };
1067
+ const runs = [];
1068
+ for (const task of tasks) for (let run = 1; run <= plan.runsPerArm; run++) runs.push(runOne(ctx, arm, task, run));
1069
+ const evaluated = files.map((f) => ({
1070
+ path: f.path,
1071
+ sha256: sha256(f.content),
1072
+ kind: "file"
1073
+ }));
1074
+ if (arm.subject.kind === "file" && arm.subject.from.startsWith("path:")) {
1075
+ const source = arm.subject.from.slice(5);
1076
+ evaluated.push({
1077
+ path: source,
1078
+ sha256: sha256(readFileSync(join(root, source))),
1079
+ kind: "file"
1080
+ });
1081
+ }
1082
+ const reported = runs.find((r) => r.model)?.model;
1083
+ const clean = runs.map(({ model: _, ...r }) => r);
1084
+ const tags = {};
1085
+ for (const t of tasks) if (t.tags) tags[t.id] = t.tags;
1086
+ const record = {
1087
+ schemaVersion: 3,
1088
+ layer: "measured",
1089
+ suite: plan.suite,
1090
+ arm: arm.label,
1091
+ subject: arm.subject,
1092
+ harness: plan.harness,
1093
+ adapter: adapter.name,
1094
+ runner: adapter.runner,
1095
+ taskSetCommit: plan.taskSetCommit,
1096
+ taskSetHash: plan.taskSetHash,
1097
+ tasks: tasks.map((t) => t.id),
1098
+ tags,
1099
+ model: plan.model,
1100
+ scoring_model: reported ?? plan.model,
1101
+ createdAt,
1102
+ evaluated,
1103
+ runs: clean,
1104
+ summary: {
1105
+ overall: summarize(clean),
1106
+ tasks: tasks.map((t) => ({
1107
+ task: t.id,
1108
+ ...summarize(clean.filter((r) => r.task === t.id))
1109
+ }))
1110
+ }
1111
+ };
1112
+ const path = `${outDir}/${stamp(createdAt)}.${arm.label}.json`;
1113
+ writeFileSync(join(root, path), `${JSON.stringify(record, null, 2)}\n`);
1114
+ written.push(path);
1115
+ if (plan.baseline) {
1116
+ const baseline = {
1117
+ ...record,
1118
+ runs: clean.map(({ transcript: _, ...r }) => r)
1119
+ };
1120
+ baselinePath = `${suiteDir(plan.suite)}/baseline.json`;
1121
+ writeFileSync(join(root, baselinePath), `${JSON.stringify(baseline, null, " ")}\n`);
1122
+ }
1123
+ }
1124
+ return {
1125
+ records: written,
1126
+ ...baselinePath ? { baseline: baselinePath } : {}
1127
+ };
1128
+ }
1129
+ function loadRecord(path) {
1130
+ let record;
1131
+ try {
1132
+ record = JSON.parse(readFileSync(path, "utf8"));
1133
+ } catch (e) {
1134
+ throw new BenchError(`cannot read the record ${path}: ${e.message}`);
1135
+ }
1136
+ if (record?.schemaVersion !== 3) throw new BenchError(`${path} has schemaVersion ${record?.schemaVersion === void 0 ? "absent" : JSON.stringify(record.schemaVersion)}; this engine reads only schema version 3`);
1137
+ if (!Array.isArray(record.runs)) throw new BenchError(`${path} is not a run record: it has no runs`);
1138
+ return record;
1139
+ }
1140
+ function compare(root, args) {
1141
+ const suite = checkSuiteName(args.suite);
1142
+ if (!args.before || !args.after) throw new UsageError("compare needs --before and --after");
1143
+ const tags = {};
1144
+ for (const t of args.tag ?? []) {
1145
+ const eq = t.indexOf("=");
1146
+ if (eq <= 0) throw new UsageError(`--tag "${t}" needs key=value`);
1147
+ tags[t.slice(0, eq)] = t.slice(eq + 1);
1148
+ }
1149
+ let beforePath = resolve(args.before);
1150
+ if (args.before === "baseline") {
1151
+ beforePath = join(root, suiteDir(suite), "baseline.json");
1152
+ if (!existsSync(beforePath)) throw new BenchError(`suite "${suite}" has no baseline: ${suiteDir(suite)}/baseline.json does not exist`);
1153
+ }
1154
+ const before = loadRecord(beforePath);
1155
+ const after = loadRecord(resolve(args.after));
1156
+ const comparison = compareRecords(before, after);
1157
+ const outDir = join(root, resultsDir(suite));
1158
+ mkdirSync(outDir, { recursive: true });
1159
+ const createdAt = freshCreatedAt(outDir, ".json", 0);
1160
+ const side = (r, source) => ({
1161
+ source,
1162
+ arm: r.arm,
1163
+ createdAt: r.createdAt,
1164
+ subject: r.subject
1165
+ });
1166
+ const record = {
1167
+ schemaVersion: 3,
1168
+ kind: "comparison",
1169
+ suite,
1170
+ createdAt,
1171
+ before: side(before, args.before === "baseline" ? "baseline" : args.before),
1172
+ after: side(after, args.after),
1173
+ tags,
1174
+ ...comparison
1175
+ };
1176
+ const path = `${resultsDir(suite)}/compare-${stamp(createdAt)}.json`;
1177
+ writeFileSync(join(root, path), `${JSON.stringify(record, null, 2)}\n`);
1178
+ return {
1179
+ path,
1180
+ record
1181
+ };
1182
+ }
1183
+ function init(cwd, args) {
1184
+ const suite = checkSuiteName(args.suite);
1185
+ const r = git(cwd, ["rev-parse", "--show-toplevel"]);
1186
+ const root = r.status === 0 ? r.stdout.trim() : cwd;
1187
+ const path = `${suiteDir(suite)}/tasks.json`;
1188
+ if (existsSync(join(root, path))) throw new BenchError(`${path} already exists; init never overwrites a task set`);
1189
+ mkdirSync(join(root, suiteDir(suite)), { recursive: true });
1190
+ writeFileSync(join(root, path), `${JSON.stringify(TEMPLATE, null, " ")}\n`);
1191
+ return path;
1192
+ }
1193
+ var UsageError = class extends Error {};
1194
+ const USAGE = `usage:
1195
+ aced-bench init --suite <s>
1196
+ aced-bench plan --suite <s> --arm <label>=<subject> … [--harness claude-code] [--runs N] [--task <id>] [--baseline] --out <file>
1197
+ aced-bench run --plan <file> --consent
1198
+ aced-bench compare --suite <s> --before <record|baseline> --after <record> [--tag k=v …]`;
1199
+ function main(argv) {
1200
+ const cwd = process.cwd();
1201
+ const [verb, ...rest] = argv;
1202
+ if (verb === "help" || verb === "--help" || verb === "-h") {
1203
+ process.stdout.write(`${USAGE}\n`);
1204
+ return 0;
1205
+ }
1206
+ try {
1207
+ const { values } = parseArgs({
1208
+ args: rest,
1209
+ strict: true,
1210
+ options: {
1211
+ suite: { type: "string" },
1212
+ arm: {
1213
+ type: "string",
1214
+ multiple: true
1215
+ },
1216
+ harness: { type: "string" },
1217
+ runs: { type: "string" },
1218
+ task: { type: "string" },
1219
+ baseline: { type: "boolean" },
1220
+ out: { type: "string" },
1221
+ plan: { type: "string" },
1222
+ consent: { type: "boolean" },
1223
+ before: { type: "string" },
1224
+ after: { type: "string" },
1225
+ tag: {
1226
+ type: "string",
1227
+ multiple: true
1228
+ }
1229
+ }
1230
+ });
1231
+ const args = values;
1232
+ let output;
1233
+ if (verb === "init") output = { tasksFile: init(cwd, args) };
1234
+ else if (verb === "plan") {
1235
+ const p = plan(repoRoot(cwd), args);
1236
+ const out = resolve(cwd, args.out);
1237
+ mkdirSync(dirname(out), { recursive: true });
1238
+ writeFileSync(out, `${JSON.stringify(p, null, 2)}\n`);
1239
+ output = p;
1240
+ } else if (verb === "run") output = runPlan(repoRoot(cwd), args);
1241
+ else if (verb === "compare") output = compare(repoRoot(cwd), args).record;
1242
+ else throw new UsageError(verb ? `unknown verb "${verb}"` : "no verb given");
1243
+ process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
1244
+ return 0;
1245
+ } catch (e) {
1246
+ if (e instanceof UsageError || e.code?.startsWith("ERR_PARSE_ARGS")) {
1247
+ process.stderr.write(`aced-bench: ${e.message}\n${USAGE}\n`);
1248
+ return 2;
1249
+ }
1250
+ process.stderr.write(`aced-bench: ${e.message}\n`);
1251
+ return 1;
1252
+ }
1253
+ }
1254
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
1255
+ //#endregion
1256
+ export {};