v1.15.0: the kit stops believing its own comments
Released 2026-08-27. Two failures with one shape: a checker that read a program’s comments instead of its code, and a worker that scaffolded a second pull request for an issue that already had one.
What we were trying to fix
scripts/decision-check.py enforces that every registered decision is single-homed, invoked, and not restated in prose. Several of its rules did that by reading comments and docstrings rather than the statements that actually emit a verdict or read an input. A rule that reads comments passes on a program whose comments are aspirational, and fails on one whose comments are terse and whose code is right. Rules R4, R5 and R6 were all doing it (#253, #261).
The bigger failure was on the worker side. A single implement-issue session dispatched for issue #195 produced two open pull requests, one empty scaffold and one carrying the whole implementation, under two different branch slugs and with identical titles and bodies. Every resume check in implement-issue at the time was keyed on an exact branch-name match, so a second worktree, branch and pull request under a different slug was invisible to all of them, and the empty scaffold was left standing where it could have won the merge (#214). Why one session did that was never established, and the issue says so in as many words; what was established is that nothing in the skill stopped it.
auto-dev had the same exposure one level up, where nothing stopped the orchestrator dispatching one issue twice (#248).
What we decided
Ask the authority, not the artifact.
For the decision rules, that meant rewriting R4, R5 and R6 to read what a program actually emits and actually reads. For the duplicate pull request, it meant adding a fallback that runs whenever the branch-name check finds nothing: ask GitHub whether this issue already has an open pull request whose body closes it, using GitHub’s own closing-keyword set rather than a bare mention. A branch name is a guess about identity; the closing link is the identity.
The release also gave the untrusted-input boundary a statement at every ingest point (#266), so a skill that reads an issue body, a comment or a report knows in the same paragraph that the text is data rather than instruction. And it gave the main-working-tree derivation one home (#125) after two callers had been carrying two broken spellings of it, with a guard test that fails if either spelling comes back.
Tier routing in auto-dev was priced and refined (#271), and the three guarded-git guarantees that had only been prose finally got checks that can fail CI (#161).
What got cut
The pull-request existence guard resolves duplicates rather than preventing them. When it finds two open pull requests already closing the same issue, it picks the more implemented one by a documented tie-break and requires the worker to name the duplicate in its report. Closing the loser automatically was rejected: a standing duplicate is worth a human’s attention, and a script that closes pull requests on a heuristic is a worse failure than the one it fixes.
#245 explicitly left its wider option on the table. It made implement-issue’s title fallback path-first rather than diff-shape-first, and stopped short of touching the release gate’s RELEASABLE_TYPES or its shipped-path classification, which stayed #55 and #58’s territory.
What bit us
Null bodies, again, and a fix that had to be written twice in two places.
The plan locator crashed on a null comment body (#278) and the pull-request existence guard crashed on a null pull-request body (#259), a week apart, for the same reason: GitHub returns null where the code expected a string, and neither reader had been written against a fixture that contained one.
The title-derivation bug was the more instructive one. It had already been fixed once in create-issue (#233, in v1.14.0), and it came straight back in implement-issue because the same rule lived in two skills as two paragraphs (#245). The same lesson _shared/ exists for, learned the expensive way one more time.