--- name: dankvoice-silence-triage description: "Triage silent or late dankvoice auto-speak on the fleet: arming key, /say grammar, endpoint/sink, event granularity, humanizer latency, plus the black-box bats driver." --- # Triage "dankvoice is silent" on the fleet Symptom that recurs: auto-speak only speaks when `/say ` is typed explicitly. Work the gates in this order; each has an objective check. 1. **Armed?** `cat ~/.omp/cache/dankvoice.json | jq .auto` — must be `true` (or `DANKVOICE_AUTO=1`). Arming is read at **startup only** and `/say on` never persists, so a session started before the key was added is silent even though fresh sessions work. `/say start` is *not* a subcommand: unknown arguments are spoken verbatim (it read out the word "start"). `/say on` is the verb; `/say status` must print `auto-speak on`. 2. **Endpoint + sink?** `/say status` must show a real endpoint (not `NO ENDPOINT`) and the saved sink must exist: `pw-dump | grep -o 'alsa_output\.[^"]*' | sort -u`. 3. **Did it synthesize?** `pgrep -a pw-play` during a turn: one process per segment, `--target `, wav under `/tmp/dankvoice-*/say-.wav`. One temp **directory per utterance chain** — count dirs to count utterances, pids to count segments. 4. **Event granularity (the structural cause).** `agent_end` fires once per *prompt* when the loop settles and is skipped when the harness maps a non-terminal settle (`willContinue`) — long runs with background jobs or subagents therefore narrate nothing. Mid-run narration needs `turn_end` (fires per assistant message + tool results). Verify delivery, don't infer it: skill `omp-extension-event-probe`. 5. **Latency is not silence.** Narration passes through the humanizer (frank-zealot) before synthesis, so an utterance lands several seconds after its turn; under endpoint load that stretches to tens of seconds. `/say humanize off` = mechanical voice, near-instant. Judge cadence from probe-log timestamps, never from when audio starts. ## Design rules the plugin now encodes (dank/dankvoice#2→#3) - Narrate `turn_end` per turn **plus** `agent_end` for the settled answer; dedupe identical text per run (reset on `agent_start`) so the answer speaks once. - A turn landing while the player is alive is **dropped** — no queue, no interrupting; a tool loop would cut every sentence off after a word. Busy test: `state.child !== undefined && state.child.exitCode === null`. - Tool-only turns (no prose, no narration-flagged thinking) stay silent. - `print`/`json` modes stay silent, so benches never chatter. - Settings writes must **round-trip the file** (`{...onDisk, ...owned keys}`). Rebuilding from owned keys deleted `"auto"` on any menu choice and silently un-armed every later session — the classic cause of "it worked yesterday". ## Black-box test harness (repo `dank/dankvoice`) `tests/behavior.bats` drives the built artifact through an in-process driver: `const say = DV_EXT(ctx, cfg)[0]`, handlers via `const events = new Map(); const pi = { on: (n,h)=>events.set(n,h) }`, then `events.get("turn_end")?.({ turnIndex:0, message:{role:"assistant",content:[{type:"text",text:"…"}]}, toolResults:[]}, ctx)`. Assert on `CALL`/`PLAYS`/`NOTIFY` lines and `SETTINGS `; stubs are `DV_PLAYER_CMD` (logs voice+input, honours `DV_HOLD`) and `DV_REWRITE_URL` (404 → falls back to mechanical text). `setup()` runs `bun run build` per test → serialize suites on the shared `dist/`. `tests/packaging.bats` requires `dist/` **committed** (compares to HEAD), so build → commit → the drift pin goes green.