Tagout

v3.1.1: six defects the kit found in itself

Released 2026-09-21. Six bug fixes, five of them measured rather than reported.

What we were trying to fix

Every issue in this patch came out of review-sessions, the skill that reads past transcripts and counts what the kit itself cost. Running it over 2,078 sessions turned up three things at once.

Two gates were wrong about their own denials. The roseline gate offers exactly one way out of a denial, and its message promises it twice: issue the identical Read again and the retry is allowed. The marker file that escape keys on carried no namespace, so it was byte identical to the one ~/.claude/hooks/roseline-gate builds, the hand written hook the kit’s gate is a hardened rewrite of, which any host that ever had it still has registered. Both hooks ran on the same Read, one armed the token and the other consumed it, the deny won, and the retry found nothing. 264 instructed retries in the window, 264 denied again, none allowed. That is a fail closed path in a gate ADR 0002 says has none.

The same predecessor hook poisoned the count that found it. harvest.py decided a hook-deny on the Blocked by the ... lead sentence, which both hooks print, so 1,939 of one run’s 2,395 signals were another hook’s work credited to this kit. 81 percent of the artifact the skill exists to produce.

Two gates refused clean trees. plan-freshness.sh read a backticked command in a plan’s **Files:** field as a path and reported a fresh plan STALE. host-adapters.py check read a __pycache__/*.pyc as a source, found it was not UTF-8, and reached no verdict at all. Both are the same shape: a walk that sees what is merely present rather than what the repository ships.

One convention had five spellings. Which repository a script addresses was spelled five different ways across five scripts, three of them inside one skill that a caller reads in sequence. Agents transfer the shape they used a call ago. 11 refusals in 18 days, 6 on 3.1.0.

What we decided

The marker gets a namespace it owns. harvest.py matches wording only the kit’s gates print, taken from the function that prints it rather than from a call site, so a deny added later is covered the day it is written. Both walks go through one predicate instead of asking the question twice. -R <[host/]owner/repo> is accepted everywhere the repository is named, every current spelling keeps working, and a wrong shape now prints the script’s usage line under the refusal, which turns a lost turn into a correction.

The sixth fix is a fixture rather than a fix. The write gate decides whether to honour a GIT_GATE=off prefix by reading agent_id from the tool payload, and forty test cases drove a payload the suite writes for itself. Nothing in the repository had ever seen a real one. Two are recorded now, one from a sub agent and one from the main thread, and the suite holds the synthetic ones to their shape.

What got cut

Rewriting all five scripts onto one argument parser. It is the right end state and far more than 11 refusals justify; accepting an alias is what those refusals actually paid for.

What bit us

The journal’s own coverage gate, which requires an article for every published release but the newest. v3.1.0’s CI run finished ten seconds before its tag was created, so the run that went green was the last one where v3.0.1 was still exempt. From that second on, tests/skills/test.sh was red on main and on every pull request opened against it, and nobody found out until the next branch was built four days later.

docs/journal/index.md predicts this exact situation and names the recovery. Predicting a failure in prose is not the same as being told when it happens: nothing watches main between merges, so a gate that can only go red on a pull request stays silent about the branch everything is cut from.