Six releases and 128 commits between August 1 and August 11. Most of them chase one defect that kept reappearing in different costumes: a thing that did not happen looking exactly like a thing that did. A guard that never ran reporting as installed. A run that lost its budget recorded as a score of zero. An unpriced job reading as free.
Here are the ten changes that matter, roughly in the order a user meets them.
1. Gates that stop asking β when the agent has earned it
The headline feature of v2.96. An agent whose holdout eval conclusively passes β the confidence interval clears the bar, at three samples with a majority judge, never a point estimate β has its gate drop to notify-only. The pipeline proceeds; the entry still lands in the board's inbox; you may intervene and need not.
Fifteen of sixty-two agents qualify today. The interesting part is who doesn't:
gated devops Class A β the question at a production deploy
is not competence, so no score drops this gate
devops holds the best evidence in the repository β 82%, interval [72%, 89%], n=77 β and stays gated, because a production deploy needs a human for reasons that have nothing to do with pass rates. A tier that can't refuse the agent with the best numbers is a rubber stamp, so that refusal is a test, not a comment.
The set is derived from eval history at read time, never stored as a list. A stored list of blessed agents rots silently: an agent that regresses keeps its pass until someone remembers to revoke it. Computed, a regression restores the gate on the next measurement with nobody acting. Opt-in per project (gate-tiering: evidence in PROJECT.md), and every failure path fails closed β a mis-read leaves every gate standing.
2. Approving a gate now actually moves the pipeline
Before v2.96, approving a gate in the board closed the task, wrote the decision log, sent an SSE event β and told the pipeline nothing. Worse: the session-resume hook opens with a freshness shortcut ("newest verdict is over a day old β this is history, not work waiting") and returned before reading a single gate. Approve gate:arch on a stage that ran three days ago, and the strongest possible evidence that work is waiting β a human saying go β was the one fact never consulted.
Now the board records the approval where the next session looks, and a recorded approval overrides the shortcut. It overrides nothing else: only ready-to-dispatch moves, the same transition never dispatches twice, Class A agents never run unattended. You still open a session β but it already knows what it's for, however long ago the stage ran.
3. The self-improvement loop runs β and prices itself first
The weekly eval loop (run the holdout suite, detect drift, alarm on regressions) now runs locally with one command:
bash scripts/loop-local.sh # drift check only β free
bash scripts/loop-local.sh --evals # price the run, then ask
Pricing it surfaced a number nobody had felt: the scheduled configuration costs about $47 per run β roughly $200/month for a weekly job whose bill nobody had ever seen, because the workflow that would have spent it never executed. So the loop prints the price, states its confidence and the history it's standing on, and stops. A shape that has never been priced reports unknown, not $0 β an unpriced run shown as free reads exactly like a run that costs nothing.
4. A run that runs out of money is not a row of zeros
Mid-baseline, a 75-file eval run spent $13.99, hit the provider's credit limit at file 58 β and then made 147 more calls that could not possibly succeed, one per remaining case, each returning the same 402. The dropout gate correctly reported those files as NOT MEASUREDβ¦ and the run had already written them into trend history with rate: 0.
The drift detector reads rate. The next comparison would have seen thirteen evals collapse from ~0.85 to zero overnight and alarmed on a regression that was really an empty wallet.
Now: the run stops at the first terminal failure (402/401 are terminal; 429 is the provider asking you to slow down, and resolves on its own), unmeasured files never reach the baseline, and the stop report names the money spent β because that's the next question anyone asks.
5. Drift compares like with like
Two fixes to the drift detector that each flipped conclusions:
- Sample count is part of the comparison shape. A single sample of a three-case eval can only score 0, 0.33, 0.67 or 1.00 β its own history swings 0.83 β 1.00 β 0.83 with nothing changing. A three-sample mean is a better estimate of the same quantity, and read as a drop against the average of those swings. The first real scheduled-shape run reported 49 of 75 evals "drifted" β 29 up, 20 down. Symmetric drift is not a regression; it's a ruler change.
- A rise is not an alarm. The loop painted "DRIFT DETECTED" in red over two evals that had improved. Drops exit non-zero; rises are printed as worth-a-look. A red banner over good news is how red banners stop meaning anything.
6. gate:prompt β a candidate prompt has to beat the incumbent's interval
When the improvement loop produces a candidate prompt, it reaches a human only if: tuning conclusively passes, holdout conclusively passes, and holdout's lower bound clears the current prompt's rate. An interval that contains the incumbent has not beaten it β it has shown it might be better, which is not the same claim.
This is the rule that would have stopped a real campaign: four prompt iterations that went 5 β 11 β 12 β 11 β 10 passing cases, every one of which looked like progress at the time. And when approval lands, the holdout rotates β a quarter of its cases exchange with tuning, seeded by the approved prompt's own hash. The improver only ever sees the holdout's number, but a number is a channel: enough rounds of "0.61, 0.66, 0.59" is enough bits to fit a fixed case set through a straw. Rotation keeps the straw from draining the glass.
7. The privacy guard that had never run
The uncomfortable one. The pre-push hook that blocks private project names from reaching the public repo was installed, executable, up to date β and had never run. core.hooksPath pointed at a directory the repository had moved out of months earlier, and git honours that setting even when the path no longer exists. Every push went unscanned. The installer had printed "installed pre-push hook", which was true about the file it wrote and false about the file git reads.
Fixes, in order of generality:
- A reachability checker resolves the directory git will actually read hooks from and names the state β
unreachable,missing,not-executable,stale,okβ each with the command that fixes it. It runs first in local CI, because it's the check that fails silently. - Private terms are now derived from the workspace at push time instead of hand-maintained. A denylist entry nobody added produces the same silence as a name that's safe; a project created tomorrow is covered the moment its directory exists.
- Matching moved from substring to word boundary, after one real term turned out to be a substring of an ordinary identifier and lit up thirteen innocent files. A guard that cries wolf gets pushed past with
--no-verifyβ and then it protects nothing.
Proven live, not by argument: a probe branch carrying a private name pushed cleanly to the public remote before the fix, and is refused after it.
8. Receipts: what was reviewed is what shipped
The evidence ladder had four rungs: a verdict exists β the artifact exists β the check re-ran β an independent re-verification agreed. v2.96 adds the fifth: the bytes that were reviewed are the bytes that shipped. The pre-push hook compares reviewed files against the reviewer's approval and reports what changed after it:
receipt: against code-reviewer APPROVED at 2026-08-11T07:07:00Z
6 reviewed file(s) changed after the approval
Warn-only, and honest in both directions β the release that shipped this feature was itself flagged by it, correctly.
9. Lessons become mechanical rules
We've measured three separate times that a rule enforced by structure lands ~5Γ more reliably than the same rule as prose (18% vs 92% adherence on one campaign). Yet our own learning loop ended in prose. Now incidents become deterministic checks that run on every file an agent edits β before any reviewer β and findings return to the agent that just wrote the defect, so it fixes them while the context is still its own.
Three rules to start, each bought by a specific incident: a catch that reports a cause it never observed (ours printed a confident guess over a ReferenceError three lines up), multi-statement error-swallowing with no trace, and exclusion lists with no stated reason. The tuning is the feature: the first sweep flagged 341 sites, 338 of them false, and each false class narrowed a rule and became a named test. 341 β 0 in four iterations β and the sweep now gates CI at zero, so any future finding is a regression against a rule an incident paid for.
10. The board stopped counting things twice
A cluster of smaller honesty fixes in the admin board: a directory inside a registered project is not a second project (the CLI package's own config had it counted as one β same 228 tasks, two names); the home directory and the plugin cache are not projects; an empty project reads differently from one that could not be read (X-Board-Degraded instead of a confident zero); documents open at half the window instead of a task-sized 480px column; and metrics actually change when you switch the 1/7/30-day window.
The pattern, if there is one
Almost none of this is new capability. It's the same system, minus the places where silence was ambiguous. A check that didn't run, a name nobody listed, a run that never happened, a cost nobody priced β each one used to produce nothing, and nothing looked like success. The two-week theme was making absence say something.
v2.96.0 is on npm: npx great-cto install. MIT, as always.