Module 07 · Agent evals, every modality

Evaluating coding agents

Code is the friendliest domain in all of agent evaluation, because you can run the answer. It is also where the field learned, the hard way, that "the tests passed" and "the agent solved the problem" are not the same sentence.

IntermediateAdvanced~80 minPrereqs: Modules 03, 04, 05

By the end of this module you can

  • Trace the evolution of coding evals from single functions (HumanEval, MBPP) to whole repositories (SWE-bench) to whole machines (Terminal-Bench, MLE-bench, PaperBench).
  • Explain exactly how a SWE-bench task is graded: base commit, model patch, hidden test patch, FAIL_TO_PASS and PASS_TO_PASS, Docker.
  • List the ways a coding score can lie (contamination, leaked solutions, git history, weak tests, over-strict tests, flaky tests) and how each is mitigated.
  • Grade beyond pass/fail: regressions, diff size, code quality, security, cost, time and turns.
  • Build your own coding-agent eval from your repository's merged pull requests, and run it safely in containers.
  • Explain why the same model scores differently in different agent harnesses, and what that means when you read a leaderboard.
  • Evaluate a coding assistant as a product: the acceptance, survival, merge and revert funnel, developer-level A/B tests, and why developer self-reports mislead.

Why code is the easy case (and the trap)

In most of this course, the hardest part of an eval is the grader: how do you decide, automatically, whether an answer is good? For a customer-support reply you may need an LLM judge or a human (Module 03). For code, you have something much better: you can execute the program and check what it does. This is called execution-based grading, and it is the reason coding became the flagship domain for agent evaluation.

Execution-based grading has three wonderful properties:

  • It accepts many correct answers. Two completely different implementations both pass if they behave the same. You do not need the output to match a reference string.
  • It is cheap and deterministic (mostly). Running a test suite costs seconds, not a human's afternoon.
  • It is hard to argue with. A failing test is concrete evidence, not a judge's opinion.

And one big trap: the tests are only as good as whoever wrote them. If the tests are thin, wrong code passes. If they are too strict, correct code fails. If the agent can see or guess the tests, it can pass without solving anything. Most of the expert material in this module is about that trap.

Good tests score ≈ truth Weak tests inflate the score Over-strict tests deflate the score correct ≈ passes correct wrong code that passes special-cased example input passes correct code that fails "wrong" helper name or message truly correct programs programs that pass the tests
What the tests actually measure. A test suite defines the blue set; the task defines the green one. The score is only honest where the two overlap. EvalPlus showed the middle case (adding tests lowered HumanEval scores) and OpenAI's 2026 SWE-bench Verified audit showed the right-hand case. (Conceptual picture, not to scale.)
Analogy: the driving examiner with a checklist

Imagine a driving test graded only by a checklist: "car ends up in the parking spot", "no alarms went off". A student who parks perfectly passes. So does one who drives over the kerb but happens to stop in the right spot. And a student who parks beautifully in the spot next door fails. Hidden unit tests are that checklist. They are enormously useful, but you have to keep asking what they do not check.

Three generations of coding evals

Coding benchmarks have grown in three big steps. Each step made the task more realistic, more expensive to run, and harder to grade well.

each step up: more realistic, costlier, harder to grade well 1 · Function-level HumanEval, MBPP (2021) in: signature + docstring out: one function, graded by asserts 2 · Repository-level SWE-bench (2023) and its family: Lite, Verified, Multimodal, Pro, … in: a GitHub issue + the whole repo out: a patch (a diff) graded by hidden tests in Docker minutes per task, tens of steps 3 · Terminal and long-horizon Terminal-Bench, Aider polyglot, LiveCodeBench, MLE-bench, RE-Bench, PaperBench, SWE-Lancer in: a goal + a whole machine out: the machine's end state, a trained model, a replicated paper graded by tests, private leaderboards, rubrics, or dollars earned hours per task, hundreds of steps
The staircase of coding evals. Each generation widens what the agent controls (a function, then a repository, then a whole computer) and what the grader has to check.

Generation 1: function-level benchmarks

HumanEval

HumanEval (Chen et al., OpenAI, 2021, the "Codex paper") is 164 hand-written Python problems. Each gives the model a function signature and a docstring (the comment that says what the function should do). The model writes the body. A hidden check() function with, on average, about 7.7 unit tests decides whether it is right.

Prompt (given) def has_close_elements(xs, t):     """True if any two     numbers in xs are     closer than t.""" Completion (model writes)   for i, a in enumerate(xs):     for b in xs[i+1:]:       if abs(a - b) < t:         return True   return False check() (hidden tests) def check(f):   assert not f([1.0, 2.8], 0.3)   assert f([1.0, 1.1], 0.3)   assert f([5.0, 3.9, 4.0], 0.2)   assert not f([], 1.0) Sample n = 10 completions for the same prompt and run check() on each: ✓ ✗ ✗ ✓ ✗ ✗ ✗ ✓ ✗ ✗ c = 3 correct out of n = 10 pass@1 = 3/10 = 0.30 pass@5 = 1 − C(7,5) / C(10,5)        = 1 − 21/252 ≈ 0.92
Anatomy of a function-level eval. The problem is abridged from HumanEval/0 (renamed arguments, illustrative asserts). pass@k asks "if I drew k of my samples, how likely is at least one correct?", so pass@5 is far higher than pass@1 on the same 10 samples.

The metric the Codex paper introduced is pass@k: the probability that at least one of k sampled answers is correct. Naively you would sample k answers and check, but that is very noisy. The paper's trick is to sample n ≥ k answers, count c correct, and compute an unbiased estimate:

pass@k = 1 − C(n−c, k) / C(n, k), averaged over problems.

(C(a, b) is "a choose b", the number of ways to pick b items from a.) In words: of all the ways to pick k samples from your n, what fraction contain no correct one? Subtract that from 1.

import numpy as np

def pass_at_k(n: int, c: int, k: int) -> float:
    """Unbiased pass@k from the Codex paper (Chen et al., 2021).
    n = samples drawn, c = how many passed the tests, k = budget."""
    if n - c < k:          # every k-subset must contain a correct sample
        return 1.0
    # numerically stable form of 1 - C(n-c, k) / C(n, k)
    return 1.0 - np.prod(1.0 - k / np.arange(n - c + 1, n + 1))

print(pass_at_k(10, 3, 1))   # 0.30
print(pass_at_k(10, 3, 5))   # 0.9167
pass@k is generous. Agents usually need the opposite.

pass@k rewards "any one of k tries worked", which fits a human picking the best suggestion from an autocomplete list. An autonomous coding agent that opens one pull request gets one try, so report pass@1. If it will run the same kind of job hundreds of times, look at pass^k (all k tries succeed) from Module 04.

MBPP, and why both are now "solved"

MBPP (Austin et al., Google, 2021) is 974 crowd-sourced, entry-level Python problems, each with 3 assert tests, plus a cleaned "sanitized" subset of 427. Same idea, easier problems.

Both are now considered saturated (frontier models score near the ceiling) and contaminated (the problems and solutions have been on the public internet for years, so they are almost certainly in training data). They also have too few tests: the EvalPlus project (Liu et al., 2023) added about 80 times more tests to make HumanEval+ and MBPP+, and scores dropped, meaning some "passing" solutions had been wrong all along. That last finding is worth remembering: adding tests lowered the scores. Weak tests silently inflate every number built on them.

Function-level benchmarks are still useful for quick sanity checks and for small models, but they tell you almost nothing about whether an agent can work in a real codebase. For that, we need generation 2.

Generation 2: SWE-bench, exactly how it works

SWE-bench (Jimenez, Yang et al., Princeton, ICLR 2024) changed the question from "can you write a function?" to "can you fix a real bug in a real project?". It contains 2,294 task instances built from real GitHub issues and the pull requests that fixed them, drawn from 12 popular Python repositories (Django, scikit-learn, sympy, and others).

A few terms first:

  • Issue: a bug report or feature request on GitHub, written by a human, in prose.
  • Pull request (PR): the set of code changes that fixed it, usually with new or changed tests.
  • Commit: a snapshot of the repository at one moment. The base commit is the snapshot just before the fix.
  • Patch / diff: a text description of changes to files ("remove line 12, add these 3 lines"). git apply applies a patch to a checkout.
  • Docker container: a lightweight isolated Linux environment with exactly the right Python version and dependencies installed, built from a recipe called an image.

Each PR is split into two parts. The gold patch is the change to the source code: the human's fix, kept as a reference answer. The test patch is the change to the test files: kept secret and used for grading. The agent sees only the issue text and the repository at the base commit.

① Generation phase (the agent) Task instance instance_id repo base_commit problem_statement hidden: patch, test_patch, FAIL_TO_PASS, PASS_TO_PASS issue Docker container (the agent's sandbox) repo @ base_commit Agent model + harness read files · edit · run code · repeat step, time and cost limits apply diff Prediction {"instance_id": …,  "model_patch":   "diff --git a/…    +the fix …"} one line of preds.jsonl model_patch hidden tests + lists ② Evaluation phase (the harness) fresh container @ base_commit git apply model_patch apply test_patch (agent never saw it) run test files, parse the log RESOLVED iff every FAIL_TO_PASS test now passes AND every PASS_TO_PASS test still passes patch won't apply: unresolved anything else: unresolved
The SWE-bench pipeline, end to end. The agent works in one container and only its diff leaves. Grading happens in a fresh container that the agent never touched, with tests the agent never saw. The score is the percentage of instances resolved.

FAIL_TO_PASS and PASS_TO_PASS

These two lists are the heart of SWE-bench grading. Both are computed once, when the benchmark is built, by running the tests with and without the gold patch:

  • FAIL_TO_PASS (F2P): tests that fail at the base commit and pass after the gold patch. These encode "the bug is fixed" or "the feature now works". Usually they are the new tests in the test patch.
  • PASS_TO_PASS (P2P): tests that pass both before and after the gold patch. These encode "you didn't break anything else". They are the regression check.

The agent's patch counts as resolved only if every F2P test passes and every P2P test still passes. It is all-or-nothing per instance: fixing half the bug earns nothing.

at base_commit gold patch agent patch F2Ptest_empty_header ✗ ✓ ✓ F2Ptest_unicode_header ✗ ✓ ✗ P2Ptest_parse_basic ✓ ✓ ✓ P2Ptest_roundtrip ✓ ✓ ✗ The gold patch column defines the lists: F2P = fail then pass, P2P = pass then pass. Agent: F2P 1/2, P2P 1/2 → UNRESOLVED all P2P pass some P2P fail all F2P F2P fails RESOLVED scores 1 fixed it, but broke something harmless, but didn't fix it worse than doing nothing three of four quadrants score 0
Worked example of the resolve rule. The test names are invented for illustration. Note that "fixed it, but broke something" scores exactly the same as "did nothing" on SWE-bench, which is why you may want finer-grained metrics in your own evals.

What an instance and a prediction look like

The dataset lives on Hugging Face. Each row is one task instance. The field names below are the real ones, and they are the same across SWE-bench, Lite and Verified.

from datasets import load_dataset
import json

ds = load_dataset("princeton-nlp/SWE-bench_Verified", split="test")   # 500 rows
row = ds[0]

row["instance_id"]        # e.g. "astropy__astropy-12907"
row["repo"]               # "astropy/astropy"
row["base_commit"]        # the commit to check out BEFORE the fix
row["problem_statement"]  # the GitHub issue text: this is the task prompt
row["patch"]              # gold source patch (reference answer, never shown)
row["test_patch"]         # hidden test changes, applied only at grading time
json.loads(row["FAIL_TO_PASS"])   # list of test ids, stored as a JSON string
json.loads(row["PASS_TO_PASS"])   # list of test ids that must keep passing

# Your agent produces one JSON object per instance, written to preds.jsonl:
pred = {
    "instance_id": row["instance_id"],
    "model_name_or_path": "my-agent-v3",
    "model_patch": "diff --git a/astropy/modeling/separable.py ...",
}

Then the official harness builds the Docker images, applies each patch, runs the tests and writes a report:

pip install swebench
python -m swebench.harness.run_evaluation \
    --dataset_name princeton-nlp/SWE-bench_Verified \
    --predictions_path preds.jsonl \
    --max_workers 8 \
    --run_id my-agent-v3
# -> per-instance logs + a JSON report listing resolved / unresolved / error ids
Separate generation from grading

Notice the design: your agent can be anything (your own loop, a commercial CLI agent, a human). All the harness needs is a diff. This clean interface (patch in, verdict out) is why SWE-bench became the common currency for comparing coding agents, and it is a pattern worth copying in your own evals.

The SWE-bench family

SWE-bench spawned a family of variants. Each fixes a specific problem with the one before.

VariantSizeWhat it changesWhy it exists
SWE-bench (full)2,294The original: 12 Python repos, real issues and PRs.First realistic repo-level benchmark.
Lite (Mar 2024)300A cheaper subset of more self-contained issues.The full set is slow and expensive to run with agents.
Verified (Aug 2024)500Human-screened by OpenAI Preparedness with the SWE-bench authors.Many original tasks were impossible to solve fairly (see below).
Multimodal (Oct 2024)617 (480 in the test split listed on swebench.com)JavaScript and visual front-end repos; issues include screenshots.Python-only, text-only was too narrow.
Pro (Scale AI, Sep 2025)1,865 from 41 reposLonger, multi-file tasks; public, held-out and commercial (private codebase) splits; copyleft and private repos.Contamination resistance and harder, more "enterprise" tasks.
OthersvariesMultilingual, SWE-bench-Live, SWE-rebench (fresh tasks over time), SWE-Gym and SWE-smith (for training).More languages, freshness, training data.

Why Verified was created

In 2024, OpenAI's Preparedness team noticed that many SWE-bench failures were not the agent's fault. They had 93 professional Python developers annotate 1,699 randomly sampled SWE-bench instances, with three independent reviews each. The findings:

2,294 · full SWE-bench test set, 12 Python repos 1,699 randomly sampled and annotated · 93 developers, 3 reviews each flags (they overlap): 38.3% underspecified issue · 61.1% tests may reject valid fixes 68.3% removed 31.7% kept 500 sampled 500 Verified Aug 2024, human-screened 300 Lite Mar 2024, filtered by rules, not by humans Feb 2026: OpenAI audits 138 Verified problems that o3 kept failing 59.4% have material test or spec flaws · OpenAI stops reporting Verified and recommends SWE-bench Pro
From SWE-bench to Verified, and beyond. Bar widths are proportional to instance counts. Filtering out broken tasks made scores jump (GPT-4o went from 16% on the original set to 33.2% on Verified, per OpenAI), because the removed tasks were unfair, not just hard.

Two kinds of problem dominated:

Underspecified issues

The issue text did not contain enough information to know what the "right" fix was. For example, the maintainers chose one of several reasonable behaviors in the PR, but the issue never said which. No agent can reliably guess that.

Unfair tests

The hidden tests checked something the issue never asked for: a specific function name, an exact error message wording, or a new helper the human happened to add. A correct fix written differently fails.

Annotators also estimated difficulty in time buckets; fewer than 10% of Verified tasks were estimated to take an experienced engineer more than an hour. That is worth remembering: Verified is mostly short tasks.

How a coding score can lie

A "percent resolved" number can be wrong in two directions. It can be inflated (the agent gets credit it did not earn) or deflated (the agent is marked wrong when it was right). An expert reads every coding leaderboard with both columns in mind.

Inflates the score (false passes) Deflates the score (false fails) Contaminationmodel has memorized the gold patch Solution in the issuethe issue text already shows the fix Git history leakgit log --all reveals the future commit Weak testsa wrong patch still passes F2P Over-strict teststests demand one specific implementation Undocumented behaviortests check what the issue never said Environment flakinessnetwork, timeouts, version drift Harness limitstoo few steps, weak tools, low budget Reported "% resolved"
Eight ways a resolve rate lies. Every item on this map has been documented for SWE-bench or its variants. Contamination and leaks push scores up; bad tests and brittle environments push them down, and the two effects can partly cancel, which makes them easy to miss.

Inflation: getting credit you didn't earn

  • Contamination. SWE-bench tasks come from public GitHub repos, and the fixes have been public for years. In February 2026 OpenAI reported that all frontier models it tested could reproduce gold patches or verbatim problem-statement details for some Verified tasks (for example, quoting inline comments from the original diff). A model that remembers the answer is not demonstrating problem solving.
  • Solution leakage in the issue. Some issue reporters paste a suggested fix, or even a full diff, into the issue. The agent then only has to copy it. Researchers have flagged Verified instances where the solution is reachable directly from the problem statement (SWE-bench GitHub issue #669).
  • Git history leakage. The Docker images originally contained the full git repository, including commits and tags created after the base commit. Agents discovered they could run commands like git log --all and read the future fix. SWE-bench issue #465 (2025) documents trajectories from several model families doing exactly this; the maintainers rebuilt images to strip future branches, tags and reflogs in release 4.1.0. A similar report was filed against SWE-bench Pro's open-source set. The lesson is general: anything reachable from inside the sandbox is part of the prompt.
  • Weak tests. If the F2P tests cover only the exact example from the issue, a patch that special-cases that example passes. The UTBoost work argued that Verified still has instances whose tests are insufficient to reject wrong patches.

Deflation: failing correct work

  • Over-strict and off-spec tests. OpenAI's 2026 audit looked at 138 Verified problems that o3 did not consistently solve over 64 runs. Engineers found material flaws in 59.4% of them: 35.5% had tests enforcing specific implementation details, 18.8% tested functionality the issue never described, and 5.1% had other problems. OpenAI stopped reporting Verified and recommended SWE-bench Pro instead.
  • Environment problems. A dependency that no longer installs, a test that needs network access, a timeout on a slow machine. These look like agent failures in the aggregate number.
  • Harness limits. The same model with a 30-step budget and with a 300-step budget are different systems. More on this below.
Expert habit: read the trajectories, not just the number

Nearly every one of these problems was found by someone reading agent transcripts, not by staring at scores. When your coding agent's score jumps, sample 20 "newly resolved" trajectories and check how they were solved. When it drops, sample 20 newly failing ones and check whether the failure is real. Module 12 turns this into a method.

Generation 3: terminals, research, and money

SWE-bench measures "fix this issue in this repo". Real engineering also means setting up environments, running long jobs, debugging builds, training models and working for hours. Generation 3 benchmarks give the agent a whole machine and a longer goal.

Terminal-Bench

Terminal-Bench (Stanford and the Laude Institute) gives an agent a Linux terminal inside a Docker container and a task in plain English: compile this project, fix this server config, train this small model, recover this corrupted file. The 2.0 version described in the January 2026 paper has 89 tasks across software engineering, system administration, ML, security, bioinformatics and more, each with a human-written reference solution and test scripts. Scoring is binary pass/fail from the tests, usually averaged over several runs, and results are reported for model × harness pairs (for example the Terminus reference agent, Claude Code, or Codex CLI). At paper time, "frontier models and agents score less than 65%".

Tasks are run with the Harbor framework. Every task is a folder with a fixed layout:

my-task/ ├── instruction.md ├── task.toml ├── environment/ │   └── Dockerfile ├── solution/ │   └── solve.sh └── tests/     └── test.sh task.toml: metadata, agent and verifier timeouts, CPUs, memory, image 1. Build the image from environment/, start a container 2. Agent (any harness) reads instruction.md, works in the shell 3. Agent stops; tests/ is copied to /tests; test.sh runs 4. test.sh writes 1 or 0 to /logs/verifier/reward.txt Task QA: the "oracle" agent runs solution/solve.sh; it must earn reward 1
A Terminal-Bench task in the Harbor format. The environment folder is kept separate so tests and solutions are not accidentally baked into the image the agent works in. The oracle run proves the task is solvable in its environment, which catches broken tasks before any model is blamed for failing them.

A test script is usually just pytest checking the end state of the container (a file exists with the right contents, a server responds, a model's accuracy is above a threshold), then writing the reward:

#!/bin/bash
# tests/test.sh  (runs inside the task container after the agent stops)
cd /app
if pytest /tests/test_outputs.py -q; then
  echo 1 > /logs/verifier/reward.txt
else
  echo 0 > /logs/verifier/reward.txt
fi
# Run Terminal-Bench 2.0 with a given agent harness and model (Harbor README)
harbor run --dataset terminal-bench@2.0 --agent claude-code \
           --model anthropic/claude-opus-5-5 --n-concurrent 4

Because the grader checks the state of the machine, not a diff, Terminal-Bench is a natural bridge to the computer-use evals of Module 08. Its known weaknesses: results are sensitive to the harness, some tasks can be flaky (network, timeouts), and 89 tasks gives wide confidence intervals (a single task is more than one percentage point).

Aider polyglot

The Aider polyglot benchmark (Paul Gauthier, Dec 2024) uses 225 of the hardest Exercism exercises in C++, Go, Java, JavaScript, Python and Rust. The model edits code through Aider's edit formats and gets two attempts: after a first failure, it sees the unit-test output and tries again. It reports percent solved, percent well-formed edits (did the model follow the edit format at all?) and cost. It is a good example of measuring the interface skill (producing edits a tool can apply) as well as the coding skill. Exercism solutions are public, so contamination is a real risk.

LiveCodeBench: fighting contamination with time

LiveCodeBench (Jain et al., 2024) continuously collects new competitive-programming problems from LeetCode, AtCoder and Codeforces and tags each with its release date. You evaluate a model only on problems published after its training cutoff, so it cannot have seen them. This is a contamination-aware time window. It covers code generation, self-repair, code execution and test-output prediction, scored with pass@1. The idea generalizes: SWE-rebench and SWE-bench-Live apply the same freshness logic to repository tasks.

new contest problems, each tagged with its release date time cutoff A cutoff B Model A may be in training data A's clean window Model B may be in training data B's clean window compare A vs B only here problems after both cutoffs the window shrinks as models get newer, so the benchmark must keep adding problems
A contamination-aware time window. LiveCodeBench scores each model only on problems released after its training cutoff. Newer models get smaller windows, and a fair head-to-head uses the overlap. The same logic powers SWE-rebench, SWE-bench-Live and the "fresh PRs" slice of your own eval. (Schematic: dots and cutoffs are illustrative.)

Research-engineering benchmarks: MLE-bench, RE-Bench, PaperBench

  • MLE-bench (OpenAI, Oct 2024): 75 Kaggle competitions run offline. The agent must train models and submit predictions, which are graded against each competition's private leaderboard. The metric is the percentage of competitions where the agent would have earned a medal. At release, o1-preview with the AIDE scaffold earned a medal in 16.9% of competitions. Runs are expensive (up to 24 hours on a GPU per competition).
  • RE-Bench (METR, Nov 2024): 7 open-ended ML research-engineering environments, such as optimizing a GPU kernel or fine-tuning a small model. Scores are continuous, normalized so the starting solution is 0 and a reference solution is 1, and compared against 71 eight-hour attempts by 61 human experts. Agents did better than humans at 2-hour budgets; humans won at 8 to 32 hours. It feeds METR's time-horizon measurements (Module 11).
  • PaperBench (OpenAI, Apr 2025): replicate 20 ICML 2024 Spotlight and Oral papers from scratch. There are 8,316 gradable sub-tasks in hierarchical rubrics co-written with the papers' authors, graded by an LLM judge that was itself validated on a judge-eval set. At release, the best agent (Claude 3.5 Sonnet, new) averaged 21.0%; ML PhDs reached 41.4% on a subset. This is what execution-based grading looks like when the "answer" is too big to test with asserts: you fall back to a rubric and a judge (Module 13).

SWE-Lancer: pricing the work

SWE-Lancer (OpenAI, Feb 2025) uses more than 1,400 real freelance tasks from Upwork on the Expensify codebase, worth $1M in actual payouts. Two task types:

  • IC SWE (individual contributor) tasks: implement the fix. Graded by end-to-end Playwright tests (browser automation that clicks through the real app) that were triple-verified by engineers. End-to-end tests are much harder to game than unit tests because they check behavior a user would see.
  • SWE Manager tasks: choose the best of several implementation proposals. Graded against the choice the original engineering manager made.

Its headline metric is dollars earned: each task is weighted by what it actually paid. The public "Diamond" split is worth $500.8K; at release, Claude 3.5 Sonnet earned $208K of it. Dollar weighting is a neat idea: it automatically gives hard, valuable tasks more weight than trivial ones, which an unweighted percentage does not.

Coding benchmarks side by side

BenchmarkScopeSizeGraderMetricContamination risk
HumanEval (2021)one Python function164hidden assertspass@kHigh: public for years
MBPP (2021)one Python function974 (427 sanitized)3 asserts eachpass@kHigh
SWE-bench / Verifiedfix a GitHub issue in a Python repo2,294 / 500F2P + P2P tests in Docker% resolvedHigh: documented for Verified (OpenAI, Feb 2026)
SWE-bench Multimodalvisual JS front-end issues617tests in Docker% resolvedMedium: public repos
SWE-bench Prolong, multi-file changes1,865, 41 repostests in Docker% resolvedLower: copyleft, held-out and private repos
Terminal-Bench 2.0whole Linux machine89test scripts on end state% pass (multi-run)Medium: new, hand-made tasks
Aider polyglotExercism exercises, 6 languages225unit tests, 2 attempts% solved, % well-formedHigh: public solutions
LiveCodeBenchcompetitive programminggrowinghidden testspass@1 in a time windowLow by design (post-cutoff problems)
MLE-benchKaggle competition end to end75 (22 Lite)private leaderboard% with any medalMedium: public Kaggle write-ups
RE-Benchopen-ended ML research engineering7task-specific scorenormalized score vs humansLow
PaperBenchreplicate an ML paper20 papers, 8,316 rubric itemsLLM judge on rubricsavg replication scoreMedium: papers are public, code often not
SWE-Lancerpaid freelance tasks1,400+ ($1M)E2E Playwright tests; manager choice% solved, $ earnedMedium: public repo

Contamination ratings are this course's judgment based on each benchmark's construction, not a measured value. Sources for sizes and graders are listed at the end of the module. Deliberately, there are no leaderboard numbers here: they change monthly, and several are no longer meaningful (see the Verified audit above).

Execution-based grading done right

Whether you use a public benchmark or build your own, the same engineering rules make execution-based grading trustworthy.

Hidden tests

The tests used for grading must not be visible to the agent during its run. Otherwise the agent can (and, under optimization pressure, will) write code that satisfies the tests rather than the task, or simply edit the tests. SWE-bench applies test_patch only in a fresh grading container; Harbor copies tests/ in only at verification time. Your agent may of course write and run its own tests; that is good engineering. It just must not see the grader's.

Watch for test tampering

If grading happens in the same container the agent worked in, the agent may have modified test files, conftest.py, pytest.ini, or monkey-patched the test runner. Reapply the pristine test files at grading time, and diff the agent's patch for changes to test infrastructure. Some teams simply reject any patch that touches test paths.

Flaky tests

A flaky test passes or fails on the same code depending on timing, randomness, network or ordering. Flaky tests add noise to both F2P and P2P, and noise can look like a regression or an improvement. Standard defenses:

  • When building the benchmark, run the gold solution several times (3 to 5) and drop any test whose result varies.
  • Run the grader with network disabled and random seeds fixed, so behavior cannot depend on the outside world.
  • Track per-test flip rates across runs; quarantine tests that flip.
  • Never "re-run failed tests until they pass" only for the agent: that biases the score upward. Whatever policy you choose, apply it identically to every system.

Sandboxing

Coding agents execute arbitrary code. That code may be buggy, may delete files, or may (under adversarial conditions) try to escape. The sandbox is both a safety boundary and a measurement boundary: it defines exactly what the agent can see and do.

host machine / CI runner (never exposed to the agent) Container: one per task, per attempt ✓ no network, or a strict allow-list ✓ CPU, memory and wall-clock caps ✓ non-root user, no Docker socket ✓ throwaway filesystem, destroyed after ✓ git history trimmed to base_commit ✓ pinned dependency versions repo checkout what the task is about agent process shell + edit tools grading step (after agent) Hidden test vault test files + F2P / P2P lists stored outside the container, never mounted during the run copied in only after the agent stops Flake filter gold run 3-5x; drop unstable tests
Sandbox layers for a coding eval. Each constraint closes a specific loophole: no network stops downloading the upstream fix, trimmed git history stops reading future commits, and keeping tests outside stops the agent from reading or editing them.

Here is a minimal grader that applies these rules with plain Docker. It assumes you already have an image per repository with dependencies installed.

import subprocess, tempfile, pathlib, re

def run(cmd, **kw):
    return subprocess.run(cmd, capture_output=True, text=True, **kw)

def run_tests(image: str, repo: str, commit: str, patches: list[str],
              test_files: list[str], timeout: int = 900) -> dict[str, bool]:
    """Fresh checkout at `commit`, apply patches in order, run pytest in a
    locked-down container. Returns {test_id: passed}."""
    with tempfile.TemporaryDirectory() as tmp:
        wt = pathlib.Path(tmp) / "repo"
        run(["git", "-C", repo, "worktree", "add", "--detach", str(wt), commit], check=True)
        try:
            for p in patches:
                r = run(["git", "-C", str(wt), "apply", "-"], input=p)
                if r.returncode != 0:
                    return {}                      # patch does not apply: unresolved
            r = run(["docker", "run", "--rm",
                     "--network", "none",          # no downloading the upstream fix
                     "--memory", "4g", "--cpus", "2",
                     "--user", "1000:1000",
                     "-v", f"{wt}:/repo", "-w", "/repo", image,
                     "python", "-m", "pytest", "-rA", "-p", "no:cacheprovider",
                     *test_files], timeout=timeout)
        finally:
            run(["git", "-C", repo, "worktree", "remove", "--force", str(wt)])
    # "-rA" prints one summary line per test, e.g. "PASSED tests/test_x.py::test_a"
    results = {}
    for status, test_id in re.findall(r"^(PASSED|FAILED|ERROR) (\S+)", r.stdout, re.M):
        results[test_id] = status == "PASSED"
    return results

def grade(inst: dict, model_patch: str, image: str, repo: str) -> bool:
    """SWE-bench-style resolve rule."""
    res = run_tests(image, repo, inst["base_commit"],
                    [model_patch, inst["test_patch"]],   # hidden tests applied LAST
                    inst["test_files"])
    f2p_ok = all(res.get(t, False) for t in inst["FAIL_TO_PASS"])
    p2p_ok = all(res.get(t, False) for t in inst["PASS_TO_PASS"])
    return f2p_ok and p2p_ok
Why apply the test patch after the model patch?

If the agent edited a test file, applying the pristine test patch on top either overwrites its change or fails to apply (and the instance fails loudly). Either way, the agent cannot quietly weaken the grader. Production harnesses go further and copy whole test files rather than patching them.

Beyond pass/fail

"Tests pass" is necessary but not sufficient. Two patches can both resolve an issue while one is a clean three-line fix and the other rewrites a module, deletes a warning, adds a dependency and costs ten times as much. For your own evals, record a scorecard per task, not a single bit.

DimensionHow to measureGrader type
CorrectnessHidden F2P tests passCode (execution)
No regressionsP2P tests, plus the whole suite if affordable (not just the listed tests)Code
Minimal diffLines and files changed, relative to the gold patch; touched files outside the expected areaCode (diff stats)
Code qualityReadability, idiomatic style, naming, comments, matches repo conventionsLLM judge with a rubric, plus linters (ruff, eslint)
SecurityStatic analysis (for example Bandit, Semgrep, CodeQL), secret scanning, new dependenciesCode (scanners)
Scope disciplineDid it modify tests, CI config, lockfiles, or unrelated files?Code (path rules)
CostInput + output tokens × price; tool/compute costCode (from traces)
Time and turnsWall-clock seconds, number of agent steps, number of tool callsCode (from traces)

An LLM judge is the practical way to score the soft dimensions. Give it the issue, the diff, and a specific rubric with anchored levels (Module 13 covers why anchors matter). Keep correctness out of the judge's hands: the tests already decided that.

import json

RUBRIC = """You are reviewing a code change that ALREADY passes its tests.
Do not judge correctness. Score each criterion 1-3 using these anchors.

minimality:  3 = only lines needed for the issue; 2 = small unrelated edits;
             1 = rewrites or reformats code the issue did not concern
readability: 3 = clear names, follows the file's existing style;
             2 = understandable but awkward; 1 = hard to follow
robustness:  3 = handles the edge cases implied by the issue;
             2 = handles the reported case only; 1 = special-cases the example
             input (e.g. `if x == "foo": return ...`)
Return JSON: {"minimality": n, "readability": n, "robustness": n, "notes": "..."}"""

def judge_patch(issue: str, diff: str, call_llm) -> dict:
    """call_llm(system, user) -> str : any chat model client you use."""
    user = f"<issue>\n{issue}\n</issue>\n<diff>\n{diff}\n</diff>"
    scores = json.loads(call_llm(RUBRIC, user))
    return scores

def scorecard(passed: bool, diff: str, trace: dict, judge: dict) -> dict:
    added = sum(1 for l in diff.splitlines() if l.startswith("+") and not l.startswith("+++"))
    removed = sum(1 for l in diff.splitlines() if l.startswith("-") and not l.startswith("---"))
    return {
        "resolved": passed,
        "lines_changed": added + removed,
        "touched_tests": "tests/" in diff,          # crude scope check
        "cost_usd": trace["cost_usd"],
        "turns": trace["num_turns"],
        "wall_s": trace["wall_seconds"],
        **{f"judge_{k}": v for k, v in judge.items() if k != "notes"},
    }
The "robustness" row catches test gaming

The most common way a weak test suite is gamed is special-casing: the agent notices the issue's example input and hard-codes it. A judge instructed to look for this, plus a few extra property-based or held-out tests you write yourself, catches a surprising share of false passes. This is the EvalPlus lesson applied to agents: more and different tests lower inflated scores.

Reporting cost next to accuracy

A worked example. Agent A resolves 120 of 200 tasks (60%) at an average of $0.40 per task. Agent B resolves 128 (64%) at $2.10 per task. Cost per resolved task: A is 200 × 0.40 / 120 = $0.67; B is 200 × 2.10 / 128 = $3.28. B is 4 points better (check whether that is significant with a paired test, Module 04: on 200 tasks it may not be) and about 5 times more expensive per success. Whether that is worth it is a product decision, but you can only make it if you measured both.

Resolve rate (200 tasks) Agent A 60% Agent B 64% +4 points: may not be significant on 200 tasks Cost per resolved task Agent A $0.67 Agent B $3.28 about 4.9× more per success Cost per resolved task = tasks × cost per task ÷ tasks resolved
The same comparison, two columns. Numbers from the worked example above. A leaderboard that shows only the left chart makes B look strictly better; the right chart shows what the extra four points cost.

The scaffold matters as much as the model

A coding agent is model + harness. The harness (also called scaffold) is everything around the model: the system prompt, the tools (shell, file viewer, editor, search), how edits are applied, how much output is shown, how many steps are allowed, whether it retries, whether it runs tests before submitting. Change the harness and the score changes, sometimes more than a model upgrade would.

Same model identical weights One-shot retrieval (no loop) search for relevant files, write a patch in one go, never runs the code (SWE-bench paper baseline) Custom agent-computer interface SWE-agent: file viewer, search, edit commands with a linter that rejects broken edits Minimal loop: bash only mini-SWE-agent: about 100 lines of Python, one shell tool; used for SWE-bench's bash-only view score A score B score C score = f(model, harness, budget)
One model, many agents. This diagram is conceptual: the three scores are placeholders, not measurements. The real examples in the text show gaps of many percentage points from harness changes alone.

Documented examples:

  • The SWE-agent paper (Yang et al., May 2024) showed that giving a model a purpose-built agent-computer interface (ACI) raised SWE-bench pass@1 to 12.5%, "far" beyond the previous state of the art from non-interactive, retrieve-then-patch approaches. Same class of model, very different result, because the agent could now navigate, edit safely and run code.
  • Anthropic's January 2025 engineering post reported 49% on SWE-bench Verified for the upgraded Claude 3.5 Sonnet using a deliberately simple scaffold with just two tools (a Bash tool and an edit tool), beating the previous best of 45%. The post states that performance "can vary significantly based on this scaffolding, even when using the same underlying AI model".
  • mini-SWE-agent reached 65% on Verified in July 2025 with about 100 lines of Python. SWE-bench's official "bash-only" leaderboard view runs every model in this same minimal harness, precisely so that model comparisons are not confounded by harness engineering.
  • Terminal-Bench reports results per model × harness pair for the same reason.
How to read a coding leaderboard

For every number, ask: which harness? how many steps or dollars allowed? how many attempts (pass@1 or best-of-k)? which subset and which version of the dataset? was it run by the benchmark maintainers or self-reported by a vendor? Two numbers are comparable only if all five answers match.

For your own work, this cuts both ways. If you are choosing a model, hold the harness fixed and swap models. If you are building an agent product, hold the model fixed and treat the harness as the thing you are optimizing, with your eval as the scoreboard.

Go deeper

How do you prove a harness change helped, rather than just seeing a number move? Module 21 covers harness evaluation end to end.

Build your own coding-agent eval from your repo's history

Public benchmarks tell you about general skill on popular open-source Python projects. What you want to know is whether an agent can work in your codebase: your frameworks, your conventions, your weird build. The good news: your git history is a ready-made benchmark generator. Every merged pull request that added tests is a task with a known answer and a built-in grader. This is exactly how SWE-bench was built, applied to your repo.

1 · Merged PRs gh pr list --state merged 2 · Filter touches src AND tests has a real description diff not huge 3 · Split diff src files → gold patch test files → hidden test patch 4 · Label tests parent + test patch vs + gold patch too → F2P and P2P 5 · Validate F2P not empty gold passes 3 of 3 drop flaky tests e.g. 3,000 PRs 600 600 260 200 tasks Task instance base_commit   = PR's parent commit task          = issue / PR description (scrubbed) test_patch    = PR's test changes (hidden) FAIL_TO_PASS, PASS_TO_PASS gold_patch    = reference only, never shown
Mining an eval from your own history. The counts are illustrative, not real data, but a large attrition from raw PRs to usable tasks is normal. Step 5 is what separates a trustworthy benchmark from a noisy one.

Step by step, here is what each stage does and why:

  1. Collect merged PRs. Use the GitHub CLI or API. Linked issues make the best task descriptions, because they were written before the fix, by someone who did not know the answer.
  2. Filter. Keep PRs that change both source and test files, that have a description a human could act on, and whose diff is a reasonable size. Drop dependency bumps, reformatting and pure refactors.
  3. Split the diff by path into the gold source patch and the hidden test patch.
  4. Label the tests by running them at the parent commit with only the test patch (new tests should fail), then with both patches (they should pass). Tests that fail then pass are F2P; tests that pass both times are P2P.
  5. Validate. Throw away tasks with no F2P tests, tasks where the gold patch doesn't pass reliably, and tests that flip between runs.
"""Mine SWE-bench-style tasks from your own repository. A sketch: adapt the
path rules, test runner and Docker image to your project."""
import json, re, subprocess
from pathlib import Path

REPO = str(Path("~/src/myrepo").expanduser())
GH_REPO = "my-org/myrepo"
IMAGE = "myrepo-eval:latest"      # Dockerfile with your deps preinstalled

def sh(*cmd, **kw) -> str:
    return subprocess.run(cmd, capture_output=True, text=True, check=True, **kw).stdout

def is_test(path: str) -> bool:
    return path.startswith("tests/") or re.search(r"(^|/)test_[^/]+\.py$", path) is not None

def merged_prs(limit: int = 1000) -> list[dict]:
    out = sh("gh", "pr", "list", "--repo", GH_REPO, "--state", "merged",
             "--limit", str(limit),
             "--json", "number,title,body,mergeCommit,closingIssuesReferences")
    return json.loads(out)

def build_instance(pr: dict) -> dict | None:
    merge = pr["mergeCommit"]["oid"]
    base = sh("git", "-C", REPO, "rev-parse", f"{merge}^1").strip()  # pre-PR commit
    files = sh("git", "-C", REPO, "diff", "--name-only", base, merge).split()
    src = [f for f in files if f.endswith(".py") and not is_test(f)]
    tests = [f for f in files if is_test(f)]
    if not src or not tests or len(files) > 15:
        return None                                   # step 2: filter
    gold = sh("git", "-C", REPO, "diff", base, merge, "--", *src)
    test_patch = sh("git", "-C", REPO, "diff", base, merge, "--", *tests)

    # step 4: label tests (run_tests is the Docker helper from the grading section)
    before = run_tests(IMAGE, REPO, base, [test_patch], tests)
    after = run_tests(IMAGE, REPO, base, [gold, test_patch], tests)
    f2p = [t for t, ok in after.items() if ok and not before.get(t, False)]
    p2p = [t for t, ok in after.items() if ok and before.get(t, False)]

    # step 5: validate stability of the gold solution
    for _ in range(2):
        again = run_tests(IMAGE, REPO, base, [gold, test_patch], tests)
        f2p = [t for t in f2p if again.get(t)]
        p2p = [t for t in p2p if again.get(t)]
    if not f2p:
        return None

    issue = pr["closingIssuesReferences"]
    task_text = pr["title"] + "\n\n" + (pr["body"] or "")
    return {
        "instance_id": f"{GH_REPO.replace('/', '__')}-{pr['number']}",
        "base_commit": base,
        "problem_statement": scrub(task_text),   # remove code the PR itself added!
        "linked_issues": [i["number"] for i in issue],
        "patch": gold, "test_patch": test_patch, "test_files": tests,
        "FAIL_TO_PASS": f2p, "PASS_TO_PASS": p2p,
    }

def scrub(text: str) -> str:
    """PR descriptions often describe HOW the fix works. Strip code blocks and
    diff snippets; better still, prefer the linked issue's text, or have a
    human rewrite the task as a user would have reported it."""
    text = re.sub(r"```.*?```", "[code removed]", text, flags=re.S)
    return re.sub(r"^[+-].*$", "", text, flags=re.M)

if __name__ == "__main__":
    with open("my_bench.jsonl", "w") as f:
        for pr in merged_prs():
            inst = build_instance(pr)
            if inst:
                f.write(json.dumps(inst) + "\n")
Five traps when mining your own history
  • PR descriptions leak the solution. They are written by the person who fixed the bug. Prefer linked issue text, or rewrite the task by hand as a user report.
  • Git history leaks the future. If the agent's container has your full .git, it can find the merge commit. Give it a shallow copy at base_commit only, with no remote and no later refs.
  • The model may have trained on your repo if it is public. Track task dates and compare scores on PRs from before and after the model's training cutoff.
  • Tests written for the gold patch are over-fitted to it. They may test helper names that only exist in that implementation. Review a sample by hand, like Verified did, and drop or loosen unfair ones.
  • Environments rot. Old commits may need old dependency versions. Build one image per era of your lockfile, and check with the gold patch that each task still passes before trusting it.

A practical recipe for teams

  1. Start with 30 to 50 hand-reviewed tasks. That is enough to catch big regressions and to compare harness choices.
  2. Run each agent configuration 3 or more times per task: agents are stochastic, and you want pass@1 with a confidence interval, and ideally pass^3.
  3. Record the full scorecard (resolved, regressions, diff size, judge scores, cost, turns) and store every trajectory.
  4. Add new tasks every month from freshly merged PRs. They are, by construction, after most models' training data, which gives you a private, contamination-resistant slice for free.
  5. Wire the suite into CI for your agent's prompt and tool changes (Module 15).

Code-review agents

A growing class of coding agents does not write the fix at all: it reviews a pull request and leaves comments. Evaluating review is harder than evaluating fixes because there is no test to run. The usual building blocks:

  • Seeded-bug recall. Take known-good PRs, inject a bug (or use historical PRs that were later reverted or hot-fixed), and measure how often the reviewer flags the right location. This gives recall: of the real problems, what fraction did it catch?
  • Comment precision. Have humans or a calibrated LLM judge label each comment as valid-and-useful, valid-but-trivial (nitpick), or wrong. Precision matters enormously here: a reviewer that raises ten false alarms per PR gets ignored, no matter its recall.
  • Acceptance in production. Once deployed, track the rate at which developers resolve, apply or dismiss the agent's comments. This is an online eval signal (Module 15), noisy but grounded in real behavior.
  • Matching comments to ground truth. A comment "counts" if it points at the right file and line range and describes the same problem. Line matching is code; "same problem" usually needs a judge.

Report precision and recall together (or an F-score), per severity level, and look at examples. A reviewer tuned only for recall becomes a noise machine.

Known bugs (ground truth) Reviewer's comments B1 · line 42off-by-one in loop bound B2 · line 88missing None check B3 · line 130SQL built by string concat missed C1 · lines 41-43"loop stops one early"match C2 · line 88"user may be None here"match C3 · line 12"rename tmp to buffer"nitpick C4 · line 60"possible race condition"false alarm C5 · line 101"add a docstring"nitpick same lines + same problem precision = 2 useful / 5 comments = 0.40 how much of what it says is worth reading recall = 2 caught / 3 bugs = 0.67 how many real problems it finds
Scoring a review agent on one PR. Illustrative example. Matching "same lines" is code; deciding "same problem" usually needs a judge. Notice the missed bug is the most severe one: report precision and recall per severity level, not only overall.

Evaluating coding assistants as products

Everything so far asks one question: can this model and harness solve a coding task? A company that ships a coding assistant (an autocomplete like Copilot or Cursor Tab, an in-editor agent, or a terminal agent like Claude Code) has a different question: does this product make real developers ship better software, faster, on their own code, week after week? A benchmark cannot answer that, for the same reason a car's lab emissions test cannot tell you how people actually drive it. You need measurements taken from real use.

Analogy: the test kitchen and the restaurant

A benchmark is the test kitchen: the chef cooks a fixed menu and a panel scores each plate. The product is the restaurant. What matters there is whether diners order the dish, finish it, come back next week, and do not get sick. The test kitchen is still essential (you do not serve an untested dish), but the restaurant needs its own numbers.

The product metrics funnel

Product teams think of assistant output as flowing through a funnel. Each step down is a stricter answer to "did the assistant help?":

  • Shown (impressions). How many suggestions, proposed edits or agent diffs the developer actually saw. This is the denominator, and the product itself controls it, which matters a lot below.
  • Accepted. The developer pressed Tab, clicked apply, or kept the agent's change. Acceptance rate = accepted / shown. Teams also track accepted characters or lines, because accepting a one-character suggestion and a 40-line function are very different events.
  • Survived (edit survival, also called code retention or persistence). Of the text that was accepted, how much is still there after N minutes, hours or days? Accepting a suggestion and then deleting it thirty seconds later is not help. Survival is usually computed by comparing the accepted span with the file at a later time.
  • Merged. Did the change reach a merged pull request? For agent products the natural unit is the task or PR: PR merge rate for agent-authored PRs, time to merge (open to merged), and review rounds before merge.
  • Stayed good. No revert, no hot-fix to the same lines, no incident traced back to it, within some window after merge. This is the closest you get to "the code was actually correct".
One team, one week of assistant output (illustrative numbers) stricter definition of "helped" Suggestions shown 1,000 completions or proposed edits Accepted 300 (Tab, apply, or kept) Still there after 30 days 180 of the 300 In a merged PR 150 No revert or hot-fix 140 denominator you control show less and every rate below rises acceptance rate 300 / 1,000 = 30% edit survival (30 days) 180 / 300 = 60% of accepted merged 150 / 300 = 50% of accepted stayed good 140 / 150 = 93% of merged In practice survival is counted in characters or lines, not whole suggestions
The coding-assistant metrics funnel. All numbers are illustrative, not real data. Each step down is a stricter test of whether the assistant helped. Acceptance is the easiest to measure and the easiest to game; the lower steps are slower to observe but much closer to value.

Published work gives these metrics some grounding. GitHub researchers (Ziegler et al., 2022) compared usage measures from Copilot telemetry with developers' own survey ratings of productivity, and the paper reports that the acceptance rate of shown suggestions predicted perceived productivity better than more detailed measures such as how much accepted code persisted. Google's research blog posts on its internal ML code completion report acceptance rate and the fraction of new code characters that came from accepted suggestions as headline metrics. GitHub's Copilot usage metrics for organizations expose similar counts (suggestions shown and accepted, lines accepted, active users). So acceptance-style metrics are real, widely used and cheap. They are also the metric most easily fooled.

Why acceptance rate alone misleads

Acceptance rate is a ratio, and the product controls the denominator. If a new version shows suggestions only when it is very confident, it shows fewer of them, and the ones it shows are accepted more often. The rate goes up even if developers receive less help in total. Cursor's post on training its Tab model with online reinforcement learning reports exactly this shape of change, with fewer suggestions shown and a higher accept rate, which is a good outcome if total useful code held up. You only know that by looking at more than the rate.

Version A (shows more) Version B (shows less) shown 1,000 accepted 300 accepted chars 12,000 shown 500 accepted 200 accepted chars 9,000 acceptance 30% 12,000 characters of help acceptance 40% 9,000 characters of help (25% less) Rate up, delivered value down: always pair a rate with a volume
The acceptance-rate trap. Illustrative numbers. Version B looks better on the headline ratio because it shows fewer, safer suggestions, yet developers get less accepted code overall. Report accepted characters (or surviving characters) per developer per day alongside the rate, and whether B's 9,000 characters survive better than A's 12,000.

The other traps, each with the metric that guards against it:

MetricWhat it tells youHow it gets fooledPair it with
Acceptance rateSuggestions feel useful in the momentShowing fewer, safer suggestions; accepting and then deletingAccepted characters per developer per day; survival
Edit survival (N days)Accepted code was actually keptBoilerplate and trivial lines survive easily; files that are never touched again "survive" by defaultSurvival weighted by size and by file activity; revert rate
Time to merge, PR throughputEnd-to-end flow of workSmaller PRs, lighter review, more trivial changesReview rounds, post-merge defects and reverts
Agent task successThe agent finished what was askedLogged as "done" when the user gave up and fixed it by handFollow-up fix rate, explicit rating, merged-and-not-reverted
Weekly active users, retentionPeople keep choosing the toolMandated use; tool is on by defaultVoluntary use in opt-in teams; usage depth
Self-reported productivityHow the tool feelsNovelty, expectations, wanting to look modern (see below)Measured time and outcomes on randomized tasks

For agentic products the unit shifts from the keystroke to the task. Suggestion-level metrics fade and teams watch session-level ones instead: did the session end in an accepted change, how many times did the human have to step in or correct course, how many permission prompts and turns did it take, what did it cost in tokens per merged change, how many review comments do agent-authored PRs attract, and how often are they reverted. These map directly onto the offline scorecard from the "Beyond pass/fail" section above, which is the point: the same questions, asked of real use.

Measuring edit survival yourself

If you run an assistant inside your company, you can compute survival from two things you already have: a log of what was accepted (file, inserted text, time) and git. The idea: for each accepted span, look at the same file N days later and count how many of its lines are still there. Line-level matching is crude, but it is honest and easy to explain.

"""Edit survival: what share of accepted assistant-written lines is still in
the file N days later. A sketch; a real product logs a stable suggestion id and
the exact inserted span at accept time."""
import subprocess
from collections import Counter
from datetime import datetime, timedelta

MIN_LEN = 8   # ignore lines like "}" or "else:" that survive by accident

def git(repo: str, *args: str) -> str:
    return subprocess.run(["git", "-C", repo, *args], capture_output=True,
                          text=True, check=True).stdout

def rev_at(repo: str, when: datetime, branch: str = "main") -> str:
    """Last commit on the branch at or before a moment in time."""
    return git(repo, "rev-list", "-1", f"--before={when.isoformat()}", branch).strip()

def file_at(repo: str, path: str, rev: str) -> str:
    try:
        return git(repo, "show", f"{rev}:{path}")
    except subprocess.CalledProcessError:
        return ""                      # file deleted: nothing survived

def surviving(inserted: str, later_file: str) -> tuple[int, int]:
    wanted = [l.strip() for l in inserted.splitlines() if len(l.strip()) >= MIN_LEN]
    pool = Counter(l.strip() for l in later_file.splitlines())
    kept = 0
    for line in wanted:                # multiset match: each later line used once
        if pool[line] > 0:
            pool[line] -= 1
            kept += 1
    return kept, len(wanted)

def survival_rate(events: list[dict], repo: str, days: int = 30) -> float:
    """events: [{"path": "src/app.py", "inserted": "...", "accepted_at": datetime}]"""
    kept = total = 0
    for e in events:
        rev = rev_at(repo, e["accepted_at"] + timedelta(days=days))
        k, n = surviving(e["inserted"], file_at(repo, e["path"], rev))
        kept, total = kept + k, total + n
    return kept / total if total else float("nan")

Three honest caveats. First, a line that was rewritten slightly (a renamed variable) counts as lost, so survival is a lower bound. Second, code in files nobody touches again "survives" by default; compare against a baseline of human-written lines in the same files over the same window, otherwise you are measuring how busy the file is. Third, a renamed or moved file looks deleted unless you follow renames (git log --follow). Code-analytics vendors such as GitClear have published repository-level analyses arguing that short-lived and duplicated code rose as AI assistants spread. Those studies are observational (they cannot separate the tool from everything else that changed), so treat them as a hypothesis to check in your own history, not as a measured effect.

Online A/B tests for coding assistants

To know whether version B of the assistant is better than version A in real use, run a randomized experiment, exactly as in Module 15, with a few coding-specific rules:

  • Randomize by developer, not by request. A developer who sees A in the morning and B in the afternoon learns habits from both, and a session that switches versions mid-task is contaminated.
  • Pick one primary metric in advance (for example surviving characters per developer per day) and list guardrail metrics that must not get worse: latency, error rate, revert rate, cost per developer.
  • Analyze at the developer level. One heavy user produces thousands of correlated suggestions. If you treat every suggestion as independent, your confidence interval will be far too narrow.
  • Run long enough to see survival and merges, which lag acceptance by days or weeks, and long enough for novelty to wear off.
import numpy as np

rng = np.random.default_rng(0)

def dev_bootstrap_ci(shown_a, acc_a, shown_b, acc_b, n_boot=5000):
    """95% CI for (rate_B - rate_A), resampling DEVELOPERS, not suggestions.
    Each array has one entry per developer: suggestions shown / accepted."""
    shown_a, acc_a = np.asarray(shown_a), np.asarray(acc_a)
    shown_b, acc_b = np.asarray(shown_b), np.asarray(acc_b)
    diffs = np.empty(n_boot)
    for i in range(n_boot):
        ia = rng.integers(0, len(shown_a), len(shown_a))   # resample developers
        ib = rng.integers(0, len(shown_b), len(shown_b))
        diffs[i] = acc_b[ib].sum() / shown_b[ib].sum() - acc_a[ia].sum() / shown_a[ia].sum()
    return np.percentile(diffs, [2.5, 97.5])
Worked example: the interval that shrank too much

Illustrative numbers. 400 developers per arm; version A's acceptance rate is 30.1%, version B's is 31.0%. Treating each of roughly 600,000 suggestions as independent gives a 95% interval for the difference of about [+0.5, +1.3] points: "significant". Resampling developers instead gives something like [−0.4, +2.2]: not distinguishable from zero, because a handful of very heavy users dominate the suggestion count and their behavior moves together. The developer-level interval is the honest one. The same clustering idea appears in Module 04.

OFFLINE (before users see it) ONLINE (with real users) public benchmarks (SWE-bench, Terminal-Bench) repo-history suite (your merged PRs) replayed real sessions (with consent) + minutes to hours, cheap, reproducible + safe: no user sees a bad version - blind to how people really use it - tests only what you thought of telemetry funnel (accept, survive, merge) A/B test randomized by developer surveys, interviews, diary studies + real tasks, real behavior, real value + catches what no one thought to test - weeks to reach a clear answer - noisy; only for shipped candidates only what passes surprises become new offline tasks ship, iterate or roll back
Offline and online evaluation of a coding assistant. Offline suites are the gate: cheap, fast, repeatable, and they keep bad versions away from users. Online measurement is the verdict on real value, but it is slow and only possible for versions you dared to ship. Every online surprise (a drop in survival on one language, a spike in reverts) should become a new offline task, so the gate learns.

Developer-reported productivity, and why it misleads

The cheapest way to ask "does this help?" is to ask developers. Surveys are genuinely useful for finding friction ("it keeps suggesting the old API") and for measuring satisfaction. They are a poor measure of speed. The clearest evidence is a randomized controlled trial by METR, published in July 2025. Experienced open-source developers worked on real issues from large, mature repositories they already maintained; each issue was randomly assigned to "AI tools allowed" or "AI tools not allowed", and the time to finish was recorded. The paper reports that developers forecast before starting that AI would cut their completion time by about 24%, estimated afterwards that it had cut it by about 20%, and in fact took about 19% longer when AI was allowed.

Expected, felt and measured effect of AI tools (METR, early-2025 study) Forecast (before) 24% less time Estimate (after) 20% less time Measured (RCT) 19% more time -20% 0 +20% +40% change in completion time with AI allowed (right = less time) same developers, same kind of task, AI randomly allowed or not per issue
Feeling faster is not being faster. Values as reported by METR's July 2025 paper (arXiv 2507.09089), rounded; bar lengths are to scale. The gap between perception and measurement is the lesson, not the exact percentage, which applies to one group of experienced developers, on their own mature repositories, with early-2025 tools.

Read this result carefully, in both directions. It does not show that AI tools slow everyone down: the study was small (the paper reports 16 developers and 246 tasks), the developers were experts in codebases they knew deeply, and the tools were those of early 2025. METR itself lists these limits, and in February 2026 it published a note explaining that it was changing its experiment design for follow-up studies. What it does show, very clearly, is that self-reports can point in the opposite direction from measured time. Reasons include: waiting on the model feels like work but is idle time, reviewing and fixing generated code is easy to forget, novelty feels productive, and nobody wants to seem slow to adopt new tools.

Rules for productivity claims
  • Never report a survey result as a speedup. Report it as what it is: perceived benefit or satisfaction.
  • If you want a speed number, randomize: per task (like METR), per developer, or per team with staggered rollout, and measure time and outcomes, not feelings.
  • Measure quality next to speed. Faster merges with more reverts is not a win.
  • Combine methods. Anthropic's late-2025 study of its own engineers ("How AI is transforming work at Anthropic") mixed a survey, interviews and Claude Code usage data, and surfaced concerns such as skill erosion that no single metric would show.

Putting it together: a product eval program for a coding assistant

  1. Offline gate. A repo-history suite (previous section) plus a few public benchmarks, run on every model, prompt or harness change. Nothing ships that regresses here.
  2. Replay set. With consent, keep a stream of anonymized real requests (the prompt, the repository state, what the developer finally committed). Re-run candidates on them offline and compare their output with what the developer actually shipped, using tests where they exist and a calibrated judge otherwise (Module 13).
  3. Dogfood and canary. Internal users first, then a small percentage of real users, watching guardrail metrics (latency, errors, reverts).
  4. A/B test for anything big, randomized by developer, with one pre-registered primary metric from low in the funnel (surviving code, merged and not reverted) and developer-level confidence intervals.
  5. Qualitative loop. Read sessions where the developer rejected or rewrote the output (Module 12). Each new failure mode becomes an offline task.
Go deeper

Randomized rollouts, guardrail metrics, sampling live traffic and dashboards are covered for all agent products in Module 15. How to judge whether the harness (not the model) caused a product change is in Module 21.

Key takeaways

  1. Code can be graded by running it, which accepts many correct solutions, but a score is only as honest as the tests: weak tests inflate it, over-strict tests deflate it.
  2. SWE-bench grading: apply the agent's patch at the base commit in a fresh container, then the hidden test patch; the instance is resolved only if every FAIL_TO_PASS test passes and every PASS_TO_PASS test still passes.
  3. Scores lie in both directions: contamination, leaked solutions and git history push them up; unfair tests, flaky environments and tight harness budgets push them down. Read trajectories, not just numbers.
  4. Keep grading tests out of the sandbox, apply them last, block the network, trim git history and filter flaky tests by running the gold solution several times.
  5. Report a scorecard, not a bit: regressions, diff size, judged quality, security, cost per resolved task and turns.
  6. A coding score belongs to model × harness × budget. To compare models, hold the harness and budget fixed.
  7. Your own merged PRs are a ready-made, contamination-resistant benchmark, as long as you scrub leaked solutions and validate every task with the gold patch.
  8. A coding assistant is judged as a product by a funnel: shown, accepted, survived, merged, not reverted. Acceptance rate is cheap and useful but easy to game by showing less, so always pair a rate with a volume and a later-funnel metric.
  9. Offline suites are the gate; online A/B tests (randomized and analyzed by developer) are the verdict. Self-reported speedups are not measurements: in METR's 2025 randomized study, developers felt faster while measured time went up.

Check yourself

1. On SWE-bench, an agent's patch makes all FAIL_TO_PASS tests pass but breaks one PASS_TO_PASS test. The instance is…

The resolve rule is all-or-nothing. Breaking existing behavior counts the same as not fixing the bug at all.

2. Why was SWE-bench Verified created?

OpenAI's annotators flagged 38.3% of sampled tasks as underspecified and 61.1% for unfair tests, and filtered 68.3% out. Scores went up on the cleaned set because impossible tasks were removed.

3. Agents on SWE-bench were found running git log --all. Why is that a problem, and what is the fix?

Past history is fine and realistic. Future history is the answer key. Anything reachable in the sandbox is effectively part of the prompt.

4. You compare two models on your repo eval. Model X used your in-house harness with 200 steps, model Y was run in a vendor CLI agent with 50 steps. X scores higher. What can you conclude?

An agent's score is a property of model × harness × budget. To compare models, change only the model.

5. You sample 10 solutions per problem and 4 pass. What is pass@1, and is pass@10 appropriate for an autonomous agent that opens one PR?

pass@1 = c/n = 0.4. pass@k with k > 1 assumes someone picks the winner from k tries, which an unattended agent doesn't have.

6. When mining tasks from your own PRs, what is the biggest risk of using the PR description as the task prompt?

That is solution leakage. Prefer linked issue text written before the fix, or rewrite the task as a user report.

7. A new autocomplete model raises acceptance rate from 30% to 40%. The logs also show it displays half as many suggestions. What should you check before calling it a win?

Showing fewer, safer suggestions raises the rate even if developers get less help overall. Pair the rate with a volume metric and a later-funnel metric such as survival or merged-and-not-reverted code.

8. An internal survey says developers feel 25% faster with your coding agent. How should you report this?

METR's 2025 randomized trial found experienced developers estimated a speedup while their measured time went up. Self-reports measure perception, which is worth knowing, but they are not a measurement of speed.
Expert corner: interview-style questions

Q: Design an eval to tell whether a new coding agent is better than the current one for your company. Mine 100 to 200 tasks from recent merged PRs with tests; hand-review a sample for fairness; build per-era Docker images; run both agents 3+ times per task in identical sandboxes (no network, trimmed git, hidden tests applied last); report pass@1 with paired confidence intervals, pass^3, regressions on the full suite, diff size, judge-scored quality, cost per resolved task and turns; read 20 flipped trajectories in each direction before deciding.

Q: Your agent's resolve rate jumped 12 points overnight after a prompt change. What do you check? Whether new passes are genuine (read trajectories), whether the agent started touching test files or configs, whether it found a leak (git history, network, cached artifacts), whether the eval set or harness version changed, and whether the jump survives re-runs (variance).

Q: Why might SWE-bench Pro be more trustworthy than Verified in 2026? Its commercial and held-out splits use code that is not public, and its public split uses copyleft repos that training pipelines tend to avoid, so contamination is less likely. OpenAI reported that it "empirically seems to suffer less from contamination issues". It also has longer, multi-file tasks, so it is not saturated. It still depends on test quality, and its open-source set has had its own git-leak report, so the same hygiene applies.

Q: When would you choose continuous scoring (RE-Bench) or dollar weighting (SWE-Lancer) over binary pass rate? Continuous scores fit open-ended optimization tasks where "better" is a matter of degree. Dollar or effort weighting fits when tasks vary hugely in value and you want the metric to track economic usefulness rather than counting trivial and hard tasks equally.

Sources and further reading