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,369 @@
1
+ // bench-compare — the statistics between two run records, and the verdict.
2
+ //
3
+ // Per task and compared metric: each side's mean, median and min–max, the relative change, and a
4
+ // two-sided permutation test on the difference of means. Agent runs vary a lot from one to the
5
+ // next, so a mean moving by 10% is often noise; the p-value says how often relabelling the runs at
6
+ // random moves the mean at least as far as the real labels did. Up to 200,000 relabellings are
7
+ // enumerated (exact); past that, 20,000 seeded relabellings are drawn and p = (hits + 1) / (N + 1),
8
+ // identical on every run of the same records.
9
+ //
10
+ // The pooled row is the geometric mean of the per-task after/before ratios, so a cheap task and an
11
+ // expensive one weigh the same; its labels are permuted within each task, never across. A task
12
+ // enters a pooled row only when every one of its runs is positive on that metric on both sides,
13
+ // so no relabelling can make a log ratio undefined. `pass` has no pooled row: a ratio of pass rates
14
+ // is undefined whenever a side is 0%.
15
+ //
16
+ // THE VERDICT is a closed form (spec: eval-run/bench/engine/README.md). Let G be every row — per
17
+ // task and pooled — of a gated metric. Wrong-way: the after mean is worse in that metric's
18
+ // direction. Significant: p < 0.05. Then: incomparable > regressed (wrong-way AND significant) >
19
+ // inconclusive (wrong-way) > improved (significant) > unchanged. Multiplicity is DISCLOSED in the
20
+ // footer (test count × 0.05), never corrected.
21
+
22
+ import { GATED, METRICS, type Metric, median, type RunRecord, type RunResult, round } from './bench-record.mts'
23
+
24
+ const ALPHA = 0.05
25
+ const EXACT_LIMIT = 200_000
26
+ const SAMPLES = 20_000
27
+
28
+ // ─── permutation machinery ────────────────────────────────────────────────────
29
+
30
+ /** mulberry32: a small seeded generator, so a sampled p-value repeats exactly. */
31
+ function rng(seed: number): () => number {
32
+ let s = seed >>> 0
33
+ return () => {
34
+ s = (s + 0x6d2b79f5) >>> 0
35
+ let t = s
36
+ t = Math.imul(t ^ (t >>> 15), t | 1)
37
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
38
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
39
+ }
40
+ }
41
+
42
+ /** FNV-1a, so each row draws from its own seed and one row's sampling never shifts another's. */
43
+ function seedOf(key: string): number {
44
+ let h = 0x811c9dc5
45
+ for (let i = 0; i < key.length; i++) {
46
+ h ^= key.charCodeAt(i)
47
+ h = Math.imul(h, 0x01000193)
48
+ }
49
+ return h >>> 0
50
+ }
51
+
52
+ function choose(n: number, k: number): number {
53
+ let c = 1
54
+ for (let i = 1; i <= k; i++) c = (c * (n - k + i)) / i
55
+ return Math.round(c)
56
+ }
57
+
58
+ /**
59
+ * The smallest p any data could reach with these group sizes, one `[before, after]` pair per task.
60
+ * One pair is a per-task row; several are a pooled row. Exact when every task enumerates and the
61
+ * product of their relabellings stays within the limit: only the real split and, with equal
62
+ * groups, its mirror can be the most extreme. Sampled otherwise: one hit out of N + 1.
63
+ */
64
+ export function minAttainableP(sizes: Array<[number, number]>): number {
65
+ let product = 1
66
+ let exact = true
67
+ for (const [na, nb] of sizes) {
68
+ const c = choose(na + nb, na)
69
+ if (c > EXACT_LIMIT) exact = false
70
+ product *= c
71
+ }
72
+ if (!exact || product > EXACT_LIMIT) return 1 / (SAMPLES + 1)
73
+ return (sizes.every(([na, nb]) => na === nb) ? 2 : 1) / product
74
+ }
75
+
76
+ interface Relabellings {
77
+ stats: number[]
78
+ exact: boolean
79
+ }
80
+
81
+ /**
82
+ * Every split of the pooled runs into a "before" group of `a.length` and an "after" group of the
83
+ * rest, as `stat(beforeMean, afterMean)`. Enumerated when few enough, sampled otherwise.
84
+ */
85
+ function relabellings(
86
+ a: number[],
87
+ b: number[],
88
+ stat: (before: number, after: number) => number,
89
+ random: () => number,
90
+ ): Relabellings {
91
+ const all = [...a, ...b]
92
+ const na = a.length
93
+ const nb = b.length
94
+ const total = all.reduce((s, x) => s + x, 0)
95
+ const stats: number[] = []
96
+ const split = (before: number) => stat(before / na, (total - before) / nb)
97
+ if (choose(all.length, na) <= EXACT_LIMIT) {
98
+ const walk = (from: number, left: number, acc: number) => {
99
+ if (left === 0) {
100
+ stats.push(split(acc))
101
+ return
102
+ }
103
+ for (let i = from; i <= all.length - left; i++) walk(i + 1, left - 1, acc + (all[i] as number))
104
+ }
105
+ walk(0, na, 0)
106
+ return { stats, exact: true }
107
+ }
108
+ const idx = all.map((_, i) => i)
109
+ for (let n = 0; n < SAMPLES; n++) {
110
+ // A partial Fisher-Yates shuffle: the first `na` slots are a uniform random "before" group.
111
+ let acc = 0
112
+ for (let i = 0; i < na; i++) {
113
+ const j = i + Math.floor(random() * (idx.length - i))
114
+ const tmp = idx[i] as number
115
+ idx[i] = idx[j] as number
116
+ idx[j] = tmp
117
+ acc += all[idx[i] as number] as number
118
+ }
119
+ stats.push(split(acc))
120
+ }
121
+ return { stats, exact: false }
122
+ }
123
+
124
+ interface Test {
125
+ p: number
126
+ exact: boolean
127
+ minP: number
128
+ tooFew: boolean
129
+ }
130
+
131
+ /** Two-sided: the share of relabellings whose |statistic| reaches the observed one. */
132
+ function pValue(observed: number, { stats, exact }: Relabellings, minP: number): Test {
133
+ const tol = 1e-12 * Math.max(1, Math.abs(observed))
134
+ let hits = 0
135
+ for (const x of stats) if (Math.abs(x) >= Math.abs(observed) - tol) hits++
136
+ // A sampled test counts the real split once more: it is one of the possible ones.
137
+ const p = exact ? hits / stats.length : (hits + 1) / (stats.length + 1)
138
+ return { p, exact, minP, tooFew: minP > ALPHA }
139
+ }
140
+
141
+ const difference = (before: number, after: number) => after - before
142
+ const logRatio = (before: number, after: number) => Math.log(after / before)
143
+ const mean = (xs: number[]) => xs.reduce((s, x) => s + x, 0) / xs.length
144
+
145
+ // ─── rows ─────────────────────────────────────────────────────────────────────
146
+
147
+ interface SideStats {
148
+ n: number
149
+ mean: number
150
+ median: number
151
+ min: number
152
+ max: number
153
+ }
154
+
155
+ export interface TaskRow extends Partial<Test> {
156
+ scope: 'task'
157
+ task: string
158
+ metric: Metric
159
+ before: SideStats | null
160
+ after: SideStats | null
161
+ /** Relative change of the mean (0.1 is +10%); null when a side is empty or before is 0. */
162
+ change: number | null
163
+ wrongWay: boolean
164
+ significant: boolean
165
+ tags: Record<string, string>
166
+ }
167
+
168
+ export interface PooledRow extends Test {
169
+ scope: 'pooled'
170
+ metric: Metric
171
+ /** The geometric mean of the per-task after/before ratios. */
172
+ ratio: number
173
+ change: number
174
+ /** How many tasks the row pooled. */
175
+ tasks: number
176
+ wrongWay: boolean
177
+ significant: boolean
178
+ }
179
+
180
+ type Row = TaskRow | PooledRow
181
+
182
+ export type Verdict = 'incomparable' | 'regressed' | 'inconclusive' | 'improved' | 'unchanged'
183
+
184
+ export interface Reason {
185
+ field: string
186
+ before?: string
187
+ after?: string
188
+ message: string
189
+ }
190
+
191
+ export interface Comparison {
192
+ verdict: Verdict
193
+ incomparable: Reason[]
194
+ /** Tasks measured on only one side: listed, never compared. */
195
+ unmatched: string[]
196
+ /** Runs recorded as errors, left out of every metric. */
197
+ errorsExcluded: { before: number; after: number }
198
+ rows: Row[]
199
+ footer: { testCount: number; expectedByChance: number; alpha: number }
200
+ }
201
+
202
+ const value = (r: RunResult, m: Metric): number => (m === 'pass' ? (r.pass ? 1 : 0) : r[m])
203
+
204
+ function sideStats(xs: number[]): SideStats | null {
205
+ if (xs.length === 0) return null
206
+ return { n: xs.length, mean: mean(xs), median: median(xs), min: Math.min(...xs), max: Math.max(...xs) }
207
+ }
208
+
209
+ /** Worse in the metric's direction: lower pass is worse; higher is worse for every other metric. */
210
+ function worse(metric: Metric, before: number, after: number): boolean {
211
+ return metric === 'pass' ? after < before : after > before
212
+ }
213
+
214
+ function taskRow(task: string, metric: Metric, a: number[], b: number[], tags: Record<string, string>): TaskRow {
215
+ const before = sideStats(a)
216
+ const after = sideStats(b)
217
+ const row: TaskRow = {
218
+ scope: 'task',
219
+ task,
220
+ metric,
221
+ before,
222
+ after,
223
+ change: before && after && before.mean !== 0 ? after.mean / before.mean - 1 : null,
224
+ wrongWay: false,
225
+ significant: false,
226
+ tags,
227
+ }
228
+ if (!before || !after) return row
229
+ const test = pValue(
230
+ difference(before.mean, after.mean),
231
+ relabellings(a, b, difference, rng(seedOf(`${task}\0${metric}`))),
232
+ minAttainableP([[a.length, b.length]]),
233
+ )
234
+ Object.assign(row, test)
235
+ row.wrongWay = worse(metric, before.mean, after.mean)
236
+ row.significant = test.p < ALPHA
237
+ return row
238
+ }
239
+
240
+ function pooledRow(metric: Metric, pairs: Array<[number[], number[]]>): PooledRow | undefined {
241
+ const usable = pairs.filter(([a, b]) => a.length > 0 && b.length > 0 && [...a, ...b].every((x) => x > 0))
242
+ if (usable.length === 0) return undefined
243
+ const random = rng(seedOf(`pooled\0${metric}`))
244
+ const observed = mean(usable.map(([a, b]) => logRatio(mean(a), mean(b))))
245
+ const perTask = usable.map(([a, b]) => relabellings(a, b, logRatio, random))
246
+ const size = perTask.reduce((n, t) => n * t.stats.length, 1)
247
+ const exact = perTask.every((t) => t.exact) && size <= EXACT_LIMIT
248
+ const stats: number[] = []
249
+ if (exact) {
250
+ const walk = (i: number, acc: number) => {
251
+ if (i === perTask.length) {
252
+ stats.push(acc / perTask.length)
253
+ return
254
+ }
255
+ for (const s of (perTask[i] as Relabellings).stats) walk(i + 1, acc + s)
256
+ }
257
+ walk(0, 0)
258
+ } else {
259
+ for (let n = 0; n < SAMPLES; n++) {
260
+ let acc = 0
261
+ for (const t of perTask) acc += t.stats[Math.floor(random() * t.stats.length)] as number
262
+ stats.push(acc / perTask.length)
263
+ }
264
+ }
265
+ const test = pValue(observed, { stats, exact }, minAttainableP(usable.map(([a, b]) => [a.length, b.length])))
266
+ const ratio = Math.exp(observed)
267
+ return {
268
+ scope: 'pooled',
269
+ metric,
270
+ ratio,
271
+ change: ratio - 1,
272
+ tasks: usable.length,
273
+ ...test,
274
+ wrongWay: observed > 1e-12,
275
+ significant: test.p < ALPHA,
276
+ }
277
+ }
278
+
279
+ // ─── comparability ────────────────────────────────────────────────────────────
280
+
281
+ const MATCHED_FIELDS: Array<[string, (r: RunRecord) => string]> = [
282
+ ['layer', (r) => String(r.layer)],
283
+ ['model', (r) => String(r.model)],
284
+ ['harness', (r) => String(r.harness)],
285
+ ['adapter', (r) => String(r.adapter)],
286
+ ['runner', (r) => String(r.runner)],
287
+ ['subject kind', (r) => String(r.subject?.kind)],
288
+ ['task-set hash', (r) => String(r.taskSetHash)],
289
+ ]
290
+
291
+ function incomparableReasons(a: RunRecord, b: RunRecord): Reason[] {
292
+ const reasons: Reason[] = []
293
+ for (const [field, get] of MATCHED_FIELDS) {
294
+ const x = get(a)
295
+ const y = get(b)
296
+ if (x !== y) reasons.push({ field, before: x, after: y, message: `${field} differs: before "${x}", after "${y}"` })
297
+ }
298
+ // An unknown model never matches another unknown: two such records may have run anything.
299
+ if (
300
+ (a.model === 'unknown' && b.model === 'unknown') ||
301
+ (a.scoring_model === 'unknown' && b.scoring_model === 'unknown')
302
+ )
303
+ reasons.push({ field: 'model', message: 'the model is unknown on both sides' })
304
+ return reasons
305
+ }
306
+
307
+ function verdictOf(rows: Row[]): Verdict {
308
+ const g = rows.filter((r) => GATED.has(r.metric))
309
+ if (g.some((r) => r.wrongWay && r.significant)) return 'regressed'
310
+ if (g.some((r) => r.wrongWay)) return 'inconclusive'
311
+ if (g.some((r) => r.significant)) return 'improved'
312
+ return 'unchanged'
313
+ }
314
+
315
+ /** Compares `after` against `before`. Never refuses: a mismatch is an `incomparable` verdict. */
316
+ export function compareRecords(before: RunRecord, after: RunRecord): Comparison {
317
+ const incomparable = incomparableReasons(before, after)
318
+ const beforeTasks = [...new Set(before.runs.map((r) => r.task))]
319
+ const afterTasks = [...new Set(after.runs.map((r) => r.task))]
320
+ const unmatched = [
321
+ ...beforeTasks.filter((t) => !afterTasks.includes(t)),
322
+ ...afterTasks.filter((t) => !beforeTasks.includes(t)),
323
+ ]
324
+ const empty: Comparison = {
325
+ verdict: 'incomparable',
326
+ incomparable,
327
+ unmatched,
328
+ errorsExcluded: { before: 0, after: 0 },
329
+ rows: [],
330
+ footer: { testCount: 0, expectedByChance: 0, alpha: ALPHA },
331
+ }
332
+ if (incomparable.length > 0) return empty
333
+
334
+ const tasks = beforeTasks.filter((t) => afterTasks.includes(t))
335
+ const errorsExcluded = { before: 0, after: 0 }
336
+ const kept = (record: RunRecord, task: string, side: 'before' | 'after') => {
337
+ const rs = record.runs.filter((r) => r.task === task)
338
+ const ok = rs.filter((r) => !r.error)
339
+ errorsExcluded[side] += rs.length - ok.length
340
+ return ok
341
+ }
342
+ const rows: Row[] = []
343
+ const pairs = new Map<Metric, Array<[number[], number[]]>>()
344
+ for (const task of tasks) {
345
+ const a = kept(before, task, 'before')
346
+ const b = kept(after, task, 'after')
347
+ const tags = after.tags?.[task] ?? before.tags?.[task] ?? {}
348
+ for (const metric of METRICS) {
349
+ const va = a.map((r) => value(r, metric))
350
+ const vb = b.map((r) => value(r, metric))
351
+ rows.push(taskRow(task, metric, va, vb, { ...tags }))
352
+ if (metric !== 'pass') pairs.set(metric, [...(pairs.get(metric) ?? []), [va, vb]])
353
+ }
354
+ }
355
+ for (const metric of METRICS) {
356
+ if (metric === 'pass') continue
357
+ const row = pooledRow(metric, pairs.get(metric) ?? [])
358
+ if (row) rows.push(row)
359
+ }
360
+ const testCount = rows.filter((r) => r.p !== undefined).length
361
+ return {
362
+ verdict: verdictOf(rows),
363
+ incomparable,
364
+ unmatched,
365
+ errorsExcluded,
366
+ rows,
367
+ footer: { testCount, expectedByChance: round(testCount * ALPHA), alpha: ALPHA },
368
+ }
369
+ }
@@ -0,0 +1,133 @@
1
+ // bench-record — the shapes and locations the measured layer reads and writes.
2
+ //
3
+ // A run record is schema version 3 — the first version this engine writes and the ONLY one it
4
+ // reads. A record another tool wrote is converted by that tool; there is no reader here for any
5
+ // other shape, so a consumer never silently misreads a record of another version.
6
+
7
+ import { join } from 'node:path'
8
+
9
+ export const SCHEMA_VERSION = 3
10
+
11
+ /** `^[a-z0-9]+(?:[-.][a-z0-9]+)*$` — lowercase segments joined by single `-` or `.`. */
12
+ export const SUITE_NAME_RULE = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/
13
+
14
+ export const BENCH_ROOT = join('.agents', 'aced', 'bench')
15
+ export const RESULTS_ROOT = join('.agents', 'aced', 'results', 'bench')
16
+
17
+ export function suiteDir(suite: string): string {
18
+ return join(BENCH_ROOT, suite)
19
+ }
20
+
21
+ export function resultsDir(suite: string): string {
22
+ return join(RESULTS_ROOT, suite)
23
+ }
24
+
25
+ /** An ISO timestamp safe in a file name: `:` and `.` become `-`. */
26
+ export function stamp(createdAt: string): string {
27
+ return createdAt.replace(/[:.]/g, '-')
28
+ }
29
+
30
+ /** The compared metrics. `pass` is 0/1 and has no pooled row. */
31
+ export const METRICS = [
32
+ 'pass',
33
+ 'inputTokens',
34
+ 'outputTokens',
35
+ 'cacheReadTokens',
36
+ 'turns',
37
+ 'toolCalls',
38
+ 'wallMs',
39
+ 'costUsd',
40
+ ] as const
41
+ export type Metric = (typeof METRICS)[number]
42
+
43
+ /** Metrics that can make a verdict `regressed`. Cost and cache reads are compared but never gate. */
44
+ export const GATED: ReadonlySet<Metric> = new Set([
45
+ 'pass',
46
+ 'turns',
47
+ 'toolCalls',
48
+ 'inputTokens',
49
+ 'outputTokens',
50
+ 'wallMs',
51
+ ])
52
+
53
+ export type Subject =
54
+ | { kind: 'git-ref'; ref: string; commit: string }
55
+ | { kind: 'package'; name: string; version: string }
56
+ | { kind: 'file'; path: string; from: string }
57
+
58
+ export interface RunResult {
59
+ task: string
60
+ run: number
61
+ pass: boolean
62
+ wallMs: number
63
+ inputTokens: number
64
+ outputTokens: number
65
+ cacheReadTokens: number
66
+ cacheCreationTokens: number
67
+ turns: number
68
+ toolCalls: number
69
+ costUsd: number
70
+ /** Stopped without a success result: budget cap, error result, timeout, or the harness dying. */
71
+ capped: boolean
72
+ /** Set only when setup failed before the agent ran. */
73
+ error: string | null
74
+ /** The gzipped transcript, repo-relative. Absent from `baseline.json`. */
75
+ transcript?: string | null
76
+ }
77
+
78
+ export interface EvaluatedEntry {
79
+ path: string
80
+ sha256: string
81
+ kind: 'file'
82
+ }
83
+
84
+ export interface Summary {
85
+ runs: number
86
+ passes: number
87
+ passRate: number
88
+ capped: number
89
+ errors: number
90
+ medianInputTokens: number
91
+ medianOutputTokens: number
92
+ medianCacheReadTokens: number
93
+ medianTurns: number
94
+ medianToolCalls: number
95
+ medianWallMs: number
96
+ totalCostUsd: number
97
+ /** Total cost over passes; absent when nothing passed. */
98
+ costPerSuccessUsd?: number
99
+ }
100
+
101
+ export interface RunRecord {
102
+ schemaVersion: typeof SCHEMA_VERSION
103
+ layer: 'measured'
104
+ suite: string
105
+ arm: string
106
+ subject: Subject
107
+ harness: string
108
+ adapter: string
109
+ runner: string
110
+ taskSetCommit: string
111
+ taskSetHash: string
112
+ tasks: string[]
113
+ /** Each measured task's `tags` from tasks.json, by task id. */
114
+ tags: Record<string, Record<string, string>>
115
+ model: string
116
+ scoring_model: string
117
+ createdAt: string
118
+ evaluated: EvaluatedEntry[]
119
+ runs: RunResult[]
120
+ summary: { overall: Summary; tasks: Array<Summary & { task: string }> }
121
+ }
122
+
123
+ export function median(values: number[]): number {
124
+ if (values.length === 0) return 0
125
+ const sorted = [...values].sort((a, b) => a - b)
126
+ const mid = Math.floor(sorted.length / 2)
127
+ return sorted.length % 2 ? (sorted[mid] as number) : ((sorted[mid - 1] as number) + (sorted[mid] as number)) / 2
128
+ }
129
+
130
+ export function round(n: number, places = 4): number {
131
+ const f = 10 ** places
132
+ return Math.round(n * f) / f
133
+ }