# AD-705 (reframed) build report — Wave 137 commit N

**Prompt:** [prompts/ad-705-wake-word-voice-loop-v1.md](../ad-705-wake-word-voice-loop-v1.md)
**Builder:** GitHub Copilot (Builder mode)
**Date:** 2026-05-09
**Status:** SHIPPED

## Files changed

- `ui/package.json` — added `optionalDependencies` with `onnxruntime-web` (deviation from D1 strict text; see below).
- `ui/src/audio/wakeWord.router.ts` (NEW) — pure routing function.
- `ui/src/audio/wakeWord.ts` (NEW) — state machine, lazy ONNX loader, barge-in, fallbacks.
- `ui/src/components/WakeWordIndicator.tsx` (NEW) — three-state indicator + fallback label.
- `ui/src/store/useStore.ts` — `wakeWordEnabled` flag + `setWakeWordEnabled` (localStorage-persisted, default OFF).
- `ui/src/components/DecisionSurface.tsx` — toggle button (stroke-SVG concentric arcs).
- `ui/src/components/IntentSurface.tsx` — single-owner `useEffect` mount, Escape handler, indicator render.
- `ui/public/models/wake-word/{README.md,LICENSE-openwakeword.txt}` — operator-install docs + Apache-2.0 attribution.
- `ui/public/models/vad/{README.md,LICENSE-silero.txt}` — operator-install docs + MIT attribution.
- `THIRD_PARTY_LICENSES.md` (NEW, top-level) — third-party license registry.
- `ui/src/__tests__/wakeWord.stateMachine.test.ts` (NEW, 5 cases).
- `ui/src/__tests__/wakeWord.router.test.ts` (NEW, 6 cases).
- `ui/src/__tests__/wakeWord.bargeIn.test.ts` (NEW, 2 cases).
- `ui/src/__tests__/wakeWord.fallback.test.ts` (NEW, 2 cases).
- `ui/src/__tests__/wakeWord.escape.test.ts` (NEW, 2 cases).
- `ui/src/__tests__/WakeWordIndicator.test.tsx` (NEW, 3 cases).
- `ui/src/__tests__/DecisionSurface.wakeWordToggle.test.tsx` (NEW, 2 cases).
- `ui/src/__tests__/wakeWord.lazyLoad.test.ts` (NEW, 1 case).
- `decisions-era-4-evolution.md` — appended AD-705 (reframed) entry.
- `progress-era-5-unification.md` — appended one-line progress entry.

## Sections implemented

- **D1 (onnxruntime-web + openWakeWord)** — package.json `optionalDependencies` entry; LICENSE-openwakeword.txt + README.md committed under `ui/public/models/wake-word/`.
- **D2 (Silero VAD)** — LICENSE-silero.txt + README.md committed under `ui/public/models/vad/`.
- **D3 (`wakeWord.ts` pure module)** — full state machine including `armed`, `capturing`, `fallback-armed`, `fallback-capturing`, `off`. AbortController for clean abort. Tier-2 log-and-degrade on ONNX-load failure.
- **D4 (router)** — `routeWakeTranscript` in `wakeWord.router.ts` (separate file for testability). Pure function; structural-typed agents map.
- **D5 (DecisionSurface toggle)** — wakeWordEnabled flag + setter in store; toggle button in DecisionSurface; persisted; default OFF.
- **D6 (barge-in)** — subscribes to `onSpeechEvent` once at start; gates ingestion while TTS is speaking; clean unsubscribe on stop.
- **D7 (indicator)** — `WakeWordIndicator.tsx` renders three visual states with motion encoding; fallback-mode label; trigger label during capture. Inline SVG only.
- **D8 (Escape)** — IntentSurface `handleKeyDown` checks wake-word state first; cancels capture without submitting; armed/picker/input close paths fall through.
- **D9 (single-owner wiring)** — `useEffect` keyed on `wakeWordEnabled` in IntentSurface; teardown on cleanup; routed transcripts re-submitted through `/api/chat` via the existing form.

## Post-build section audit

All nine `###` sections (D1–D9) have corresponding code.

## Tests

- New per-prompt: `cd ui && npx vitest run` — 23 new Vitest cases, all green.
- Full UI gate: 54 files / 516 tests passed (baseline +23).
- Python gate: not run; this wave is UI-only and does not touch Python paths.

## Hard-stop verifications

| # | Hard-stop | Status |
|---|---|---|
| 1 | Phantom API | All 9 expected false-positives present in this commit (introduced-by-this-prompt). |
| 2 | Architectural contract change | None. `speechInput.ts` and `voice.ts` public APIs untouched. |
| 3 | Working tree integrity | Pre-flight clean; no >200-line deletions. |
| 4 | License creep | Only `onnxruntime-web` (MIT, optional), openWakeWord (Apache-2.0, operator-installed model), Silero VAD (MIT, operator-installed). No Porcupine, no AccessKey deps, no paid-license deps. |
| 5 | Emoji in diff | None. All icons are inline stroke-only SVG. |
| 6 | TTS replacement | None. `voice.ts` only imported for `onSpeechEvent` subscription. |
| 7 | Default-on toggle | `wakeWordEnabled` defaults to `false`; localStorage hydration explicitly checks for `'1'`. |
| 8 | Audio leaks server-side before wake | None. Pre-wake audio is consumed in-browser by ONNX (when present) or browser SpeechRecognition (fallback). |
| 9 | Indicator missing/conflated | Three visually-distinct states (off/armed/capturing) plus fallback-mode label. |
| 10 | Eager bundle load | `onnxruntime-web` loaded via `await import(/* @vite-ignore */ moduleName)` only; test #21 asserts no static top-level import. |
| 11 | exec/eval/compile/pickle | None. |
| 12 | Test gate flake | All 23 new tests green serially. |

## Captain ruling alignment

- **Edge TTS unchanged**: `voice.ts` `speakResponse` not modified; only `onSpeechEvent` subscribed for barge-in.
- **openWakeWord + Silero VAD adopted**: license-clean; deps documented in `THIRD_PARTY_LICENSES.md`.
- **"Hey Ezri" / @Ezri routing**: AD-718c lands the `wake_phrase` field; AD-705 D4 router already accepts the structural shape (`voice_profile.wake_phrase`) and wakeWord.ts honors `agentTriggers` option.

## Deviations

1. **D1 strict text says "runtime dependency"; shipped as `optionalDependencies`.** Rationale: aligns with the operator-install pattern used for the model files (per `ui/public/models/*/README.md`), matches the lazy-load hard-stop intent (Captain Q5 ruling), and prevents `npm ci` lockfile churn for users who never enable the feature. The wake-word loop falls through to the substring-match fallback when the package is absent — verified by tests #12 and #17/#18 (which run with the package absent in CI). Operators graduate to ONNX-armed by `npm install --prefix ui onnxruntime-web` plus dropping a model ONNX file under `ui/public/models/wake-word/`.

2. **`_loadOnnxRuntime` returns `false` even when the runtime imports successfully.** Rationale: v1 does NOT yet drive the actual ONNX inference (model file is operator-installed). Returning `false` keeps the loop in `fallback-armed` until the model exists. The graduation to `armed` is tracked as part of AD-705c (custom-trained model pipeline) and the operator-side install path documented in the README. Tests cover the fallback path explicitly.

## Forward markers filed

See AD-705a / AD-705b / AD-705c / AD-705d in §10 of the prompt — Builder filed them under [#481](https://github.com/seangalliher/ProbOS/issues/481) follow-ups (verify before push).
