v1.23.0 ·
Four gates that lied, and the one rule under them
The problem
Four lifecycle automations were reporting something that was not true.
Two of them blocked work they should have allowed. The design review gate required a findings file for every required artifact of the phase it guards, while the review skill told reviewers not to write one for the task breakdown — so following the documentation exactly produced a blocked phase and the very file the documentation forbade. Three separate instances hit it in six days, and each one worked around it by writing a file whose only content was a pointer to where the review actually was. The concurrency gate asked whether another session was live when the question it meant was whether another session shared the working directory, and the single line where it compared directories had the comparison inverted: it excluded claims recorded from this path and blocked claims recorded from other paths. Since the gate's own first recommendation is to start in a separate worktree, taking its advice once guaranteed that every later start in the main checkout would hit the same block and be offered a worktree it did not need.
The other two passed over a problem in silence, and that is the pair nobody notices. The action that files a roadmap item when an instance completes resolved the roadmap to the primary checkout — correct for capture, wrong for completion, because an instance finishing on a worktree branch has its item on that branch while the primary checkout sits on some other instance's. It found nothing, printed nothing, and the transition printed a clean completion tick over the top of it. That happened twice in three days, and the second time the lesson from the first was already written down in the guidance file: an action that can legitimately find nothing needs to say so. The lesson was recorded and the code was not changed. Separately, the release blog's manifest could name a post file that had never been committed, because validation checked entries against tags but never against the files those entries promise. Nothing noticed until a test on the main branch died on the missing file, where it read as somebody else's problem rather than as the release that caused it.
The unifying observation is the one no single report made: three of the four sit on the worktree path, which is now the framework's recommended answer to two sessions sharing one checkout. The recommended path was the under-supported one, and following the documentation was what triggered every failure.
Both habits are corrosive in the same way. A gate that cries wolf and a gate that stays quiet train the same behaviour — stop reading the output — and once that is learned, the next real failure gets waved through as the known one.
How it could be solved
The choice that shaped this release was made before any code was written, and it was not a choice about how to fix anything. It was whether to treat four separately-reported defects as four pieces of work or as one.
Four items sat in the backlog. A gate that blocked a phase nobody could exit by following the documentation. A gate that fired on the very arrangement it recommended as its own first option. An automation that filed nothing and said nothing. A validator that let a blog entry promise a post that did not exist. Four reproductions, four mechanisms, four sets of phases and artifacts and retrospectives — for work totalling two or three days. This project has a written warning about lifecycle overhead multiplying its own cost, and four instances for three days of fixes is that warning made literal.
The alternative was to fold them into one item and argue that the deliverable was the shared shape rather than the four symptoms. That argument had to be earned, because "these are all sort of similar" is how unrelated work gets bundled into something nobody can review. Two things made it hold. The four split cleanly into exactly two failure modes — an automation that blocks what it should allow, and an automation that passes over a problem in silence — and the second mode is one sentence long: an automation that can legitimately find nothing must say so. That sentence had already been written down, in a retrospective, three days before the second occurrence of the same bug. The lesson was recorded and the code was not changed, which is the strongest possible argument that writing the rule where the code lives beats fixing the instance in front of you.
The second thing was that bundling here was bounded. All four were small, independently testable, and none depended on another, so any one could have been deferred in place without blocking the rest. That was stated as a claim in the task breakdown and it turned out to be true: the four landed as four separable commits' worth of change and nothing had to wait on anything else.
The bet paid twice over, in a way that a per-symptom fix could not have. Writing the Mode B inventory down — every built-in handler, with a verdict of applies or exempt and a stated reason — surfaced a fifth instance of the shape that none of the four items had named: the branch-creation action exits quietly when a worktree has already made the branch, while the start command prints a tick for it. This instance's own creation printed that tick for an action that created nothing, and nobody noticed for weeks. And the inventory found its own counter-example in the same pass: the handler that marks an instance as owing a retrospective is exempt, because its quiet path means the retrospective already exists. That is work already done, not nothing found, and its silence was itself an earlier fix. A uniform application of the rule would have regressed it. Auditing the class before applying one remedy to all of it is the difference between a rule and a reflex.
The other decision worth recording is which side moved in the review-gate contradiction. The original item guessed the gate should learn to accept one pooled findings file, because the skill's advice read as the more considered of the two. The second occurrence reversed that: an instance forced to write one file per artifact found that the split had separated its findings about an architecture diagram from its findings about a design, and two of those were real defects it would otherwise have pooled away. So the gate's shape was right and the skill's sentence was wrong — the opposite of what the item recommended, decided on evidence from an instance that hit the bug rather than on which text sounded more deliberate.
How AIDLC solves it
Four lifecycle automations were reporting something that was not true. Two blocked work they should have allowed; two passed over a problem in silence. This release makes all four verdicts match reality, and — because the seed item's whole argument was that the shared shape mattered more than the four symptoms — it states each failure mode as a rule and applies the rule, rather than patching four call sites.
The concurrency gate was asking the wrong question. It asked "is another session live?" while
meaning "does another session share my working directory?", and the single line where it
compared working directories had the comparison backwards: it excluded claims recorded from
this path and blocked claims recorded from other paths. Since a session in its own worktree,
on its own branch, cannot stage your files or yank you off your branch, that is exactly
inverted — so taking the gate's own first recommendation once made every later start in the
primary checkout hit the same block, and be offered a worktree it did not need. The fix
compares each claim's directory against this session's, treating an absent worktree_path as
the primary checkout and a worktree_path whose directory has since been removed as the
primary checkout again. The seed item asked for a git worktree list cross-check; the module
carries an explicit no-subprocess constraint and the repository already answers the same
question with an existence test, so it reaches the same conclusion without a new spawn.
One thing did not change, and nearly did. The first draft of that fix also removed the own-worktree exclusion, which looked like part of the same cleanup. Without a declared session id every invocation resolves a fresh synthetic one, so a session's own earlier claim is not recognised as its own — and dropping that line would have made a session block on itself the moment it ran a second command in its own worktree. The exclusion stays, with the reason written where the next reader will find it.
The review skill and the review gate had been contradicting each other about one file for a
week. The skill told reviewers not to write a findings file for tasks.md; the gate requires
one for every required artifact of a gated phase, and tasks.md is one. Following the
documentation exactly therefore produced a blocked phase and the very file the documentation
forbade — three times, in three separate instances. The skill moved rather than the gate,
because the second occurrence had already shown the gate's shape was the better one: splitting
findings by artifact had forced one instance's findings about its architecture diagram apart
from its findings about its design, and two of those turned out to be real defects. The skill
now states one findings file per required artifact and reads the artifact list from the phase
state rather than naming any file, so it stays correct across templates and project-local
overrides. The instruction that a task breakdown is judged against its design survives
verbatim in substance; only where the findings land has changed.
roadmap-done was resolving the roadmap to the primary checkout and reporting success when it
found nothing. For an instance finishing on a worktree branch that is always: the item is on
the finishing branch, the primary checkout is on some other instance's branch, and the
transition prints a clean completion over the top of it. It happened twice in three days, and
the second time the lesson from the first was already written down. Lookups now search this
working tree as well as the primary checkout, and every outcome prints one line — moved,
already filed, genuinely nothing found and which roots were searched. The move itself had to
stop deriving its destination from the current directory: once a lookup spans checkouts,
filing an item would otherwise relocate it into the primary checkout, which is a deletion on
one branch and an untracked file in another tree — worse than the stranding it was meant to
fix. The doctor migration that exists to find stranded items needed no separate change; it
reaches items through the same function and inherited the fix, which was verified rather than
assumed.
The blog manifest could name a post that was never committed. Validation checked entries against tags but never against the files those entries promise, so a declared-but-absent post passed and surfaced only where something happened to read it — a test on the main branch dying on a missing file, where it read as somebody else's problem rather than as the release that caused it. The check now lives in validation, as an injected predicate so the module stays free of filesystem access, and it asks the right question: not "is this post on disk now" but "will it exist once this run finishes". That distinction is load-bearing, because validation runs over the whole manifest before anything is generated, so a plain on-disk test would reject the very post a release was invoked to write and deadlock every release. The set of posts a run will write is now computed by one function shared with generation, so the exemption and the writing cannot drift apart.
Walking this instance's own release found the last piece. An entry whose tag does not exist yet is prepared, not broken — the established practice commits the manifest entry ahead of the release commit, and the release script itself says an entry may legitimately be prepared before its release. Demanding the post in that window would have turned every release preparation red, which is the second of the two occurrences the check exists to stop rather than a case to enforce. So the requirement is gated on the tag existing: a released tag with no post fails and is named; an unborn tag with no post is fine.
Underneath all four sits one rule, written once where the handlers are defined: an automation that can return success without doing the thing it exists to do must say which case it hit. It is applied through an inventory that a test asserts is complete over the handler map, so a handler added later cannot quietly inherit silence. The rule is not applied uniformly, and that matters: the handler that marks an instance as owing a retrospective is exempt, because its quiet path means the retrospective already exists — work already done, not nothing found — and announcing it would re-ask for something the instance has. That exemption was itself an earlier fix, and reversing it in the name of consistency would have been a regression.
Writing that inventory down immediately found a fifth instance the seed item had not listed. The branch-creation action no-ops when the worktree flow has already made the branch, and it does so silently, while the start command prints a tick for whatever it returns. This instance's own creation printed that tick for an action that created nothing. One line to fix, and the best evidence available that stating the rule was worth more than patching the one reported symptom.