Interruptible speech and short spoken replies in voice mode
I ran Phase 5b live and found two problems. Long router replies blocked for 15 seconds with no way to interrupt, and markdown formatting was read verbatim by TTS.
Barge-in support
I replaced the blocking sd.wait() call in tools/speak.py with a new _wait_or_interrupt(sd) function that polls every 50ms. Ctrl+space stops playback and returns "interrupted", Esc stops playback and returns "exit", and normal completion returns None. The keyboard library is imported lazily with try/except. If it is unavailable the function falls back to sd.wait() so barge-in support can never be the reason TTS stops working.
I checked keys separately (ctrl and space as two is_pressed() calls, not the combo string) to match the pattern in tools/listen.py.
In foreman.py I added a local speak() wrapper that turns the "exit" return into "Leaving voice mode." and returns the exit signal. I updated execute() and run_command() to return that signal so Esc mid-speech unwinds the whole voice loop. Every in-loop say() call now routes through speak().
Short spoken output
I added strip_for_speech() in tools/router.py. It removes asterisks, backticks, and leading bullet markers, turns each source line into its own sentence, drops trailing list-intro punctuation, and caps at two sentences. The full reply still prints to the terminal but only the stripped version goes to TTS.
I appended an instruction to the router system prompt: "Replies will be read aloud. Answer in at most two short plain sentences. No markdown, no bullet points, no offers to help further."
I applied strip_for_speech() to both the denial branch and describe_capabilities() in foreman.py. The live weather denial arrived as two plain sentences.
Verification
Headless checks passed. The markdown weather denial stripped to two sentences with no markdown. Clean input passed through untouched. The say() fallback called sd.wait() when keyboard import failed. Typed commands loaded none of the speech modules.
The live test passed. Barge-in cut speech and recorded immediately. The weather denial spoke as two plain sentences. Esc mid-speech exited clean.
I changed the active TTS voice to af_bella in memory/voice_settings.json. That file is gitignored.
Commit 563bca3 is on main and dev with tracking clean.