The previous post in this series ended with a recommendation: extract a shared helper for readline configuration so that future tweaks land in one place. The recommendation was specifically because the fixes from 4.5.76 and 4.5.80 had each only landed on the main REPL's readline instance, leaving the other six call sites unfixed for weeks. The seventh fix — 4.5.81 — applied the same two options to all the remaining sites individually.

This release does the refactor. lib/readline-factory.mjs exports a makeRl(opts) function that supplies the keyboard-UX defaults and passes through any caller-specific options. The four files with readline interfaces (the main REPL, the walker, the override confirm prompt, and the two setup wizards in src/setup.mjs, plus two more in bin/aaicase.mjs itself) all import and use it. There are no createInterface calls left in the codebase outside the factory.

What the helper does

The function is short:

export function makeRl(opts) {
  const input = opts.input || process.stdin;
  const output = opts.output || process.stdout;
  return createInterface({
    input,
    output,
    escapeCodeTimeout: 50,
    terminal: input.isTTY && output.isTTY,
    ...opts,
  });
}

Two defaults are supplied. The 50ms escape-sequence timeout (so Esc-then-letter doesn't get interpreted as a meta-key sequence and eat the letter). The TTY-conditional terminal mode (so piped output doesn't get polluted with ANSI cursor escapes). The caller's opts spread after the defaults, so any call site that genuinely wants to override the defaults still can — but in practice none of them do.

The caller passes input and output streams, plus whatever else they need. The main REPL passes a prompt string, a history array, a history-size limit, and a completer function. The walker passes nothing. The override confirm passes stderr as the output (so the confirmation question doesn't get mixed into the token output on stdout). Each call site keeps the configuration that's specific to it; the shared defaults come from the factory.

When refactoring is worth it

The standard advice on extracting helpers is "two or three sites, leave it; five or more, extract." This codebase had seven call sites with identical config concerns, and the original three keyboard-UX fixes had each only landed on one of them. The empirical evidence that the duplication was costing us was the bug pattern: fix once, miss six, fix once more, miss four, fix in all seven manually, then realize that the eighth time we want to change a default the same thing will happen.

The helper is worth extracting because the defaults are properties of "how we want readline to behave in this codebase" rather than properties of any specific call site. The factory captures the policy in code; the call sites just pass through. A future engineer reading any call site sees makeRl({...}) and knows to look at the factory for the policy. A future fix to the policy edits the factory and propagates to every call site.

This is different from the kind of "refactor for elegance" that's worth less. The motivation isn't aesthetic — it's the concrete observation that we've already paid the cost of forgetting once.

Doing it while the lesson is fresh

The reason for shipping the refactor immediately after 4.5.81 rather than "next time we touch these files" is small but real. The lesson from 4.5.81 — "fix one site, miss the others" — has been demonstrated concretely. Right now, looking at the codebase, the cost of forgetting is visible. In a month, when other things are on top of mind, the same fix-one-miss-the-others mistake will be easy to make again.

The cost of the refactor is small: 40 lines of helper, edits to four files, the same verification we already run on the keyboard surfaces. The benefit is structural — the next time a keyboard-UX bug is found and we change a default, we touch one place.

I don't always do the refactor immediately. Sometimes the duplication isn't load-bearing enough to be worth the change. Sometimes the call sites differ enough that a shared helper would have to be parameterized in awkward ways. In this case the call sites were structurally identical — same library, same two defaults missed, same fix needed in each. The factory was straightforward to write and the call sites became simpler after.

The release

The version on the install URL is 4.5.82. The change ships lib/readline-factory.mjs as a new file. The four caller files lose roughly thirty lines of duplicated configuration each. The behavior is identical — verified by running the same keyboard-UX probe suite as the previous releases.

The probe series found six interactive bugs over the past two weeks, the last of which was the duplication problem itself. The work continues at the pace of one verified release at a time, and the keyboard surface of this tool — the part the attorney actually touches — is now both correct in behavior and clean in its structure.