Pin what a graded run covered, so a fast one cannot be published as a score

4960a2a2c50c · Claude Fable 5 · · parent 04cb84d364fb

Pin what a graded run covered, so a fast one cannot be published as a score

Issue #34's contract asks each run to pin a task digest and says a fast/smoke
run is never a published score. Both were missing: a suite was a list of task
names in a text file, and nothing stopped a three-of-twelve run from becoming
a row in the file the trend reads.

A suite is now a manifest that pins each task by content — for a public task
the dataset, git url, commit, and path Harbor's registry resolves it to; for an
owned one the tracker issue and the commit the forge recorded as closing it.
The task digest is over that identity, the suite digest over the sorted task
digests and the tier. `regex-log` at one dataset commit and `regex-log` at
another are different work, and two rows now say so instead of comparing as
one measurement.

The smoke rule is enforced rather than advised. `classifyRun` compares the
trials on disk against the manifest's pinned list, so a partial run is a smoke
run whatever the invocation called itself — the trial directories are the
evidence, and the runner cannot argue with them. Two independent consequences,
neither flaggable: the store refuses the row (`smoke_run`, or
`unclassified_run` when the run named no manifest at all), and the gate carries
a `run_tier=score` criterion with no passing branch, so the report exits 2. The
second refusal matters as much as the first — declining to name a suite must
not be the cheap way past the coverage check, because naming it is exactly what
exposes whether you ran it.

Six suites, from the two sources the issue names. `coder-effectiveness-v1` is
the headline twenty: the twelve-task Terminal-Bench cross-section plus eight
`swebench-verified@1.0` instances, one per repository, chosen by rule (the
lexicographically first id in each of the eight largest) rather than by hand,
so nobody has to wonder whether the picks flatter the coder. `swebench-verified`
stands in for the issue's SWE-bench-lite candidate: it is the human-validated
subset, which is what a floor wants underneath it, and it reaches us through the
same `harbor run --dataset` contract with no new harness code.

The owned half is real and not yet runnable. Six closed issues carry a
closing-reference commit in their forge evidence, and none has a container that
can grade it, so each is `environmentProven: false` and the suite that holds
them is `smoke`. A score suite refuses an unproven task on purpose: a trial
nobody could run reads as the coder failing rather than as a missing
environment, and the suite would get quietly worse the day an image broke.
`bench/tasks/owned/README.md` carries the construction and the two things that
have to hold before the first one is admitted.

Store rows move to `openagents.bench_result.v2` and carry the suite pin. A v1
row is named for what it is on read rather than coerced: it recorded no suite
digest, so nothing in it says which task list it measured, and a digest cannot
be invented after the fact.

44 new tests over the pin, the classifier, both halves of the enforcement, and
the CLI exit codes. No model called, no Docker image, clock injected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoZMfWRSGnf6FZX2Ar9rQ2
Co-Authored-By
Claude Fable 5 <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified bench-results/README.md
  • added bench/build-suites.mjs
  • modified bench/run-suite.sh
  • added bench/suites/coder-effectiveness-v1.suite.json
  • added bench/suites/owned-closed-issues.suite.json
  • added bench/suites/smoke.suite.json
  • added bench/suites/swebench-verified-subset.suite.json
  • added bench/suites/tb2-cross-section.suite.json
  • added bench/suites/tb2-quick.suite.json
  • added bench/tasks/owned/README.md
  • modified package.json
  • modified packages/coder-effectiveness/README.md
  • added packages/coder-effectiveness/fixtures/fixture-suite-3.suite.json
  • added packages/coder-effectiveness/fixtures/fixture-suite-5.suite.json
  • added packages/coder-effectiveness/fixtures/fixture-suite.suite.json
  • modified packages/coder-effectiveness/src/cli.ts
  • modified packages/coder-effectiveness/src/compare.test.ts
  • modified packages/coder-effectiveness/src/index.ts
  • modified packages/coder-effectiveness/src/render.ts
  • modified packages/coder-effectiveness/src/results-store.test.ts
  • modified packages/coder-effectiveness/src/results-store.ts
  • modified packages/coder-effectiveness/src/store-cli.test.ts
  • added packages/coder-effectiveness/src/suite-manifest.test.ts
  • added packages/coder-effectiveness/src/suite-manifest.ts
  • modified packages/coder-effectiveness/src/thresholds.test.ts
  • modified packages/coder-effectiveness/src/thresholds.ts
  • added packages/coder-effectiveness/thresholds/tb2-quick.json

Diff

27 files changed, +2510 -58

bench-results/README.md modified +31 -9

@@ -13,12 +13,13 @@ sitting in the file that the trend line reads.

13 13
14 14
```sh
15 15
# 1. Run the suite. Harbor grades the trials.
16
bench/run-suite.sh bench/suites/tb2-cross-section.txt \
16
bench/run-suite.sh bench/suites/tb2-cross-section.suite.json \
17 17
  --model openai/gpt-5.6-luna --jobs-dir /tmp/gym-jobs-run
18 18
19 19
# 2. Score it and record it.
20 20
pnpm run effectiveness:report -- /tmp/gym-jobs-run/<job-dir> \
21 21
  --suite tb2-cross-section --lane proxy \
22
  --suite-manifest bench/suites/tb2-cross-section.suite.json \
22 23
  --thresholds packages/coder-effectiveness/thresholds/tb2-cross-section.json \
23 24
  --append bench-results/tb2-cross-section.jsonl
24 25
```

@@ -28,6 +29,25 @@ floor was breached, `2` the gate was unverifiable. A fourth code, `3`, means the

28 29
run was scored but the store refused to record it, and it only ever replaces a
29 30
`0`.
30 31
32
## Only a full run of a named suite gets in
33
34
`--suite-manifest` is not optional here. The store refuses two shapes outright,
35
and neither refusal has a flag:
36
37
- **`unclassified_run`** — the run named no manifest, so nothing records which
38
  pinned task list it was supposed to cover. A row whose task list is only
39
  "whatever ran" can be compared to a later row that ran less, and the trend
40
  would read the difference as the coder changing.
41
- **`smoke_run`** — the run did not cover every task the manifest pins, or the
42
  manifest declares itself a fast lane. Either way the figures are over a
43
  different set of work than the suite's other rows.
44
45
That is how "a fast/smoke run is never a published score" is enforced rather
46
than advised. Publishing means reaching this directory, and the coverage check
47
reads the trial directories on disk, so a run cannot describe itself as
48
complete when it is not. A refused run still prints its full report; it just
49
does not become a row, and it exits `3`.
50
31 51
## Reading it
32 52
33 53
```sh

@@ -76,14 +96,16 @@ A comparison follows the same rule: a delta against an unpriced side is

76 96
77 97
## Row fields
78 98
79
| Field                         | Meaning                                                                                                                                                                                  |
80
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
81
| `runDigest`                   | The report's pin over suite, lane, tasks, CLI version, model, rates.                                                                                                                     |
82
| `suiteKey`                    | The narrower pin two rows must share to be comparable: suite, sorted task list, rate catalog. Lane, model, and CLI version are excluded, because those are the axes a comparison varies. |
83
| `jobId`                       | The Harbor job. A store refuses a job it already holds — re-scoring a run does not make it a second run.                                                                                 |
84
| `costPerAcceptedOutcomeUsd`   | Total run cost over accepted outcomes, failures included, or `null`.                                                                                                                     |
85
| `gateStatus`                  | `passed`, `failed`, `unverifiable`, or `null` when no thresholds file was given.                                                                                                         |
86
| `previousReceipt` / `receipt` | The chain.                                                                                                                                                                               |
99
| Field                         | Meaning                                                                                                                                                                                                |
100
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
101
| `runDigest`                   | The report's pin over suite, lane, tasks, CLI version, model, rates.                                                                                                                                   |
102
| `suiteKey`                    | The narrower pin two rows must share to be comparable: suite, suite digest, sorted task list, rate catalog. Lane, model, and CLI version are excluded, because those are the axes a comparison varies. |
103
| `suiteId` / `suiteDigest`     | The manifest the run claimed, and the digest over its pinned tasks — dataset, git url, commit, and path per task, so the pin is over content and not over names.                                       |
104
| `tier`                        | Always `score`. Written down anyway, so a reader of the file never has to know the store's refusal rule to trust what the rows are.                                                                    |
105
| `jobId`                       | The Harbor job. A store refuses a job it already holds — re-scoring a run does not make it a second run.                                                                                               |
106
| `costPerAcceptedOutcomeUsd`   | Total run cost over accepted outcomes, failures included, or `null`.                                                                                                                                   |
107
| `gateStatus`                  | `passed`, `failed`, `unverifiable`, or `null` when no thresholds file was given.                                                                                                                       |
108
| `previousReceipt` / `receipt` | The chain.                                                                                                                                                                                             |
87 109
88 110
The full type is `BenchResultRow` in
89 111
`packages/coder-effectiveness/src/results-store.ts`.
bench/build-suites.mjs added +376

@@ -0,0 +1,376 @@

1
#!/usr/bin/env node
2
/**
3
 * Regenerate the suite manifests under `bench/suites/`.
4
 *
5
 * A manifest pins each task by its content rather than its name — for a public
6
 * task, the git url, commit, and path Harbor's registry resolves it to; for an
7
 * owned task, the tracker issue and the commit the forge recorded as closing
8
 * it. This script is where those pins are read from their sources once, so that
9
 * afterwards the manifests are self-contained and every consumer (the digest,
10
 * the gate, the store) works offline from the checked-in file alone. Same split
11
 * as `src/pricing.ts`: resolve from the source of truth, then pin the snapshot,
12
 * and keep the path back to the source written down.
13
 *
14
 * Two sources, neither of which this repo owns:
15
 *
16
 *   --registry <path>   Harbor's `registry.json`, an 80-dataset index that
17
 *                       already carries `terminal-bench@2.0` and
18
 *                       `swebench-verified@1.0` behind one contract. Defaults
19
 *                       to the clone at ../projects/repos/harbor.
20
 *   --issues <path>     A `openagents issue list --state closed --json` body
21
 *                       for OpenAgentsInc/openagents. Closed issues whose
22
 *                       evidence carries a `closing_reference` commit are the
23
 *                       owned tasks; the rest are skipped by name.
24
 *
25
 * Usage:
26
 *   node bench/build-suites.mjs --registry <registry.json> --issues <closed.json>
27
 *   node bench/build-suites.mjs ... --check   # rebuild and diff, write nothing
28
 *
29
 * `--check` is what CI runs: it rebuilds every manifest and fails if the result
30
 * differs from what is committed, so a manifest cannot drift from the registry
31
 * it claims to pin without somebody noticing.
32
 */
33
34
import { readFileSync, writeFileSync } from "node:fs";
35
import { dirname, join, resolve } from "node:path";
36
import { fileURLToPath } from "node:url";
37
38
const BENCH_DIR = dirname(fileURLToPath(import.meta.url));
39
const REPO_ROOT = dirname(BENCH_DIR);
40
const SUITES_DIR = join(BENCH_DIR, "suites");
41
42
const SCHEMA = "openagents.effectiveness_suite.v1";
43
44
/**
45
 * The twelve Terminal-Bench 2.0 tasks selected in
46
 * `bench/suites/tb2-cross-section.md`, which records why each slot exists.
47
 * Kept as a name list here because the pins come from the registry.
48
 */
49
const TB2_CROSS_SECTION = [
50
  "git-leak-recovery",
51
  "sanitize-git-repo",
52
  "merge-diff-arc-agi-task",
53
  "build-cython-ext",
54
  "sqlite-with-gcov",
55
  "fix-code-vulnerability",
56
  "regex-log",
57
  "count-dataset-tokens",
58
  "password-recovery",
59
  "openssl-selfsigned-cert",
60
  "nginx-request-logging",
61
  "schemelike-metacircular-eval",
62
];
63
64
/**
65
 * The bounded public SWE-bench subset: eight repositories, one instance each.
66
 *
67
 * Issue #34 names SWE-bench-lite as the candidate. Harbor's registry carries
68
 * `swebench-verified@1.0` instead — the 500-instance human-validated subset —
69
 * and that is the better half of the same idea: it is the subset whose task
70
 * statements and tests a human confirmed are solvable and correctly graded,
71
 * which is what a floor wants underneath it, and it runs through the same
72
 * `harbor run --dataset` contract with no new harness code. Lite's
73
 * distinguishing property is being small, and this suite is bounding itself.
74
 *
75
 * ONE INSTANCE PER REPOSITORY, CHOSEN BY RULE RATHER THAN BY HAND. The 500 are
76
 * dominated by Django (231) and SymPy (75); a random eight would usually be
77
 * five Djangos, and a score over it would mostly measure how well the coder
78
 * knows one codebase. So: the eight repositories with the most instances, and
79
 * from each the lexicographically first instance id.
80
 *
81
 * The second half of that rule matters more than it looks. Any hand-picked
82
 * instance invites the question of whether it was picked because the coder does
83
 * well on it, and there is no way to answer that question from the outside. A
84
 * mechanical rule answers it in advance, survives a registry refresh, and can be
85
 * re-derived by anyone with the registry — which is also what lets `--check`
86
 * mean something.
87
 */
88
const SWEBENCH_REPOS = [
89
  ["astropy", "astropy: table and unit handling in a large scientific codebase"],
90
  ["django", "Django: the framework half of the benchmark, wide blast radius per change"],
91
  ["matplotlib", "matplotlib: rendering state, where the test is the only oracle"],
92
  ["pydata", "xarray: array semantics over pandas and numpy, dtype-sensitive"],
93
  ["pytest-dev", "pytest: the test runner itself, so a fix has to be reentrant"],
94
  ["scikit-learn", "scikit-learn: estimator API conformance, contract-shaped"],
95
  ["sphinx-doc", "Sphinx: documentation tooling, heavy on configuration surface"],
96
  ["sympy", "SymPy: symbolic evaluation, where a plausible-looking fix is usually wrong"],
97
];
98
99
const parseArguments = () => {
100
  const argv = process.argv.slice(2);
101
  let registry = resolve(REPO_ROOT, "../projects/repos/harbor/registry.json");
102
  let issues = null;
103
  let check = false;
104
  for (let index = 0; index < argv.length; index += 1) {
105
    const argument = argv[index];
106
    if (argument === "--check") check = true;
107
    else if (argument === "--registry") registry = argv[(index += 1)];
108
    else if (argument === "--issues") issues = argv[(index += 1)];
109
    else throw new Error(`unknown option: ${argument}`);
110
  }
111
  return { registry, issues, check };
112
};
113
114
/** Index a Harbor registry into `dataset@version -> { taskName -> pin }`. */
115
const readRegistry = (path) => {
116
  const datasets = new Map();
117
  for (const entry of JSON.parse(readFileSync(path, "utf8"))) {
118
    const tasks = new Map();
119
    for (const task of entry.tasks ?? []) {
120
      tasks.set(task.name, {
121
        gitUrl: task.git_url,
122
        commit: task.git_commit_id,
123
        path: task.path,
124
      });
125
    }
126
    datasets.set(`${entry.name}@${entry.version}`, tasks);
127
  }
128
  return datasets;
129
};
130
131
/**
132
 * Resolve a task name in a dataset to its pin.
133
 *
134
 * A missing name throws rather than being skipped. A suite that quietly dropped
135
 * a task it could not resolve would still build, still digest, and still
136
 * classify a run over the remaining tasks as full coverage — which is the
137
 * failure the digest exists to prevent, arriving through the front door.
138
 */
139
const pinFor = (datasets, dataset, name) => {
140
  const tasks = datasets.get(dataset);
141
  if (tasks === undefined) {
142
    throw new Error(`registry has no dataset ${dataset}`);
143
  }
144
  const pin = tasks.get(name);
145
  if (pin === undefined) {
146
    throw new Error(`registry dataset ${dataset} has no task ${name}`);
147
  }
148
  if (pin.commit === "HEAD") {
149
    throw new Error(
150
      `registry dataset ${dataset} pins ${name} at HEAD, which names a moving target rather than a commit; this suite cannot pin it`,
151
    );
152
  }
153
  return { kind: "harbor-registry", dataset, ...pin };
154
};
155
156
/**
157
 * The lexicographically first instance id a repository contributes to a
158
 * dataset. SWE-bench instance ids are `<org>__<repo>-<pr-number>`, so the
159
 * repository is the part before the double underscore.
160
 */
161
const firstInstanceOf = (datasets, dataset, repo) => {
162
  const tasks = datasets.get(dataset);
163
  if (tasks === undefined) throw new Error(`registry has no dataset ${dataset}`);
164
  const names = [...tasks.keys()].filter((name) => name.split("__")[0] === repo).toSorted();
165
  if (names.length === 0) {
166
    throw new Error(`registry dataset ${dataset} holds no instance from ${repo}`);
167
  }
168
  return names[0];
169
};
170
171
const registryTask = (datasets, dataset, name, rationale, environmentProven) => ({
172
  id: name,
173
  pin: pinFor(datasets, dataset, name),
174
  environmentProven,
175
  ...(rationale === undefined ? {} : { rationale }),
176
});
177
178
/**
179
 * Owned tasks: closed issues in this tracker that carry a closing commit.
180
 *
181
 * The forge records a closing reference as evidence on the issue, so the
182
 * accepted outcome is a fact the tracker already holds rather than a judgement
183
 * this script makes. An issue with no such evidence is skipped and named: it may
184
 * be perfectly well closed, but without a commit there is nothing to grade
185
 * against.
186
 *
187
 * Every one of these is `environmentProven: false` today. The pin is real — the
188
 * issue, its instruction, and the commit that satisfied it — and no container
189
 * has been built that can grade it, so `parseSuiteManifest` will refuse to let
190
 * them into a score-tier suite until one has. See `bench/tasks/owned/README.md`.
191
 */
192
const ownedTasks = (issuesPath) => {
193
  const body = JSON.parse(readFileSync(issuesPath, "utf8"));
194
  const issues = Array.isArray(body) ? body : (body.issues ?? []);
195
  const tasks = [];
196
  const skipped = [];
197
  for (const issue of issues) {
198
    const commits = (issue.openagents?.evidence ?? [])
199
      .filter((entry) => entry.source === "closing_reference" && typeof entry.commit === "string")
200
      .map((entry) => entry.commit);
201
    if (commits.length === 0) {
202
      skipped.push(issue.number);
203
      continue;
204
    }
205
    // Several closing references means the work landed over more than one push.
206
    // The last one is the state the issue was closed in, so that is the pin.
207
    const acceptedCommit = commits.at(-1);
208
    tasks.push({
209
      id: `owned-issue-${issue.number}`,
210
      pin: {
211
        kind: "tracker-closed-issue",
212
        repo: "OpenAgentsInc/openagents",
213
        issue: issue.number,
214
        acceptedCommit,
215
      },
216
      environmentProven: false,
217
      rationale: issue.title,
218
    });
219
  }
220
  tasks.sort((left, right) => left.pin.issue - right.pin.issue);
221
  return { tasks, skipped };
222
};
223
224
const manifest = (id, tier, description, tasks) => ({
225
  schema: SCHEMA,
226
  id,
227
  tier,
228
  description,
229
  tasks,
230
});
231
232
const write = (name, value, check) => {
233
  const path = join(SUITES_DIR, name);
234
  const text = `${JSON.stringify(value, null, 2)}\n`;
235
  if (check) {
236
    const existing = readFileSync(path, "utf8");
237
    if (existing !== text) {
238
      throw new Error(
239
        `${name} is out of date with its sources. Run bench/build-suites.mjs without --check and commit the result.`,
240
      );
241
    }
242
    process.stdout.write(`ok    ${name}\n`);
243
    return;
244
  }
245
  writeFileSync(path, text, "utf8");
246
  process.stdout.write(`wrote ${name} (${String(value.tasks.length)} tasks)\n`);
247
};
248
249
const main = () => {
250
  const { registry, issues, check } = parseArguments();
251
  const datasets = readRegistry(registry);
252
253
  const tb2 = TB2_CROSS_SECTION.map((name) =>
254
    registryTask(datasets, "terminal-bench@2.0", name, undefined, true),
255
  );
256
  const swe = SWEBENCH_REPOS.map(([repo, rationale]) =>
257
    registryTask(
258
      datasets,
259
      "swebench-verified@1.0",
260
      firstInstanceOf(datasets, "swebench-verified@1.0", repo),
261
      rationale,
262
      true,
263
    ),
264
  );
265
266
  write(
267
    "tb2-cross-section.suite.json",
268
    manifest(
269
      "tb2-cross-section",
270
      "score",
271
      "Twelve Terminal-Bench 2.0 tasks across git forensics, builds, C extensions, coverage, security fixes, log parsing, tokenisation, certificates, web-server configuration, and an interpreter. Selection rationale in tb2-cross-section.md.",
272
      tb2,
273
    ),
274
    check,
275
  );
276
277
  write(
278
    "swebench-verified-subset.suite.json",
279
    manifest(
280
      "swebench-verified-subset",
281
      "score",
282
      "The bounded public subset issue #34 asks for: eight swebench-verified@1.0 instances, one per repository, so no single project's idioms dominate the score.",
283
      swe,
284
    ),
285
    check,
286
  );
287
288
  /**
289
   * The two quickest tasks in the cross-section, declared `score` rather than
290
   * `smoke` and floored accordingly.
291
   *
292
   * This is not a fast lane wearing a score badge, and the distinction is worth
293
   * being precise about because the whole smoke rule depends on it. `smoke` is
294
   * for a suite whose result should never be published — a liveness check.
295
   * `tb2-quick` is a real, if narrow, measurement: two tasks the coder is
296
   * genuinely expected to solve, always run to completion, scored against
297
   * floors set for two tasks rather than for twelve. It exists because a suite
298
   * you can run three times in an hour is the only kind you can prove a
299
   * regression with on one machine, and because the headline suite's floors are
300
   * useless if nobody ever runs anything against them.
301
   *
302
   * What it cannot do is stand in for the headline number. It shares no suite
303
   * key with `coder-effectiveness-v1`, so no comparison will ever place its rows
304
   * beside that suite's — which is the property that makes shipping a narrow
305
   * score suite safe rather than a slow leak.
306
   */
307
  write(
308
    "tb2-quick.suite.json",
309
    manifest(
310
      "tb2-quick",
311
      "score",
312
      "Two quick Terminal-Bench 2.0 tasks: a narrow but real score suite, small enough to run repeatedly on one machine, floored for its own size. Never comparable to coder-effectiveness-v1 — different suite key.",
313
      [
314
        [
315
          "regex-log",
316
          "near-zero tool surface: the suite's test of whether the agent can just answer",
317
        ],
318
        [
319
          "openssl-selfsigned-cert",
320
          "a fully specified checklist: every command is known upfront, so round count is a tool habit rather than a reasoning result",
321
        ],
322
      ].map(([name, rationale]) =>
323
        registryTask(datasets, "terminal-bench@2.0", name, rationale, true),
324
      ),
325
    ),
326
    check,
327
  );
328
329
  write(
330
    "smoke.suite.json",
331
    manifest(
332
      "smoke",
333
      "smoke",
334
      "The fast lane: two quick Terminal-Bench tasks for checking that the harness, the adapter, and the lane are alive. Declared smoke, so its result is never a published score however completely it runs.",
335
      TB2_CROSS_SECTION.filter(
336
        (name) => name === "regex-log" || name === "fix-code-vulnerability",
337
      ).map((name) => registryTask(datasets, "terminal-bench@2.0", name, "quick-shaped", true)),
338
    ),
339
    check,
340
  );
341
342
  if (issues !== null) {
343
    const owned = ownedTasks(issues);
344
    write(
345
      "owned-closed-issues.suite.json",
346
      manifest(
347
        "owned-closed-issues",
348
        "smoke",
349
        "The owned half of issue #34's suite: closed issues in this tracker whose forge evidence carries a closing commit, so the accepted outcome is recorded rather than assumed. Tier smoke until the environments are built — see bench/tasks/owned/README.md.",
350
        owned.tasks,
351
      ),
352
      check,
353
    );
354
    process.stdout.write(
355
      `      ${String(owned.skipped.length)} closed issue(s) carry no closing-reference commit and are not tasks: ${owned.skipped.join(", ")}\n`,
356
    );
357
358
    write(
359
      "coder-effectiveness-v1.suite.json",
360
      manifest(
361
        "coder-effectiveness-v1",
362
        "score",
363
        "Issue #34's headline suite: the twelve-task Terminal-Bench cross-section plus the eight-instance swebench-verified subset. The owned closed-issue tasks join it once their environments are proven; until then a score suite cannot hold them.",
364
        [...tb2, ...swe],
365
      ),
366
      check,
367
    );
368
  }
369
};
370
371
try {
372
  main();
373
} catch (error) {
374
  process.stderr.write(`build-suites: ${error.message}\n`);
375
  process.exitCode = 1;
376
}
bench/run-suite.sh modified +56 -9

@@ -13,9 +13,20 @@ usage() {

13 13
Usage: bench/run-suite.sh <suite-file> --model <harbor-model> [options]
14 14
15 15
Arguments:
16
  <suite-file>            Path to a suite file: one task name per line. Lines
17
                          that are empty or start with # (after whitespace) are
18
                          ignored.
16
  <suite-file>            A suite manifest (*.suite.json) or a plain task list.
17
18
                          Prefer the manifest. It pins each task by content —
19
                          dataset, git url, commit, path — so the digest a run
20
                          records means something, and it is what
21
                          `coder-effectiveness report --suite-manifest` scores
22
                          the run against. Every pinned task is included, so a
23
                          run of a manifest is a full run or it is not that
24
                          suite; the report says which.
25
26
                          A plain task list is one task name per line, with
27
                          empty lines and lines starting with # ignored. It
28
                          still runs, and a run of one cannot be recorded as a
29
                          score: nothing in it says what the suite was.
19 30
20 31
Required options:
21 32
  --model <model>         Harbor model string, e.g. openai/gpt-5.6-luna,

@@ -241,12 +252,28 @@ fi

241 252
# Normalize the suite file path to an absolute path.
242 253
SUITE_FILE="$(cd "$(dirname "$SUITE_FILE")" && pwd)/$(basename "$SUITE_FILE")"
243 254
244
# Parse tasks: strip comments, trim whitespace, drop blanks.
255
# Parse tasks. A manifest carries them under .tasks[].id; a plain list is one
256
# name per line with comments stripped.
245 257
TASKS=()
246
while IFS= read -r line; do
247
  [ -n "$line" ] || continue
248
  TASKS+=("$line")
249
done < <(sed -e 's/#.*//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' "$SUITE_FILE" | sed -n '/./p')
258
case "$SUITE_FILE" in
259
  *.suite.json)
260
    while IFS= read -r line; do
261
      [ -n "$line" ] || continue
262
      TASKS+=("$line")
263
    done < <(python3 -c '
264
import json, sys
265
manifest = json.load(open(sys.argv[1]))
266
for task in manifest["tasks"]:
267
    print(task["id"])
268
' "$SUITE_FILE")
269
    ;;
270
  *)
271
    while IFS= read -r line; do
272
      [ -n "$line" ] || continue
273
      TASKS+=("$line")
274
    done < <(sed -e 's/#.*//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' "$SUITE_FILE" | sed -n '/./p')
275
    ;;
276
esac
250 277
251 278
if [ "${#TASKS[@]}" -eq 0 ]; then
252 279
  log "Suite file contains no tasks: $SUITE_FILE"

@@ -258,7 +285,7 @@ for task in "${TASKS[@]}"; do

258 285
  TASK_ARGS+=("-i" "$task")
259 286
done
260 287
261
SUITE_NAME="$(basename "$SUITE_FILE" .txt)"
288
SUITE_NAME="$(basename "$(basename "$SUITE_FILE" .txt)" .suite.json)"
262 289
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
263 290
264 291
# The catalog name the run registers under: Harbor spells models

@@ -459,3 +486,23 @@ if ! python3 "$BENCH_DIR/post_gym_run.py" "$JOB_DIR" --api-url "$API_URL" --lane

459 486
fi
460 487
461 488
log "Suite run complete: $SUITE_NAME"
489
490
# Score it. Left as an instruction rather than run here: the report needs a
491
# thresholds file and a store path, both of which are choices about what this
492
# run is for, and running a gate the operator did not ask for would make a
493
# suite run exit non-zero for reasons the runner did not decide.
494
case "$SUITE_FILE" in
495
  *.suite.json)
496
    log "To score and record this run:"
497
    log "  pnpm run effectiveness:report -- $(printf '%q' "$JOB_DIR") \\"
498
    log "    --suite $(printf '%q' "$SUITE_NAME") --lane $(printf '%q' "$LANE") \\"
499
    log "    --suite-manifest $(printf '%q' "$SUITE_FILE") \\"
500
    log "    --thresholds packages/coder-effectiveness/thresholds/${SUITE_NAME}.json \\"
501
    log "    --append bench-results/${SUITE_NAME}.jsonl"
502
    ;;
503
  *)
504
    log "This run used a plain task list, so it carries no suite pin and cannot"
505
    log "be recorded as a score. Re-run it against bench/suites/${SUITE_NAME}.suite.json"
506
    log "to produce a recordable row."
507
    ;;
508
esac
bench/suites/coder-effectiveness-v1.suite.json added +236

@@ -0,0 +1,236 @@

1
{
2
  "schema": "openagents.effectiveness_suite.v1",
3
  "id": "coder-effectiveness-v1",
4
  "tier": "score",
5
  "description": "Issue #34's headline suite: the twelve-task Terminal-Bench cross-section plus the eight-instance swebench-verified subset. The owned closed-issue tasks join it once their environments are proven; until then a score suite cannot hold them.",
6
  "tasks": [
7
    {
8
      "id": "git-leak-recovery",
9
      "pin": {
10
        "kind": "harbor-registry",
11
        "dataset": "terminal-bench@2.0",
12
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
13
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
14
        "path": "git-leak-recovery"
15
      },
16
      "environmentProven": true
17
    },
18
    {
19
      "id": "sanitize-git-repo",
20
      "pin": {
21
        "kind": "harbor-registry",
22
        "dataset": "terminal-bench@2.0",
23
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
24
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
25
        "path": "sanitize-git-repo"
26
      },
27
      "environmentProven": true
28
    },
29
    {
30
      "id": "merge-diff-arc-agi-task",
31
      "pin": {
32
        "kind": "harbor-registry",
33
        "dataset": "terminal-bench@2.0",
34
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
35
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
36
        "path": "merge-diff-arc-agi-task"
37
      },
38
      "environmentProven": true
39
    },
40
    {
41
      "id": "build-cython-ext",
42
      "pin": {
43
        "kind": "harbor-registry",
44
        "dataset": "terminal-bench@2.0",
45
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
46
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
47
        "path": "build-cython-ext"
48
      },
49
      "environmentProven": true
50
    },
51
    {
52
      "id": "sqlite-with-gcov",
53
      "pin": {
54
        "kind": "harbor-registry",
55
        "dataset": "terminal-bench@2.0",
56
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
57
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
58
        "path": "sqlite-with-gcov"
59
      },
60
      "environmentProven": true
61
    },
62
    {
63
      "id": "fix-code-vulnerability",
64
      "pin": {
65
        "kind": "harbor-registry",
66
        "dataset": "terminal-bench@2.0",
67
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
68
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
69
        "path": "fix-code-vulnerability"
70
      },
71
      "environmentProven": true
72
    },
73
    {
74
      "id": "regex-log",
75
      "pin": {
76
        "kind": "harbor-registry",
77
        "dataset": "terminal-bench@2.0",
78
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
79
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
80
        "path": "regex-log"
81
      },
82
      "environmentProven": true
83
    },
84
    {
85
      "id": "count-dataset-tokens",
86
      "pin": {
87
        "kind": "harbor-registry",
88
        "dataset": "terminal-bench@2.0",
89
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
90
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
91
        "path": "count-dataset-tokens"
92
      },
93
      "environmentProven": true
94
    },
95
    {
96
      "id": "password-recovery",
97
      "pin": {
98
        "kind": "harbor-registry",
99
        "dataset": "terminal-bench@2.0",
100
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
101
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
102
        "path": "password-recovery"
103
      },
104
      "environmentProven": true
105
    },
106
    {
107
      "id": "openssl-selfsigned-cert",
108
      "pin": {
109
        "kind": "harbor-registry",
110
        "dataset": "terminal-bench@2.0",
111
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
112
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
113
        "path": "openssl-selfsigned-cert"
114
      },
115
      "environmentProven": true
116
    },
117
    {
118
      "id": "nginx-request-logging",
119
      "pin": {
120
        "kind": "harbor-registry",
121
        "dataset": "terminal-bench@2.0",
122
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
123
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
124
        "path": "nginx-request-logging"
125
      },
126
      "environmentProven": true
127
    },
128
    {
129
      "id": "schemelike-metacircular-eval",
130
      "pin": {
131
        "kind": "harbor-registry",
132
        "dataset": "terminal-bench@2.0",
133
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
134
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
135
        "path": "schemelike-metacircular-eval"
136
      },
137
      "environmentProven": true
138
    },
139
    {
140
      "id": "astropy__astropy-12907",
141
      "pin": {
142
        "kind": "harbor-registry",
143
        "dataset": "swebench-verified@1.0",
144
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
145
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
146
        "path": "datasets/swebench-verified/astropy__astropy-12907"
147
      },
148
      "environmentProven": true,
149
      "rationale": "astropy: table and unit handling in a large scientific codebase"
150
    },
151
    {
152
      "id": "django__django-10097",
153
      "pin": {
154
        "kind": "harbor-registry",
155
        "dataset": "swebench-verified@1.0",
156
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
157
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
158
        "path": "datasets/swebench-verified/django__django-10097"
159
      },
160
      "environmentProven": true,
161
      "rationale": "Django: the framework half of the benchmark, wide blast radius per change"
162
    },
163
    {
164
      "id": "matplotlib__matplotlib-13989",
165
      "pin": {
166
        "kind": "harbor-registry",
167
        "dataset": "swebench-verified@1.0",
168
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
169
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
170
        "path": "datasets/swebench-verified/matplotlib__matplotlib-13989"
171
      },
172
      "environmentProven": true,
173
      "rationale": "matplotlib: rendering state, where the test is the only oracle"
174
    },
175
    {
176
      "id": "pydata__xarray-2905",
177
      "pin": {
178
        "kind": "harbor-registry",
179
        "dataset": "swebench-verified@1.0",
180
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
181
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
182
        "path": "datasets/swebench-verified/pydata__xarray-2905"
183
      },
184
      "environmentProven": true,
185
      "rationale": "xarray: array semantics over pandas and numpy, dtype-sensitive"
186
    },
187
    {
188
      "id": "pytest-dev__pytest-10051",
189
      "pin": {
190
        "kind": "harbor-registry",
191
        "dataset": "swebench-verified@1.0",
192
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
193
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
194
        "path": "datasets/swebench-verified/pytest-dev__pytest-10051"
195
      },
196
      "environmentProven": true,
197
      "rationale": "pytest: the test runner itself, so a fix has to be reentrant"
198
    },
199
    {
200
      "id": "scikit-learn__scikit-learn-10297",
201
      "pin": {
202
        "kind": "harbor-registry",
203
        "dataset": "swebench-verified@1.0",
204
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
205
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
206
        "path": "datasets/swebench-verified/scikit-learn__scikit-learn-10297"
207
      },
208
      "environmentProven": true,
209
      "rationale": "scikit-learn: estimator API conformance, contract-shaped"
210
    },
211
    {
212
      "id": "sphinx-doc__sphinx-10323",
213
      "pin": {
214
        "kind": "harbor-registry",
215
        "dataset": "swebench-verified@1.0",
216
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
217
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
218
        "path": "datasets/swebench-verified/sphinx-doc__sphinx-10323"
219
      },
220
      "environmentProven": true,
221
      "rationale": "Sphinx: documentation tooling, heavy on configuration surface"
222
    },
223
    {
224
      "id": "sympy__sympy-11618",
225
      "pin": {
226
        "kind": "harbor-registry",
227
        "dataset": "swebench-verified@1.0",
228
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
229
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
230
        "path": "datasets/swebench-verified/sympy__sympy-11618"
231
      },
232
      "environmentProven": true,
233
      "rationale": "SymPy: symbolic evaluation, where a plausible-looking fix is usually wrong"
234
    }
235
  ]
236
}
bench/suites/owned-closed-issues.suite.json added +74

@@ -0,0 +1,74 @@

1
{
2
  "schema": "openagents.effectiveness_suite.v1",
3
  "id": "owned-closed-issues",
4
  "tier": "smoke",
5
  "description": "The owned half of issue #34's suite: closed issues in this tracker whose forge evidence carries a closing commit, so the accepted outcome is recorded rather than assumed. Tier smoke until the environments are built — see bench/tasks/owned/README.md.",
6
  "tasks": [
7
    {
8
      "id": "owned-issue-21",
9
      "pin": {
10
        "kind": "tracker-closed-issue",
11
        "repo": "OpenAgentsInc/openagents",
12
        "issue": 21,
13
        "acceptedCommit": "586edf191fb221d1e1f5c600b4a714d40fc95820"
14
      },
15
      "environmentProven": false,
16
      "rationale": "Add Ollama local model support to openagents coder"
17
    },
18
    {
19
      "id": "owned-issue-24",
20
      "pin": {
21
        "kind": "tracker-closed-issue",
22
        "repo": "OpenAgentsInc/openagents",
23
        "issue": 24,
24
        "acceptedCommit": "f6366a80930ade654bc29b3b1217268011506061"
25
      },
26
      "environmentProven": false,
27
      "rationale": "Resume a coder thread with --resume"
28
    },
29
    {
30
      "id": "owned-issue-31",
31
      "pin": {
32
        "kind": "tracker-closed-issue",
33
        "repo": "OpenAgentsInc/openagents",
34
        "issue": 31,
35
        "acceptedCommit": "cf1861c9cbd2b2bf3e02593bf4c291db82c88e58"
36
      },
37
      "environmentProven": false,
38
      "rationale": "Adopt the proxy's reasoning and tool-call fidelity in the coder"
39
    },
40
    {
41
      "id": "owned-issue-36",
42
      "pin": {
43
        "kind": "tracker-closed-issue",
44
        "repo": "OpenAgentsInc/openagents",
45
        "issue": 36,
46
        "acceptedCommit": "2a631cf63107d1b706e7e8058f49c1bd283447ed"
47
      },
48
      "environmentProven": false,
49
      "rationale": "Teach the coder's tools token economy, per model family"
50
    },
51
    {
52
      "id": "owned-issue-40",
53
      "pin": {
54
        "kind": "tracker-closed-issue",
55
        "repo": "OpenAgentsInc/openagents",
56
        "issue": 40,
57
        "acceptedCommit": "fb705a506a2d56263b07698aa02b3223861d0df6"
58
      },
59
      "environmentProven": false,
60
      "rationale": "Coder presents model tiers, never vendor model names"
61
    },
62
    {
63
      "id": "owned-issue-41",
64
      "pin": {
65
        "kind": "tracker-closed-issue",
66
        "repo": "OpenAgentsInc/openagents",
67
        "issue": 41,
68
        "acceptedCommit": "a04c4fcf4708ff30b264970eb08acbebe5f20ed5"
69
      },
70
      "environmentProven": false,
71
      "rationale": "Read a conversation on request: the read-conversation plugin, discovered, loaded, and streamed in the coder"
72
    }
73
  ]
74
}
bench/suites/smoke.suite.json added +32

@@ -0,0 +1,32 @@

1
{
2
  "schema": "openagents.effectiveness_suite.v1",
3
  "id": "smoke",
4
  "tier": "smoke",
5
  "description": "The fast lane: two quick Terminal-Bench tasks for checking that the harness, the adapter, and the lane are alive. Declared smoke, so its result is never a published score however completely it runs.",
6
  "tasks": [
7
    {
8
      "id": "fix-code-vulnerability",
9
      "pin": {
10
        "kind": "harbor-registry",
11
        "dataset": "terminal-bench@2.0",
12
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
13
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
14
        "path": "fix-code-vulnerability"
15
      },
16
      "environmentProven": true,
17
      "rationale": "quick-shaped"
18
    },
19
    {
20
      "id": "regex-log",
21
      "pin": {
22
        "kind": "harbor-registry",
23
        "dataset": "terminal-bench@2.0",
24
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
25
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
26
        "path": "regex-log"
27
      },
28
      "environmentProven": true,
29
      "rationale": "quick-shaped"
30
    }
31
  ]
32
}
bench/suites/swebench-verified-subset.suite.json added +104

@@ -0,0 +1,104 @@

1
{
2
  "schema": "openagents.effectiveness_suite.v1",
3
  "id": "swebench-verified-subset",
4
  "tier": "score",
5
  "description": "The bounded public subset issue #34 asks for: eight swebench-verified@1.0 instances, one per repository, so no single project's idioms dominate the score.",
6
  "tasks": [
7
    {
8
      "id": "astropy__astropy-12907",
9
      "pin": {
10
        "kind": "harbor-registry",
11
        "dataset": "swebench-verified@1.0",
12
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
13
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
14
        "path": "datasets/swebench-verified/astropy__astropy-12907"
15
      },
16
      "environmentProven": true,
17
      "rationale": "astropy: table and unit handling in a large scientific codebase"
18
    },
19
    {
20
      "id": "django__django-10097",
21
      "pin": {
22
        "kind": "harbor-registry",
23
        "dataset": "swebench-verified@1.0",
24
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
25
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
26
        "path": "datasets/swebench-verified/django__django-10097"
27
      },
28
      "environmentProven": true,
29
      "rationale": "Django: the framework half of the benchmark, wide blast radius per change"
30
    },
31
    {
32
      "id": "matplotlib__matplotlib-13989",
33
      "pin": {
34
        "kind": "harbor-registry",
35
        "dataset": "swebench-verified@1.0",
36
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
37
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
38
        "path": "datasets/swebench-verified/matplotlib__matplotlib-13989"
39
      },
40
      "environmentProven": true,
41
      "rationale": "matplotlib: rendering state, where the test is the only oracle"
42
    },
43
    {
44
      "id": "pydata__xarray-2905",
45
      "pin": {
46
        "kind": "harbor-registry",
47
        "dataset": "swebench-verified@1.0",
48
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
49
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
50
        "path": "datasets/swebench-verified/pydata__xarray-2905"
51
      },
52
      "environmentProven": true,
53
      "rationale": "xarray: array semantics over pandas and numpy, dtype-sensitive"
54
    },
55
    {
56
      "id": "pytest-dev__pytest-10051",
57
      "pin": {
58
        "kind": "harbor-registry",
59
        "dataset": "swebench-verified@1.0",
60
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
61
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
62
        "path": "datasets/swebench-verified/pytest-dev__pytest-10051"
63
      },
64
      "environmentProven": true,
65
      "rationale": "pytest: the test runner itself, so a fix has to be reentrant"
66
    },
67
    {
68
      "id": "scikit-learn__scikit-learn-10297",
69
      "pin": {
70
        "kind": "harbor-registry",
71
        "dataset": "swebench-verified@1.0",
72
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
73
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
74
        "path": "datasets/swebench-verified/scikit-learn__scikit-learn-10297"
75
      },
76
      "environmentProven": true,
77
      "rationale": "scikit-learn: estimator API conformance, contract-shaped"
78
    },
79
    {
80
      "id": "sphinx-doc__sphinx-10323",
81
      "pin": {
82
        "kind": "harbor-registry",
83
        "dataset": "swebench-verified@1.0",
84
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
85
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
86
        "path": "datasets/swebench-verified/sphinx-doc__sphinx-10323"
87
      },
88
      "environmentProven": true,
89
      "rationale": "Sphinx: documentation tooling, heavy on configuration surface"
90
    },
91
    {
92
      "id": "sympy__sympy-11618",
93
      "pin": {
94
        "kind": "harbor-registry",
95
        "dataset": "swebench-verified@1.0",
96
        "gitUrl": "https://github.com/laude-institute/harbor-datasets.git",
97
        "commit": "86723674f04e4209ac479d0fb75d9d9f44b4377e",
98
        "path": "datasets/swebench-verified/sympy__sympy-11618"
99
      },
100
      "environmentProven": true,
101
      "rationale": "SymPy: symbolic evaluation, where a plausible-looking fix is usually wrong"
102
    }
103
  ]
104
}
bench/suites/tb2-cross-section.suite.json added +140

@@ -0,0 +1,140 @@

1
{
2
  "schema": "openagents.effectiveness_suite.v1",
3
  "id": "tb2-cross-section",
4
  "tier": "score",
5
  "description": "Twelve Terminal-Bench 2.0 tasks across git forensics, builds, C extensions, coverage, security fixes, log parsing, tokenisation, certificates, web-server configuration, and an interpreter. Selection rationale in tb2-cross-section.md.",
6
  "tasks": [
7
    {
8
      "id": "git-leak-recovery",
9
      "pin": {
10
        "kind": "harbor-registry",
11
        "dataset": "terminal-bench@2.0",
12
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
13
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
14
        "path": "git-leak-recovery"
15
      },
16
      "environmentProven": true
17
    },
18
    {
19
      "id": "sanitize-git-repo",
20
      "pin": {
21
        "kind": "harbor-registry",
22
        "dataset": "terminal-bench@2.0",
23
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
24
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
25
        "path": "sanitize-git-repo"
26
      },
27
      "environmentProven": true
28
    },
29
    {
30
      "id": "merge-diff-arc-agi-task",
31
      "pin": {
32
        "kind": "harbor-registry",
33
        "dataset": "terminal-bench@2.0",
34
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
35
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
36
        "path": "merge-diff-arc-agi-task"
37
      },
38
      "environmentProven": true
39
    },
40
    {
41
      "id": "build-cython-ext",
42
      "pin": {
43
        "kind": "harbor-registry",
44
        "dataset": "terminal-bench@2.0",
45
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
46
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
47
        "path": "build-cython-ext"
48
      },
49
      "environmentProven": true
50
    },
51
    {
52
      "id": "sqlite-with-gcov",
53
      "pin": {
54
        "kind": "harbor-registry",
55
        "dataset": "terminal-bench@2.0",
56
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
57
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
58
        "path": "sqlite-with-gcov"
59
      },
60
      "environmentProven": true
61
    },
62
    {
63
      "id": "fix-code-vulnerability",
64
      "pin": {
65
        "kind": "harbor-registry",
66
        "dataset": "terminal-bench@2.0",
67
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
68
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
69
        "path": "fix-code-vulnerability"
70
      },
71
      "environmentProven": true
72
    },
73
    {
74
      "id": "regex-log",
75
      "pin": {
76
        "kind": "harbor-registry",
77
        "dataset": "terminal-bench@2.0",
78
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
79
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
80
        "path": "regex-log"
81
      },
82
      "environmentProven": true
83
    },
84
    {
85
      "id": "count-dataset-tokens",
86
      "pin": {
87
        "kind": "harbor-registry",
88
        "dataset": "terminal-bench@2.0",
89
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
90
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
91
        "path": "count-dataset-tokens"
92
      },
93
      "environmentProven": true
94
    },
95
    {
96
      "id": "password-recovery",
97
      "pin": {
98
        "kind": "harbor-registry",
99
        "dataset": "terminal-bench@2.0",
100
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
101
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
102
        "path": "password-recovery"
103
      },
104
      "environmentProven": true
105
    },
106
    {
107
      "id": "openssl-selfsigned-cert",
108
      "pin": {
109
        "kind": "harbor-registry",
110
        "dataset": "terminal-bench@2.0",
111
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
112
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
113
        "path": "openssl-selfsigned-cert"
114
      },
115
      "environmentProven": true
116
    },
117
    {
118
      "id": "nginx-request-logging",
119
      "pin": {
120
        "kind": "harbor-registry",
121
        "dataset": "terminal-bench@2.0",
122
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
123
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
124
        "path": "nginx-request-logging"
125
      },
126
      "environmentProven": true
127
    },
128
    {
129
      "id": "schemelike-metacircular-eval",
130
      "pin": {
131
        "kind": "harbor-registry",
132
        "dataset": "terminal-bench@2.0",
133
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
134
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
135
        "path": "schemelike-metacircular-eval"
136
      },
137
      "environmentProven": true
138
    }
139
  ]
140
}
bench/suites/tb2-quick.suite.json added +32

@@ -0,0 +1,32 @@

1
{
2
  "schema": "openagents.effectiveness_suite.v1",
3
  "id": "tb2-quick",
4
  "tier": "score",
5
  "description": "Two quick Terminal-Bench 2.0 tasks: a narrow but real score suite, small enough to run repeatedly on one machine, floored for its own size. Never comparable to coder-effectiveness-v1 — different suite key.",
6
  "tasks": [
7
    {
8
      "id": "regex-log",
9
      "pin": {
10
        "kind": "harbor-registry",
11
        "dataset": "terminal-bench@2.0",
12
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
13
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
14
        "path": "regex-log"
15
      },
16
      "environmentProven": true,
17
      "rationale": "near-zero tool surface: the suite's test of whether the agent can just answer"
18
    },
19
    {
20
      "id": "openssl-selfsigned-cert",
21
      "pin": {
22
        "kind": "harbor-registry",
23
        "dataset": "terminal-bench@2.0",
24
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
25
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
26
        "path": "openssl-selfsigned-cert"
27
      },
28
      "environmentProven": true,
29
      "rationale": "a fully specified checklist: every command is known upfront, so round count is a tool habit rather than a reasoning result"
30
    }
31
  ]
32
}
bench/tasks/owned/README.md added +85

@@ -0,0 +1,85 @@

1
# Owned tasks: closed issues as graded work
2
3
Issue [#34](https://openagents.com/OpenAgentsInc/openagents/issues/34) asks the
4
effectiveness suite to draw on two sources: a bounded public subset, and "an
5
owned set drawn from this tracker's closed issues with known accepted
6
outcomes". The public half is
7
`bench/suites/swebench-verified-subset.suite.json`, resolved from Harbor's
8
registry. This directory is the owned half.
9
10
## Where the tasks come from
11
12
The forge records a closing reference on an issue as evidence, with the commit
13
that closed it. `bench/build-suites.mjs --issues <closed.json>` reads a
14
`openagents issue list --state closed --json` body and turns every issue that
15
carries one into a task pinned to that commit:
16
17
```json
18
{
19
  "id": "owned-issue-31",
20
  "pin": {
21
    "kind": "tracker-closed-issue",
22
    "repo": "OpenAgentsInc/openagents",
23
    "issue": 31,
24
    "acceptedCommit": "cf1861c9cb..."
25
  },
26
  "environmentProven": false,
27
  "rationale": "Adopt the proxy's reasoning and tool-call fidelity in the coder"
28
}
29
```
30
31
The accepted outcome is a fact the tracker holds, not a judgement this tooling
32
makes. That is the whole reason these are worth grading against: unlike a
33
synthesised task, somebody already decided what "done" looked like and shipped
34
it, and the diff is on record. An issue closed without a closing reference is
35
skipped by number rather than guessed at — it may be perfectly well closed, but
36
there is nothing to grade against.
37
38
At the time of writing, six of the tracker's fifty-eight closed issues carry a
39
closing-reference commit. All six land in `packages/openagents-cli`, and five of
40
the six touch a test file in the same commit, which is the shape the verifier
41
below needs.
42
43
## What is not built yet
44
45
**No container exists that can grade any of these.** The pin is real and the
46
suite is real; the environment is not. Every owned task therefore carries
47
`environmentProven: false`, and `parseSuiteManifest` refuses to let an unproven
48
task into a `score`-tier suite — so `owned-closed-issues.suite.json` is
49
`smoke`, and `coder-effectiveness-v1` holds the twenty proven public tasks and
50
none of these.
51
52
That refusal is the point rather than a limitation. A score suite that included
53
a task nobody could run would report those trials as missing, and a missing
54
trial reads as the coder failing rather than as an absent environment. The
55
suite would get quietly worse the day the environment broke, for a reason that
56
has nothing to do with the thing being measured.
57
58
## The verifier these tasks want
59
60
The construction is SWE-bench's, applied to our own history. For an issue whose
61
closing commit touches both source and test files:
62
63
1. Base the environment at the closing commit's **parent**, with the test-file
64
   half of the closing commit applied and the source half left out. The test
65
   then exists and fails.
66
2. The instruction is the issue body, which is what a human coder was given.
67
3. The verifier runs the touched test files. Pass means the agent made the
68
   recorded test pass; it does not mean the agent reproduced the recorded diff,
69
   and it should not.
70
71
Two things have to be true before the first of these is `environmentProven`,
72
and neither is cheap:
73
74
- **The image.** `packages/openagents-cli` sits in a 117-package pnpm
75
  workspace, so the environment is a repo snapshot plus an install, not a
76
  `FROM node:22-slim` and a copy. Build time and image size are the open
77
  question, and the answer decides whether the owned lane is run per release or
78
  per week.
79
- **The test half really failing at base.** A closing commit that only added a
80
  test for behaviour that already worked is not a graded task at all — it is a
81
  task that passes before the agent starts. Each candidate needs that checked
82
  by running the test at the base commit, once, before it is admitted.
83
84
Until both hold for a task, it stays here: pinned, described, and out of every
85
published score.
package.json modified +1

@@ -174,6 +174,7 @@

174 174
    "check:assure-repo-audit": "node --import tsx packages/assure-repo/src/cli.ts audit-check",
175 175
    "effectiveness:compare": "node --import tsx packages/coder-effectiveness/src/compare-cli.ts",
176 176
    "effectiveness:report": "node --import tsx packages/coder-effectiveness/src/cli.ts",
177
    "effectiveness:suites": "node bench/build-suites.mjs",
177 178
    "test:coder-effectiveness": "vp test --run packages/coder-effectiveness/src",
178 179
    "test:product-spec": "vp test --run packages/product-spec",
179 180
    "check:assure-repo-drift": "node --import tsx packages/assure-repo/src/cli.ts drift-check",
packages/coder-effectiveness/README.md modified +95 -7

@@ -16,12 +16,13 @@ question the run was for.

16 16
17 17
```sh
18 18
# 1. Run the suite. Harbor grades the trials.
19
bench/run-suite.sh bench/suites/tb2-cross-section.txt \
19
bench/run-suite.sh bench/suites/tb2-cross-section.suite.json \
20 20
  --model openai/gpt-5.6-luna --jobs-dir /tmp/gym-jobs-run
21 21
22 22
# 2. Score it, and record it.
23 23
pnpm run effectiveness:report -- /tmp/gym-jobs-run/<job-dir> \
24 24
  --suite tb2-cross-section --lane proxy \
25
  --suite-manifest bench/suites/tb2-cross-section.suite.json \
25 26
  --thresholds packages/coder-effectiveness/thresholds/tb2-cross-section.json \
26 27
  --append bench-results/tb2-cross-section.jsonl
27 28

@@ -34,6 +35,83 @@ the artifacts the run already leaves: each trial's `result.json` for the

34 35
verifier's decision, the coder's own ATIF `trajectory.json` for tokens and tool
35 36
calls, and `coder.txt` for the thread the trial ran in.
36 37
38
## The suites
39
40
`bench/suites/*.suite.json`, regenerated by `bench/build-suites.mjs` from
41
Harbor's registry and the tracker's closed issues.
42
43
| Suite                      | Tier  | Tasks | What it is                                                                                                                                                                                  |
44
| -------------------------- | ----- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
| `coder-effectiveness-v1`   | score | 20    | The headline suite: the terminal-bench cross-section plus the public SWE-bench subset.                                                                                                      |
46
| `tb2-cross-section`        | score | 12    | Twelve Terminal-Bench 2.0 tasks across distinct skill families. Selection rationale in `bench/suites/tb2-cross-section.md`.                                                                 |
47
| `swebench-verified-subset` | score | 8     | One `swebench-verified@1.0` instance from each of the eight repositories that contribute most, chosen by rule: the lexicographically first id in each.                                      |
48
| `tb2-quick`                | score | 2     | Narrow but real, small enough to run repeatedly on one machine, floored for its own size. Shares no suite key with the headline suite, so no comparison will ever place its rows beside it. |
49
| `smoke`                    | smoke | 2     | A liveness check. Declared smoke, so it can never be recorded.                                                                                                                              |
50
| `owned-closed-issues`      | smoke | 6     | Closed issues in this tracker with their closing commits. Smoke until the environments exist — see `bench/tasks/owned/README.md`.                                                           |
51
52
Issue #34 names SWE-bench-lite as the public candidate. Harbor's registry
53
carries `swebench-verified@1.0` instead, and that is the better half of the same
54
idea: the human-validated subset, whose statements and tests somebody confirmed
55
are solvable and correctly graded, reachable through the same
56
`harbor run --dataset` contract with no new harness code.
57
58
### The pin
59
60
A manifest task carries its identity rather than its label:
61
62
```json
63
{
64
  "id": "regex-log",
65
  "pin": {
66
    "kind": "harbor-registry",
67
    "dataset": "terminal-bench@2.0",
68
    "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
69
    "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
70
    "path": "regex-log"
71
  },
72
  "environmentProven": true
73
}
74
```
75
76
That triple is what the task digest is over, and the suite digest is over the
77
sorted task digests plus the tier. A bare list of names is not a digest of
78
anything: `regex-log` names a task whose content moved when the dataset moved,
79
and a suite that pinned only names would report two different measurements
80
under one heading.
81
82
`environmentProven` is whether a container for this task has ever been built
83
and graded. A `score` suite may not hold an unproven task — a trial nobody
84
could run reads as the coder failing rather than as a missing environment — so
85
an owned task lives in a `smoke` suite until somebody proves it.
86
87
## A smoke run is never a published score, structurally
88
89
The issue says so, and a README cannot make it true. Every benchmark grows a
90
fast lane, and every fast lane eventually gets read as the number. So the fast
91
run is made unable to reach the file the trend reads.
92
93
`classifyRun` compares the trials that actually ran against the manifest's
94
pinned task list. A run is `smoke` when the manifest declares it, when a pinned
95
task produced no trial, or when a trial ran a task the suite does not pin. The
96
runner cannot lie about the second: the evidence is the trial directories on
97
disk.
98
99
Two independent consequences follow, and neither can be flagged past:
100
101
- **The store refuses the row.** `smoke_run`, or `unclassified_run` when the run
102
  named no manifest at all. Publishing means reaching `bench-results/`, which is
103
  what the trend and the lane comparison read, so the check is at that door.
104
- **The gate carries a criterion it cannot pass.** `run_tier=score` is
105
  `unverifiable` on a smoke run, so the report exits 2 rather than 0. Every
106
  other criterion here scores a measurement and can legitimately be waived by an
107
  operator willing to set a low floor. This one says the run is not the thing
108
  the floors describe, so waiving it would waive the question rather than the
109
  answer.
110
111
Declining to name a suite is not the cheap way past the coverage check: an
112
unclassified run is refused too. Naming the suite is what exposes whether you
113
ran it.
114
37 115
## The metric
38 116
39 117
**Cost per accepted outcome is the run's total cost divided by the outcomes a

@@ -112,12 +190,12 @@ A measured breach outranks an unmeasurable criterion: a run that fails the

112 190
success floor and cannot be priced is `failed`, because something _was_
113 191
measured and it broke.
114 192
115
| Exit | Meaning                                                 |
116
| ---- | ------------------------------------------------------- |
117
| 0    | Every applicable floor passed.                          |
118
| 1    | A floor was breached.                                   |
119
| 2    | The gate could not be verified. Not a pass.             |
120
| 3    | The run was scored but `--append` refused to record it. |
193
| Exit | Meaning                                                                    |
194
| ---- | -------------------------------------------------------------------------- |
195
| 0    | Every applicable floor passed.                                             |
196
| 1    | A floor was breached.                                                      |
197
| 2    | The gate could not be verified. Not a pass. A smoke run always lands here. |
198
| 3    | The run was scored but `--append` refused to record it.                    |
121 199
122 200
Code 3 only ever replaces a 0. A breach or an unverifiable gate is the more
123 201
important finding, so a non-zero gate always outranks a bookkeeping refusal.

@@ -198,6 +276,11 @@ called, no Docker image runs, and no clock is read.

198 276
| `crashed-verifier` | Ungraded trials stay out of both buckets.                         |
199 277
| `regressed-lane`   | A regression raises cost per accepted outcome and trips the gate. |
200 278
279
The suite manifests beside them cover the pin and the smoke rule:
280
`fixture-suite` pins the four tasks the priced and regressed jobs ran,
281
`fixture-suite-3` the three the others ran, and `fixture-suite-5` adds a task
282
nobody ran, so a complete-looking run classifies as the partial run it is.
283
201 284
The store and comparison cases build their rows from the same fixtures, into a
202 285
temporary store, with the clock injected.
203 286

@@ -219,6 +302,11 @@ leaves unpriced stays unpriced here — that omission is the signal.

219 302
220 303
## Not yet done
221 304
305
- **The owned lane's environments.** Six closed issues are pinned to their
306
  closing commits and none of them has a container that can grade it, so they
307
  are `environmentProven: false` and their suite is `smoke`.
308
  `bench/tasks/owned/README.md` has the construction and the two things that
309
  have to be true before the first one is admitted to a score.
222 310
- **Per-model cost from the coder's own trajectory.** The ATIF exporter writes
223 311
  `total_prompt_tokens` and `total_completion_tokens` and no cost, and cached
224 312
  reads survive only per step as `metrics.extra.cache_read_input_tokens`, which
packages/coder-effectiveness/fixtures/fixture-suite-3.suite.json added +41

@@ -0,0 +1,41 @@

1
{
2
  "schema": "openagents.effectiveness_suite.v1",
3
  "id": "fixture-suite-3",
4
  "tier": "score",
5
  "description": "The three tasks the unpriced-lane, mixed-lane, and crashed-verifier fixture jobs ran.",
6
  "tasks": [
7
    {
8
      "id": "build-cmake",
9
      "pin": {
10
        "kind": "harbor-registry",
11
        "dataset": "terminal-bench@2.0",
12
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
13
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
14
        "path": "build-cmake"
15
      },
16
      "environmentProven": true
17
    },
18
    {
19
      "id": "fix-git",
20
      "pin": {
21
        "kind": "harbor-registry",
22
        "dataset": "terminal-bench@2.0",
23
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
24
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
25
        "path": "fix-git"
26
      },
27
      "environmentProven": true
28
    },
29
    {
30
      "id": "parse-log",
31
      "pin": {
32
        "kind": "harbor-registry",
33
        "dataset": "terminal-bench@2.0",
34
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
35
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
36
        "path": "parse-log"
37
      },
38
      "environmentProven": true
39
    }
40
  ]
41
}
packages/coder-effectiveness/fixtures/fixture-suite-5.suite.json added +63

@@ -0,0 +1,63 @@

1
{
2
  "schema": "openagents.effectiveness_suite.v1",
3
  "id": "fixture-suite-5",
4
  "tier": "score",
5
  "description": "The four tasks the priced-lane fixture ran, plus one it did not, so a complete-looking run classifies as the partial run it is.",
6
  "tasks": [
7
    {
8
      "id": "build-cmake",
9
      "pin": {
10
        "kind": "harbor-registry",
11
        "dataset": "terminal-bench@2.0",
12
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
13
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
14
        "path": "build-cmake"
15
      },
16
      "environmentProven": true
17
    },
18
    {
19
      "id": "fix-git",
20
      "pin": {
21
        "kind": "harbor-registry",
22
        "dataset": "terminal-bench@2.0",
23
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
24
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
25
        "path": "fix-git"
26
      },
27
      "environmentProven": true
28
    },
29
    {
30
      "id": "parse-log",
31
      "pin": {
32
        "kind": "harbor-registry",
33
        "dataset": "terminal-bench@2.0",
34
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
35
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
36
        "path": "parse-log"
37
      },
38
      "environmentProven": true
39
    },
40
    {
41
      "id": "port-forward",
42
      "pin": {
43
        "kind": "harbor-registry",
44
        "dataset": "terminal-bench@2.0",
45
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
46
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
47
        "path": "port-forward"
48
      },
49
      "environmentProven": true
50
    },
51
    {
52
      "id": "never-run",
53
      "pin": {
54
        "kind": "harbor-registry",
55
        "dataset": "terminal-bench@2.0",
56
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
57
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
58
        "path": "never-run"
59
      },
60
      "environmentProven": true
61
    }
62
  ]
63
}
packages/coder-effectiveness/fixtures/fixture-suite.suite.json added +52

@@ -0,0 +1,52 @@

1
{
2
  "schema": "openagents.effectiveness_suite.v1",
3
  "id": "fixture-suite",
4
  "tier": "score",
5
  "description": "The four tasks the priced-lane and regressed-lane fixture jobs ran.",
6
  "tasks": [
7
    {
8
      "id": "build-cmake",
9
      "pin": {
10
        "kind": "harbor-registry",
11
        "dataset": "terminal-bench@2.0",
12
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
13
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
14
        "path": "build-cmake"
15
      },
16
      "environmentProven": true
17
    },
18
    {
19
      "id": "fix-git",
20
      "pin": {
21
        "kind": "harbor-registry",
22
        "dataset": "terminal-bench@2.0",
23
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
24
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
25
        "path": "fix-git"
26
      },
27
      "environmentProven": true
28
    },
29
    {
30
      "id": "parse-log",
31
      "pin": {
32
        "kind": "harbor-registry",
33
        "dataset": "terminal-bench@2.0",
34
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
35
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
36
        "path": "parse-log"
37
      },
38
      "environmentProven": true
39
    },
40
    {
41
      "id": "port-forward",
42
      "pin": {
43
        "kind": "harbor-registry",
44
        "dataset": "terminal-bench@2.0",
45
        "gitUrl": "https://github.com/laude-institute/terminal-bench-2.git",
46
        "commit": "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
47
        "path": "port-forward"
48
      },
49
      "environmentProven": true
50
    }
51
  ]
52
}
packages/coder-effectiveness/src/cli.ts modified +26 -4

@@ -30,6 +30,7 @@ import {

30 30
} from "./pricing.ts";
31 31
import { renderReport } from "./render.ts";
32 32
import { appendResultRow } from "./results-store.ts";
33
import { classifyRun, parseSuiteManifest, type RunClassification } from "./suite-manifest.ts";
33 34
import { evaluateThresholds, parseThresholds, type ThresholdGate } from "./thresholds.ts";
34 35
35 36
const USAGE = `Usage: coder-effectiveness report <job-dir> [options]

@@ -40,6 +41,12 @@ Arguments:

40 41
41 42
Options:
42 43
  --suite <name>         Suite name recorded in the report. Default: terminal-bench@2.0
44
  --suite-manifest <file>
45
                         The suite manifest this run claims to have covered.
46
                         Required by --append: a run that names no manifest has
47
                         nothing to say which pinned task list it measured. A
48
                         run that did not cover every pinned task is a smoke
49
                         run, whatever it was called, and cannot be recorded.
43 50
  --lane <proxy|local>   Lane the run used. Default: proxy
44 51
  --thresholds <file>    JSON floors to score the run against. Without it the
45 52
                         report is printed and no gate runs.

@@ -69,6 +76,7 @@ const APPEND_REFUSED_EXIT = 3;

69 76
interface Arguments {
70 77
  readonly jobDir: string;
71 78
  readonly suite: string;
79
  readonly manifestPath: string | null;
72 80
  readonly lane: string;
73 81
  readonly thresholdsPath: string | null;
74 82
  readonly modelsPath: string | null;

@@ -80,6 +88,7 @@ interface Arguments {

80 88
const parseArguments = (argv: ReadonlyArray<string>): Arguments | "help" => {
81 89
  let jobDir: string | null = null;
82 90
  let suite = "terminal-bench@2.0";
91
  let manifestPath: string | null = null;
83 92
  let lane = "proxy";
84 93
  let thresholdsPath: string | null = null;
85 94
  let modelsPath: string | null = null;

@@ -98,6 +107,10 @@ const parseArguments = (argv: ReadonlyArray<string>): Arguments | "help" => {

98 107
      suite = expectValue(argv, (index += 1), "--suite");
99 108
      continue;
100 109
    }
110
    if (argument === "--suite-manifest") {
111
      manifestPath = expectValue(argv, (index += 1), "--suite-manifest");
112
      continue;
113
    }
101 114
    if (argument === "--lane") {
102 115
      lane = expectValue(argv, (index += 1), "--lane");
103 116
      continue;

@@ -127,7 +140,7 @@ const parseArguments = (argv: ReadonlyArray<string>): Arguments | "help" => {

127 140
  if (lane !== "proxy" && lane !== "local") {
128 141
    throw new Error(`--lane must be proxy or local, got: ${lane}`);
129 142
  }
130
  return { jobDir, suite, lane, thresholdsPath, modelsPath, appendPath, json };
143
  return { jobDir, suite, manifestPath, lane, thresholdsPath, modelsPath, appendPath, json };
131 144
};
132 145
133 146
const expectValue = (argv: ReadonlyArray<string>, index: number, option: string): string => {

@@ -173,25 +186,34 @@ const main = (argv: ReadonlyArray<string>): number => {

173 186
    });
174 187
    const report = summarizeRun(run, catalog, catalogVersion);
175 188
189
    const classification: RunClassification | null =
190
      parsed.manifestPath === null
191
        ? null
192
        : classifyRun(
193
            parseSuiteManifest(JSON.parse(readFileSync(parsed.manifestPath, "utf8"))),
194
            run.trials.map((trial) => trial.task),
195
          );
196
176 197
    const gate =
177 198
      parsed.thresholdsPath === null
178 199
        ? null
179 200
        : evaluateThresholds(
180 201
            report,
181 202
            parseThresholds(JSON.parse(readFileSync(parsed.thresholdsPath, "utf8"))),
203
            classification,
182 204
          );
183 205
184 206
    const appended =
185 207
      parsed.appendPath === null
186 208
        ? null
187
        : appendResultRow(parsed.appendPath, report, gate, {
209
        : appendResultRow(parsed.appendPath, report, gate, classification, {
188 210
            recordedAt: new Date().toISOString(),
189 211
          });
190 212
191 213
    process.stdout.write(
192 214
      parsed.json
193
        ? `${JSON.stringify({ report, gate, appended }, null, 2)}\n`
194
        : renderReport(report, gate),
215
        ? `${JSON.stringify({ report, classification, gate, appended }, null, 2)}\n`
216
        : renderReport(report, gate, classification),
195 217
    );
196 218
    if (appended !== null && !parsed.json) {
197 219
      process.stdout.write(
packages/coder-effectiveness/src/compare.test.ts modified +24 -11

@@ -7,6 +7,7 @@

7 7
 * runs.
8 8
 */
9 9
10
import { readFileSync } from "node:fs";
10 11
import { fileURLToPath } from "node:url";
11 12
import { describe, expect, test } from "vite-plus/test";
12 13

@@ -16,23 +17,35 @@ import { readHarborJob } from "./harbor-job.ts";

16 17
import { CODER_RATE_CATALOG_VERSION } from "./pricing.ts";
17 18
import { renderComparison } from "./render-compare.ts";
18 19
import { buildResultRow, type BenchResultRow } from "./results-store.ts";
20
import { classifyRun, parseSuiteManifest } from "./suite-manifest.ts";
19 21
20 22
const fixture = (name: string): string =>
21 23
  fileURLToPath(new URL(`../fixtures/${name}`, import.meta.url));
22 24
23
const row = (name: string, lane: string, recordedAt: string): BenchResultRow =>
24
  buildResultRow(
25
    summarizeRun(
26
      readHarborJob(fixture(name), {
27
        suite: "tb2-cross-section",
28
        lane,
29
        rateCatalogVersion: CODER_RATE_CATALOG_VERSION,
30
      }),
25
/**
26
 * Every stored row is a full score run of some suite — the store refuses
27
 * anything else — so the rows these cases compare are built the same way, from
28
 * a manifest over exactly the tasks the fixture job ran.
29
 */
30
const row = (name: string, lane: string, recordedAt: string): BenchResultRow => {
31
  const report = summarizeRun(
32
    readHarborJob(fixture(name), {
33
      suite: "tb2-cross-section",
34
      lane,
35
      rateCatalogVersion: CODER_RATE_CATALOG_VERSION,
36
    }),
37
  );
38
  const tasks = report.perTrial.map((trial) => trial.task);
39
  const manifest = parseSuiteManifest(
40
    JSON.parse(
41
      readFileSync(
42
        fixture(tasks.length === 4 ? "fixture-suite.suite.json" : "fixture-suite-3.suite.json"),
43
        "utf8",
44
      ),
31 45
    ),
32
    null,
33
    null,
34
    { recordedAt },
35 46
  );
47
  return buildResultRow(report, null, classifyRun(manifest, tasks), null, { recordedAt });
48
};
36 49
37 50
describe("trend on one lane", () => {
38 51
  test("reads a regression between two consecutive runs as cost rising", () => {
packages/coder-effectiveness/src/index.ts modified +13

@@ -46,6 +46,19 @@ export {

46 46
  suiteKeyOf,
47 47
  verifyResultChain,
48 48
} from "./results-store.ts";
49
export {
50
  classifyRun,
51
  parseSuiteManifest,
52
  type RunClassification,
53
  type RunTier,
54
  type SmokeReason,
55
  SUITE_MANIFEST_SCHEMA,
56
  type SuiteManifest,
57
  type SuiteTask,
58
  type SuiteTaskPin,
59
  suiteDigestOf,
60
  taskDigestOf,
61
} from "./suite-manifest.ts";
49 62
export {
50 63
  type EffectivenessThresholds,
51 64
  evaluateThresholds,
packages/coder-effectiveness/src/render.ts modified +33 -1

@@ -9,6 +9,7 @@

9 9
10 10
import type { EffectivenessReport } from "./effectiveness.ts";
11 11
import { CODER_RATE_CATALOG_SOURCE_REF } from "./pricing.ts";
12
import type { RunClassification } from "./suite-manifest.ts";
12 13
import type { ThresholdGate } from "./thresholds.ts";
13 14
14 15
const usd = (value: number): string => `$${value.toFixed(4)}`;

@@ -19,7 +20,37 @@ const rate = (value: number | null): string =>

19 20
const count = (value: number | null): string =>
20 21
  value === null ? "unknown" : value.toLocaleString("en-US");
21 22
22
export const renderReport = (report: EffectivenessReport, gate: ThresholdGate | null): string => {
23
/**
24
 * The suite pin, and the smoke verdict where there is one.
25
 *
26
 * A smoke run says so in the header rather than in a footnote, because the
27
 * header is the part that gets pasted into an issue comment. Everything below
28
 * it is arithmetic that is perfectly correct about a run that is not the suite.
29
 */
30
const tierLines = (classification: RunClassification | null): ReadonlyArray<string> => {
31
  if (classification === null) {
32
    return ["  suite pin       none — this run named no manifest, so it is a report, not a score"];
33
  }
34
  const lines = [
35
    `  suite pin       ${classification.suiteId} (${classification.suiteDigest})`,
36
    `  coverage        ${String(classification.ran.length)} of ${String(classification.expected.length)} pinned tasks ran`,
37
  ];
38
  if (classification.tier === "smoke") {
39
    lines.push("  tier            SMOKE — this run is not a publishable score");
40
    for (const reason of classification.smokeReasons) {
41
      lines.push(`    ${reason.kind.padEnd(20)} ${reason.detail}`);
42
    }
43
  } else {
44
    lines.push("  tier            score");
45
  }
46
  return lines;
47
};
48
49
export const renderReport = (
50
  report: EffectivenessReport,
51
  gate: ThresholdGate | null,
52
  classification: RunClassification | null = null,
53
): string => {
23 54
  const lines: Array<string> = [];
24 55
25 56
  lines.push(`Coder effectiveness — ${report.suite} on the ${report.lane} lane`);

@@ -29,6 +60,7 @@ export const renderReport = (report: EffectivenessReport, gate: ThresholdGate |

29 60
  lines.push(`  cli version     ${report.agentVersions.join(", ") || "unknown"}`);
30 61
  lines.push(`  rate catalog    ${report.rateCatalogVersion}`);
31 62
  lines.push(`  rate source     ${CODER_RATE_CATALOG_SOURCE_REF}`);
63
  lines.push(...tierLines(classification));
32 64
  lines.push("");
33 65
34 66
  lines.push("Outcomes");
packages/coder-effectiveness/src/results-store.test.ts modified +139 -8

@@ -25,6 +25,12 @@ import {

25 25
  verifyResultChain,
26 26
  type BenchResultRow,
27 27
} from "./results-store.ts";
28
import {
29
  classifyRun,
30
  parseSuiteManifest,
31
  SUITE_MANIFEST_SCHEMA,
32
  type RunClassification,
33
} from "./suite-manifest.ts";
28 34
import { evaluateThresholds, parseThresholds } from "./thresholds.ts";
29 35
30 36
const fixture = (name: string): string =>

@@ -39,6 +45,37 @@ const report = (name: string, lane = "proxy"): EffectivenessReport =>

39 45
    }),
40 46
  );
41 47
48
/**
49
 * A manifest over exactly the tasks a fixture job ran, so the fixture reads as
50
 * a complete score run. The store's smoke refusal has its own cases below; the
51
 * rest of the file is about the chain, and a smoke classification there would
52
 * only ever be measuring the refusal twice.
53
 */
54
const manifestFor = (name: string) =>
55
  parseSuiteManifest({
56
    schema: SUITE_MANIFEST_SCHEMA,
57
    id: "fixture-suite",
58
    tier: "score",
59
    description: "the tasks this fixture job ran",
60
    tasks: report(name).perTrial.map((trial) => ({
61
      id: trial.task,
62
      pin: {
63
        kind: "harbor-registry",
64
        dataset: "terminal-bench@2.0",
65
        gitUrl: "https://github.com/laude-institute/terminal-bench-2.git",
66
        commit: "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
67
        path: trial.task,
68
      },
69
      environmentProven: true,
70
    })),
71
  });
72
73
const scoreOf = (name: string): RunClassification =>
74
  classifyRun(
75
    manifestFor(name),
76
    report(name).perTrial.map((trial) => trial.task),
77
  );
78
42 79
const floors = parseThresholds(
43 80
  JSON.parse(
44 81
    readFileSync(

@@ -61,7 +98,7 @@ afterEach(() => {

61 98
});
62 99
63 100
const append = (name: string, lane: string, recordedAt: string) =>
64
  appendResultRow(store, report(name, lane), null, { recordedAt });
101
  appendResultRow(store, report(name, lane), null, scoreOf(name), { recordedAt });
65 102
66 103
const rows = (): ReadonlyArray<BenchResultRow> => readResultRows(store);
67 104

@@ -83,7 +120,7 @@ describe("appending a run", () => {

83 120
84 121
  test("creates the store directory rather than requiring it to exist", () => {
85 122
    const nested = join(directory, "does", "not", "exist", "suite.jsonl");
86
    const result = appendResultRow(nested, report("priced-lane"), null, {
123
    const result = appendResultRow(nested, report("priced-lane"), null, scoreOf("priced-lane"), {
87 124
      recordedAt: "2026-08-25T10:00:00.000Z",
88 125
    });
89 126

@@ -100,6 +137,7 @@ describe("appending a run", () => {

100 137
      store,
101 138
      report("priced-lane"),
102 139
      evaluateThresholds(report("priced-lane"), floors),
140
      scoreOf("priced-lane"),
103 141
      {
104 142
        recordedAt: "2026-08-25T10:00:00.000Z",
105 143
      },

@@ -148,7 +186,98 @@ describe("what the store refuses", () => {

148 186
  test("throws on a row written under another schema", () => {
149 187
    writeFileSync(store, `${JSON.stringify({ schema: "something.else.v1" })}\n`, "utf8");
150 188
151
    expect(() => readResultRows(store)).toThrow(/expected openagents\.bench_result\.v1/u);
189
    expect(() => readResultRows(store)).toThrow(/expected openagents\.bench_result\.v2/u);
190
  });
191
192
  test("names a v1 row for what it is rather than reading it as a v2 one", () => {
193
    writeFileSync(store, `${JSON.stringify({ schema: "openagents.bench_result.v1" })}\n`, "utf8");
194
195
    expect(() => readResultRows(store)).toThrow(/carries no suite digest/u);
196
  });
197
});
198
199
/**
200
 * The structural half of "a fast/smoke run is never a published score".
201
 *
202
 * Publishing means reaching this file, because this file is what the trend and
203
 * the lane comparison read. These cases are the enforcement — the gate's
204
 * `run_tier` criterion in `thresholds.test.ts` is the other half, and neither
205
 * relies on the other.
206
 */
207
describe("the smoke rule at the store door", () => {
208
  test("refuses a run that covered only part of the suite it named", () => {
209
    const partial = classifyRun(manifestFor("priced-lane"), ["fix-git"]);
210
211
    const result = appendResultRow(store, report("priced-lane"), null, partial, {
212
      recordedAt: "2026-08-25T10:00:00.000Z",
213
    });
214
215
    expect(result).toMatchObject({ appended: false, refusal: "smoke_run" });
216
    expect(readResultRows(store)).toEqual([]);
217
  });
218
219
  test("refuses a run of a suite that declares itself a fast lane", () => {
220
    const declared = classifyRun(
221
      parseSuiteManifest({ ...manifestFor("priced-lane"), tier: "smoke" }),
222
      report("priced-lane").perTrial.map((trial) => trial.task),
223
    );
224
225
    const result = appendResultRow(store, report("priced-lane"), null, declared, {
226
      recordedAt: "2026-08-25T10:00:00.000Z",
227
    });
228
229
    expect(result).toMatchObject({ appended: false, refusal: "smoke_run" });
230
  });
231
232
  test("refuses a run that named no suite at all", () => {
233
    // Naming the suite is what exposes whether you ran it, so declining to name
234
    // one cannot be the cheap way past the coverage check.
235
    const result = appendResultRow(store, report("priced-lane"), null, null, {
236
      recordedAt: "2026-08-25T10:00:00.000Z",
237
    });
238
239
    expect(result).toMatchObject({ appended: false, refusal: "unclassified_run" });
240
  });
241
242
  test("refuses a smoke run before it can even read the store", () => {
243
    // Order matters: a smoke run must not learn the head receipt, so it cannot
244
    // get most of the way through an append and be finished by hand.
245
    writeFileSync(store, "{ not json\n", "utf8");
246
    const partial = classifyRun(manifestFor("priced-lane"), ["fix-git"]);
247
248
    const result = appendResultRow(store, report("priced-lane"), null, partial, {
249
      recordedAt: "2026-08-25T10:00:00.000Z",
250
    });
251
252
    expect(result).toMatchObject({ appended: false, refusal: "smoke_run" });
253
  });
254
255
  test("records the suite pin on a row it does accept", () => {
256
    append("priced-lane", "proxy", "2026-08-25T10:00:00.000Z");
257
258
    const row = rows()[0]!;
259
    expect(row.tier).toBe("score");
260
    expect(row.suiteId).toBe("fixture-suite");
261
    expect(row.suiteDigest).toMatch(/^suite-manifest:[0-9a-f]{64}$/u);
262
  });
263
264
  test("gives two runs of one task list different suite keys under different pins", () => {
265
    // The task names match; the dataset commit does not. Without the suite
266
    // digest in the key these two would compare as the same measurement.
267
    const moved = parseSuiteManifest({
268
      ...manifestFor("priced-lane"),
269
      tasks: manifestFor("priced-lane").tasks.map((task) =>
270
        Object.assign({}, task, { pin: Object.assign({}, task.pin, { commit: "0".repeat(40) }) }),
271
      ),
272
    });
273
    const movedRun = classifyRun(
274
      moved,
275
      report("priced-lane").perTrial.map((trial) => trial.task),
276
    );
277
278
    expect(suiteKeyOf(report("priced-lane"), movedRun)).not.toBe(
279
      suiteKeyOf(report("priced-lane"), scoreOf("priced-lane")),
280
    );
152 281
  });
153 282
});
154 283

@@ -185,10 +314,10 @@ describe("the receipt chain", () => {

185 314
  });
186 315
187 316
  test("is stable across two builds of the same row", () => {
188
    const first = buildResultRow(report("priced-lane"), null, null, {
317
    const first = buildResultRow(report("priced-lane"), null, scoreOf("priced-lane"), null, {
189 318
      recordedAt: "2026-08-25T10:00:00.000Z",
190 319
    });
191
    const second = buildResultRow(report("priced-lane"), null, null, {
320
    const second = buildResultRow(report("priced-lane"), null, scoreOf("priced-lane"), null, {
192 321
      recordedAt: "2026-08-25T10:00:00.000Z",
193 322
    });
194 323

@@ -216,13 +345,15 @@ describe("what a row records", () => {

216 345
  });
217 346
218 347
  test("gives two runs of the same tasks the same suite key across different lanes", () => {
219
    expect(suiteKeyOf(report("priced-lane", "proxy"))).toBe(
220
      suiteKeyOf(report("regressed-lane", "local")),
348
    expect(suiteKeyOf(report("priced-lane", "proxy"), scoreOf("priced-lane"))).toBe(
349
      suiteKeyOf(report("regressed-lane", "local"), scoreOf("regressed-lane")),
221 350
    );
222 351
  });
223 352
224 353
  test("gives two runs of different task lists different suite keys", () => {
225
    expect(suiteKeyOf(report("priced-lane"))).not.toBe(suiteKeyOf(report("unpriced-lane")));
354
    expect(suiteKeyOf(report("priced-lane"), scoreOf("priced-lane"))).not.toBe(
355
      suiteKeyOf(report("unpriced-lane"), scoreOf("unpriced-lane")),
356
    );
226 357
  });
227 358
228 359
  test("gives two lanes of the same tasks different run digests", () => {
packages/coder-effectiveness/src/results-store.ts modified +85 -7

@@ -23,6 +23,15 @@

23 23
 * being read next to its reason, which is the whole failure this suite exists
24 24
 * to prevent. The disposition and the coverage travel with every row so a later
25 25
 * reader can tell "we did not measure this" from "this cost nothing".
26
 *
27
 * THIS FILE IS WHERE "A SMOKE RUN IS NEVER A PUBLISHED SCORE" IS ENFORCED.
28
 * Publishing, here, means exactly one thing: reaching this store, which is what
29
 * the trend and the lane comparison read and what a status panel would read
30
 * after them. So the check belongs at the door rather than in the renderer.
31
 * {@link appendResultRow} takes a {@link RunClassification} and refuses two
32
 * shapes outright — a run that did not cover its suite, and a run that declined
33
 * to name a suite at all. Neither refusal can be flagged past, because a flag
34
 * that turned them off would be the feature the rule exists to prevent.
26 35
 */
27 36
28 37
import { createHash } from "node:crypto";

@@ -35,9 +44,19 @@ import type {

35 44
  EffectivenessReport,
36 45
} from "./effectiveness.ts";
37 46
import type { RateBasis } from "./pricing.ts";
47
import type { RunClassification } from "./suite-manifest.ts";
38 48
import type { CriterionVerdict, ThresholdGate } from "./thresholds.ts";
39 49
40
export const BENCH_RESULT_SCHEMA = "openagents.bench_result.v1";
50
/**
51
 * Bumped from v1 when the suite pin became mandatory. A v1 row carried no
52
 * `suiteId` or `suiteDigest`, so nothing in it says which pinned task list it
53
 * measured — readable as history, not comparable to a v2 row, and
54
 * {@link readResultRows} says so by name rather than by silently coercing it.
55
 */
56
export const BENCH_RESULT_SCHEMA = "openagents.bench_result.v2";
57
58
/** The v1 rows this store used to write, kept only to name them in an error. */
59
const BENCH_RESULT_SCHEMA_V1 = "openagents.bench_result.v1";
41 60
42 61
/**
43 62
 * One graded run, flattened to the columns a trend or a lane comparison reads.

@@ -63,6 +82,16 @@ export interface BenchResultRow {

63 82
  readonly suiteKey: string;
64 83
  readonly jobId: string | null;
65 84
85
  /** The manifest this run claimed, and the digest over its pinned tasks. */
86
  readonly suiteId: string;
87
  readonly suiteDigest: string;
88
  /**
89
   * Always `score` in a stored row: the store refuses anything else. It is
90
   * written down anyway so a reader of the file never has to know that rule to
91
   * trust what the rows are.
92
   */
93
  readonly tier: "score";
94
66 95
  readonly models: ReadonlyArray<string>;
67 96
  readonly agentVersions: ReadonlyArray<string>;
68 97
  readonly rateCatalogVersion: string;

@@ -115,10 +144,19 @@ export interface AppendOptions {

115 144
 * comparison varies. The rate catalog version is in, because a cost figure
116 145
 * computed from one catalog and a cost figure computed from another are not the
117 146
 * same measurement even when the tasks match.
147
 *
148
 * The manifest's suite digest is in too, and it is the stronger half. The task
149
 * names alone say a run touched `regex-log`; the suite digest says which
150
 * `regex-log` — which dataset, at which commit — so two rows that agree here
151
 * agree about the work and not merely about its labels.
118 152
 */
119
export const suiteKeyOf = (report: EffectivenessReport): string => {
153
export const suiteKeyOf = (
154
  report: EffectivenessReport,
155
  classification: RunClassification,
156
): string => {
120 157
  const source = JSON.stringify({
121 158
    suite: report.suite,
159
    suiteDigest: classification.suiteDigest,
122 160
    tasks: report.perTrial.map((trial) => trial.task).toSorted(),
123 161
    rateCatalogVersion: report.rateCatalogVersion,
124 162
  });

@@ -129,6 +167,7 @@ export const suiteKeyOf = (report: EffectivenessReport): string => {

129 167
export const buildResultRow = (
130 168
  report: EffectivenessReport,
131 169
  gate: ThresholdGate | null,
170
  classification: RunClassification,
132 171
  previousReceipt: string | null,
133 172
  options: AppendOptions,
134 173
): BenchResultRow => {

@@ -139,9 +178,13 @@ export const buildResultRow = (

139 178
    suite: report.suite,
140 179
    lane: report.lane,
141 180
    runDigest: report.runDigest,
142
    suiteKey: suiteKeyOf(report),
181
    suiteKey: suiteKeyOf(report, classification),
143 182
    jobId: report.jobId,
144 183
184
    suiteId: classification.suiteId,
185
    suiteDigest: classification.suiteDigest,
186
    tier: "score",
187
145 188
    models: report.models,
146 189
    agentVersions: report.agentVersions,
147 190
    rateCatalogVersion: report.rateCatalogVersion,

@@ -260,7 +303,14 @@ export const readResultRows = (storePath: string): ReadonlyArray<BenchResultRow>

260 303
    } catch {
261 304
      throw new Error(`${storePath} line ${String(index + 1)} is not JSON`);
262 305
    }
263
    const row = parsed as BenchResultRow;
306
    // Deliberately widened: a row on disk can carry any schema string, and the
307
    // point of the next two checks is to find out which.
308
    const row = parsed as { schema: string } as BenchResultRow;
309
    if ((row.schema as string) === BENCH_RESULT_SCHEMA_V1) {
310
      throw new Error(
311
        `${storePath} line ${String(index + 1)} is a ${BENCH_RESULT_SCHEMA_V1} row, written before a run had to name the suite manifest it covered. It carries no suite digest, so nothing in it says which pinned task list it measured and it cannot be compared to a ${BENCH_RESULT_SCHEMA} row. Move it to an archive file rather than migrating it: a digest cannot be invented for a run that never recorded one.`,
312
      );
313
    }
264 314
    if (row.schema !== BENCH_RESULT_SCHEMA) {
265 315
      throw new Error(
266 316
        `${storePath} line ${String(index + 1)} has schema ${String(row.schema)}, expected ${BENCH_RESULT_SCHEMA}`,

@@ -270,7 +320,7 @@ export const readResultRows = (storePath: string): ReadonlyArray<BenchResultRow>

270 320
  });
271 321
};
272 322
273
export type AppendRefusal = "duplicate_job" | "chain_broken";
323
export type AppendRefusal = "duplicate_job" | "chain_broken" | "smoke_run" | "unclassified_run";
274 324
275 325
export type AppendResult =
276 326
  | { readonly appended: true; readonly row: BenchResultRow }

@@ -279,9 +329,16 @@ export type AppendResult =

279 329
/**
280 330
 * Append one graded run to a store.
281 331
 *
282
 * Two refusals, both returned rather than thrown, because both are ordinary
332
 * Four refusals, all returned rather than thrown, because all four are ordinary
283 333
 * operator situations rather than programming errors:
284 334
 *
335
 * - `unclassified_run` — the run named no suite manifest, so nothing says what
336
 *   it was supposed to cover. A row whose task list is only "whatever ran" can
337
 *   be compared to a later row that ran less, and the trend would read the
338
 *   difference as the coder changing.
339
 * - `smoke_run` — the run did not cover the suite it named, or the suite says
340
 *   it is a fast lane. Either way the number is over a different set of work
341
 *   than the suite's other rows, and this is the door it does not get through.
285 342
 * - `duplicate_job` — this Harbor job is already in the store. Re-scoring a job
286 343
 *   with a different thresholds file is a useful thing to do and a second row
287 344
 *   is not what it produces; two rows for one execution would double-count it

@@ -289,13 +346,34 @@ export type AppendResult =

289 346
 * - `chain_broken` — the existing store does not verify, so appending to it
290 347
 *   would extend a history that has already been rewritten and bury the break
291 348
 *   one row deeper.
349
 *
350
 * The order matters: the two classification refusals are checked before the
351
 * file is read at all, so a smoke run cannot even learn the store's head
352
 * receipt, and a caller cannot get most of the way through an append and then
353
 * be tempted to finish it.
292 354
 */
293 355
export const appendResultRow = (
294 356
  storePath: string,
295 357
  report: EffectivenessReport,
296 358
  gate: ThresholdGate | null,
359
  classification: RunClassification | null,
297 360
  options: AppendOptions,
298 361
): AppendResult => {
362
  if (classification === null) {
363
    return {
364
      appended: false,
365
      refusal: "unclassified_run",
366
      reason: `this run named no suite manifest, so nothing records which pinned task list it was supposed to cover; pass --suite-manifest to record a run in ${storePath}`,
367
    };
368
  }
369
  if (classification.tier !== "score") {
370
    return {
371
      appended: false,
372
      refusal: "smoke_run",
373
      reason: `this is a smoke run and a smoke run is never a published score, so it was not recorded in ${storePath}: ${classification.smokeReasons.map((reason) => reason.detail).join("; ")}`,
374
    };
375
  }
376
299 377
  const rows = readResultRows(storePath);
300 378
  const verdict = verifyResultChain(rows);
301 379
  if (!verdict.ok) {

@@ -313,7 +391,7 @@ export const appendResultRow = (

313 391
    };
314 392
  }
315 393
316
  const row = buildResultRow(report, gate, verdict.head, options);
394
  const row = buildResultRow(report, gate, classification, verdict.head, options);
317 395
  mkdirSync(dirname(storePath), { recursive: true });
318 396
  appendFileSync(storePath, `${JSON.stringify(row)}\n`, "utf8");
319 397
  return { appended: true, row };
packages/coder-effectiveness/src/store-cli.test.ts modified +78 -1

@@ -44,11 +44,25 @@ afterEach(() => {

44 44
  rmSync(directory, { recursive: true, force: true });
45 45
});
46 46
47
/**
48
 * The manifest a fixture job fully covers. Recording a run requires naming one,
49
 * so every case that expects a row has to pass it; the cases that expect a
50
 * refusal pass a different one, or none.
51
 */
52
const manifestFor = (job: string): string =>
53
  fixture(
54
    job === "priced-lane" || job === "regressed-lane"
55
      ? "fixture-suite.suite.json"
56
      : "fixture-suite-3.suite.json",
57
  );
58
47 59
const record = (job: string, lane: string) =>
48 60
  spawn(reportCli, [
49 61
    fixture(job),
50 62
    "--suite",
51 63
    "tb2-cross-section",
64
    "--suite-manifest",
65
    manifestFor(job),
52 66
    "--lane",
53 67
    lane,
54 68
    "--append",

@@ -93,7 +107,14 @@ describe("report --append", () => {

93 107
  });
94 108
95 109
  test("carries the append result into --json", () => {
96
    const result = spawn(reportCli, [fixture("priced-lane"), "--append", store, "--json"]);
110
    const result = spawn(reportCli, [
111
      fixture("priced-lane"),
112
      "--suite-manifest",
113
      manifestFor("priced-lane"),
114
      "--append",
115
      store,
116
      "--json",
117
    ]);
97 118
    const parsed = JSON.parse(result.stdout) as {
98 119
      appended: { appended: boolean; row: { receipt: string } };
99 120
    };

@@ -104,6 +125,62 @@ describe("report --append", () => {

104 125
  });
105 126
});
106 127
128
/**
129
 * The smoke rule as an operator meets it: at the command line, with an exit
130
 * code. The unit cases prove the refusals; these prove a scheduled job cannot
131
 * route around them by leaving an argument off or by running fewer tasks.
132
 */
133
describe("report --append and the smoke rule", () => {
134
  test("refuses to record a run that named no suite manifest", () => {
135
    const result = spawn(reportCli, [fixture("priced-lane"), "--append", store]);
136
137
    expect(result.status).toBe(3);
138
    expect(result.stdout).toContain("unclassified_run");
139
    expect(readResultRows(store)).toEqual([]);
140
  });
141
142
  test("refuses to record a run that covered part of the suite it named", () => {
143
    // `crashed-verifier` ran three tasks; `fixture-suite` pins four. Nothing on
144
    // the command line says so — the trial directories do.
145
    const result = spawn(reportCli, [
146
      fixture("crashed-verifier"),
147
      "--suite-manifest",
148
      fixture("fixture-suite.suite.json"),
149
      "--append",
150
      store,
151
    ]);
152
153
    expect(result.status).toBe(3);
154
    expect(result.stdout).toContain("smoke_run");
155
    expect(result.stdout).toContain("SMOKE");
156
    expect(readResultRows(store)).toEqual([]);
157
  });
158
159
  test("cannot be talked into a passing gate by a partial run", () => {
160
    // priced-lane clears every floor in this file. The only thing wrong with it
161
    // is that the suite it names pins a fifth task it never ran — so 2, not 0.
162
    const passing = spawn(reportCli, [
163
      fixture("priced-lane"),
164
      "--suite-manifest",
165
      fixture("fixture-suite.suite.json"),
166
      "--thresholds",
167
      fixture("floors-fixture-scale-placeholder-ok.json"),
168
    ]);
169
    const partial = spawn(reportCli, [
170
      fixture("priced-lane"),
171
      "--suite-manifest",
172
      fixture("fixture-suite-5.suite.json"),
173
      "--thresholds",
174
      fixture("floors-fixture-scale-placeholder-ok.json"),
175
    ]);
176
177
    expect(passing.status).toBe(0);
178
    expect(partial.status).toBe(2);
179
    expect(partial.stdout).toContain("run_tier=score");
180
    expect(partial.stdout).toContain("never-run");
181
  });
182
});
183
107 184
describe("compare", () => {
108 185
  test("prints the trend two consecutive runs on one lane produce", () => {
109 186
    record("priced-lane", "proxy");
packages/coder-effectiveness/src/suite-manifest.test.ts added +222

@@ -0,0 +1,222 @@

1
/**
2
 * The suite pin and the smoke rule.
3
 *
4
 * Two things are under test here and they are not the same thing. The digest
5
 * tests ask whether two runs that claim to be comparable really ran the same
6
 * work. The classification tests ask whether a run that did not cover its suite
7
 * can present itself as one that did — the question the store's refusal and the
8
 * gate's fourth criterion both hang off.
9
 */
10
11
import { readdirSync, readFileSync } from "node:fs";
12
import { fileURLToPath } from "node:url";
13
import { describe, expect, test } from "vite-plus/test";
14
15
import {
16
  classifyRun,
17
  parseSuiteManifest,
18
  SUITE_MANIFEST_SCHEMA,
19
  type SuiteManifest,
20
  type SuiteTask,
21
  suiteDigestOf,
22
  taskDigestOf,
23
} from "./suite-manifest.ts";
24
25
const suitesDir = fileURLToPath(new URL("../../../bench/suites", import.meta.url));
26
27
const readSuite = (name: string): unknown =>
28
  JSON.parse(readFileSync(`${suitesDir}/${name}`, "utf8"));
29
30
const registryTask = (id: string, overrides: Partial<{ commit: string; path: string }> = {}) => ({
31
  id,
32
  pin: {
33
    kind: "harbor-registry" as const,
34
    dataset: "terminal-bench@2.0",
35
    gitUrl: "https://github.com/laude-institute/terminal-bench-2.git",
36
    commit: overrides.commit ?? "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
37
    path: overrides.path ?? id,
38
  },
39
  environmentProven: true,
40
});
41
42
const suite = (overrides: Partial<SuiteManifest> = {}): SuiteManifest =>
43
  parseSuiteManifest({
44
    schema: SUITE_MANIFEST_SCHEMA,
45
    id: "test-suite",
46
    tier: "score",
47
    description: "two tasks",
48
    tasks: [registryTask("regex-log"), registryTask("fix-code-vulnerability")],
49
    ...overrides,
50
  });
51
52
describe("parseSuiteManifest", () => {
53
  test("reads every checked-in suite manifest", () => {
54
    const files = readdirSync(suitesDir).filter((name) => name.endsWith(".suite.json"));
55
56
    expect(files.length).toBeGreaterThan(0);
57
    for (const file of files) {
58
      const manifest = parseSuiteManifest(readSuite(file));
59
      expect(manifest.id).toBe(file.replace(".suite.json", ""));
60
      expect(manifest.tasks.length).toBeGreaterThan(0);
61
    }
62
  });
63
64
  test("the headline suite holds the 20-30 tasks issue #34 asks for", () => {
65
    const manifest = parseSuiteManifest(readSuite("coder-effectiveness-v1.suite.json"));
66
67
    expect(manifest.tier).toBe("score");
68
    expect(manifest.tasks.length).toBeGreaterThanOrEqual(20);
69
    expect(manifest.tasks.length).toBeLessThanOrEqual(30);
70
    // Both sources the issue names: the terminal-bench cross-section and the
71
    // bounded public SWE-bench subset.
72
    const datasets = new Set(
73
      manifest.tasks.map((task) => (task.pin.kind === "harbor-registry" ? task.pin.dataset : "")),
74
    );
75
    expect(datasets).toContain("terminal-bench@2.0");
76
    expect(datasets).toContain("swebench-verified@1.0");
77
  });
78
79
  test("the owned suite pins each task to the commit that closed its issue", () => {
80
    const manifest = parseSuiteManifest(readSuite("owned-closed-issues.suite.json"));
81
82
    for (const task of manifest.tasks) {
83
      expect(task.pin.kind).toBe("tracker-closed-issue");
84
      if (task.pin.kind !== "tracker-closed-issue") continue;
85
      expect(task.pin.repo).toBe("OpenAgentsInc/openagents");
86
      expect(task.pin.acceptedCommit).toMatch(/^[0-9a-f]{40}$/u);
87
    }
88
  });
89
90
  test("refuses a score suite holding a task nobody has ever run", () => {
91
    // The owned tasks are pinned and real and have no container yet. A score
92
    // over them would read their absence as the coder failing.
93
    expect(() =>
94
      suite({ tasks: [{ ...registryTask("regex-log"), environmentProven: false }] }),
95
    ).toThrow(/environment has never been built/u);
96
  });
97
98
  test("allows an unproven task in a smoke suite", () => {
99
    const manifest = suite({
100
      tier: "smoke",
101
      tasks: [{ ...registryTask("regex-log"), environmentProven: false }],
102
    });
103
104
    expect(manifest.tier).toBe("smoke");
105
  });
106
107
  test("refuses a suite with no tasks", () => {
108
    expect(() => suite({ tasks: [] })).toThrow(/covers itself/u);
109
  });
110
111
  test("refuses a suite that names one task twice", () => {
112
    expect(() => suite({ tasks: [registryTask("regex-log"), registryTask("regex-log")] })).toThrow(
113
      /twice/u,
114
    );
115
  });
116
});
117
118
describe("taskDigestOf", () => {
119
  test("changes when the task's content moves under the same name", () => {
120
    const before = taskDigestOf(registryTask("regex-log") as SuiteTask);
121
    const after = taskDigestOf(registryTask("regex-log", { commit: "0".repeat(40) }) as SuiteTask);
122
123
    // This is the whole reason a suite pins more than a name: `regex-log` at
124
    // one dataset commit and `regex-log` at another are different work.
125
    expect(after).not.toBe(before);
126
  });
127
128
  test("ignores the rationale, which is prose about the suite and not the task", () => {
129
    const bare = taskDigestOf(registryTask("regex-log") as SuiteTask);
130
    const annotated = taskDigestOf({
131
      ...(registryTask("regex-log") as SuiteTask),
132
      rationale: "the suite's fast lane",
133
    });
134
135
    expect(annotated).toBe(bare);
136
  });
137
});
138
139
describe("suiteDigestOf", () => {
140
  test("is stable across task order", () => {
141
    const forward = suite();
142
    const reversed = suite({ tasks: [...forward.tasks].toReversed() });
143
144
    expect(suiteDigestOf(reversed)).toBe(suiteDigestOf(forward));
145
  });
146
147
  test("changes when a task is added, removed, or repinned", () => {
148
    const base = suiteDigestOf(suite());
149
150
    expect(suiteDigestOf(suite({ tasks: [registryTask("regex-log")] }))).not.toBe(base);
151
    expect(
152
      suiteDigestOf(
153
        suite({
154
          tasks: [
155
            registryTask("regex-log", { commit: "1".repeat(40) }),
156
            registryTask("fix-code-vulnerability"),
157
          ],
158
        }),
159
      ),
160
    ).not.toBe(base);
161
  });
162
163
  test("changes when the tier flips, because the claim changed", () => {
164
    // Same tasks, but one manifest's results may be published and the other's
165
    // may not. A digest that ignored the tier would let a stored row's pin
166
    // match a manifest that now says something else about it.
167
    expect(suiteDigestOf(suite({ tier: "smoke" }))).not.toBe(suiteDigestOf(suite()));
168
  });
169
});
170
171
describe("classifyRun", () => {
172
  test("a full run of a score suite is a score", () => {
173
    const result = classifyRun(suite(), ["regex-log", "fix-code-vulnerability"]);
174
175
    expect(result.tier).toBe("score");
176
    expect(result.smokeReasons).toEqual([]);
177
    expect(result.missing).toEqual([]);
178
  });
179
180
  test("a partial run is a smoke run, whatever it was called", () => {
181
    const result = classifyRun(suite(), ["regex-log"]);
182
183
    expect(result.tier).toBe("smoke");
184
    expect(result.missing).toEqual(["fix-code-vulnerability"]);
185
    expect(result.smokeReasons.map((reason) => reason.kind)).toEqual(["incomplete_coverage"]);
186
  });
187
188
  test("an extra task is its own finding, not the same as a missing one", () => {
189
    // Trimming a suite and substituting a task are different mistakes and the
190
    // messages send an operator to different places.
191
    const result = classifyRun(suite(), ["regex-log", "fix-code-vulnerability", "fix-git"]);
192
193
    expect(result.tier).toBe("smoke");
194
    expect(result.unexpected).toEqual(["fix-git"]);
195
    expect(result.smokeReasons.map((reason) => reason.kind)).toEqual(["unexpected_tasks"]);
196
  });
197
198
  test("a declared smoke suite stays smoke even when it ran completely", () => {
199
    const result = classifyRun(suite({ tier: "smoke" }), ["regex-log", "fix-code-vulnerability"]);
200
201
    expect(result.tier).toBe("smoke");
202
    expect(result.missing).toEqual([]);
203
    expect(result.smokeReasons.map((reason) => reason.kind)).toEqual(["declared_smoke"]);
204
  });
205
206
  test("counts a task once however many trials it produced", () => {
207
    const result = classifyRun(suite(), ["regex-log", "regex-log", "fix-code-vulnerability"]);
208
209
    expect(result.tier).toBe("score");
210
    expect(result.ran).toEqual(["fix-code-vulnerability", "regex-log"]);
211
  });
212
213
  test("the checked-in smoke suite classifies as smoke on a complete run", () => {
214
    const manifest = parseSuiteManifest(readSuite("smoke.suite.json"));
215
    const result = classifyRun(
216
      manifest,
217
      manifest.tasks.map((task) => task.id),
218
    );
219
220
    expect(result.tier).toBe("smoke");
221
  });
222
});
packages/coder-effectiveness/src/suite-manifest.ts added +295

@@ -0,0 +1,295 @@

1
/**
2
 * The suite manifest: what a graded run is supposed to have run, pinned hard
3
 * enough that the run cannot quietly have been something else.
4
 *
5
 * Issue #34 asks each run to pin "CLI version, model catalog revision, plugin
6
 * set, and task digest". The first three already travel with the report — the
7
 * CLI version off each trial's ATIF trajectory, the model and the rate catalog
8
 * version off the pricing layer. The fourth needs a manifest, because a task
9
 * digest can only mean something against a declared list. A bare list of task
10
 * names is not a digest of anything: `regex-log` names a task whose content
11
 * moved when the dataset moved, and a suite that pins only names will report
12
 * two different measurements under one heading.
13
 *
14
 * So a manifest task carries its identity, not its label: for a Harbor registry
15
 * task, the dataset it came from and the git url, commit, and path the registry
16
 * resolves it to. That triple is the task's content, and the digest is over the
17
 * triple. Two runs whose task digests agree ran the same work.
18
 *
19
 * THE SMOKE RULE, AND WHY IT IS STRUCTURAL RATHER THAN DOCUMENTED.
20
 *
21
 * The issue says "a fast/smoke run is never a published score", and a comment
22
 * in a README cannot make that true. Every benchmark eventually grows a fast
23
 * lane — three tasks, one lane, run on every commit — and every fast lane
24
 * eventually gets read as the number, usually by a dashboard nobody asked and
25
 * usually on the day it disagrees with the real one. The way to prevent that is
26
 * not to warn about it; it is to make the fast run unable to reach the file the
27
 * trend reads.
28
 *
29
 * {@link classifyRun} is where that happens. It compares the trials that
30
 * actually ran against the manifest's pinned task list, and a run that did not
31
 * cover the suite is `smoke` no matter what it was called on the command line.
32
 * The runner cannot lie about it, because the evidence is the trial directories
33
 * on disk. A smoke classification then does two things it cannot talk its way
34
 * out of: the gate carries a criterion it can never pass, and the results store
35
 * refuses the row. See `results-store.ts` and `thresholds.ts`.
36
 *
37
 * A run scored with no manifest at all is `unclassified`, which is a legitimate
38
 * thing to do — printing a report about a job directory is useful — and it is
39
 * equally unable to reach the store. Publishing requires naming the suite you
40
 * claim to have run, and naming it is what exposes whether you ran it.
41
 */
42
43
import { createHash } from "node:crypto";
44
import { Schema as S } from "effect";
45
46
/**
47
 * `score` is a suite whose result may be published. `smoke` is one that may
48
 * not, declared at the manifest rather than inferred, for a suite that exists
49
 * to be fast — a pre-push check over three tasks, say. A smoke manifest is
50
 * smoke even when every one of its tasks ran.
51
 */
52
export const SUITE_MANIFEST_SCHEMA = "openagents.effectiveness_suite.v1";
53
54
/**
55
 * Where a task's definition comes from, which decides what pins it.
56
 *
57
 * - `harbor-registry` — a task in a dataset Harbor's registry resolves, pinned
58
 *   by git url, commit, and path. This is the whole public-subset story: the
59
 *   registry already carries `terminal-bench@2.0`, `swebench-verified@1.0`,
60
 *   `aider-polyglot@1.0` and 77 others behind one contract, so a public subset
61
 *   costs a manifest entry rather than a loader.
62
 * - `tracker-closed-issue` — an owned task drawn from a closed issue in this
63
 *   tracker, pinned by the issue number and the commit that closed it. The
64
 *   accepted outcome is not a guess: the forge recorded that commit as the
65
 *   issue's closing reference.
66
 */
67
export const SuiteTaskPinSchema = S.Union([
68
  S.Struct({
69
    kind: S.Literal("harbor-registry"),
70
    /** `<name>@<version>` exactly as `harbor run --dataset` takes it. */
71
    dataset: S.String,
72
    gitUrl: S.String,
73
    commit: S.String,
74
    path: S.String,
75
  }),
76
  S.Struct({
77
    kind: S.Literal("tracker-closed-issue"),
78
    repo: S.String,
79
    issue: S.Number,
80
    /** The commit the forge recorded as this issue's closing reference. */
81
    acceptedCommit: S.String,
82
  }),
83
]);
84
85
export type SuiteTaskPin = typeof SuiteTaskPinSchema.Type;
86
87
export const SuiteTaskSchema = S.Struct({
88
  /** The Harbor task name: the `<task>` half of a `<task>__<uuid>` trial dir. */
89
  id: S.String,
90
  pin: SuiteTaskPinSchema,
91
  /**
92
   * Whether a container for this task has been built and graded at least once.
93
   *
94
   * An owned task drawn from a closed issue is a real, pinned piece of work
95
   * long before anybody has built an image that can grade it, and writing it
96
   * into the manifest early is how the suite records the intent. But a score
97
   * cannot include a task nobody has ever run: its absence from the results
98
   * would read as a failure of the coder rather than an absence of an
99
   * environment. So {@link parseSuiteManifest} refuses a `score` manifest that
100
   * holds an unproven task, and the suite that holds them stays `smoke` until
101
   * somebody proves them.
102
   */
103
  environmentProven: S.Boolean,
104
  /** One line on why this task is in the suite. Not digested. */
105
  rationale: S.optional(S.String),
106
});
107
108
export type SuiteTask = typeof SuiteTaskSchema.Type;
109
110
export const SuiteManifestSchema = S.Struct({
111
  schema: S.Literal(SUITE_MANIFEST_SCHEMA),
112
  id: S.String,
113
  tier: S.Literals(["score", "smoke"]),
114
  description: S.String,
115
  tasks: S.Array(SuiteTaskSchema),
116
});
117
118
export type SuiteManifest = typeof SuiteManifestSchema.Type;
119
120
export type RunTier = "score" | "smoke";
121
122
const decodeManifest = S.decodeUnknownSync(SuiteManifestSchema);
123
124
/**
125
 * Parse a manifest, rejecting the shapes that would make a digest a lie.
126
 *
127
 * Duplicate task ids are refused because Harbor names a trial directory after
128
 * the task, so two entries under one id cannot be told apart in a result; an
129
 * empty suite is refused because a suite with nothing in it trivially covers
130
 * itself, and would classify as a fully covered `score` run that measured
131
 * nothing at all.
132
 */
133
export const parseSuiteManifest = (value: unknown): SuiteManifest => {
134
  const manifest = decodeManifest(value);
135
136
  if (manifest.tasks.length === 0) {
137
    throw new Error(
138
      `suite ${manifest.id} declares no tasks; an empty suite covers itself and would score a run that measured nothing`,
139
    );
140
  }
141
142
  const seen = new Set<string>();
143
  for (const task of manifest.tasks) {
144
    if (seen.has(task.id)) {
145
      throw new Error(
146
        `suite ${manifest.id} names task ${task.id} twice; Harbor writes one trial directory per task name, so two entries could never be told apart in a result`,
147
      );
148
    }
149
    seen.add(task.id);
150
  }
151
152
  if (manifest.tier === "score") {
153
    const unproven = manifest.tasks
154
      .filter((task) => !task.environmentProven)
155
      .map((task) => task.id);
156
    if (unproven.length > 0) {
157
      throw new Error(
158
        `suite ${manifest.id} is tier score but holds ${String(unproven.length)} task(s) whose environment has never been built and graded (${unproven.join(", ")}); a task nobody has run would read as a failure of the coder rather than a missing environment`,
159
      );
160
    }
161
  }
162
163
  return manifest;
164
};
165
166
/**
167
 * The digest of one task: its identity, never its label or its rationale.
168
 *
169
 * `id` is in because it is how a trial directory names itself, so a suite that
170
 * renamed a task is running against a different result shape. `rationale` and
171
 * `environmentProven` are out: prose about why a task was chosen, and whether
172
 * anybody has built it yet, are facts about the suite's bookkeeping rather than
173
 * about the work the coder is asked to do.
174
 */
175
export const taskDigestOf = (task: SuiteTask): string =>
176
  `task:${createHash("sha256")
177
    .update(JSON.stringify(canonicalPin(task)))
178
    .digest("hex")}`;
179
180
const canonicalPin = (task: SuiteTask): unknown =>
181
  task.pin.kind === "harbor-registry"
182
    ? {
183
        id: task.id,
184
        kind: task.pin.kind,
185
        dataset: task.pin.dataset,
186
        gitUrl: task.pin.gitUrl,
187
        commit: task.pin.commit,
188
        path: task.pin.path,
189
      }
190
    : {
191
        id: task.id,
192
        kind: task.pin.kind,
193
        repo: task.pin.repo,
194
        issue: task.pin.issue,
195
        acceptedCommit: task.pin.acceptedCommit,
196
      };
197
198
/**
199
 * The digest of a whole suite: the tier and the sorted task digests.
200
 *
201
 * The tier is inside the digest deliberately. Flipping a suite from `smoke` to
202
 * `score` without changing a task is a change in what its results claim, and a
203
 * digest that ignored it would let a stored row's pin match a manifest that now
204
 * says something else about it.
205
 */
206
export const suiteDigestOf = (manifest: SuiteManifest): string => {
207
  const source = JSON.stringify({
208
    schema: manifest.schema,
209
    id: manifest.id,
210
    tier: manifest.tier,
211
    tasks: manifest.tasks.map(taskDigestOf).toSorted(),
212
  });
213
  return `suite-manifest:${createHash("sha256").update(source).digest("hex")}`;
214
};
215
216
/** Why a run is not a publishable score. Empty on a clean score run. */
217
export type SmokeReason =
218
  | { readonly kind: "declared_smoke"; readonly detail: string }
219
  | { readonly kind: "incomplete_coverage"; readonly detail: string }
220
  | { readonly kind: "unexpected_tasks"; readonly detail: string };
221
222
export interface RunClassification {
223
  readonly suiteId: string;
224
  readonly suiteDigest: string;
225
  readonly tier: RunTier;
226
  /** Task ids the manifest pins. */
227
  readonly expected: ReadonlyArray<string>;
228
  /** Task ids trials were actually found for. */
229
  readonly ran: ReadonlyArray<string>;
230
  readonly missing: ReadonlyArray<string>;
231
  readonly unexpected: ReadonlyArray<string>;
232
  /** Empty exactly when {@link RunClassification.tier} is `score`. */
233
  readonly smokeReasons: ReadonlyArray<SmokeReason>;
234
}
235
236
/**
237
 * Classify what a run actually was, from the manifest it claims and the trials
238
 * on disk.
239
 *
240
 * The three ways a run stops being a score, in the order they matter:
241
 *
242
 * 1. The manifest says so. A suite built to be fast is fast forever.
243
 * 2. It did not run every pinned task. This is the one that cannot be argued
244
 *    with: `--include` three of twelve tasks and the missing nine are missing
245
 *    from the job directory, whatever the invocation called itself. A partial
246
 *    run's success rate is over a different, easier or harder, set of work, and
247
 *    its cost per accepted outcome is over a different denominator.
248
 * 3. It ran tasks the manifest does not pin. A suite plus one extra task is a
249
 *    different suite, and the digest would otherwise say they were the same.
250
 *
251
 * Note that (2) and (3) are separate findings rather than one "task set
252
 * differs". An operator who trimmed a suite and an operator who substituted a
253
 * task have made different mistakes, and a message that named only the symptom
254
 * would send both to the same wrong place.
255
 */
256
export const classifyRun = (
257
  manifest: SuiteManifest,
258
  ranTaskIds: ReadonlyArray<string>,
259
): RunClassification => {
260
  const expected = manifest.tasks.map((task) => task.id).toSorted();
261
  const ran = [...new Set(ranTaskIds)].toSorted();
262
  const missing = expected.filter((id) => !ran.includes(id));
263
  const unexpected = ran.filter((id) => !expected.includes(id));
264
265
  const smokeReasons: Array<SmokeReason> = [];
266
  if (manifest.tier === "smoke") {
267
    smokeReasons.push({
268
      kind: "declared_smoke",
269
      detail: `suite ${manifest.id} declares tier smoke, so its results are never a published score however completely it ran`,
270
    });
271
  }
272
  if (missing.length > 0) {
273
    smokeReasons.push({
274
      kind: "incomplete_coverage",
275
      detail: `${String(missing.length)} of ${String(expected.length)} pinned tasks produced no trial (${missing.join(", ")}); a partial run scores a different set of work than the suite it names`,
276
    });
277
  }
278
  if (unexpected.length > 0) {
279
    smokeReasons.push({
280
      kind: "unexpected_tasks",
281
      detail: `${String(unexpected.length)} trial(s) ran tasks the suite does not pin (${unexpected.join(", ")}); a suite plus an extra task is a different suite`,
282
    });
283
  }
284
285
  return {
286
    suiteId: manifest.id,
287
    suiteDigest: suiteDigestOf(manifest),
288
    tier: smokeReasons.length === 0 ? "score" : "smoke",
289
    expected,
290
    ran,
291
    missing,
292
    unexpected,
293
    smokeReasons,
294
  };
295
};
packages/coder-effectiveness/src/thresholds.test.ts modified +101

@@ -10,6 +10,7 @@ import { describe, expect, test } from "vite-plus/test";

10 10
import { summarizeRun } from "./effectiveness.ts";
11 11
import { readHarborJob } from "./harbor-job.ts";
12 12
import { CODER_RATE_CATALOG_VERSION } from "./pricing.ts";
13
import { classifyRun, parseSuiteManifest } from "./suite-manifest.ts";
13 14
import { type EffectivenessThresholds, evaluateThresholds, parseThresholds } from "./thresholds.ts";
14 15
15 16
const fixture = (name: string): string =>

@@ -203,3 +204,103 @@ describe("evaluateThresholds", () => {

203 204
    expect(criterion(gate, "success_rate").detail).toContain("no verifier ran");
204 205
  });
205 206
});
207
208
/**
209
 * The smoke rule at the gate: the other half of the enforcement whose first
210
 * half lives in `results-store.test.ts`. The two are independent on purpose —
211
 * a CI step that only reads the exit code and one that only reads the store
212
 * should each be unable to mistake a fast run for a score.
213
 */
214
/** A manifest over the named tasks, at the tier the case wants to test. */
215
const tierManifest = (tier: "score" | "smoke", tasks: ReadonlyArray<string>) =>
216
  parseSuiteManifest({
217
    schema: "openagents.effectiveness_suite.v1",
218
    id: "fixture-suite",
219
    tier,
220
    description: "the tasks the priced-lane fixture ran",
221
    tasks: tasks.map((id) => ({
222
      id,
223
      pin: {
224
        kind: "harbor-registry",
225
        dataset: "terminal-bench@2.0",
226
        gitUrl: "https://github.com/laude-institute/terminal-bench-2.git",
227
        commit: "69671fbaac6d67a7ef0dfec016cc38a64ef7a77c",
228
        path: id,
229
      },
230
      environmentProven: true,
231
    })),
232
  });
233
234
describe("the run tier criterion", () => {
235
  const fullTasks = ["build-cmake", "fix-git", "parse-log", "port-forward"];
236
237
  test("passes a run that covered every pinned task", () => {
238
    const gate = evaluateThresholds(
239
      report("priced-lane"),
240
      floors(),
241
      classifyRun(tierManifest("score", fullTasks), fullTasks),
242
    );
243
244
    expect(criterion(gate, "run_tier").verdict).toBe("passed");
245
    expect(gate.status).toBe("passed");
246
  });
247
248
  test("cannot be passed by a run that covered part of the suite", () => {
249
    const gate = evaluateThresholds(
250
      report("priced-lane"),
251
      floors(),
252
      classifyRun(tierManifest("score", fullTasks), ["fix-git"]),
253
    );
254
255
    expect(criterion(gate, "run_tier").verdict).toBe("unverifiable");
256
    expect(gate.status).toBe("unverifiable");
257
  });
258
259
  test("has no floor that turns a smoke run into a pass", () => {
260
    // Every other criterion here can be waived by an operator willing to set a
261
    // generous floor. This one is not a measurement, so waiving it would waive
262
    // the question rather than the answer — and the floors below are as
263
    // generous as the schema allows.
264
    const gate = evaluateThresholds(
265
      report("priced-lane"),
266
      floors({ minGradedTrials: 0, minSuccessRate: 0, maxUngradedRatio: 1 }),
267
      classifyRun(tierManifest("smoke", fullTasks), fullTasks),
268
    );
269
270
    expect(gate.criteria.filter((entry) => entry.verdict === "failed")).toEqual([]);
271
    expect(criterion(gate, "run_tier").verdict).toBe("unverifiable");
272
    expect(gate.status).toBe("unverifiable");
273
  });
274
275
  test("adds no criterion when the run named no suite, which is report-only mode", () => {
276
    const gate = evaluateThresholds(report("priced-lane"), floors());
277
278
    expect(gate.criteria.some((entry) => entry.name.includes("run_tier"))).toBe(false);
279
    expect(gate.status).toBe("passed");
280
  });
281
282
  test("a measured breach still outranks a smoke classification", () => {
283
    const gate = evaluateThresholds(
284
      report("regressed-lane"),
285
      floors({ minSuccessRate: 0.5 }),
286
      classifyRun(tierManifest("score", fullTasks), ["fix-git"]),
287
    );
288
289
    expect(criterion(gate, "success_rate").verdict).toBe("failed");
290
    expect(criterion(gate, "run_tier").verdict).toBe("unverifiable");
291
    expect(gate.status).toBe("failed");
292
  });
293
});
294
295
describe("the checked-in quick-suite floors", () => {
296
  test("floor the run to its own size rather than to the cross-section's", () => {
297
    const path = fileURLToPath(new URL("../thresholds/tb2-quick.json", import.meta.url));
298
    const thresholds = parseThresholds(JSON.parse(readFileSync(path, "utf8")));
299
300
    expect(thresholds.id).toBe("tb2-quick");
301
    expect(thresholds.minGradedTrials).toBe(2);
302
    // No dollar ceiling: the lane it runs on bills no metered tokens, and a
303
    // ceiling that is unverifiable on every run is a gate nobody reads.
304
    expect(thresholds.maxCostPerAcceptedOutcomeUsd).toBeUndefined();
305
  });
306
});
packages/coder-effectiveness/src/thresholds.ts modified +46 -1

@@ -17,11 +17,18 @@

17 17
 * the thresholds file opts in with `acceptPlaceholderRates: true` — which is
18 18
 * a reasonable thing to do for a relative regression check, and an unreasonable
19 19
 * thing to do quietly.
20
 *
21
 * The third verdict is also where the smoke rule reaches the gate. A run that
22
 * did not cover its suite gets a `run_tier=score` criterion that is
23
 * `unverifiable` and has no passing branch at all, so a fast run cannot exit 0
24
 * through a thresholds file however generous the floors in it are. See
25
 * `suite-manifest.ts` for why that is a criterion rather than a warning.
20 26
 */
21 27
22 28
import { Schema as S } from "effect";
23 29
24 30
import type { EffectivenessReport } from "./effectiveness.ts";
31
import type { RunClassification } from "./suite-manifest.ts";
25 32
26 33
export const EffectivenessThresholdsSchema = S.Struct({
27 34
  /** Names this floor set, so a report can say what it was scored against. */

@@ -87,10 +94,19 @@ export interface ThresholdGate {

87 94
  readonly criteria: ReadonlyArray<ThresholdCriterion>;
88 95
}
89 96
90
/** Score a report against its floors. Pure. */
97
/**
98
 * Score a report against its floors. Pure.
99
 *
100
 * `classification` is what the run was measured to be against its suite
101
 * manifest, or `null` when the run was scored without one. A `null`
102
 * classification adds no criterion: printing a report about a job directory
103
 * without naming a suite is a legitimate thing to do, and it is already
104
 * incapable of being published — the results store refuses an unclassified row.
105
 */
91 106
export const evaluateThresholds = (
92 107
  report: EffectivenessReport,
93 108
  thresholds: EffectivenessThresholds,
109
  classification: RunClassification | null = null,
94 110
): ThresholdGate => {
95 111
  const criteria: Array<ThresholdCriterion> = [
96 112
    gradedTrialsCriterion(report, thresholds),

@@ -99,6 +115,8 @@ export const evaluateThresholds = (

99 115
  ];
100 116
  const cost = costCriterion(report, thresholds);
101 117
  if (cost !== null) criteria.push(cost);
118
  const tier = tierCriterion(classification);
119
  if (tier !== null) criteria.push(tier);
102 120
103 121
  return {
104 122
    thresholdsId: thresholds.id,

@@ -111,6 +129,33 @@ export const evaluateThresholds = (

111 129
  };
112 130
};
113 131
132
/**
133
 * The smoke rule at the gate. `null` when the run carries no classification.
134
 *
135
 * A score run passes it, and that is the only passing branch: a smoke run is
136
 * `unverifiable` and there is no threshold, no flag, and no floor value that
137
 * turns it into a pass. That asymmetry is the point. Every other criterion here
138
 * scores a measurement, and a measurement can legitimately be waived by an
139
 * operator who sets the floor low enough. This one is not a measurement — it
140
 * says the run is not the thing the floors describe — so waiving it would be
141
 * waiving the question rather than the answer.
142
 */
143
const tierCriterion = (classification: RunClassification | null): ThresholdCriterion | null => {
144
  if (classification === null) return null;
145
  if (classification.tier === "score") {
146
    return {
147
      name: "run_tier=score",
148
      verdict: "passed",
149
      detail: `covered all ${String(classification.expected.length)} tasks pinned by suite ${classification.suiteId} (${classification.suiteDigest})`,
150
    };
151
  }
152
  return {
153
    name: "run_tier=score",
154
    verdict: "unverifiable",
155
    detail: `this run is a smoke run, not a score: ${classification.smokeReasons.map((reason) => reason.detail).join("; ")}`,
156
  };
157
};
158
114 159
const gradedTrialsCriterion = (
115 160
  report: EffectivenessReport,
116 161
  thresholds: EffectivenessThresholds,
packages/coder-effectiveness/thresholds/tb2-quick.json added +30

@@ -0,0 +1,30 @@

1
{
2
  "$comment": [
3
    "Floors for the tb2-quick suite (bench/suites/tb2-quick.suite.json), the",
4
    "two-task score suite that exists to be run repeatedly on one machine.",
5
    "minGradedTrials is 2, the whole suite. A quick suite that graded one of its",
6
    "two tasks is not a quick suite that half passed: the missing trial means the",
7
    "verifier crashed or the environment never came up, and either way the",
8
    "success rate is over a denominator of one. The coverage check in the suite",
9
    "manifest already makes a partial run a smoke run, so this floor is the",
10
    "belt to that suspenders, and it costs nothing on a healthy run.",
11
    "minSuccessRate is 0.5: with two tasks the only rates available are 0, 0.5,",
12
    "and 1, so this floor says at least one task must be solved. It is the",
13
    "regression floor, not a target — a lane that drops to solving neither task",
14
    "is the failure this suite exists to catch, and a lane that solves one is",
15
    "not evidence of health so much as evidence of not being broken.",
16
    "maxUngradedRatio is 0 rather than a fraction, for the same reason as the",
17
    "trial floor: with two trials the smallest non-zero ratio is a half, and a",
18
    "suite that tolerated half its trials going ungraded would be reporting a",
19
    "success rate over a single task while calling it a suite.",
20
    "No maxCostPerAcceptedOutcomeUsd. The lane this suite is run on is local",
21
    "Ollama, which bills no metered tokens, so a dollar ceiling here would be",
22
    "unverifiable on every run and would make the gate exit 2 forever — which",
23
    "is a gate nobody reads. When this suite is run through the proxy, score it",
24
    "against a thresholds file that declares one."
25
  ],
26
  "id": "tb2-quick",
27
  "minGradedTrials": 2,
28
  "minSuccessRate": 0.5,
29
  "maxUngradedRatio": 0
30
}

This page updates live while a promote is in flight · changelog