# voice_interface — bootstrap understanding + plan (2026-06-13)

## What I am
The voice BRAIN behind the Even G2 HUD. Voice -> Whisper transcript -> lands in
my tmux pane as a prompt -> I interpret intent, act across the fleet via
tmux send-keys / capture-pane -> emit ONE short ASCII reply (<=300 chars) sized
for the glasses TextContainer. I am the interpreter/dispatcher, not a frontend
dev (glasses owns the app; website_builder owns serve.py).

## Two core verbs (validated)
A. "check on the <X> agent" -> `tmux capture-pane -p -t agents:<X>` -> summarize
   in 1-3 sentences: current activity, last status, any error. READ-ONLY.
   Dry-run done against yams: correctly read "v3 towel finetune, step 15400,
   vol 6.71, no errors."
B. "send to <X>: <msg>" -> send-keys to X's pane (the two-Enters pattern),
   poll capture-pane until the `Working.../Crunched` status line clears, then
   summarize the reply in 1-3 sentences. Same send-keys mechanic that
   `/api/agents/<name>/send` already uses in production (serve.py:937) -> proven.

## Key gotchas discovered
- The bottom `❯ ...` line with `-- INSERT --` showing is an UNSENT DRAFT in the
  target's input box, NOT what they're doing. Never report draft text as current
  activity. Read the activity from the body above the prompt box.
- Busy signal = a `Working… / Crunched for Ns · N shells` status line is present.
  Its disappearance = reply complete (the poll cue for verb B).
- Glasses font silently drops out-of-font chars -> ALWAYS run captured text
  through sanitize.py before quoting. Helper written + tested in this dir.

## Output rules (hard)
<=300 chars default, plain ASCII, lead with the answer, no markdown/emoji/
politeness. Gibberish transcript -> "didn't catch that". Unknown agent ->
"no agent named X". Ambiguous ("the wandb agent") -> pick the live one
(our_wandb), note the pick in one clause.

## Wiring contract (TBD — coordinate with glasses + website_builder)
Proposed, reusing existing infra:
1. Glasses WebView already does mic->/api/transcribe (Whisper, faster_whisper
   'small', multipart field "audio", returns {text}). That part exists.
2. NEW `POST /api/voice/ask {text}` on serve.py: send-keys the transcript into
   `agents:voice_interface` (reuse `_find_tmux_window` + the send pattern at
   serve.py:937). Returns 202 immediately.
3. I process, then write my reply.
4. NEW `GET /api/voice/answer` -> returns my latest reply.

CONTRACT REFINEMENT I'll propose: deliver the answer via a dedicated single-line
file `agents/voice_interface/answer.txt` that I overwrite each turn, rather than
`tail -1 outbox.md` (outbox is multi-line human log; tail-1 is fragile). The
glasses already poll feeds every ~2s, so /api/voice/answer can read answer.txt.
Server (or glasses_renderer) runs sanitize.py before display as a backstop.

Open contract questions for glasses/website_builder:
- request/response JSON shape + auth (existing /api uses pw=fabregas / agents
  auth header) and CORS (already `*` on serve.py per glasses memory).
- polling cadence + how the glasses know an answer is "fresh" vs stale (turn id?).
- does voice route to ME (router) always, or can the user target an agent
  directly ("tell yams to ...")? I think: always to me; I parse the target.

## Status
Bootstrapped, both verbs validated, sanitizer shipped. Idle, awaiting either
pasted test transcripts from Cameron or the wiring coordination with glasses +
website_builder.

<!-- LIVE ANSWER (tail consumers read below this line) -->
ready
