v1.16.0: a check that never ran is not a check that passed
Released 2026-08-30. A release almost entirely about the difference between a guard that verified something and a guard that merely failed to complain.
What we were trying to fix
The kit had accumulated a lot of guards, and several of them answered two very different questions with the same silence.
guarded-push.sh was the clearest case. It pushed, read the remote back, and returned non-zero if the remote disagreed. But if the read itself failed, because the remote could not be listed, or the --remote named something the push had never written to, the caller got a code that looked like any other failure. Nothing there disproved the push and nothing there confirmed it, and the caller had no way to tell that apart from a remote that had actively contradicted the delivery (#172).
wait-ci.sh had the same shape from the other direction: it waited on one hardcoded check name, so a PR whose gating checks had been renamed or multiplied was declared green by a wait that had never looked at them (#188).
And the registry rule R10, which is supposed to prove that every script making a decision is either registered or explicitly recorded as not one, only covered part of the tree, so an executable could sit outside its reach and be neither (#252).
What we decided
Give “I could not find out” its own answer, everywhere it was being folded into “fine”.
guarded-push.sh gained a distinct exit code for unverified, separate from the code that means the remote was read and disagrees, plus a --verify-only mode that repeats the branch assertion and the remote read-back without pushing again. That is the precise instrument a caller needs after fixing whatever broke the listing.
wait-ci.sh now waits on every gating check rather than one name. R10 was widened so that an executable inside the paths it covers is either registered in decisions/registry.json or named in that file’s not_decisions map with a one-line reason.
The same release also made implement-issue’s Step 4 a single guarded call (#280), because the step had grown into a prose recipe that workers were re-spelling by hand, and one of them re-spelled the worktree-ignore check badly enough to commit in the main checkout. A recipe a worker retypes is a recipe a worker gets wrong; make-worktree.sh is that recipe made executable.
pinned-literals-check.py was widened from the single xunit.v3 pin to every restated pin in the tree (#158), and merge-pr started routing unresolved review threads into the review verdict instead of past it (#294).
What got cut
The new unverified exit code deliberately does nothing on its own. It does not retry, it does not back off, and it does not attempt a second push. All it does is refuse to claim either outcome and hand the recovery to the caller, who is the one who can see whether the problem is credentials, connectivity or a wrong remote name. An automatic retry was the tempting version and it would have turned an honest “unknown” back into a confident wrong answer.
R10’s widening also stopped short of hooks and skill-root scripts, which stayed outside its globs until #307 in v2.0.0. Sweeping the whole repository for anything executable was considered and left for later, since the value is in the directories where decisions actually live.
What bit us
The plan locator’s handling of an empty comment body, twice, in two consecutive releases. v1.15.0 fixed it crashing on a null comment body (#278); this release fixed it reporting the literal string "null" as though that were a plan (#286). One field, two spellings of empty, and the second only surfaced because the first fix made the code path reachable often enough to notice.
The bundle guard for the CI template needed three separate corrections in this window: a BUNDLE_DIST escaping BUNDLE_SRC through .. (#301), the dist-collapses-to-root refusal not naming BUNDLE_SRC (#293), and the two paths not being normalised in the guard body at all (#285). Path containment keeps looking like one check and turning out to be three.
Finally, the usage report was undercounting: it did not count sub-agent transcripts (#281), so every cost figure the fleet had been producing was low by whatever its sub-agents had spent. Measurement code deserves the same suspicion as the code it measures.