--- name: omp-extension-live-tui-verify description: "Verify an omp extension's slash commands and event hooks against a real TUI session: pty submit semantics, observable side-effect instrument, transient-toast trap, agent_end willContinue law, PipeWire output triage." --- Verify that a `.ts` file in `agent/extensions/` actually works inside the interactive TUI — the black-box bats seam cannot see harness-side wiring (event names, arg passing, mode gating), and it will pass beside a dead hook. Distinct from `omp-tui-live-visual-verify` (config/theme rendering proven by screenshot): here the instrument is **observable side effects**, not pixels, because extension notifications are transient toasts that vanish before the pty buffer is read. ## 1. Launch the exact build under test ```bash omp --no-extensions --no-session -e /abs/path/agent/extensions/.ts ``` as a **pty service** (`pty: true`, `ready: {"log": "╭|❯|Tip:|─", "timeout": 60}`). `--no-extensions` + explicit `-e` proves the file loads by path and nothing else interferes. Neutral cwd (`/tmp`) so repo state cannot color the run. ## 2. Submit input correctly (the costly gotcha) `write proc://` with a trailing `\n` does **not** submit — the text just sits in the composer. - **Works:** two writes — first `"/cmd args"` (no terminator), then a **separate** write whose whole content is `\r`. - **Broken:** `"/cmd args\r"` in one write. It still submits, but the trailing `\r` arrives inside the command args. Symptom: `unknown voice 'narrator1\r'`, an unexpected extra utterance, or a chat message whose transcript line ends in a literal `\r`. That is a harness artifact — never "fix" it by stripping control characters in the extension (`.trim()` already covers what a keyboard can produce). ## 3. Instrument with a stub, never with ears or eyes Point the extension's player/handler at a script that appends evidence: ```bash #!/usr/bin/env bash echo "$1" >> /tmp/stub.log; cp "$1" "/tmp/out-$$.wav"; exit 0 ``` Run the TUI with that stub in the env the extension reads (`DANKVOICE_PLAYER=…` style). Then every assertion is a file: `wc -l` of the log (how many times it fired), `ffprobe -show_entries format=duration` (what it produced), `ls -d ` (that cleanup ran). Sequence per behavior: command → CR → short `sleep` → count. **Count before and after** each command; the delta is the only honest proof a subcommand stayed silent. ## 4. Discriminate "fired twice" from "fired once per turn" Stray CR writes can create extra empty turns, each with its own `agent_end` — two utterances for one intended prompt is usually **two turns**, not a duplicate speak. Confirm by counting assistant turns in the transcript (`read proc://`), and by matching each clip's duration to its own answer's length. ## 5. Event-hook contract traps - `agent_end` payload (`dist/types/extensibility/shared-events.d.ts`): `{ type, messages: AgentMessage[], willContinue? }`. **`willContinue: true` means the harness already scheduled a continuation** (auto-retry / empty-stop retry) — a subscriber that narrates or notifies there speaks an answer the model is about to redo. Gate on it. - Read the real event/payload shape from `~/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/dist/types/…`; grep `dist/cli.js` for the literal event names to confirm they exist. - A driver test that *calls* the handler by hand proves nothing about emission — that is exactly the gap this live check closes. ## 6. Audio output goes silent while every command reports success `pw-play` exits 0 and prints `stream drained` even when nobody hears anything. Triage order: ```bash wpctl status # Sinks, the `*` default, and live Streams wpctl get-volume @DEFAULT_AUDIO_SINK@ ``` - A **`Default Configured Devices` entry naming a `bluez_output.*` node that is not in the Sinks list** = remembered-but-offline Bluetooth: the classic silent-route cause. - Confirm the stream really links: a short clip can vanish before inspection, so use a multi-second clip and read `wpctl status` mid-play, or target explicitly: `pw-play --target ` (node ids from `wpctl status`; `wpctl inspect ` gives `node.name`). - Prove the synth path independently of the player: `curl -o x.wav …` then `file x.wav` (expect `RIFF … WAVE … PCM`). ## 7. Land it Live result belongs in the commit message, the reference page, and the work log — and the guard the live check uncovered gets its own behavior test (assert observable output only: fired count, payload content, silence). ## Cleanup `/exit` or `proc:///kill` the service; move stub scripts, logs, and clips to `~/.AGENTTRASH/-/` — never `rm`. Kill background jobs that keep playing to the operator's speakers.