v2.2.0: the never-wait rule becomes a hook

Released 2026-09-06. A release about moving the fleet’s most expensive rule out of prose and into the runtime, and about the bench learning to refuse a number it did not measure.

What we were trying to fix

The auto-dev supervisor has one rule that costs more than any other when it is broken: never end a turn waiting for something. A sub-agent whose final message is “I’ll resume when CI finishes” is a sub-agent whose run is over, and the PR it was landing stays open with nobody left to land it. The rule was written down in three places and linted by a test that read the prompt files. Every worker session lost to it since 2.0 had passed that lint.

The evals had a sibling problem. run_all.py reported recall and specificity for each trigger contract, and on a run where the bench could only see the opening move of a conversation it would happily print a perfect specificity next to a zero recall. A result that good on a run that starved is not a result. It is the absence of one, printed in the shape of one.

What we decided

The never-wait invariant is now a Stop hook (#417). hooks/autodev-stop-gate.sh runs when a session tries to end, reads the fleet’s state file for this repository, and refuses the stop while an auto-dev run in this checkout still has undrained work. The prompt lint stays, but it is no longer the thing that holds the line. A rule that a lost session could pass is not the rule; the hook is.

wait-ci.sh learned the same lesson from the other side (#413). A check set that is final on one poll can still grow on the next, when a workflow posts its jobs late, so the script now waits for the set to be stable as well as final before it hands the table to a merge worker.

The bench refuses results it cannot believe (#450, #461). A zero-recall, perfect-specificity run and a starved run are both rejected by name, and the four trigger-contract rows no harness could observe were dropped rather than left to report a number nobody could reproduce. review-sessions went on a weekly timer (#436), so the transcripts get read whether or not anyone remembers to ask.

What got cut

We did not make the Stop hook block on a board or a network call. It reads one local state file, keyed by owner and repository, and every path that is not “an auto-dev fleet has undrained work here” exits zero without a word. A hook that could refuse a stop because a server was down would be worse than no hook, and the fail-open decision of ADR 0002 applies to it unchanged.

We also stopped hand-copying the release title rules into skill prose (#258). implement-issue now dry-runs release-title-gate.sh on the candidate title before opening the PR, which is the only copy of the rule that cannot drift.

What bit us

Parsers again, and one hook that judged nothing. The git write-gate looked at the first line of a Bash call, and a heredoc opened on that line hid the git push that followed; the gate now judges the line that opens the heredoc (#440). A grep -q fed by a streaming producer under pipefail returned the producer’s SIGPIPE as the test’s verdict, so a positive match could read as a failure; a check now refuses that idiom in the suites (#391).

survey.sh had two windows that were smaller than the backlog. Its open-issue window stopped at 300 rows, so a blocker named on a body line past that point never resolved (#367), and it paged with a flag that only the raw API honours (#452). Both are fixed, and the second one is the kind of bug that only shows up once a backlog is large enough to page, which is to say once the kit is being used.