cyber-aced 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.claude-plugin/plugin.json +18 -0
  2. package/.plugin/pins.json +1 -0
  3. package/.plugin/plugin.json +18 -0
  4. package/agents/aced-case-judge.md +165 -0
  5. package/agents/aced-impl-judge.md +55 -0
  6. package/agents/aced-scenario-writer.md +76 -0
  7. package/agents/aced-spec-validator.md +66 -0
  8. package/governances/README.md +52 -0
  9. package/governances/agent-tool-output.md +86 -0
  10. package/governances/cli-resolution.md +102 -0
  11. package/governances/skill-design.md +258 -0
  12. package/governances/skill-repo-structure.md +173 -0
  13. package/package.json +44 -0
  14. package/readme.md +124 -0
  15. package/skills/aced-architect-impl/README.md +9 -0
  16. package/skills/aced-architect-impl/SKILL.md +73 -0
  17. package/skills/aced-builder-impl/README.md +14 -0
  18. package/skills/aced-builder-impl/SKILL.md +46 -0
  19. package/skills/aced-builder-spec/README.md +15 -0
  20. package/skills/aced-builder-spec/SKILL.md +130 -0
  21. package/skills/aced-fit/README.md +21 -0
  22. package/skills/aced-fit/SKILL.md +105 -0
  23. package/skills/aced-impl-producer/SKILL.md +75 -0
  24. package/skills/add-scenario/README.md +25 -0
  25. package/skills/add-scenario/SKILL.md +75 -0
  26. package/skills/bench/README.md +64 -0
  27. package/skills/bench/SKILL.md +199 -0
  28. package/skills/bench/scripts/bench-claude-code.mts +187 -0
  29. package/skills/bench/scripts/bench-compare.mts +369 -0
  30. package/skills/bench/scripts/bench-record.mts +133 -0
  31. package/skills/bench/scripts/bench.mts +930 -0
  32. package/skills/check-freshness/README.md +57 -0
  33. package/skills/check-freshness/SKILL.md +76 -0
  34. package/skills/check-freshness/scripts/check-freshness.mts +399 -0
  35. package/skills/compare/README.md +25 -0
  36. package/skills/compare/SKILL.md +87 -0
  37. package/skills/contribute-skill/README.md +25 -0
  38. package/skills/contribute-skill/SKILL.md +241 -0
  39. package/skills/contribute-skill/scripts/source.mts +188 -0
  40. package/skills/define-agent/README.md +40 -0
  41. package/skills/define-agent/SKILL.md +279 -0
  42. package/skills/define-command/README.md +36 -0
  43. package/skills/define-command/SKILL.md +174 -0
  44. package/skills/define-governance/SKILL.md +151 -0
  45. package/skills/define-skill/README.md +37 -0
  46. package/skills/define-skill/SKILL.md +200 -0
  47. package/skills/extract-situation/README.md +32 -0
  48. package/skills/extract-situation/SKILL.md +54 -0
  49. package/skills/extract-situation/scripts/extract-situation.mts +287 -0
  50. package/skills/improve/README.md +25 -0
  51. package/skills/improve/SKILL.md +100 -0
  52. package/skills/improve-skill/README.md +38 -0
  53. package/skills/improve-skill/SKILL.md +169 -0
  54. package/skills/improve-skill/references/check-definitions.md +215 -0
  55. package/skills/improve-skill/scripts/validate.mts +1033 -0
  56. package/skills/init-aced/README.md +5 -0
  57. package/skills/init-aced/SKILL.md +76 -0
  58. package/skills/init-aced/scripts/ensure-results-ignored.mts +117 -0
  59. package/skills/list-skills/README.md +19 -0
  60. package/skills/list-skills/SKILL.md +55 -0
  61. package/skills/list-skills/scripts/list-skills.mts +210 -0
  62. package/skills/manage/README.md +35 -0
  63. package/skills/manage/SKILL.md +65 -0
  64. package/skills/manage-model-runners/README.md +25 -0
  65. package/skills/manage-model-runners/SKILL.md +113 -0
  66. package/skills/manage-skill-dirs/README.md +17 -0
  67. package/skills/manage-skill-dirs/SKILL.md +54 -0
  68. package/skills/manage-skill-dirs/scripts/manage-skill-dirs.mts +328 -0
  69. package/skills/repair-private-skills/README.md +17 -0
  70. package/skills/repair-private-skills/SKILL.md +59 -0
  71. package/skills/repair-private-skills/scripts/repair-private-skills.mts +279 -0
  72. package/skills/report/README.md +25 -0
  73. package/skills/report/SKILL.md +74 -0
  74. package/skills/run/README.md +25 -0
  75. package/skills/run/SKILL.md +220 -0
  76. package/skills/skillify/README.md +25 -0
  77. package/skills/skillify/SKILL.md +141 -0
@@ -0,0 +1,930 @@
1
+ #!/usr/bin/env node
2
+ // aced-bench — the measured layer's engine: plan, run, and compare real headless agent runs.
3
+ //
4
+ // aced-bench init --suite <s>
5
+ // aced-bench plan --suite <s> --arm <label>=<subject> … [--harness claude-code] [--runs N]
6
+ // [--task <id>] [--baseline] --out <plan.json>
7
+ // aced-bench run --plan <plan.json> --consent
8
+ // aced-bench compare --suite <s> --before <record.json|baseline> --after <record.json> [--tag k=v …]
9
+ //
10
+ // A subject is `git:<ref>`, `package:<name>@<version>`, or `file:<path>=ref:<ref>` /
11
+ // `file:<path>=path:<source>` / `file:<path>=absent`.
12
+ //
13
+ // `plan` spends nothing: it prices the run (ceiling and estimate), checks it can start, and states
14
+ // every warning. `run` spends only with `--consent`, and only on the plan it was given — a suite
15
+ // that changed since the plan is refused, because the consent was given to a different plan.
16
+ // `compare` reads schema-version-3 run records only and writes a comparison record whose verdict
17
+ // follows the spec's closed form (bench-compare.mts).
18
+ //
19
+ // Every verb prints JSON on stdout and exits 0 on success; a refusal prints `aced-bench: <reason>`
20
+ // on stderr and exits 1; a malformed command line exits 2.
21
+ //
22
+ // Spec: .agents/specs/aced/eval-run/bench/engine/README.md. A port of repobuddy's
23
+ // agent-readiness bench, generalized to three subject kinds and a harness adapter seam.
24
+
25
+ import { type SpawnSyncReturns, spawnSync } from 'node:child_process'
26
+ import { createHash } from 'node:crypto'
27
+ import {
28
+ accessSync,
29
+ constants,
30
+ existsSync,
31
+ mkdirSync,
32
+ mkdtempSync,
33
+ readdirSync,
34
+ readFileSync,
35
+ realpathSync,
36
+ rmSync,
37
+ writeFileSync,
38
+ } from 'node:fs'
39
+ import { tmpdir } from 'node:os'
40
+ import { delimiter, dirname, isAbsolute, join, normalize, resolve } from 'node:path'
41
+ import { pathToFileURL } from 'node:url'
42
+ import { parseArgs } from 'node:util'
43
+ import { gzipSync } from 'node:zlib'
44
+ import { ADAPTER_HARNESSES, type Adapter, adapterFor } from './bench-claude-code.mts'
45
+ import { compareRecords, minAttainableP } from './bench-compare.mts'
46
+ import {
47
+ type EvaluatedEntry,
48
+ median,
49
+ type RunRecord,
50
+ type RunResult,
51
+ resultsDir,
52
+ round,
53
+ SCHEMA_VERSION,
54
+ SUITE_NAME_RULE,
55
+ type Subject,
56
+ type Summary,
57
+ stamp,
58
+ suiteDir,
59
+ } from './bench-record.mts'
60
+
61
+ class BenchError extends Error {}
62
+
63
+ // ─── task set ─────────────────────────────────────────────────────────────────
64
+
65
+ interface Task {
66
+ id: string
67
+ prompt: string
68
+ check: string
69
+ setup?: string
70
+ tags?: Record<string, string>
71
+ }
72
+
73
+ interface TaskSet {
74
+ model: string
75
+ runs: number
76
+ maxBudgetUsd: number
77
+ permissionMode: string
78
+ timeoutMinutes: number
79
+ setup?: string
80
+ tasks: Task[]
81
+ }
82
+
83
+ const DEFAULTS = {
84
+ model: 'sonnet',
85
+ runs: 3,
86
+ maxBudgetUsd: 0.5,
87
+ permissionMode: 'bypassPermissions',
88
+ timeoutMinutes: 20,
89
+ }
90
+
91
+ const TEMPLATE = {
92
+ model: DEFAULTS.model,
93
+ runs: DEFAULTS.runs,
94
+ maxBudgetUsd: DEFAULTS.maxBudgetUsd,
95
+ permissionMode: DEFAULTS.permissionMode,
96
+ timeoutMinutes: DEFAULTS.timeoutMinutes,
97
+ tasks: [
98
+ {
99
+ id: 'example-task',
100
+ prompt:
101
+ 'Replace this with one small, self-contained task, then write a one-line summary of what you did to SUMMARY.md.',
102
+ check: 'test -s SUMMARY.md',
103
+ tags: { area: 'example' },
104
+ },
105
+ ],
106
+ }
107
+
108
+ function positive(raw: Record<string, unknown>, key: keyof typeof DEFAULTS, integer = false): number {
109
+ const v = raw[key]
110
+ if (v === undefined) return DEFAULTS[key] as number
111
+ if (typeof v !== 'number' || !(v > 0) || (integer && !Number.isInteger(v)))
112
+ throw new BenchError(`tasks.json: "${key}" must be a positive ${integer ? 'whole ' : ''}number`)
113
+ return v
114
+ }
115
+
116
+ function parseTaskSet(text: string): TaskSet {
117
+ let raw: Record<string, unknown>
118
+ try {
119
+ raw = JSON.parse(text)
120
+ } catch (e) {
121
+ throw new BenchError(`tasks.json is not valid JSON: ${(e as Error).message}`)
122
+ }
123
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
124
+ throw new BenchError('tasks.json must hold a JSON object')
125
+ if (!Array.isArray(raw.tasks)) throw new BenchError('tasks.json: "tasks" must be a list of tasks')
126
+ if (raw.tasks.length === 0) throw new BenchError('tasks.json: the tasks list is empty; list at least one task')
127
+ const ids = new Set<string>()
128
+ const tasks = raw.tasks.map((t: Record<string, unknown>, i: number): Task => {
129
+ if (t === null || typeof t !== 'object') throw new BenchError(`tasks.json: tasks[${i}] must be an object`)
130
+ const label = typeof t.id === 'string' && t.id ? `tasks[${i}] ("${t.id}")` : `tasks[${i}]`
131
+ for (const key of ['id', 'prompt', 'check'] as const) {
132
+ if (typeof t[key] !== 'string' || t[key] === '') throw new BenchError(`tasks.json: ${label} has no "${key}"`)
133
+ }
134
+ if (t.setup !== undefined && typeof t.setup !== 'string')
135
+ throw new BenchError(`tasks.json: ${label} "setup" must be a string`)
136
+ if (
137
+ t.tags !== undefined &&
138
+ (t.tags === null ||
139
+ typeof t.tags !== 'object' ||
140
+ Array.isArray(t.tags) ||
141
+ Object.values(t.tags).some((v) => typeof v !== 'string'))
142
+ )
143
+ throw new BenchError(`tasks.json: ${label} "tags" must map keys to string values`)
144
+ const id = t.id as string
145
+ if (ids.has(id)) throw new BenchError(`tasks.json: the task id "${id}" appears twice`)
146
+ ids.add(id)
147
+ return {
148
+ id,
149
+ prompt: t.prompt as string,
150
+ check: t.check as string,
151
+ ...(t.setup ? { setup: t.setup as string } : {}),
152
+ ...(t.tags ? { tags: t.tags as Record<string, string> } : {}),
153
+ }
154
+ })
155
+ if (raw.setup !== undefined && typeof raw.setup !== 'string')
156
+ throw new BenchError('tasks.json: "setup" must be a string')
157
+ for (const key of ['model', 'permissionMode'] as const)
158
+ if (raw[key] !== undefined && (typeof raw[key] !== 'string' || raw[key] === ''))
159
+ throw new BenchError(`tasks.json: "${key}" must be a non-empty string`)
160
+ return {
161
+ model: (raw.model as string | undefined) ?? DEFAULTS.model,
162
+ runs: positive(raw, 'runs', true),
163
+ maxBudgetUsd: positive(raw, 'maxBudgetUsd'),
164
+ permissionMode: (raw.permissionMode as string | undefined) ?? DEFAULTS.permissionMode,
165
+ timeoutMinutes: positive(raw, 'timeoutMinutes'),
166
+ ...(raw.setup ? { setup: raw.setup as string } : {}),
167
+ tasks,
168
+ }
169
+ }
170
+
171
+ // ─── git ──────────────────────────────────────────────────────────────────────
172
+
173
+ function git(cwd: string, args: string[]): SpawnSyncReturns<string> {
174
+ return spawnSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 })
175
+ }
176
+
177
+ function gitOk(cwd: string, args: string[], what: string): string {
178
+ const r = git(cwd, args)
179
+ if (r.status !== 0) throw new BenchError(`${what}: ${(r.stderr || r.error?.message || '').trim()}`)
180
+ return r.stdout
181
+ }
182
+
183
+ function repoRoot(cwd: string): string {
184
+ const r = git(cwd, ['rev-parse', '--show-toplevel'])
185
+ if (r.status !== 0) throw new BenchError('not inside a git repository')
186
+ return r.stdout.trim()
187
+ }
188
+
189
+ function resolveCommit(root: string, ref: string): string | undefined {
190
+ const r = git(root, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`])
191
+ return r.status === 0 ? r.stdout.trim() : undefined
192
+ }
193
+
194
+ function showAt(root: string, commit: string, path: string): Buffer | undefined {
195
+ const r = spawnSync('git', ['show', `${commit}:${path}`], { cwd: root, maxBuffer: 256 * 1024 * 1024 })
196
+ return r.status === 0 ? r.stdout : undefined
197
+ }
198
+
199
+ /** The task set's committed files at `commit`: tasks.json and everything under checks/, path-sorted. */
200
+ function taskSetFiles(root: string, suite: string, commit: string): Array<{ path: string; content: Buffer }> {
201
+ const dir = suiteDir(suite)
202
+ const listed = git(root, ['ls-tree', '-r', '--name-only', commit, '--', `${dir}/tasks.json`, `${dir}/checks`])
203
+ if (listed.status !== 0) return []
204
+ return listed.stdout
205
+ .split('\n')
206
+ .filter(Boolean)
207
+ .sort()
208
+ .map((path) => ({ path, content: showAt(root, commit, path) ?? Buffer.alloc(0) }))
209
+ }
210
+
211
+ function taskSetHash(files: Array<{ path: string; content: Buffer }>): string {
212
+ const h = createHash('sha256')
213
+ for (const f of files) h.update(f.path).update('\0').update(f.content).update('\0')
214
+ return h.digest('hex')
215
+ }
216
+
217
+ const sha256 = (content: Buffer) => createHash('sha256').update(content).digest('hex')
218
+
219
+ // ─── subjects ─────────────────────────────────────────────────────────────────
220
+
221
+ interface Arm {
222
+ label: string
223
+ subject: Subject & { commit?: string }
224
+ }
225
+
226
+ const ARM_LABEL = /^[A-Za-z0-9][A-Za-z0-9_-]*$/
227
+
228
+ type ArmSpec =
229
+ | { label: string; kind: 'git-ref'; ref: string }
230
+ | { label: string; kind: 'package'; name: string; version: string }
231
+ | { label: string; kind: 'file'; path: string; from: string }
232
+
233
+ function parseArm(text: string): ArmSpec {
234
+ const eq = text.indexOf('=')
235
+ const label = text.slice(0, eq)
236
+ const subject = text.slice(eq + 1)
237
+ if (eq <= 0 || !ARM_LABEL.test(label)) throw new UsageError(`--arm "${text}" needs <label>=<subject>`)
238
+ if (subject.startsWith('git:') && subject.length > 4) return { label, kind: 'git-ref', ref: subject.slice(4) }
239
+ if (subject.startsWith('package:')) {
240
+ const spec = subject.slice(8)
241
+ const at = spec.lastIndexOf('@')
242
+ if (at > 0 && at < spec.length - 1)
243
+ return { label, kind: 'package', name: spec.slice(0, at), version: spec.slice(at + 1) }
244
+ }
245
+ const file = /^file:(.+?)=(ref:.+|path:.+|absent)$/.exec(subject)
246
+ if (file) return { label, kind: 'file', path: file[1] as string, from: file[2] as string }
247
+ throw new UsageError(
248
+ `--arm "${text}": the subject must be git:<ref>, package:<name>@<version>, or file:<path>=ref:<ref>|path:<source>|absent`,
249
+ )
250
+ }
251
+
252
+ function insideRepo(path: string): boolean {
253
+ const n = normalize(path)
254
+ return !isAbsolute(n) && n !== '..' && !n.startsWith(`..${'/'}`) && n !== '.'
255
+ }
256
+
257
+ function resolveArm(root: string, spec: ArmSpec): Arm {
258
+ if (spec.kind === 'git-ref') {
259
+ const commit = resolveCommit(root, spec.ref)
260
+ if (!commit)
261
+ throw new BenchError(`arm "${spec.label}": the git ref "${spec.ref}" names no commit in this repository`)
262
+ return { label: spec.label, subject: { kind: 'git-ref', ref: spec.ref, commit } }
263
+ }
264
+ if (spec.kind === 'package') {
265
+ const r = spawnSync('npm', ['view', `${spec.name}@${spec.version}`, 'version', '--json'], {
266
+ cwd: root,
267
+ encoding: 'utf8',
268
+ })
269
+ const found = r.status === 0 && r.stdout.trim() !== '' && r.stdout.trim() !== '[]'
270
+ if (!found)
271
+ throw new BenchError(
272
+ `arm "${spec.label}": the registry has no package "${spec.name}" at version "${spec.version}"`,
273
+ )
274
+ return { label: spec.label, subject: { kind: 'package', name: spec.name, version: spec.version } }
275
+ }
276
+ if (!insideRepo(spec.path))
277
+ throw new BenchError(`arm "${spec.label}": the file path "${spec.path}" is outside the repository`)
278
+ const subject: Arm['subject'] = { kind: 'file', path: spec.path, from: spec.from }
279
+ if (spec.from.startsWith('ref:')) {
280
+ const ref = spec.from.slice(4)
281
+ const commit = resolveCommit(root, ref)
282
+ if (!commit) throw new BenchError(`arm "${spec.label}": the git ref "${ref}" names no commit in this repository`)
283
+ if (git(root, ['cat-file', '-e', `${commit}:${spec.path}`]).status !== 0)
284
+ throw new BenchError(`arm "${spec.label}": ${spec.path} does not exist at "${ref}"`)
285
+ subject.commit = commit
286
+ } else if (spec.from.startsWith('path:')) {
287
+ const source = spec.from.slice(5)
288
+ if (!insideRepo(source) || !existsSync(join(root, source)))
289
+ throw new BenchError(`arm "${spec.label}": the source file ${source} does not exist`)
290
+ }
291
+ return { label: spec.label, subject }
292
+ }
293
+
294
+ function onPath(executable: string): boolean {
295
+ for (const dir of (process.env.PATH ?? '').split(delimiter)) {
296
+ if (!dir) continue
297
+ try {
298
+ accessSync(join(dir, executable), constants.X_OK)
299
+ return true
300
+ } catch {}
301
+ }
302
+ return false
303
+ }
304
+
305
+ // ─── plan ─────────────────────────────────────────────────────────────────────
306
+
307
+ interface Warning {
308
+ code: 'suite-uncommitted' | 'uncommitted-not-benched' | 'too-few-to-call'
309
+ message: string
310
+ }
311
+
312
+ interface Plan {
313
+ planVersion: 1
314
+ suite: string
315
+ harness: string
316
+ adapter: string
317
+ runner: string
318
+ model: string
319
+ permissionMode: string
320
+ permissionScope: string
321
+ maxBudgetUsd: number
322
+ timeoutMinutes: number
323
+ runsPerArm: number
324
+ baseline: boolean
325
+ arms: Arm[]
326
+ tasks: string[]
327
+ counts: { arms: number; tasks: number; runsPerArm: number; totalRuns: number }
328
+ ceilingUsd: number
329
+ estimate: {
330
+ totalUsd: number
331
+ tasks: Record<string, { usd: number; perRunUsd: number; source: 'stored' | 'cap'; storedRuns: number }>
332
+ }
333
+ warnings: Warning[]
334
+ taskSetCommit: string
335
+ taskSetHash: string
336
+ }
337
+
338
+ function checkSuiteName(suite: string | undefined): string {
339
+ if (suite === undefined) throw new UsageError('--suite is required')
340
+ if (!SUITE_NAME_RULE.test(suite))
341
+ throw new BenchError(
342
+ `suite name "${suite}" is outside the suite naming rule ${SUITE_NAME_RULE.source} (lowercase letters and digits, segments joined by a single "-" or ".")`,
343
+ )
344
+ return suite
345
+ }
346
+
347
+ function loadCommittedTaskSet(root: string, suite: string, commit: string): TaskSet {
348
+ const path = `${suiteDir(suite)}/tasks.json`
349
+ const text = showAt(root, commit, path)
350
+ if (text === undefined) {
351
+ if (existsSync(join(root, path)))
352
+ throw new BenchError(`${path} is not committed; the committed task set is what runs, so commit it first`)
353
+ throw new BenchError(
354
+ `suite "${suite}" has no task set: ${path} does not exist (create one with \`aced-bench init --suite ${suite}\`)`,
355
+ )
356
+ }
357
+ return parseTaskSet(text.toString('utf8'))
358
+ }
359
+
360
+ /** Per task: the cost of each stored, error-free run on the same model, harness, and runner. */
361
+ function storedCosts(
362
+ root: string,
363
+ suite: string,
364
+ model: string,
365
+ harness: string,
366
+ runner: string,
367
+ ): Map<string, number[]> {
368
+ const dir = join(root, resultsDir(suite))
369
+ const costs = new Map<string, number[]>()
370
+ if (!existsSync(dir)) return costs
371
+ for (const file of readdirSync(dir)) {
372
+ if (!file.endsWith('.json') || file.startsWith('compare-')) continue
373
+ let record: Partial<RunRecord>
374
+ try {
375
+ record = JSON.parse(readFileSync(join(dir, file), 'utf8'))
376
+ } catch {
377
+ continue
378
+ }
379
+ if (
380
+ record?.schemaVersion !== SCHEMA_VERSION ||
381
+ record.layer !== 'measured' ||
382
+ record.model !== model ||
383
+ record.harness !== harness ||
384
+ record.runner !== runner ||
385
+ !Array.isArray(record.runs)
386
+ )
387
+ continue
388
+ for (const r of record.runs) {
389
+ if (r.error || typeof r.costUsd !== 'number') continue
390
+ costs.set(r.task, [...(costs.get(r.task) ?? []), r.costUsd])
391
+ }
392
+ }
393
+ return costs
394
+ }
395
+
396
+ function porcelain(root: string, pathspecs: string[]): string {
397
+ return gitOk(root, ['status', '--porcelain', '--untracked-files=all', '--', ...pathspecs], 'git status failed').trim()
398
+ }
399
+
400
+ function plan(root: string, args: Args): Plan {
401
+ const suite = checkSuiteName(args.suite)
402
+ const specs = (args.arm ?? []).map(parseArm)
403
+ if (specs.length === 0) throw new UsageError('plan needs at least one --arm <label>=<subject>')
404
+ if (new Set(specs.map((s) => s.label)).size !== specs.length) throw new UsageError('each --arm needs its own label')
405
+ if (!args.out) throw new UsageError('plan needs --out <file>')
406
+ let runsOverride: number | undefined
407
+ if (args.runs !== undefined) {
408
+ runsOverride = Number(args.runs)
409
+ if (!Number.isInteger(runsOverride) || runsOverride < 1)
410
+ throw new UsageError('--runs must be a positive whole number')
411
+ }
412
+ const harness = args.harness ?? 'claude-code'
413
+
414
+ const taskSetCommit = resolveCommit(root, 'HEAD')
415
+ if (!taskSetCommit) throw new BenchError('the repository has no commit; the task set must be committed')
416
+ const taskSet = loadCommittedTaskSet(root, suite, taskSetCommit)
417
+
418
+ let tasks = taskSet.tasks
419
+ if (args.task !== undefined) {
420
+ tasks = tasks.filter((t) => t.id === args.task)
421
+ if (tasks.length === 0)
422
+ throw new BenchError(
423
+ `no task "${args.task}" in suite "${suite}" (tasks: ${taskSet.tasks.map((t) => t.id).join(', ')})`,
424
+ )
425
+ }
426
+
427
+ const adapter = adapterFor(harness)
428
+ if (!adapter)
429
+ throw new BenchError(
430
+ `the harness "${harness}" has no adapter (harnesses with one: ${ADAPTER_HARNESSES.join(', ')})`,
431
+ )
432
+ if (!onPath(adapter.executable))
433
+ throw new BenchError(
434
+ `the ${harness} command "${adapter.executable}" is not on the PATH, so no run could start; install it or fix PATH before planning`,
435
+ )
436
+ if (args.baseline && specs.length > 1)
437
+ throw new BenchError(`a suite records one baseline: --baseline takes exactly one --arm, not ${specs.length}`)
438
+
439
+ const arms = specs.map((s) => resolveArm(root, s))
440
+
441
+ const warnings: Warning[] = []
442
+ const dir = suiteDir(suite)
443
+ if (porcelain(root, [`${dir}/tasks.json`, `${dir}/checks`]) !== '')
444
+ warnings.push({
445
+ code: 'suite-uncommitted',
446
+ message: `the suite has uncommitted changes; the committed suite (${taskSetCommit.slice(0, 12)}) is what runs`,
447
+ })
448
+ if (
449
+ arms.some((a) => a.subject.kind === 'git-ref') &&
450
+ porcelain(root, [
451
+ '.',
452
+ `:(exclude)${dir}/tasks.json`,
453
+ `:(exclude)${dir}/checks`,
454
+ ':(exclude).agents/aced/results',
455
+ ]) !== ''
456
+ )
457
+ warnings.push({
458
+ code: 'uncommitted-not-benched',
459
+ message:
460
+ 'the working tree has uncommitted changes; they are not benched, since a git-ref arm runs its committed tree',
461
+ })
462
+
463
+ const runsPerArm = runsOverride ?? taskSet.runs
464
+ const ceilingUsd = round(arms.length * tasks.length * runsPerArm * taskSet.maxBudgetUsd)
465
+ const stored = storedCosts(root, suite, taskSet.model, harness, adapter.runner)
466
+ const estimateTasks: Plan['estimate']['tasks'] = {}
467
+ for (const t of tasks) {
468
+ const costs = stored.get(t.id) ?? []
469
+ const perRunUsd = costs.length > 0 ? median(costs) : taskSet.maxBudgetUsd
470
+ estimateTasks[t.id] = {
471
+ usd: round(arms.length * runsPerArm * perRunUsd),
472
+ perRunUsd: round(perRunUsd),
473
+ source: costs.length > 0 ? 'stored' : 'cap',
474
+ storedRuns: costs.length,
475
+ }
476
+ }
477
+ const totalUsd = round(Object.values(estimateTasks).reduce((s, e) => s + e.usd, 0))
478
+
479
+ const perTaskMinP = minAttainableP([[runsPerArm, runsPerArm]])
480
+ if (perTaskMinP > 0.05) {
481
+ const pooledMinP = minAttainableP(tasks.map(() => [runsPerArm, runsPerArm]))
482
+ const pooled =
483
+ tasks.length >= 2 && pooledMinP <= 0.05
484
+ ? `a pooled result across the ${tasks.length} tasks still can (smallest attainable p ${round(pooledMinP, 6)})`
485
+ : `a pooled result across ${tasks.length === 1 ? 'one task' : `the ${tasks.length} tasks`} cannot either`
486
+ warnings.push({
487
+ code: 'too-few-to-call',
488
+ 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}`,
489
+ })
490
+ }
491
+
492
+ return {
493
+ planVersion: 1,
494
+ suite,
495
+ harness,
496
+ adapter: adapter.name,
497
+ runner: adapter.runner,
498
+ model: taskSet.model,
499
+ permissionMode: taskSet.permissionMode,
500
+ permissionScope: `the agent runs with --permission-mode ${taskSet.permissionMode}; it applies only inside the throwaway checkout each run gets, never to this working tree`,
501
+ maxBudgetUsd: taskSet.maxBudgetUsd,
502
+ timeoutMinutes: taskSet.timeoutMinutes,
503
+ runsPerArm,
504
+ baseline: Boolean(args.baseline),
505
+ arms,
506
+ tasks: tasks.map((t) => t.id),
507
+ counts: { arms: arms.length, tasks: tasks.length, runsPerArm, totalRuns: arms.length * tasks.length * runsPerArm },
508
+ ceilingUsd,
509
+ estimate: { totalUsd, tasks: estimateTasks },
510
+ warnings,
511
+ taskSetCommit,
512
+ taskSetHash: taskSetHash(taskSetFiles(root, suite, taskSetCommit)),
513
+ }
514
+ }
515
+
516
+ // ─── run ──────────────────────────────────────────────────────────────────────
517
+
518
+ const SHELL_TIMEOUT_MS = 15 * 60 * 1000
519
+
520
+ function shell(command: string, cwd: string): number | null {
521
+ return spawnSync('sh', ['-c', command], { cwd, stdio: 'ignore', timeout: SHELL_TIMEOUT_MS }).status
522
+ }
523
+
524
+ function zeroMetrics(): Omit<RunResult, 'task' | 'run' | 'pass' | 'error' | 'transcript'> {
525
+ return {
526
+ wallMs: 0,
527
+ inputTokens: 0,
528
+ outputTokens: 0,
529
+ cacheReadTokens: 0,
530
+ cacheCreationTokens: 0,
531
+ turns: 0,
532
+ toolCalls: 0,
533
+ costUsd: 0,
534
+ capped: false,
535
+ }
536
+ }
537
+
538
+ interface RunContext {
539
+ root: string
540
+ plan: Plan
541
+ taskSet: TaskSet
542
+ adapter: Adapter
543
+ transcriptDir: string
544
+ }
545
+
546
+ /** Applies the arm's subject to the checkout. Returns what the launch needs for a package arm. */
547
+ function applySubject(
548
+ ctx: RunContext,
549
+ arm: Arm,
550
+ checkout: string,
551
+ runTemp: string,
552
+ ): { pluginDir?: string; env?: Record<string, string> } {
553
+ const s = arm.subject
554
+ if (s.kind === 'file') {
555
+ const target = join(checkout, s.path)
556
+ if (s.from === 'absent') {
557
+ rmSync(target, { force: true, recursive: true })
558
+ } else {
559
+ const content = s.from.startsWith('ref:')
560
+ ? showAt(ctx.root, s.commit ?? s.from.slice(4), s.path)
561
+ : readFileSync(join(ctx.root, s.from.slice(5)))
562
+ if (content === undefined) throw new BenchError(`could not read ${s.path} at ${s.from}`)
563
+ mkdirSync(dirname(target), { recursive: true })
564
+ writeFileSync(target, content)
565
+ }
566
+ return {}
567
+ }
568
+ if (s.kind === 'package') {
569
+ const packDir = join(runTemp, 'package-pack')
570
+ mkdirSync(packDir, { recursive: true })
571
+ const pack = spawnSync('npm', ['pack', `${s.name}@${s.version}`, '--pack-destination', packDir], {
572
+ cwd: runTemp,
573
+ encoding: 'utf8',
574
+ })
575
+ const tarball = readdirSync(packDir).find((f) => f.endsWith('.tgz'))
576
+ if (pack.status !== 0 || !tarball)
577
+ throw new BenchError(`npm pack ${s.name}@${s.version} failed: ${(pack.stderr || '').trim()}`)
578
+ const unpacked = join(runTemp, 'package')
579
+ mkdirSync(unpacked, { recursive: true })
580
+ const untar = spawnSync('tar', ['-xzf', join(packDir, tarball), '-C', unpacked], { encoding: 'utf8' })
581
+ if (untar.status !== 0) throw new BenchError(`could not unpack ${tarball}: ${untar.stderr.trim()}`)
582
+ return { pluginDir: join(unpacked, 'package'), env: ctx.adapter.isolatedConfig(runTemp) }
583
+ }
584
+ return {}
585
+ }
586
+
587
+ /**
588
+ * Overlays the task-set commit's suite, stages the subject's change with it, and commits both with
589
+ * a throwaway identity, so the agent starts on a clean tree.
590
+ */
591
+ function commitCheckout(ctx: RunContext, checkout: string): void {
592
+ const dir = suiteDir(ctx.plan.suite)
593
+ gitOk(checkout, ['rm', '-r', '-q', '--ignore-unmatch', '--', dir], 'could not clear the suite in the checkout')
594
+ gitOk(checkout, ['checkout', ctx.plan.taskSetCommit, '--', dir], 'could not overlay the task-set commit')
595
+ gitOk(checkout, ['add', '-A'], 'could not stage the checkout')
596
+ if (git(checkout, ['diff', '--cached', '--quiet']).status === 0) return
597
+ gitOk(
598
+ checkout,
599
+ [
600
+ '-c',
601
+ 'user.name=aced-bench',
602
+ '-c',
603
+ 'user.email=aced-bench@localhost',
604
+ '-c',
605
+ 'commit.gpgsign=false',
606
+ '-c',
607
+ 'core.hooksPath=/dev/null',
608
+ 'commit',
609
+ '--quiet',
610
+ '--no-verify',
611
+ '-m',
612
+ `bench: task set from ${ctx.plan.taskSetCommit}`,
613
+ ],
614
+ 'could not commit the overlaid task set',
615
+ )
616
+ }
617
+
618
+ function runOne(ctx: RunContext, arm: Arm, task: Task, run: number): RunResult & { model?: string } {
619
+ const runTemp = realpathSync(mkdtempSync(join(tmpdir(), 'aced-bench-')))
620
+ const checkout = join(runTemp, 'repo')
621
+ const base = { task: task.id, run }
622
+ try {
623
+ const at = arm.subject.kind === 'git-ref' ? arm.subject.commit : 'HEAD'
624
+ gitOk(ctx.root, ['worktree', 'add', '--detach', '--quiet', checkout, at], 'git worktree add failed')
625
+ const launch = applySubject(ctx, arm, checkout, runTemp)
626
+ commitCheckout(ctx, checkout)
627
+ for (const [name, command] of [
628
+ ['suite setup', ctx.taskSet.setup],
629
+ [`${task.id} setup`, task.setup],
630
+ ] as const) {
631
+ if (command && shell(command, checkout) !== 0)
632
+ return { ...base, ...zeroMetrics(), pass: false, error: `${name} failed: ${command}`, transcript: null }
633
+ }
634
+ const start = performance.now()
635
+ const { transcript, model, ...metrics } = ctx.adapter.launch({
636
+ prompt: task.prompt,
637
+ model: ctx.plan.model,
638
+ maxBudgetUsd: ctx.plan.maxBudgetUsd,
639
+ permissionMode: ctx.plan.permissionMode,
640
+ checkout,
641
+ timeoutMs: ctx.plan.timeoutMinutes * 60 * 1000,
642
+ ...launch,
643
+ })
644
+ const wallMs = Math.round(performance.now() - start)
645
+ const pass = shell(task.check, checkout) === 0
646
+ let kept: string | null = null
647
+ if (transcript) {
648
+ kept = `${ctx.transcriptDir}/${task.id}-${run}.jsonl.gz`
649
+ mkdirSync(dirname(join(ctx.root, kept)), { recursive: true })
650
+ writeFileSync(join(ctx.root, kept), gzipSync(transcript))
651
+ }
652
+ return { ...base, ...metrics, wallMs, pass, error: null, transcript: kept, ...(model ? { model } : {}) }
653
+ } finally {
654
+ git(ctx.root, ['worktree', 'remove', '--force', checkout])
655
+ rmSync(runTemp, { recursive: true, force: true })
656
+ git(ctx.root, ['worktree', 'prune'])
657
+ }
658
+ }
659
+
660
+ function summarize(runs: RunResult[]): Summary {
661
+ const passes = runs.filter((r) => r.pass).length
662
+ const ok = runs.filter((r) => !r.error)
663
+ const m = (f: (r: RunResult) => number) => median(ok.map(f))
664
+ const totalCostUsd = round(runs.reduce((s, r) => s + r.costUsd, 0))
665
+ return {
666
+ runs: runs.length,
667
+ passes,
668
+ passRate: runs.length === 0 ? 0 : round(passes / runs.length),
669
+ capped: runs.filter((r) => r.capped).length,
670
+ errors: runs.length - ok.length,
671
+ medianInputTokens: m((r) => r.inputTokens),
672
+ medianOutputTokens: m((r) => r.outputTokens),
673
+ medianCacheReadTokens: m((r) => r.cacheReadTokens),
674
+ medianTurns: m((r) => r.turns),
675
+ medianToolCalls: m((r) => r.toolCalls),
676
+ medianWallMs: m((r) => r.wallMs),
677
+ totalCostUsd,
678
+ ...(passes > 0 ? { costPerSuccessUsd: round(totalCostUsd / passes) } : {}),
679
+ }
680
+ }
681
+
682
+ /** A createdAt whose record file and transcript folder are both still free. */
683
+ function freshCreatedAt(dir: string, suffix: string, last: number): string {
684
+ let t = Math.max(Date.now(), last + 1)
685
+ for (;;) {
686
+ const createdAt = new Date(t).toISOString()
687
+ if (!existsSync(join(dir, `${stamp(createdAt)}${suffix}`)) && !existsSync(join(dir, stamp(createdAt))))
688
+ return createdAt
689
+ t++
690
+ }
691
+ }
692
+
693
+ function readPlan(path: string): Plan {
694
+ let plan: Plan
695
+ try {
696
+ plan = JSON.parse(readFileSync(path, 'utf8'))
697
+ } catch (e) {
698
+ throw new BenchError(`cannot read the plan ${path}: ${(e as Error).message}`)
699
+ }
700
+ if (plan?.planVersion !== 1 || !Array.isArray(plan.arms) || !Array.isArray(plan.tasks))
701
+ throw new BenchError(`${path} is not an aced-bench plan`)
702
+ return plan
703
+ }
704
+
705
+ function runPlan(root: string, args: Args): { records: string[]; baseline?: string } {
706
+ if (!args.plan) throw new UsageError('run needs --plan <file>')
707
+ if (!args.consent)
708
+ throw new BenchError(
709
+ 'run spends real money and needs explicit consent: re-run with --consent; nothing was launched',
710
+ )
711
+ const plan = readPlan(resolve(args.plan))
712
+ checkSuiteName(plan.suite)
713
+ const adapter = adapterFor(plan.harness)
714
+ if (!adapter) throw new BenchError(`the harness "${plan.harness}" has no adapter`)
715
+ const head = resolveCommit(root, 'HEAD')
716
+ const current = head ? taskSetHash(taskSetFiles(root, plan.suite, head)) : ''
717
+ if (current !== plan.taskSetHash)
718
+ throw new BenchError(
719
+ `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`,
720
+ )
721
+ const taskSet = loadCommittedTaskSet(root, plan.suite, plan.taskSetCommit)
722
+ const tasks = plan.tasks.map((id) => {
723
+ const t = taskSet.tasks.find((x) => x.id === id)
724
+ if (!t) throw new BenchError(`the plan's task "${id}" is not in the task-set commit`)
725
+ return t
726
+ })
727
+ const files = taskSetFiles(root, plan.suite, plan.taskSetCommit)
728
+ const outDir = resultsDir(plan.suite)
729
+ mkdirSync(join(root, outDir), { recursive: true })
730
+
731
+ const written: string[] = []
732
+ let baselinePath: string | undefined
733
+ let last = 0
734
+ for (const arm of plan.arms) {
735
+ const createdAt = freshCreatedAt(join(root, outDir), `.${arm.label}.json`, last)
736
+ last = Date.parse(createdAt)
737
+ const ctx: RunContext = { root, plan, taskSet, adapter, transcriptDir: `${outDir}/${stamp(createdAt)}` }
738
+ const runs: Array<RunResult & { model?: string }> = []
739
+ for (const task of tasks) for (let run = 1; run <= plan.runsPerArm; run++) runs.push(runOne(ctx, arm, task, run))
740
+
741
+ const evaluated: EvaluatedEntry[] = files.map((f) => ({ path: f.path, sha256: sha256(f.content), kind: 'file' }))
742
+ if (arm.subject.kind === 'file' && arm.subject.from.startsWith('path:')) {
743
+ const source = arm.subject.from.slice(5)
744
+ evaluated.push({ path: source, sha256: sha256(readFileSync(join(root, source))), kind: 'file' })
745
+ }
746
+ const reported = runs.find((r) => r.model)?.model
747
+ const clean: RunResult[] = runs.map(({ model: _, ...r }) => r)
748
+ const tags: RunRecord['tags'] = {}
749
+ for (const t of tasks) if (t.tags) tags[t.id] = t.tags
750
+ const record: RunRecord = {
751
+ schemaVersion: SCHEMA_VERSION,
752
+ layer: 'measured',
753
+ suite: plan.suite,
754
+ arm: arm.label,
755
+ subject: arm.subject,
756
+ harness: plan.harness,
757
+ adapter: adapter.name,
758
+ runner: adapter.runner,
759
+ taskSetCommit: plan.taskSetCommit,
760
+ taskSetHash: plan.taskSetHash,
761
+ tasks: tasks.map((t) => t.id),
762
+ tags,
763
+ model: plan.model,
764
+ scoring_model: reported ?? plan.model,
765
+ createdAt,
766
+ evaluated,
767
+ runs: clean,
768
+ summary: {
769
+ overall: summarize(clean),
770
+ tasks: tasks.map((t) => ({ task: t.id, ...summarize(clean.filter((r) => r.task === t.id)) })),
771
+ },
772
+ }
773
+ const path = `${outDir}/${stamp(createdAt)}.${arm.label}.json`
774
+ writeFileSync(join(root, path), `${JSON.stringify(record, null, 2)}\n`)
775
+ written.push(path)
776
+ if (plan.baseline) {
777
+ const baseline = { ...record, runs: clean.map(({ transcript: _, ...r }) => r) }
778
+ baselinePath = `${suiteDir(plan.suite)}/baseline.json`
779
+ writeFileSync(join(root, baselinePath), `${JSON.stringify(baseline, null, '\t')}\n`)
780
+ }
781
+ }
782
+ return { records: written, ...(baselinePath ? { baseline: baselinePath } : {}) }
783
+ }
784
+
785
+ // ─── compare ──────────────────────────────────────────────────────────────────
786
+
787
+ function loadRecord(path: string): RunRecord {
788
+ let record: Partial<RunRecord> & { schemaVersion?: unknown }
789
+ try {
790
+ record = JSON.parse(readFileSync(path, 'utf8'))
791
+ } catch (e) {
792
+ throw new BenchError(`cannot read the record ${path}: ${(e as Error).message}`)
793
+ }
794
+ if (record?.schemaVersion !== SCHEMA_VERSION)
795
+ throw new BenchError(
796
+ `${path} has schemaVersion ${record?.schemaVersion === undefined ? 'absent' : JSON.stringify(record.schemaVersion)}; this engine reads only schema version ${SCHEMA_VERSION}`,
797
+ )
798
+ if (!Array.isArray(record.runs)) throw new BenchError(`${path} is not a run record: it has no runs`)
799
+ return record as RunRecord
800
+ }
801
+
802
+ function compare(root: string, args: Args): { path: string; record: object } {
803
+ const suite = checkSuiteName(args.suite)
804
+ if (!args.before || !args.after) throw new UsageError('compare needs --before and --after')
805
+ const tags: Record<string, string> = {}
806
+ for (const t of args.tag ?? []) {
807
+ const eq = t.indexOf('=')
808
+ if (eq <= 0) throw new UsageError(`--tag "${t}" needs key=value`)
809
+ tags[t.slice(0, eq)] = t.slice(eq + 1)
810
+ }
811
+ let beforePath = resolve(args.before)
812
+ if (args.before === 'baseline') {
813
+ beforePath = join(root, suiteDir(suite), 'baseline.json')
814
+ if (!existsSync(beforePath))
815
+ throw new BenchError(`suite "${suite}" has no baseline: ${suiteDir(suite)}/baseline.json does not exist`)
816
+ }
817
+ const before = loadRecord(beforePath)
818
+ const after = loadRecord(resolve(args.after))
819
+ const comparison = compareRecords(before, after)
820
+ const outDir = join(root, resultsDir(suite))
821
+ mkdirSync(outDir, { recursive: true })
822
+ const createdAt = freshCreatedAt(outDir, '.json', 0)
823
+ const side = (r: RunRecord, source: string) => ({ source, arm: r.arm, createdAt: r.createdAt, subject: r.subject })
824
+ const record = {
825
+ schemaVersion: SCHEMA_VERSION,
826
+ kind: 'comparison',
827
+ suite,
828
+ createdAt,
829
+ before: side(before, args.before === 'baseline' ? 'baseline' : args.before),
830
+ after: side(after, args.after),
831
+ tags,
832
+ ...comparison,
833
+ }
834
+ const path = `${resultsDir(suite)}/compare-${stamp(createdAt)}.json`
835
+ writeFileSync(join(root, path), `${JSON.stringify(record, null, 2)}\n`)
836
+ return { path, record }
837
+ }
838
+
839
+ // ─── init ─────────────────────────────────────────────────────────────────────
840
+
841
+ function init(cwd: string, args: Args): string {
842
+ const suite = checkSuiteName(args.suite)
843
+ const r = git(cwd, ['rev-parse', '--show-toplevel'])
844
+ const root = r.status === 0 ? r.stdout.trim() : cwd
845
+ const path = `${suiteDir(suite)}/tasks.json`
846
+ if (existsSync(join(root, path))) throw new BenchError(`${path} already exists; init never overwrites a task set`)
847
+ mkdirSync(join(root, suiteDir(suite)), { recursive: true })
848
+ writeFileSync(join(root, path), `${JSON.stringify(TEMPLATE, null, '\t')}\n`)
849
+ return path
850
+ }
851
+
852
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
853
+
854
+ class UsageError extends Error {}
855
+
856
+ interface Args {
857
+ suite?: string
858
+ arm?: string[]
859
+ harness?: string
860
+ runs?: string
861
+ task?: string
862
+ baseline?: boolean
863
+ out?: string
864
+ plan?: string
865
+ consent?: boolean
866
+ before?: string
867
+ after?: string
868
+ tag?: string[]
869
+ }
870
+
871
+ const USAGE = `usage:
872
+ aced-bench init --suite <s>
873
+ aced-bench plan --suite <s> --arm <label>=<subject> … [--harness claude-code] [--runs N] [--task <id>] [--baseline] --out <file>
874
+ aced-bench run --plan <file> --consent
875
+ aced-bench compare --suite <s> --before <record|baseline> --after <record> [--tag k=v …]`
876
+
877
+ function main(argv: string[]): number {
878
+ const cwd = process.cwd()
879
+ const [verb, ...rest] = argv
880
+ if (verb === 'help' || verb === '--help' || verb === '-h') {
881
+ process.stdout.write(`${USAGE}\n`)
882
+ return 0
883
+ }
884
+ try {
885
+ const { values } = parseArgs({
886
+ args: rest,
887
+ strict: true,
888
+ options: {
889
+ suite: { type: 'string' },
890
+ arm: { type: 'string', multiple: true },
891
+ harness: { type: 'string' },
892
+ runs: { type: 'string' },
893
+ task: { type: 'string' },
894
+ baseline: { type: 'boolean' },
895
+ out: { type: 'string' },
896
+ plan: { type: 'string' },
897
+ consent: { type: 'boolean' },
898
+ before: { type: 'string' },
899
+ after: { type: 'string' },
900
+ tag: { type: 'string', multiple: true },
901
+ },
902
+ })
903
+ const args = values as Args
904
+ let output: unknown
905
+ if (verb === 'init') output = { tasksFile: init(cwd, args) }
906
+ else if (verb === 'plan') {
907
+ const root = repoRoot(cwd)
908
+ const p = plan(root, args)
909
+ const out = resolve(cwd, args.out as string)
910
+ mkdirSync(dirname(out), { recursive: true })
911
+ writeFileSync(out, `${JSON.stringify(p, null, 2)}\n`)
912
+ output = p
913
+ } else if (verb === 'run') output = runPlan(repoRoot(cwd), args)
914
+ else if (verb === 'compare') output = compare(repoRoot(cwd), args).record
915
+ else throw new UsageError(verb ? `unknown verb "${verb}"` : 'no verb given')
916
+ process.stdout.write(`${JSON.stringify(output, null, 2)}\n`)
917
+ return 0
918
+ } catch (e) {
919
+ if (e instanceof UsageError || (e as NodeJS.ErrnoException).code?.startsWith('ERR_PARSE_ARGS')) {
920
+ process.stderr.write(`aced-bench: ${(e as Error).message}\n${USAGE}\n`)
921
+ return 2
922
+ }
923
+ process.stderr.write(`aced-bench: ${(e as Error).message}\n`)
924
+ return 1
925
+ }
926
+ }
927
+
928
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
929
+ process.exit(main(process.argv.slice(2)))
930
+ }