Skip to content

v1.15.1 ·

The thirteenth prompt cannot forget

The CLI had a bug where a prompt could freeze with nobody there to answer it. The fix before this one found twelve unguarded prompts and added a shared "is this a terminal?" check, applied at each of the sixteen places the CLI asks something. That made the check available everywhere. It did not make it unavoidable. The seventeenth prompt could still forget it.

The only thing standing in the way was a test that launched every command with no terminal attached and checked none of them froze. It proved the guard held on the commands that existed the day it was written. A new prompt added to a command it already covered passed for free, because the command exited for some earlier reason and never reached the new question. It also cost eighteen process launches, in the slowest and most timing-sensitive part of the suite.

What changed

Every question the CLI asks now comes from one module, packages/cli/src/core/prompt.ts, and it checks whether anyone is there before it opens a prompt. It is the only file allowed to import the prompt library. A test reads every source file in the package and fails, naming the file, if any other one does. It recognises all four ways of writing an import, so changing syntax does not get around it.

Seven modules imported the library directly before this. The rule is now a property of where code lives, not of what somebody remembered. The enumeration test and its eighteen launches are deleted.

Why the module reports and does not decide

The obvious design has the module handle the no-terminal case itself. I rejected that, because the commands disagree on purpose. Setup treats a piped run as a yes to usage-tracking consent, since its existing "ask me nothing" flag already does and the two must not differ. Skill creation exits with a failure code, so a script can tell "you declined" from "I could not ask you". The merge-driver installer prints its whole plan and then refuses, because a missing terminal must never authorise a write to your git configuration. The action creator declines on its own, because the "ask me nothing" path would save the file. One uniform policy would have quietly changed three of those.

So the module says which of three things happened: here is your answer, you cancelled, or nobody was there. The old shape returned a string or a private cancel marker, which could not express the third case. Worse, the marker is truthy, so a check written the natural way read "they gave up" as "they agreed". That matters most at the three places that write to disk the moment they see a yes.

What I checked, and what it did not buy

Nothing about using the CLI should change. I recorded sixteen no-terminal runs across eight commands and both kinds of empty input from the built tool before editing a line, recorded them again after, and compared them character by character, including which stream each message went to. All sixteen match.

The suite is not measurably faster. Removing the launches bought about what the thirty-six new checks spent, and the run-to-run variation is wider than the difference. The launch count is what mattered, and a new test keeps it from growing back. I kept the targeted checks for the commands that own a prompt, because a test that reads source can never observe an exit code.