Your Harness Didn’t Get Better, It Got Lucky: Noise Bands for Agent Evals Paolo Perrone's October 31, 2026 analysis in Data Science Collective, recomputing public Terminal-Bench 2.0 leaderboard data (89 tasks, at least 5 runs each), found that swapping the harness from Terminus 2 to Copilot CLI under Claude Opus 4.6 moved scores by -2.5 points (62.9% to 60.4%), well inside a noise band of plus or minus 6.7 points, while swapping the model from Claude Opus 4.6 to Kimi k2.5 produced a 20.4-point drop that exceeded the plus or minus 7.8-point band. Perrone's recomputation found only 39 of the 89 tasks passed on every run, 30 were flaky and 20 failed every run, and that shrinking the harness-swap band to plus or minus 2.5 points would require roughly 650 tasks at 5 runs each. Picture a pull request that changes the harness around your coding agent: a new system prompt, a different tool layout, maybe a whole new CLI wrapped around the same model. The description says “eval score up 3 points.” Somebody approves it. Two weeks later somebody else reverts half of it because “the score dropped 2 points.” Nobody in that loop ever asked the only question that matters: is a 3-point move bigger than what this eval does on its own when nothing changes? I have been on both sides of that review, and I no longer accept a harness claim without an answer to it. This article is about how to get that answer from numbers other people have already published, and how I would wire the same calculation into a .NET tool and a CI gate. I am not reporting any experiment of my own here. Every number below comes from a named source, and I say who produced it. The cleanest worked example I have seen this autumn is in Paolo Perrone’s “How to Build an Agent Eval You Can Trust” Data Science Collective, October 31, 2026 . He took the public Terminal-Bench 2.0 leaderboard data, which publishes at least 5 runs for every one of the suite’s 89 tasks, and recomputed two comparisons himself. He is explicit that these are his own figures: he writes “by our count.” So read everything in the table below as an author-run recomputation of public leaderboard data. I have not re-derived it. +----------------------------------+---------------+---------------+---------+------------+---------------+| Comparison | Before | After | Change | Noise band | Reading |+----------------------------------+---------------+---------------+---------+------------+---------------+| Harness swap, Claude Opus 4.6: | Terminus 2 | Copilot CLI | -2.5 pt | +/-6.7 pt | inside band: || Terminus 2 - Copilot CLI | 62.9% | 60.4% | | | can't call it |+----------------------------------+---------------+---------------+---------+------------+---------------+| Model swap under Terminus 2: | Opus 4.6 | Kimi k2.5 | -20.4pt | +/-7.8 pt | outside band: || Opus 4.6 - Kimi k2.5 | 62.9% | drop only | | | real drop |+----------------------------------+---------------+---------------+---------+------------+---------------++--------------------------------------------------------------+-------------------------+| Terminus 2 + Claude Opus 4.6, 89 tasks, 5+ runs each | Value |+--------------------------------------------------------------+-------------------------+| Solid tasks passed every run | 39 || Flaky tasks passed some runs | 30 || Broken tasks failed every run | 20 || Share of runs passed | 62.9% || Share of tasks passed on every run | 43.8% || Band floor even with unlimited runs harness swap | about +/-5.7 pt || Tasks needed at 5 runs each to shrink band to +/-2.5 | about 650 || Best-to-worst spread, Factory Droid + Opus 4.6, 5 full runs | 3.4 pt |+--------------------------------------------------------------+-------------------------+ Source: Paolo Perrone, “How to Build an Agent Eval You Can Trust,” Data Science Collective, Oct 31, 2026, recomputed by the author from the harborframework/terminal-bench-2-leaderboard data on Hugging Face “by our count” . The Kimi k2.5 absolute score is not given in the article, only the drop. Author-run figures, not independently verified. Two things in there changed how I read leaderboards. The first is the contrast itself. A 2.5-point move from swapping the harness sits comfortably inside a band of plus or minus 6.7. A 20.4-point move from swapping the model is nearly three bands away. One of those is a finding. The other is a coin that landed slightly differently. The second is the 3.4-point spread for a single configuration run five times. The same harness, the same model, the same tasks, and the full-suite score still moved 3.4 points between its best and worst run. That is already more than the harness swap. If one configuration can wobble that much against itself, a single run of each side of a comparison tells you almost nothing. If you have used BenchmarkDotNet, you already have the right instinct. It never prints just a mean. It prints the mean next to an error column, and nobody on a .NET team would merge “my change makes this method 2% faster” when the error column says 5%. Agent evals deserve exactly the same discipline, and the arithmetic is not much harder. Here is the calculation Perrone uses, in plain steps. 1. Rerun every task. Five runs per task is the Terminal-Bench 2.0 leaderboard’s minimum. For each task you get a pass rate: 0 out of 5, 1 out of 5, and so on up to 5 out of 5. With five runs, a task’s pass rate can only be 0, 20, 40, 60, 80 or 100 percent. That coarseness is worth remembering. 2. Pair by task. For every task, subtract its pass rate under the old configuration from its pass rate under the new one. Perrone’s example is circuit-fibsqrt: 5 of 5 under Terminus 2, 0 of 5 under Copilot CLI, so its difference is minus 100 points. You now have 89 numbers, one per task. 3. The average of those differences is the score change. For the harness swap, that is minus 2.5 points. This is the mean of per-task rates. When every task has the same number of runs, it equals the “share of all runs passed” figure on a leaderboard. 4. Measure how much the differences scatter. That is the standard deviation of the 89 differences. A few tasks swung by 100 points, most did not move at all, and the standard deviation summarizes that spread in one number. 5. Divide by the square root of the task count. This is the part people find magical, and it is just averaging. One task tells you little. The average of 89 tasks is much steadier than any single task, and it gets steadier in proportion to the square root of how many you average, not in proportion to the count. Four times the tasks halves the band. That is why Perrone’s estimate of roughly 650 tasks to get down to plus or minus 2.5 points is not a typo. 6. Multiply by 1.96. If the per-task differences behave roughly like independent draws, about 95 percent of the time the true average change sits within 1.96 standard errors of what you measured. That gives the band: band = 1.96 stddev per-task differences / sqrt number of tasks If the measured change is inside the band, the eval cannot tell the change apart from luck. That is not the same as saying the change did nothing. It says this eval, at this size, cannot see it. A quick sanity check I like to do when reading anyone’s band: run the formula backwards. A band of plus or minus 6.7 over 89 tasks implies a standard deviation of the per-task differences of roughly 32 points my arithmetic, from his published figures . In words: individual tasks are swinging by a third of the scale between the two harnesses, in both directions, and those swings almost cancel out. The headline “minus 2.5” is a small residue left over from a lot of very large, opposite movements. That is not what most people picture when they read “the score dropped a bit.” Each difference compares a task with itself. A brutally hard task and a trivial task are both measured against their own baseline, so task difficulty drops out of the band. If you compared the two configurations’ overall scores as if they were unrelated samples, task difficulty would sit inside the noise and the band would be wider. Pairing is free precision, and it only requires that both configurations ran the same tasks. My tool below refuses to compare tasks that are missing on either side for that reason. Perrone separates two sources of noise, and I think this is the most practically useful part of his piece. Run luck: the same task passes on one run and fails on the next. More runs per task shrink this. Task luck: your 89 tasks stand in for all the work you care about, and a different 89 would give a different score. More runs do nothing for this. Only more tasks help. For the harness swap, he estimates run luck at only about a quarter of the band’s variance. Even with unlimited reruns, the band would bottom out around plus or minus 5.7 points, still wider than the 2.5-point drop. I find this genuinely sobering. The instinct when a result is ambiguous is “let’s rerun it a few more times.” For this comparison, that would not have settled anything. The bottleneck was the size of the task set. Perrone’s second move is to stop treating the suite as one number and sort the tasks by what their five runs did: For Terminus 2 with Claude Opus 4.6, his count is 39 solid, 30 flaky, 20 broken. The configuration passed 62.9 percent of its runs, but only 43.8 percent of tasks passed every time. He also points out that if every task passed independently at that same 62.9 percent rate, only about 10 percent would pass all five 0.629 to the fifth power . The real suite has far more solid and far more broken tasks than that, which means the pass rate is not one coin flip repeated, it is three different populations mixed together. That matters for harness work because only the flaky tasks swing from run to run. A harness change that “improves the score” by tipping a couple of flaky tasks over on a lucky day is the classic false win. And the reverse failure exists too: under Copilot CLI, two tasks lost all five of their passes circuit-fibsqrt and schemelike-metacircular-eval, per Perrone . A verdict on the average, even a correctly banded one, hides those. I want a tool that prints them by name so somebody reads their traces before calling two harnesses equal. Perrone’s own noise floor.py is behind a paywall, and I have not seen it. What follows is my own C implementation of the formula as he describes it in the article, plus the solid, flaky and broken split. It has no dependencies beyond the base class library. It takes a CSV with one row per task, per run, per configuration: FORMAT EXAMPLE ONLY: obviously fake placeholder rows, not results from any eval.task id,run index,config,passedexample-task-a,1,baseline-harness,1example-task-a,2,baseline-harness,0example-task-a,1,candidate-harness,1example-task-a,2,candidate-harness,1example-task-b,1,baseline-harness,0example-task-b,2,baseline-harness,0example-task-b,1,candidate-harness,0example-task-b,2,candidate-harness,1 That example only shows the shape. Fed to the tool as-is, it is rejected on purpose: two runs per task is below the default minimum of five, and the tool will not compute a band from tasks that do not meet it. A few decisions I made deliberately: // NoiseBand.cs// Paired noise band for agent eval results: is a score change bigger than luck?//// Input: a CSV with one row per task per run per configuration:// task id,run index,config,passed// passed accepts 1/0, true/false, pass/fail case-insensitive . Lines starting with ' ' are ignored.// Task ids and config names must not contain commas.//// Run as a .NET 10 file-based app:// dotnet run NoiseBand.cs -- results.csv --baseline