@workos/quickstudy 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/quickstudy",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "An open-source, company-agnostic eval harness for measuring whether coding agents can complete real integration tasks, across an agent x feature x framework x surface matrix.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,7 @@ export const EMBEDDED_BUILD_INFO: {
9
9
  // (the `x-release-please-version` marker anchors the replacement). It is
10
10
  // restored as-is after a binary build (scripts/build-binary.ts), so the
11
11
  // committed source `--version` stays in lockstep with package.json.
12
- version: "0.7.0", // x-release-please-version
12
+ version: "0.8.0", // x-release-please-version
13
13
  revision: null,
14
14
  dirty: null,
15
15
  sourceHash: null,
@@ -44,7 +44,7 @@ export function discoverEvals(root: string): LoadedEval[] {
44
44
  const promptPath = join(dir, "PROMPT.md");
45
45
  if (!existsSync(promptPath)) continue;
46
46
 
47
- const { metadata, promptBody, frameworks, local } = parsePrompt(readFileSync(promptPath, "utf8"), promptPath);
47
+ const { metadata, promptBody, frameworks, local, suites } = parsePrompt(readFileSync(promptPath, "utf8"), promptPath);
48
48
 
49
49
  const scorerPath = join(dir, "EVAL.ts");
50
50
  if (!existsSync(scorerPath)) {
@@ -132,7 +132,9 @@ export function discoverEvals(root: string): LoadedEval[] {
132
132
  }
133
133
  collected.push({
134
134
  loaded: {
135
- metadata: { ...metadata, id: `${metadata.id}-${framework}`, framework },
135
+ // A `suites:` entry retiers one variant (a saturated django port can be
136
+ // smoke while the nextjs port stays benchmark) without renaming its id.
137
+ metadata: { ...metadata, suite: suites?.[framework] ?? metadata.suite, id: `${metadata.id}-${framework}`, framework },
136
138
  dir,
137
139
  promptPath,
138
140
  promptBody,
@@ -33,11 +33,13 @@ export interface ParsedPrompt {
33
33
  promptBody: string;
34
34
  /** `frameworks:` variant list — discovery expands one eval per entry. */
35
35
  frameworks?: string[];
36
+ /** Per-framework suite overrides (`suites:`); variants not named keep `suite`. */
37
+ suites?: Record<string, EvalSuite>;
36
38
  /** `local:` shared starting-state root, relative to the eval directory. */
37
39
  local?: string;
38
40
  }
39
41
 
40
- const KNOWN_KEYS = ["id", "suite", "product", "framework", "frameworks", "local", "topics"] as const;
42
+ const KNOWN_KEYS = ["id", "suite", "product", "framework", "frameworks", "suites", "local", "topics"] as const;
41
43
 
42
44
  /** Framework tokens become id suffixes (`<dirname>-<fw>`), so they must be kebab-case. */
43
45
  const FRAMEWORK_TOKEN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
@@ -137,6 +139,41 @@ export function parsePrompt(source: string, file: string): ParsedPrompt {
137
139
  }
138
140
  frameworks = tokens;
139
141
  }
142
+ let suites: Record<string, EvalSuite> | undefined;
143
+ if (fm["suites"] !== undefined) {
144
+ const value = fm["suites"];
145
+ if (frameworks === undefined) {
146
+ throw new EvalLoadError(
147
+ file,
148
+ keyLine("suites"),
149
+ `"suites" requires "frameworks" — it overrides the suite per framework variant`,
150
+ );
151
+ }
152
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
153
+ throw new EvalLoadError(file, keyLine("suites"), `"suites" must be a mapping of framework token to suite`);
154
+ }
155
+ const entries = Object.entries(value as Record<string, unknown>);
156
+ if (entries.length === 0) {
157
+ throw new EvalLoadError(file, keyLine("suites"), `"suites" must name at least one framework`);
158
+ }
159
+ for (const [token, suiteValue] of entries) {
160
+ if (!frameworks.includes(token)) {
161
+ throw new EvalLoadError(
162
+ file,
163
+ keyLine("suites"),
164
+ `"suites" names framework ${JSON.stringify(token)}, which is not in "frameworks"`,
165
+ );
166
+ }
167
+ if (typeof suiteValue !== "string" || !(EVAL_SUITES as readonly string[]).includes(suiteValue)) {
168
+ throw new EvalLoadError(
169
+ file,
170
+ keyLine("suites"),
171
+ `"suites" entry for ${JSON.stringify(token)} must be one of: ${EVAL_SUITES.join(", ")} (got ${JSON.stringify(suiteValue)})`,
172
+ );
173
+ }
174
+ }
175
+ suites = value as Record<string, EvalSuite>;
176
+ }
140
177
  let local: string | undefined;
141
178
  if (fm["local"] !== undefined) {
142
179
  const value = fm["local"];
@@ -185,6 +222,7 @@ export function parsePrompt(source: string, file: string): ParsedPrompt {
185
222
  metadata,
186
223
  promptBody,
187
224
  ...(frameworks !== undefined ? { frameworks } : {}),
225
+ ...(suites !== undefined ? { suites } : {}),
188
226
  ...(local !== undefined ? { local } : {}),
189
227
  };
190
228
  }
@@ -5,10 +5,22 @@
5
5
  * plain TypeScript handed an {@link EvalContext} over the finished workspace.
6
6
  */
7
7
 
8
- /** Closed set of eval suites. `regression` never moves headline scores. */
9
- export const EVAL_SUITES = ["benchmark", "regression", "other"] as const;
8
+ /**
9
+ * Closed set of eval suites. `benchmark` and `regression` are the headline
10
+ * suites: the leaderboard aggregates count both. `smoke` holds evals every
11
+ * arm solves — run to catch breakage, never moving the headline. `other` is
12
+ * anything else outside the headline.
13
+ */
14
+ export const EVAL_SUITES = ["benchmark", "regression", "smoke", "other"] as const;
10
15
  export type EvalSuite = (typeof EVAL_SUITES)[number];
11
16
 
17
+ /** The suites whose attempts count toward the leaderboard aggregates. */
18
+ export const HEADLINE_SUITES: readonly EvalSuite[] = ["benchmark", "regression"];
19
+
20
+ export function isHeadlineSuite(suite: string): boolean {
21
+ return (HEADLINE_SUITES as readonly string[]).includes(suite);
22
+ }
23
+
12
24
  /** PROMPT.md frontmatter. New fields require a proven filtering/reporting use case. */
13
25
  export interface EvalMetadata {
14
26
  /** Must equal the eval's directory name (prevents silent aliasing). */
@@ -2,17 +2,26 @@
2
2
  * Per-experiment leaderboard aggregate — one row per experiment, derived from
3
3
  * the same attempts and (banded) pairs as the rest of the report.
4
4
  *
5
- * Every number is restricted to benchmark-suite attempts, so regression and
6
- * other suites never move the headline row. Telemetry follows the report's
7
- * rule: sums and means cover only attempts that reported a value, and are
8
- * null (never 0) when none did.
5
+ * Every number is restricted to headline-suite attempts (HEADLINE_SUITES:
6
+ * benchmark and regression), so smoke and other suites never move the
7
+ * headline row. Telemetry follows the report's rule: sums and means cover
8
+ * only attempts that reported a value, and are null (never 0) when none did.
9
9
  */
10
+ import { isHeadlineSuite } from "../evals/types.ts";
10
11
  import type { AttemptRecord } from "../store/db.ts";
11
12
  import type { RunCostEstimate } from "./costs.ts";
12
13
  import type { ReportExperiment, ReportPair } from "./report.ts";
13
14
 
15
+ /**
16
+ * The pass-rate gate: an experiment whose headline pass rate is below this
17
+ * is listed after every experiment that clears it, whatever its cost or
18
+ * time. Pass rate is a gate, not a rank — the leaderboard orders the arms
19
+ * that clear it by time-to-pass, then cost-to-pass.
20
+ */
21
+ export const DEFAULT_PASS_GATE = 0.8;
22
+
14
23
  export interface ExperimentAggregate {
15
- /** Benchmark-suite attempts of this experiment (every status). */
24
+ /** Headline-suite attempts of this experiment (every status). */
16
25
  attempts: number;
17
26
  completed: number;
18
27
  passed: number;
@@ -21,11 +30,11 @@ export interface ExperimentAggregate {
21
30
  errors: number;
22
31
  /** Attempts whose agent phase hit the timeout limit (graded timeouts are also completed). */
23
32
  timeouts: number;
24
- /** passed / (passed + failed) over benchmark-suite pairs; null when nothing scored. */
33
+ /** passed / (passed + failed) over headline-suite pairs; null when nothing scored. */
25
34
  pass_rate_all: number | null;
26
35
  /** Same, restricted to evals whose band is "discriminating". */
27
36
  pass_rate_discriminating: number | null;
28
- /** Discriminating benchmark evals where this experiment scored at least one attempt. */
37
+ /** Discriminating headline evals where this experiment scored at least one attempt. */
29
38
  discriminating_evals: number;
30
39
  /** Numerator and denominator of pass_rate_discriminating. */
31
40
  discriminating_passed: number;
@@ -40,9 +49,25 @@ export interface ExperimentAggregate {
40
49
  /** Provider-reported, summed over the attempts that reported it. */
41
50
  cost_usd: number | null;
42
51
  cost_attempts: number;
52
+ /**
53
+ * Cost-to-pass: reported cost summed over the attempts that reported it,
54
+ * divided by the passes among those same attempts — what one success cost,
55
+ * failures included. null when nothing reported cost or none of it passed.
56
+ */
57
+ cost_per_pass_usd: number | null;
58
+ /**
59
+ * Time-to-pass: agent time summed over the scored attempts that recorded
60
+ * both stamps, divided by the passes among them. null when none did, or
61
+ * none of them passed.
62
+ */
63
+ agent_ms_per_pass: number | null;
64
+ /** pass_rate_all >= the report's pass gate; null when nothing scored. */
65
+ clears_gate: boolean | null;
43
66
  /** Present only when the report was built with --pricing. */
44
67
  estimated_cost_usd?: number | null;
45
68
  estimated_attempts?: number;
69
+ /** --pricing's estimate over the estimated attempts, divided by their passes. */
70
+ estimated_cost_per_pass_usd?: number | null;
46
71
  }
47
72
 
48
73
  const sum = (values: number[]): number => values.reduce((total, v) => total + v, 0);
@@ -66,33 +91,44 @@ export function collectExperimentAggregate(
66
91
  pairs: ReportPair[],
67
92
  experimentId: string,
68
93
  costEstimate?: RunCostEstimate,
94
+ passGate: number = DEFAULT_PASS_GATE,
69
95
  ): ExperimentAggregate {
70
- const mine = attempts.filter((a) => a.experimentId === experimentId && a.suite === "benchmark");
96
+ const mine = attempts.filter((a) => a.experimentId === experimentId && isHeadlineSuite(a.suite));
71
97
  const completed = mine.filter((a) => a.status === "completed");
72
98
  const passed = completed.filter((a) => a.result?.passed).length;
73
99
 
74
- const benchmarkPairs = pairs.filter((p) => p.experiment_id === experimentId && p.suite === "benchmark");
75
- const scoredAll = sum(benchmarkPairs.map((p) => p.scored));
76
- const passedAll = sum(benchmarkPairs.map((p) => p.passed));
77
- const discriminating = benchmarkPairs.filter((p) => p.band === "discriminating" && p.scored > 0);
100
+ const headlinePairs = pairs.filter((p) => p.experiment_id === experimentId && isHeadlineSuite(p.suite));
101
+ const scoredAll = sum(headlinePairs.map((p) => p.scored));
102
+ const passedAll = sum(headlinePairs.map((p) => p.passed));
103
+ const discriminating = headlinePairs.filter((p) => p.band === "discriminating" && p.scored > 0);
78
104
  const discScored = sum(discriminating.map((p) => p.scored));
79
105
  const discPassed = sum(discriminating.map((p) => p.passed));
80
106
 
81
- const durations = completed.flatMap((a) =>
82
- typeof a.agentStartedAt === "number" && typeof a.agentFinishedAt === "number" ? [a.agentFinishedAt - a.agentStartedAt] : [],
83
- );
107
+ const stamped = completed.filter((a) => typeof a.agentStartedAt === "number" && typeof a.agentFinishedAt === "number");
108
+ const durations = stamped.map((a) => (a.agentFinishedAt as number) - (a.agentStartedAt as number));
109
+ const stampedPasses = stamped.filter((a) => a.result?.passed).length;
84
110
  const tokensIn = mine.map((a) => a.tokensIn).filter((v): v is number => v !== null && v !== undefined);
85
111
  const tokensOut = mine.map((a) => a.tokensOut).filter((v): v is number => v !== null && v !== undefined);
86
- const costs = mine.map((a) => a.costUsd).filter((v): v is number => v !== null && v !== undefined);
112
+ const costed = mine.filter((a) => a.costUsd !== null && a.costUsd !== undefined);
113
+ const costs = costed.map((a) => a.costUsd as number);
114
+ const costedPasses = costed.filter((a) => a.status === "completed" && a.result?.passed).length;
87
115
 
88
- let estimated: Pick<ExperimentAggregate, "estimated_cost_usd" | "estimated_attempts"> = {};
116
+ let estimated: Pick<ExperimentAggregate, "estimated_cost_usd" | "estimated_attempts" | "estimated_cost_per_pass_usd"> = {};
89
117
  if (costEstimate !== undefined) {
90
- const ids = new Set(mine.map((a) => a.id));
91
- const values = costEstimate.attempts
92
- .filter((e) => ids.has(e.attempt_id))
93
- .flatMap((e) => (e.cost_usd === null ? [] : [e.cost_usd]));
94
- estimated = { estimated_cost_usd: values.length > 0 ? sum(values) : null, estimated_attempts: values.length };
118
+ const byId = new Map(mine.map((a) => [a.id, a]));
119
+ const priced = costEstimate.attempts.filter((e) => byId.has(e.attempt_id) && e.cost_usd !== null);
120
+ const values = priced.map((e) => e.cost_usd as number);
121
+ const pricedPasses = priced.filter((e) => {
122
+ const attempt = byId.get(e.attempt_id);
123
+ return attempt?.status === "completed" && attempt.result?.passed;
124
+ }).length;
125
+ estimated = {
126
+ estimated_cost_usd: values.length > 0 ? sum(values) : null,
127
+ estimated_attempts: values.length,
128
+ estimated_cost_per_pass_usd: values.length > 0 && pricedPasses > 0 ? sum(values) / pricedPasses : null,
129
+ };
95
130
  }
131
+ const passRateAll = scoredAll > 0 ? passedAll / scoredAll : null;
96
132
 
97
133
  return {
98
134
  attempts: mine.length,
@@ -102,7 +138,7 @@ export function collectExperimentAggregate(
102
138
  incomplete: mine.filter((a) => a.status === "incomplete").length,
103
139
  errors: mine.filter((a) => a.status === "error").length,
104
140
  timeouts: mine.filter((a) => a.status === "completed" && a.limitHit === "timeout").length,
105
- pass_rate_all: scoredAll > 0 ? passedAll / scoredAll : null,
141
+ pass_rate_all: passRateAll,
106
142
  pass_rate_discriminating: discScored > 0 ? discPassed / discScored : null,
107
143
  discriminating_evals: new Set(discriminating.map((p) => p.eval_id)).size,
108
144
  discriminating_passed: discPassed,
@@ -114,21 +150,41 @@ export function collectExperimentAggregate(
114
150
  token_attempts: Math.max(tokensIn.length, tokensOut.length),
115
151
  cost_usd: costs.length > 0 ? sum(costs) : null,
116
152
  cost_attempts: costs.length,
153
+ cost_per_pass_usd: costs.length > 0 && costedPasses > 0 ? sum(costs) / costedPasses : null,
154
+ agent_ms_per_pass: durations.length > 0 && stampedPasses > 0 ? sum(durations) / stampedPasses : null,
155
+ clears_gate: passRateAll === null ? null : passRateAll >= passGate,
117
156
  ...estimated,
118
157
  };
119
158
  }
120
159
 
160
+ /** Ascending with nulls (and absent values) last; equal when both are absent. */
161
+ function ascending(a: number | null | undefined, b: number | null | undefined): number {
162
+ const x = a ?? null;
163
+ const y = b ?? null;
164
+ if (x === null && y === null) return 0;
165
+ if (x === null) return 1;
166
+ if (y === null) return -1;
167
+ return x - y;
168
+ }
169
+
121
170
  /**
122
171
  * Leaderboard order: agent arms before controls (the oracle would otherwise
123
- * lead every board), then discriminating pass rate desc (nulls last), then the
124
- * all-eval pass rate desc (nulls last), then experiment id — a total order,
125
- * so every consumer renders the same rows in the same sequence.
172
+ * lead every board); arms that clear the pass gate before those that do not;
173
+ * then time-to-pass ascending (every adapter reports agent time), then
174
+ * cost-to-pass ascending (null where the provider reports no dollars sorts
175
+ * last); then discriminating and all-eval pass rate desc as tie-breaks; then
176
+ * experiment id — a total order, so every consumer renders the same rows in
177
+ * the same sequence.
126
178
  */
127
179
  export function compareLeaderboard(a: ReportExperiment, b: ReportExperiment): number {
128
180
  const rate = (value: number | null | undefined): number => (value === null || value === undefined ? -1 : value);
129
181
  const control = (experiment: ReportExperiment): number => ((experiment.role ?? "agent") === "agent" ? 0 : 1);
182
+ const gate = (experiment: ReportExperiment): number => (experiment.aggregate?.clears_gate === true ? 0 : 1);
130
183
  return (
131
184
  control(a) - control(b) ||
185
+ gate(a) - gate(b) ||
186
+ ascending(a.aggregate?.agent_ms_per_pass, b.aggregate?.agent_ms_per_pass) ||
187
+ ascending(a.aggregate?.cost_per_pass_usd, b.aggregate?.cost_per_pass_usd) ||
132
188
  rate(b.aggregate?.pass_rate_discriminating) - rate(a.aggregate?.pass_rate_discriminating) ||
133
189
  rate(b.aggregate?.pass_rate_all) - rate(a.aggregate?.pass_rate_all) ||
134
190
  a.experiment_id.localeCompare(b.experiment_id)
@@ -39,7 +39,8 @@ import {
39
39
  type ExperimentRole,
40
40
  type ScorerHealth,
41
41
  } from "./scorer-health.ts";
42
- import { collectExperimentAggregate, compareLeaderboard, type ExperimentAggregate } from "./aggregate.ts";
42
+ import { collectExperimentAggregate, compareLeaderboard, DEFAULT_PASS_GATE, type ExperimentAggregate } from "./aggregate.ts";
43
+ import { HEADLINE_SUITES } from "../evals/types.ts";
43
44
 
44
45
  export type { EvalBand, ExperimentRole, ScorerHealth, ScorerHealthSuspect } from "./scorer-health.ts";
45
46
  export type { ExperimentAggregate } from "./aggregate.ts";
@@ -53,7 +54,7 @@ export class ReportError extends Error {
53
54
  }
54
55
  }
55
56
 
56
- export const REPORT_SCHEMA_VERSION = 11;
57
+ export const REPORT_SCHEMA_VERSION = 13;
57
58
 
58
59
  /**
59
60
  * Substrings that must never appear in generated report prose: at n≤5 trials
@@ -69,7 +70,7 @@ export function findBannedLanguage(text: string): string[] {
69
70
  }
70
71
 
71
72
  /** Methodology is generated prose — it must itself pass the language guard. */
72
- export function methodologyNote(trials: number): string {
73
+ export function methodologyNote(trials: number, passGate: number = DEFAULT_PASS_GATE): string {
73
74
  return (
74
75
  `Pass rates are per eval × experiment pair: passed / (passed + failed). Incomplete and errored ` +
75
76
  `attempts are counted separately and never enter that denominator. Each pair has ${trials} planned ` +
@@ -88,7 +89,12 @@ export function methodologyNote(trials: number): string {
88
89
  `about the instrument, not about any treatment. Control experiments — an oracle that applies each eval's ` +
89
90
  `reference solution, a no-op that changes nothing — are reported alongside the agent experiments and ` +
90
91
  `tagged as controls wherever they appear; bands and scorer health are decided over the agent experiments ` +
91
- `only, and controls are ranked after every agent experiment.`
92
+ `only, and controls are ranked after every agent experiment. Pass rate is a gate, not a rank: an experiment ` +
93
+ `whose headline pass rate is below ${Math.round(passGate * 100)}% is listed after every ` +
94
+ `experiment that clears it, and the experiments that clear it are ordered by time-to-pass (agent time summed ` +
95
+ `over the scored attempts that recorded it, divided by the passes among them) and then cost-to-pass (reported ` +
96
+ `cost summed over the attempts that reported it, divided by the passes among them) — what one success took, ` +
97
+ `failures included. Both are null, never zero, where nothing was recorded.`
92
98
  );
93
99
  }
94
100
 
@@ -134,7 +140,7 @@ export interface ReportExperiment {
134
140
  */
135
141
  sdk_recall?: ReportRecall;
136
142
  /**
137
- * Leaderboard row over benchmark-suite attempts. Optional on the wire:
143
+ * Leaderboard row over headline-suite (benchmark + regression) attempts. Optional on the wire:
138
144
  * absent in schema_version < 9 artifacts; readers must tolerate absence.
139
145
  */
140
146
  aggregate?: ExperimentAggregate;
@@ -310,6 +316,8 @@ export interface ReportJson {
310
316
  /** v6: empty denial logs are observed zero; absent logs are unavailable. */
311
317
  egress_observation?: { available: string[]; unavailable: string[] };
312
318
  methodology: string;
319
+ /** The pass-rate gate the leaderboard order applies (see methodology). */
320
+ pass_gate: number;
313
321
  }
314
322
 
315
323
  // ---------------------------------------------------------------------------
@@ -652,7 +660,7 @@ export function buildReport(options: BuildReportOptions): ReportJson {
652
660
  const scorerHealth = collectScorerHealth(attempts, pairs, controls);
653
661
  const costEstimate = options.pricing === undefined ? undefined : estimateRunCosts(run, attempts, options.resultsDir, options.pricing);
654
662
 
655
- const methodology = methodologyNote(run.config.trials) + (costEstimate === undefined ? "" :
663
+ const methodology = methodologyNote(run.config.trials, DEFAULT_PASS_GATE) + (costEstimate === undefined ? "" :
656
664
  " Optional token-cost estimates supplement reported cost using the supplied pricing snapshot. They cover only the selected attempts, not superseded retries, tool fees, diagnosis or infrastructure, and are not billing records.");
657
665
  const banned = findBannedLanguage(methodology);
658
666
  if (banned.length > 0) {
@@ -695,6 +703,7 @@ export function buildReport(options: BuildReportOptions): ReportJson {
695
703
  unavailable: attempts.filter((a) => !existsSync(join(options.resultsDir, runId, a.id, "egress-denials.log"))).map((a) => a.id),
696
704
  },
697
705
  methodology,
706
+ pass_gate: DEFAULT_PASS_GATE,
698
707
  };
699
708
  }
700
709
 
@@ -707,12 +716,15 @@ function pct(rate: number | null): string {
707
716
  }
708
717
 
709
718
  function renderLeaderboard(experiments: ReportExperiment[]): string[] {
710
- const header = ["experiment", "model", "pass (all/disc)", "agent median", "tokens in", "cost", "recall"];
719
+ const header = ["experiment", "model", "gate", "$/pass", "time/pass", "pass (all/disc)", "agent median", "tokens in", "cost", "recall"];
711
720
  const rows = [...experiments].sort(compareLeaderboard).map((experiment) => {
712
721
  const agg = experiment.aggregate;
713
722
  const model = experiment.agent.model === "unspecified" ? "unpinned" : experiment.agent.model;
714
723
  const reasoning = experiment.agent.reasoning === "unspecified" ? "" : ` · ${experiment.agent.reasoning}`;
715
- if (!agg) return [experiment.experiment_id, `${model}${reasoning}`, "n/a", "n/a", "n/a", "n/a", "n/a"];
724
+ if (!agg) return [experiment.experiment_id, `${model}${reasoning}`, "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a", "n/a"];
725
+ const gate = agg.clears_gate === null ? "n/a" : agg.clears_gate ? "clears" : "below";
726
+ const perPass = agg.cost_per_pass_usd === null ? "n/a" : `$${agg.cost_per_pass_usd.toFixed(2)}`;
727
+ const timePerPass = agg.agent_ms_per_pass === null ? "n/a" : `${(agg.agent_ms_per_pass / 1000).toFixed(1)}s`;
716
728
  const all = agg.pass_rate_all === null ? "n/a" : `${agg.passed}/${agg.passed + agg.failed}`;
717
729
  const disc = agg.pass_rate_discriminating === null ? "n/a" : `${agg.discriminating_passed}/${agg.discriminating_scored}`;
718
730
  const time = agg.median_agent_ms === null ? "n/a" : `${(agg.median_agent_ms / 1000).toFixed(1)}s (${agg.agent_time_attempts})`;
@@ -723,11 +735,16 @@ function renderLeaderboard(experiments: ReportExperiment[]): string[] {
723
735
  const recall = experiment.sdk_recall;
724
736
  const recallText = recall && recall.rate !== null ? `${recall.recalled}/${recall.attempts}` : "n/a";
725
737
  const id = (experiment.role ?? "agent") === "agent" ? experiment.experiment_id : `${experiment.experiment_id} (control)`;
726
- return [id, `${model}${reasoning}`, `${all} · ${disc}`, time, tokens, cost, recallText];
738
+ return [id, `${model}${reasoning}`, gate, perPass, timePerPass, `${all} · ${disc}`, time, tokens, cost, recallText];
727
739
  });
728
740
  const widths = header.map((title, i) => Math.max(title.length, ...rows.map((row) => row[i]!.length)));
729
741
  const format = (cells: string[]): string => ` ${cells.map((cell, i) => cell.padEnd(widths[i]!)).join(" ").trimEnd()}`;
730
- return ["leaderboard (benchmark suite, sorted by discriminating pass rate; controls last):", format(header), ...rows.map(format)];
742
+ return [
743
+ `leaderboard (headline suites ${HEADLINE_SUITES.join(" + ")}; gate: pass rate ≥ ${Math.round(DEFAULT_PASS_GATE * 100)}%; ` +
744
+ `ordered by time-to-pass, then cost-to-pass; controls last):`,
745
+ format(header),
746
+ ...rows.map(format),
747
+ ];
731
748
  }
732
749
 
733
750
  /** Render the report as plain text. Renders FROM the JSON, never the store. */