Most of the work in this keyboard-UX series has been about how the tool responds to a human pressing keys. Esc to clear, Ctrl-C to clear or exit, Tab to complete, multi-line paste handling. Each one was a probe that surfaced a bug invisible to code inspection.
This week's probe was different in shape. The question wasn't "what does the attorney see when they press X?" — it was "what does the tool produce when nobody's pressing anything?" Specifically: what happens when an attorney does the perfectly reasonable thing of piping a script of queries through the tool and capturing the result to a log file:
aaicase < questions.txt > results.log
This is a normal workflow. It lets an attorney batch-process a list of questions overnight, queue up a set of case lookups before a hearing prep, or run the tool from a wrapper script that reads from one file and writes to another. None of that requires an interactive terminal.
What was wrong
The output file looked like this:
[1G[0Jaaicase> [10Gexit
The prompt and the user-typed text were interleaved with raw ANSI escape sequences. [1G is "move cursor to column 1." [0J is "erase from cursor to end of screen." [10G is "move to column 10." These are commands that mean something specific to a terminal emulator — repositioning a cursor on a screen — and mean nothing to a file. Embedded in a log file, they're noise. Worse than noise: if you cat the file later in your terminal, the escapes execute, repositioning the cursor in unpredictable ways and visually corrupting whatever's on screen.
A log file should be plain text. The output looked like the interactive session being recorded raw, including all the terminal-control bytes that a terminal emulator interprets and a text editor displays as garbage.
What was causing it
Node's readline module has a terminal option on createInterface. Setting it to true enables the rich interactive behavior: ANSI cursor escapes for redrawing the prompt on backspace, history navigation via arrow keys, the ability to position the cursor mid-line. Setting it to false uses a simpler line-buffered mode with no cursor control.
The right default is to set terminal based on whether the actual output stream is a terminal. Readline can autodetect this — if you don't pass the option, it checks process.stdout.isTTY and does the right thing. The aaicase code, written months ago, had explicitly set terminal: true at construction time. The intent was to enable the interactive features. The effect was to enable them unconditionally, including in cases where the output wasn't a terminal at all.
Readline trusted what the code said. It wrote cursor escapes to the output stream. The output stream wrote them to whatever was downstream — in this case, a file. The file accumulated terminal commands as text content.
What the fix does
One line. Replace terminal: true with terminal: process.stdin.isTTY && process.stdout.isTTY. The option is now true only when BOTH input and output are actually terminals — exactly the case where interactive features are useful.
Both conditions matter. Sometimes the attorney's input is a TTY (they're typing live) but output is redirected (they want a log without the cursor noise). Sometimes input is from a file (a batched script) but output is the terminal (they want to see the results live). The && covers both: if anything in the pipe isn't a terminal, we drop interactive mode. The simpler line-buffered path produces clean text in both directions.
Verified two cases. Interactive (both TTY): all keyboard features still work — Tab completion, bracketed paste mode, Esc-to-clear, Ctrl-C convention, arrow keys, backspace, history. Non-TTY (piped): output file contains plain text, zero ANSI escapes.
The pattern
The cause was a hardcoded assumption that turned out to be wrong in a case the original code didn't consider. The fix is to consult the environment instead of hardcoding. This is the same pattern as a couple of other fixes from earlier in the series — code that worked correctly in the "normal" case and produced subtle pollution in the cases the original author hadn't tested.
The list of "assumes interactive use" surfaces in this codebase is small but worth scanning periodically. Any code that writes to stdout/stderr with formatting hints (colors via ANSI codes, the spinner during query, the cursor-positioning escapes from readline) should check whether the output is actually a terminal before emitting the codes. Otherwise the assumption embedded in the code becomes pollution in the file the attorney expected to be plain text.
The version on the install URL is 4.5.80. Small release, small fix, but a real one for any attorney whose workflow involves the tool being part of a script rather than the endpoint of one. The keyboard probe series continues to be productive — each pass surfaces another small thing that was visible only when you actually tested the behavior in a realistic environment.