v1.24.0 ·
The refusal was working as designed
The problem
Two releases ago this project learned to project its roadmap onto GitHub Issues. One release ago it learned to write issue bodies worth reading. Both shipped working. Neither shipped findable.
The projection is opt-in, and it is inert until a roadmap.sync section exists in the
project's config file. Nothing writes that section. The setup wizard writes six other
things and not this one. No health check adds it. The config reader only ever reads. So the
only path to the feature was to already know the command existed, run it, and read the
refusal that came back.
That design is deliberate and it is correct. Absence reads as not configured rather than as an error, which means one project that never opted in cannot break an unrelated command, and a version bump can never quietly start publishing somebody's backlog. Those two properties are worth more than discoverability, and giving either of them up to make the feature easier to find would be a bad trade.
But on 7 September somebody ran the sync command in a project with no config section, read the refusal, and filed it as a possible bug. It was not a bug. The message was doing precisely what it was written to do. It named the file, said the section was absent, and stopped. And it still left its reader unable to tell whether they were supposed to write that section themselves or whether something had failed to create it for them.
That is the interesting shape of this problem. There was no defect to fix. The refusal was working as designed, the design was right, and the outcome was still that a working feature went unused and a correct message was read as a failure. A message can be accurate and complete about its own subject and still not answer the question its reader actually has, which is not "what is missing?" but "whose job is it to put it there?"
The gap is not in the code. It is in the fact that nobody is ever asked.
How it could be solved
Three ways to close a discoverability gap, and they cost very different things.
Improve the refusal and stop there. The message could name the resolved absolute path of the file it read, which in a worktree is not the directory you are standing in. That is one extra parameter and it would have answered the September question in a single line. It is also the cheapest of the three by a wide margin, and it was going to happen regardless. What it does not do is help anybody who never runs the command — and not knowing the command exists was the actual failure. A better refusal is a better answer to a question you already knew to ask.
Trigger on intent instead: a flag on the command itself. A --init form would write the
config section for somebody who has already shown they want it, and would cost every project
that never asks exactly nothing. That is a real advantage and it nearly won. It loses on the
same point the improved refusal loses on: it is discoverable only by someone who already
knows the command. It fixes nothing for the person who hit this.
Ask during setup. A step in the wizard, offered last, defaulting to no. This is the only option that reaches somebody who does not yet know the feature exists, because it arrives at the one moment they are already answering questions about their project. The cost is that every project pays for the question, including the projects that answer no — and this project holds lifecycle machinery to a deliberately high bar, because a gate added once is paid for by every future piece of work.
That bar is about gates, though, and a question is not a gate. This adds no phase, no artifact, and no check anything must pass. It is bounded by three properties: it comes last, it defaults to no, and answering no ends it.
Then a second decision, inside the chosen option, which turned out to matter more than the first. If a project says yes, which of the five roadmap stages get published?
The safe answer preselects three — the committed work — and leaves untriaged captures and parked items unchecked. The argument against it is not about safety. It is that a wizard which quietly narrows the list teaches nobody that the other two stages are choices at all, and it decides on the user's behalf which of their own stages are too embarrassing to show. The owner's call was that a project has full visibility of its own roadmap, public or private, and that hiding a stage is the user's act to take. Show all five. Check all five. Let them uncheck.
That decision only holds because the list is visible. A preselection nobody sees is a default; a preselection presented in full is a choice. Which makes the wording of the prompt load-bearing in a way it would not have been under the safer default — it is now the only thing standing between an unread question and a published backlog.
How AIDLC solves it
The setup wizard has a new final question: project this project's roadmap onto GitHub Issues? It defaults to no, and answering no ends it. Cancelling is a cancel — it aborts the whole setup rather than being read as a polite decline, which is a bug this project already shipped once in the compliance step and did not want to write twice.
Say yes and a second prompt lists all five roadmap stages with every one of them checked: inbox, backlog, in progress, done, on hold. Unchecking is yours to do. Done cannot be unchecked, because the configuration reader refuses a list without it — without the closing step, issues for finished work stay open forever and the projection never converges. Better to enforce that in the prompt than to write a file the reader will reject. Both halves of that prompt are pinned by tests: the option set and the initial values, and also that the two riskier stages carry hints saying in words what publishing them means. A hint that quietly stopped saying so would leave the preselection indefensible, and nothing else would have noticed.
Re-running setup cannot delete a configuration you already have. Three separate tests, for three separate ways to get it wrong: a project that is never asked keeps its section untouched; an existing section pre-answers yes; and — the subtle one — a section that omits the stage list pre-checks the three shipped defaults rather than all five, so pressing Enter through an update cannot silently begin publishing an inbox nobody meant to publish. The constant behind that default is deliberately unchanged. It governs hand-written configurations whose authors were never asked the question, and widening it would have made an ordinary upgrade start publishing their parked work.
The GitHub CLI is checked after you answer, and a missing one warns rather than blocks. Gating the question on the tool would make the wizard's contents depend on the machine it ran on, and initializing on a laptop while the projection runs from continuous integration is an entirely normal arrangement. So the probe runs once the answer is yes, prints the remedy if it fails, writes the section anyway, and lets setup finish successfully. The configuration is not dangerous in the meantime: the sync command runs the same probe and refuses with the same message at the moment it would matter.
The health check that reports the resulting dead configuration applies nothing. It is a
report, not a fix, and the reason is specific rather than cautious: a confirm-tier check
applies unprompted under --yes, and continuous integration is exactly where the GitHub CLI
is legitimately absent — so a confirm-tier remover would delete a working project's
configuration on its first unattended run. It also probes only whether the binary can be
found, never whether it is authenticated, because authentication can reach the network and a
health check that reports a problem when you are offline is worse than no check.
And the refusal now names the resolved absolute path of the file it read, which was the cheaper half of this all along.
Two things were reversed rather than worked around. A test from the release that built the projection asserted that it adds no health check at all — a position reached by withdrawing a different, always-on check during that instance's own review. It was amended, not deleted, and the replacement asserts strictly more than the count it replaced. Three criteria that had been living inside an unexported function were made reachable by extracting the block they sat in, so every one of the thirty-three criteria has a real test behind it rather than a claim.
The finding worth keeping is smaller than any of that. The health check's own documentation said it performs no subprocess. The first implementation called a helper that spawns git, and every new test still passed. What failed was a test about terminal prompts, which spawns the real command-line tool and timed out. The answer had been written down one file away the whole time.