The two earlier keyboard-UX fixes in this series solved problems that were visible the first time you tried them in the main interactive REPL. 4.5.76 cut readline's escape-sequence timeout from 500ms to 50ms so that pressing Esc and then immediately typing a letter didn't eat the letter as a meta-key sequence. 4.5.80 changed terminal: true from a hardcoded option to a TTY-conditional one so that piping the tool's output to a file didn't pollute the file with ANSI cursor-positioning escapes.
Both fixes were small. Both fixes only touched one file: bin/aaicase.mjs, which is the main interactive entry point. Both fixes were applied to the one readline.createInterface call at the heart of that entry point.
What this week's probe surfaced was that the codebase has five readline.createInterface calls. Five. The main REPL is one of them. The other four are:
bin/aai-task-walk.mjs— the task walker, which has free-text prompts for the "reassign to" and "update description" flows.bin/aai-override.mjs— the override-token confirm prompt, which asks "Mint this override token? (yes / no)".src/setup.mjs— the firm-setup wizard, which has two readline instances (one for the initial walkthrough, one for the new-user-onboarding flow).bin/aaicase.mjs— the main file already had a fixed instance, but it ALSO had two unfixed instances: one for the first-run setup prompt that asks for the Merus API token, and one for the token-recovery prompt that re-asks if the configured token is missing.
Every one of these had been calling createInterface({ input: process.stdin, output: process.stdout }) with no other options. That means every one inherited the 500ms escapeCodeTimeout default that 4.5.76 fixed, and the terminal: true autodetect default that 4.5.80 fixed.
What the walker bug looked like
The most visible of the four was the walker. The walker steps through open tasks one at a time. For each task, the attorney can press C to complete it, K to keep it, S to skip, R to reassign, U to update, D to review docs, A to review activities, F to switch the user filter, or Q to quit. Single-letter commands.
The R and U options open free-text prompts. R asks "Reassign to (number, name, or email):" and the attorney types a person's name. U asks for the change to make ("priority high", "due 2026-06-15", "description: call client back"). Both flows are exactly the case where the Esc-eats-next-character bug bites: an attorney starts typing a name, realizes they want a different person, presses Esc to clear, and starts typing again. The first character of the new name gets eaten as a meta-key sequence.
Probed this in expect: at the reassign-to prompt, type "garbage", press Esc, type "back" (which the walker would accept as a back-out). The terminal output showed the meta-sequence interpretation kicking in. Esc-b was Meta-b (backward word). Esc-a was Meta-a. Esc-c was Meta-c. Esc-k was Meta-k. None of those keys went into the input buffer; they all hit readline's emacs-style word-navigation bindings, which on a partially-typed input did things like move the cursor backward, insert garbage, and generally produce a line nobody asked for.
By the time the attorney pressed Enter, the input was something completely different from what they thought they'd typed. The walker correctly reported "No match" for what it saw — but what it saw was nothing the attorney typed.
The other three sites
The override-confirm prompt is short ("yes / no") so the Esc-eats-character bug bites less obviously — the attorney usually types one or two chars then Enter. But the bug was still there.
The setup wizards have free-text prompts for the firm name and user role assignments. Same exposure as the walker. An attorney mistyping the firm name, pressing Esc to clear, and re-typing would lose the first character. They might not notice until they saw the saved config later.
The first-run token prompt asks the attorney to paste their Merus API token. Pasting works fine — bracketed paste mode would handle that (although the setup-time prompt doesn't enable BPM, so the long token comes through as raw bytes; still works). The bug here would only bite if the attorney pasted, realized they pasted the wrong thing, pressed Esc to clear, and started typing — which is unusual but not impossible.
The fix and the meta-lesson
The fix was mechanical: add the same two options to every createInterface call.
escapeCodeTimeout: 50, terminal: process.stdin.isTTY && process.stdout.isTTY,
Five sites. Five identical additions. After the fix, all the readline interfaces in the codebase have consistent behavior with respect to Esc-handling and piped-output cleanliness.
The meta-lesson is the obvious one. When a codebase has five copies of the same boilerplate, fixing the boilerplate means fixing it in five places. Forgetting one (or four) is the default outcome unless you grep specifically for the pattern. Each individual fix passed its own verification — 4.5.76 fixed the main REPL, 4.5.80 fixed the same thing in the same file's piping output — and we never checked the other four sites because we didn't think about them.
The natural next move is a shared helper. Something like:
function makeRl(opts) { return createInterface({ escapeCodeTimeout: 50, terminal: process.stdin.isTTY && process.stdout.isTTY, ...opts, }); }
Then every call site uses makeRl instead of createInterface, and any future config improvement lands in one place. That refactor isn't in this release — the inline-options-on-every-site fix is what shipped today — but it's worth doing the next time we have a reason to touch any of these files. The cost of refactoring is small; the cost of forgetting one of the five sites the next time we tighten the config is exactly what just bit us.
The probe series
Five bugs in the keyboard-UX series so far. Each one was invisible to code inspection and caught by exercising the relevant key under realistic conditions.
- 4.5.76 — Esc + rapid type ate the next character (main REPL)
- 4.5.77 — Multi-line paste submitted each line as a separate query
- 4.5.78 — Tab completion was defined but never wired
- 4.5.79 — Ctrl-C killed the session instead of clearing the line
- 4.5.80 — Piped output had ANSI escapes mixed into log files
- 4.5.81 — Four other readline sites had two of the same bugs as the main REPL
The version on the install URL is 4.5.81. The keyboard surface is now uniformly handled across every interactive prompt the tool surfaces. The work continues at the pace of one verified release at a time.