# ProbOS Progress

**Status (2026-05-11).** Open OSS issues: 10 (#582 closed Wave 150; 3 from Wave 138 — AD-721b-1/-2/-3; 5 from Wave 143 — AD-722a-1/-2/-3/-4/-6 forward markers (AD-722a-5 closed Wave 147); 1 from Wave 144 — AD-723a-1 #617 DM/WR consumer-side sensorium dispatch migration; 3 from Wave 145 — AD-721d-2 #621 Counselor-mediated revision, AD-721d-3 #622 visual preview requires AD-721i, AD-721d-4 #623 proposal-history persistence). Most recent shipped wave: 150 (AD-724 DM sanity gate — behavior-preserving migration of BF-120/BF-119/AD-572 regexes into `DmSanityGate` + 3 new log-only checks; 5 forward markers AD-724-1/-2/-3/-4/-5 filed).
AD-697 + AD-698 establish the commercial-overlay seam
(`pip install -e ../<commercial-package>` → overlay active; uninstall →
back to OSS).

**Authoritative state.**
- `prompts/wave-plan.yaml` — wave roster (current wave: 150 done; next slot is 151).
- `DECISIONS.md` — append-only architectural decisions (current highest AD: AD-740 — affect-vs-intent drift trend; AD-730-3 also shipped Wave 169 as agent image generation in DM replies).
- `tests/` — 13449 pytest at HEAD (4 pre-existing flakes in test_callsign_routing/test_ad719_chat_fanout + occasional dreaming/ward_room flakes outside this wave) + 633 vitest; gate runs `-n 4 --dist=loadfile`.

**Yeo feature-complete gate shipped (2026-05-20; Wave 181 integration gate).**
- AD-758 gate checks added and passing for AD-749..AD-757 program rubric, captain-invariant exposure, delegation-policy conformance, OSS-scope boundaries, and dedupe references (#695..#704).
- Child prompts AD-749..AD-757 now carry explicit "for free leverage" acceptance bullets.
- Wave 181 closed in `prompts/wave-plan.yaml`; Wave 182 activated for post-gate execution planning.
- **Yeo gap-doc reconciliation: 24 shipped / 11 partial / 12 not-started / 1 commercial / 4 out-of-scope** across 52 capabilities — see [`docs/development/yeo-gap-reconciliation-2026-05-20.md`](docs/development/yeo-gap-reconciliation-2026-05-20.md) (AD-765, closes #711). Child-AD backlog AD-767..AD-789 drafted for Wave 185+ filing.

**Wave 180 in flight (2026-05-19) — live-use triage build (BF-317 + BF-318 + AD-746 + AD-747). Architect-only draft. Continuous-build dispatch authorized (Builder runs immediately after this commit).**
- **BF-317** — Share-screen button discoverability in DM thread. Closes [#681](https://github.com/seangalliher/ProbOS/issues/681). UI-only fix to `ui/src/components/wardroom/WardRoomThreadDetail.tsx`: AD-744 share-screen button (verified `data-testid="wardroom-dm-share-screen-button"` at line 397) gains a persistent `Share screen` text label, larger glyph (18×18 vs 14×14 attach), filled monitor base differentiating it from the line-only paperclip attach button, and a 1px divider separator between attach and share-screen affordances. HXI Design Principle #1 ("the system understands the human"). Zero backend changes. +3 vitest. Forward marker BF-317-1 (composer tool palette) when share-class affordances ≥ 3. **Status: drafted (`prompts/bf-317-share-screen-discoverability.md`).**
- **BF-318** — `speechRecognitionArbiter` priority-queue leases. Closes [#683](https://github.com/seangalliher/ProbOS/issues/683). NEW module `ui/src/audio/speechRecognitionArbiter.ts` (~120 lines) owns the singleton SpeechRecognition device with `acquire(holder, priority, onAcquired, onPreempted, onReleased)` / `release` / `currentHolder` surface. Priority constants `PRIORITY_PRESS_TO_TALK=100`, `PRIORITY_CONVERSATION=75` (reserved for AD-747), `PRIORITY_WAKE_WORD=50`. `ui/src/audio/speechInput.ts` (`activeRecognition` singleton at line 28) wired through the arbiter; `ui/src/audio/wakeWord.ts:_startContinuousRecognition` (line 322) requests `PRIORITY_WAKE_WORD` and re-arms on `onReleased` after press-to-talk preempt. VAD (`voiceActivity.ts:213` dedicated `getUserMedia` stream) is NOT in scope — separate device path. Zero backend changes. +6 vitest. **Hard prerequisite for AD-747.** **Status: drafted (`prompts/bf-318-mic-arbiter.md`).**
- **AD-746** — Camera + screen source policy (layered fusion + per-agent binding). Closes [#682](https://github.com/seangalliher/ProbOS/issues/682). Two layers, both default-ON: **Layer 1** new `src/probos/perception/aggregator.py` `VisionAggregator` inserted BETWEEN source admitters and `VisionConsumer` — when camera + screen frames arrive within `fusion_window_ms=800` (Pipecat `VisionAggregator` pattern, BSD-2-Clause), they are fused into a single multimodal intent message (`params.attachment_refs: list[str]`, `params.sources: list[str]`, `params.fused: True`). Single-source frames pass through unchanged (passthrough preserves legacy contract). AD-733c-6 budget: fused = 1 call. **Layer 2** extends `CrewProfile.perception.camera_device_id` shape (AD-742c) with `bound_sources: list[Literal["camera","screen"]]` field (default both — back-compat). `VisionConsumer._handle` early branch (extending AD-742c `bound_agent_ids` filter at consumer.py:~430) filters frames against active agent's `bound_sources`. Fused frames visible when ANY bound source intersects. Two new `PerceptionConfig` fields (`source_fusion_enabled=True`, `fusion_window_ms=800`, hot-reload). Two new API routes: `GET /api/perception/sources`, `POST /api/perception/sources/binding` (mirror AD-742c shape). HXI extends `PerceptionLivePanel.tsx` CAMERA BINDINGS → SOURCE BINDINGS with per-agent CAMERA/SCREEN check pills (HXI #3 no emoji). AD-541b anchor metadata extends `source: str` → `sources: list[str]` (legacy field preserved one wave; AD-746-5 retires). AD-731 source-scan extends to `aggregator.py`. +14 pytest, +4 vitest. Forward markers AD-746-1 (raw priority knob) / -2 (cross-modal salience scorer) / -3 (audio third source — pairs AD-747) / -4 (browser-side fusion preview) / -5 (legacy `source:str` retirement). **Status: drafted (`prompts/ad-746-camera-screen-source-policy.md`).**
- **AD-747** — ConversationController (always-on natural conversation mode). Closes [#684](https://github.com/seangalliher/ProbOS/issues/684). NEW module `ui/src/audio/conversationController.ts` (~250 lines) owns the duplex when a DM thread is open. State machine: `inactive → listening → transcribing → submitted → agent_speaking → silence_pending → inactive`. Acquires `PRIORITY_CONVERSATION=75` lease through the BF-318 arbiter; subscribes to `whisperStt.onTranscript` (AD-705a, `whisperStt.ts:184`) for STT chunks gated by VAD speech bounds (AD-733c-7, `voiceActivity.ts:195`); auto-submits transcripts to active agent's DM via existing `agent_chat` endpoint with a 750 ms edit window (transcript-preview pill, HXI #5); barge-in via `voice.stopSpeaking()` when VAD speech_start fires during `agent_speaking` (LiveKit `VoicePipelineAgent` pattern, Apache 2.0); 30 s silence timer releases the lease (matches ChatGPT advanced voice mode default per RESEARCH). Press-to-talk preserved but visually demoted when `conversation_mode_enabled=True` (preempts cleanly via arbiter `PRIORITY_PRESS_TO_TALK=100`). Three new `CognitiveConfig` fields (`conversation_mode_enabled=False` default-off transitional gate, `conversation_silence_timeout_ms=30000`, `conversation_barge_in_enabled=True`; all hot-reload). HXI: new CONV badge in `CameraLiveIndicator.tsx` (amber armed, pulse during agent_speaking); transcript-preview pill in `WardRoomThreadDetail.tsx`. AD-733c-7 privacy invariant preserved (audio bytes never leave browser; source-scan asserts no PCM in fetch body). +6 pytest, +14 vitest. Zero new deps. Forward markers AD-747-1 (Hume EVI prosody-gated barge-in) / -2 (cross-agent handoff) / -3 (Vapi interruption sensitivity knob) / -4 (goodbye classifier) / -5 (server-side streaming STT, pairs AD-705a-4) / -6 (multi-Captain voice profile binding) / -7 (audio fused into AD-746, pairs AD-746-3). **Status: drafted (`prompts/ad-747-conversation-controller.md`). Strict build order: ships AFTER BF-318.**

Prior-art research deliverable: `prompts/RESEARCH-issues-2026-05-19.md` (Captain's three issues triaged after Waves 175-179 production use; license posture per source; LiveKit Agents Apache 2.0 + Pipecat BSD-2-Clause as code-shape references; ChatGPT advanced voice mode + Claude computer-use + Operator + Pi + Gemini + Vapi + Hume EVI as architecture-only validation of v1 UX choices). Zero new pip / npm deps. **0-line diff on all 5 license files.**

Captain decisions locked at triage (NOT re-litigated):
1. AD-746 v1 = Layer 1 fusion + Layer 2 per-agent binding. Raw priority knob → AD-746-1 forward marker.
2. AD-747 ships AFTER BF-318 (hard prerequisite).
3. AD-747 v1 silence timeout = 30 s (ChatGPT advanced voice mode default).
4. Press-to-talk button preserved (becomes optional but stays in the composer).

Highest AD before Wave 180: **AD-747** (filed at triage commit `4beaba7e`). Highest AD after Wave 180 ships: **AD-747** (unchanged — no new top-level ADs in this wave).

**Wave 179 in flight (2026-05-19) — voice stack completion (AD-721b-3 + AD-705a + AD-705c). Architect-only draft. Builder dispatch deferred.**
- **AD-721b-3** — whisper.cpp WASM tiny.en model bundle. Foundation prompt; NO STT exposed. Closes #561. New `scripts/whisper-tiny-en-fetch.ps1` operator-pull script (mirrors `silero-vad-fetch.ps1`); new `ui/src/audio/whisperLoader.ts` browser-side lazy-loader (UMD `<script>` injection pattern for the whisper.cpp WASM glue — diverges from silero-vad's ESM `await import()` because the upstream artifact is UMD); new `src/probos/voice/whisper_model.py` resolver (`resolve_whisper_model_path(config) -> Path | None`); new `CognitiveConfig.whisper_model_path` field default `"whisper/ggml-tiny.en.bin"`; one new `FieldDescriptor` in the AD-741 LLM Tiers section (restart-required); new `ui/public/models/whisper/README.md` (mirrors `ui/public/models/vad/README.md`). Model bytes NEVER committed (gitignored under `data/*`). 0-line diff on `THIRD_PARTY_LICENSES.md` (entries land at AD-705a ship). +6 pytest, +3 vitest. **Status: SHIPPED Wave 179.**
- **AD-705a** — Offline STT via whisper.cpp WASM. Closes #555. Extends `ui/src/audio/voiceActivity.ts` with a `subscribePcm(callback): () => void` PCM-ring tap (zero overhead when no subscribers); new `ui/src/audio/whisperStt.ts` (~180 lines) arms on `offline_stt_enabled=true` and runs `whisper_full` between VAD `speech_start` and `speech_end`; transcript dispatches through the existing `agent_chat` keyboard path; transcript-preview pill in `IntentSurface.tsx` (HXI #5); STT badge in `CameraLiveIndicator.tsx` alongside the AD-733c-7-5 SPEECH badge (hidden / dim / amber / pulse states; HXI #3 no emoji). New `CognitiveConfig.offline_stt_enabled: bool = False` (default OFF transitional gate). Privacy invariant (AD-733c-7 extended): audio bytes NEVER leave the browser — regression test asserts no fetch body in `whisperStt.ts` contains base64/pcm/arraybuffer. Browser-native `SpeechRecognition` Tier-2 fallback preserved (forward marker AD-705a-7 for fully-offline mode). +5 pytest, +8 vitest. +2 `THIRD_PARTY_LICENSES.md` entries (whisper.cpp MIT + Whisper model weights MIT). Forward markers AD-705a-1 (transformers.js alternative) / -2 (IndexedDB cache) / -3 (lazy CDN) / -4 (streaming decode) / -5 (Moonshine backend) / -6 (multilingual / larger models) / -7 (fully-offline mode) / -8 (transcript audit log). **Status: SHIPPED Wave 179.**
- **AD-705c** — Custom wake-word training pipeline. Closes #557. New `probos wake-word` CLI subcommand (`collect / train / test / status`) in `src/probos/experience/commands/commands_wake_word.py`; new `src/probos/voice/wake_word_trainer.py` `WakeWordTrainer` service module running openWakeWord training via `loop.run_in_executor` (BF-280); new `src/probos/routers/voice.py` (or extension to existing) with three `require_crew_scope` endpoints (`POST /api/voice/wake-word/sample`, `POST /api/voice/wake-word/train`, `GET /api/voice/wake-word/training-status`); training task held in `runtime._wake_word_trainer_tasks: set[asyncio.Task]` per async discipline; new `ui/src/components/wakeword/WakeWordTrainerPanel.tsx` guided recorder UX (countdown → record-4s → upload → next-sample → train-now → polling → activate); modifies `ui/src/audio/wakeWord.ts:_loadOnnxRuntime` (SINGLE BF-274 block edit) to prefer `captain.onnx` over the stock community model. New `WakeWordConfig` Pydantic block with five fields (`wake_word_trainer_enabled`, `custom_model_filename`, `retain_training_samples`, `training_samples_max_count`, `training_audio_max_bytes`); three new `FieldDescriptor`s in the AD-741 Voice section. **No `openwakeword` in `pyproject.toml`** — operator-installed separately via the documented `pip install openwakeword[training]` instruction; CLI honest-degrades when the package is absent. Captain's brief mentions "AD-733c-3's Picovoice path"; the actual codebase uses openWakeWord — surfaced as a clarification, not a blocker. +12 pytest, +4 vitest. +1 `THIRD_PARTY_LICENSES.md` entry (openWakeWord Apache 2.0). Forward markers AD-705c-1 (negatives-fetch script) / -2 (Howl transfer learning) / -3 (EfficientWord-Net few-shot) / -4 (multi-Captain wake words) / -5 (Counselor-suggested retrain on FAR spike). **Status: SHIPPED Wave 179.**

Prior-art research deliverable: `prompts/RESEARCH-wave-179.md` (16 projects surveyed across the Whisper, wake-word, and phoneme-alignment families; license-aware absorption matrix; 8 architectural decisions documented with rationale; v1 vs forward-marker matrix; 5 open questions surfaced for Captain). Top absorption sources: whisper.cpp (MIT — direct artifact) + OpenAI Whisper weights (MIT — direct artifact) + openWakeWord (Apache 2.0 — training pipeline). Top anti-patterns to avoid: audio bytes in network requests; static-import of ONNX runtime / Whisper glue (first-paint regression); hand-rolling a wake-word training pipeline. Zero new pip / npm deps land. **0-line diff on `pyproject.toml` + `package.json` + `package-lock.json` + `LICENSE` across the wave**; +3 entries on `THIRD_PARTY_LICENSES.md` (at ship time, NOT in this commit).

Captain GATE 1 decisions surfaced (NOT auto-resolved by architect; required before Builder dispatch):
1. Confirm tiny.en (~75 MB; ~85-90% WER on clean dictation) is acceptable for v1; larger models = forward markers.
2. Trainer UX docs emphasis (CLI-first vs browser-first in the manual).
3. `retain_training_samples` default (`False` recommended — delete after train).
4. Custom model filename default (`captain.onnx`; multi-Captain deferred to AD-705c-4).
5. `SpeechRecognition` fallback co-existence (v1 allows both side-by-side; AD-705a-7 forward marker for fully-offline mode).

Highest AD before Wave 179: **AD-745** (set by Wave 178). Highest AD after Wave 179 ships: **AD-745** (unchanged — all three Wave 179 ADs were pre-assigned sub-AD numbers per AD-721b family / AD-705 family).

**Wave 178 in flight (2026-05-19) — see -> discuss -> act ladder. Architect-only draft. Builder dispatch deferred.**
- **AD-733-2** — Passive screen sensing (`getDisplayMedia` -> `vision_observation source=screen`). Closes #668 (narrowed; multi-camera already shipped via AD-742c bound_agent_ids). New `PerceptionConfig.screen` sub-block + `screen_max_fps_server`; extends `POST /api/perception/camera/frame` with `source` form field; per-source token bucket (key change `session_id` -> `(session_id, source)`); new `useScreenStream` hook + `useScreenStore` Zustand sibling slice; SCREEN LIVE indicator added to `CameraLiveIndicator.tsx`; SCREEN SOURCES section in `PerceptionLivePanel.tsx`. AD-731 invariant preserved (refs only). AD-541b anchor with new `trigger_type="screen_stream_began"`. +8 pytest, +5 vitest. Forward markers AD-733-2-1 (per-source novelty thresholds) / -2 (WebRTC track). **Status: drafted (`prompts/ad-733-2-passive-screen-sensing.md`).**
- **AD-744** — Interactive share-to-agent (one-shot DM-attached screen frame). NEW top-level AD. "Share screen to {agent}" button on DM composer; `getDisplayMedia` -> single JPEG -> POST with `source=screen` + `force=true` + `agent_ids=<id>`; bypasses supervisor novelty gate (BF-302); attaches inline via existing `attachment_ids` field on `agent_chat`; agent receives both DM text + screen frame as one composed multimodal message. NO new endpoint (reuses AD-733-2 endpoint surface). New `PerceptionConfig.explicit_share_enabled` (default-True; underlying `getDisplayMedia` requires browser consent on every click). AD-731 + AD-541b preserved. NEW `trigger_type="captain_explicit_share"`. +8 pytest, +6 vitest. Forward markers AD-744-1 (cross-agent share) / -2 (redaction primitive) / -3 (preview modal with redact-region). **Status: drafted (`prompts/ad-744-interactive-share-to-agent.md`).**
- **AD-745** — Conversation -> action handoff (browser scope v1; Playwright dispatch). NEW top-level AD. `[ACTION: <json>]` bracket marker on DM replies (mirrors AD-728d/AD-730-3/AD-743 family); new `step_5_action_dispatch` pipeline stage; dispatch via existing AD-706 BrowserTool `_HANDLERS`; tier 1/2/3 ladder via existing `classify_action`; new `BrowserToolConfig.destructive_url_patterns` allow-list (financial/auth/admin/delete patterns) that forces matched URLs to tier-3; new `BrowserToolConfig.action_dispatch_enabled` (default-OFF Wave 10 #14); reuses AD-706c-2 Guard #10 trust budget; new `POST /api/browser/actions/{id}/ack` + `/abort` + `GET /api/browser/actions/{thread_id}` (under `require_crew_scope`); new `AgentActionLog` HXI component (HXI #5 collapsed default) + tier-2 ACK card + tier-3 destructive-confirmation modal; per-action stop affordance sets `BrowserSession.aborted=True`. AD-541b anchor with new `channel="action"` + `trigger_type="agent_action_executed"` + before/after frame refs. Agent acts in BrowserSession's isolated Chromium context, NOT Captain's logged-in profile (safety invariant; AD-745-5 forward marker for consensual clone). +18 pytest, +6 vitest. Forward markers AD-745-1 (DesktopActionTool OS scope) / -2 (multi-agent quorum on destructive verbs) / -3 (OmniParser SOM grounding) / -4 (pluggable grounding strategy) / -5 (consensual profile clone) / -6 (multi-step plans per DM turn) / -7 (cross-thread action audit + SQLite persistence). **Status: drafted (`prompts/ad-745-conversation-action-handoff.md`).**

Prior-art research deliverable: `prompts/RESEARCH-wave-178.md` (13 projects surveyed; license posture per source; 5 architecture-pattern sections — action grammar, screen grounding, lifecycle/consent UX, safety/governance, multi-step task patterns; 6 architectural decisions documented with rationale; 4 open questions surfaced for Captain). Top absorption sources: Anthropic computer-use (MIT quickstart) + browser-use (MIT) + OmniParser/SeeAct (MIT/Apache). Top anti-patterns to avoid: agent-emits-raw-executable-code; single-tool-agent god class; hidden cross-surface context without operator visibility. Zero new pip / npm deps land in this wave. **0-line diff on all 5 license files.**

Captain GATE 1 decisions surfaced (NOT auto-resolved by architect; required before Builder dispatch):
1. AD-721j re-scope to "Blender as target app of AD-745-1 DesktopActionTool" (recommended).
2. AD-745 v1 OS-scope = honest-degrade-to-describe-only (recommended).
3. AD-745 v1 destructive actions = Captain-ACK-only (AD-745-2 promotes to quorum based on action volume).
4. AD-745 v1 = per-action ACK; multi-step plans deferred to AD-745-6.

Highest AD before Wave 178: **AD-743**. Highest AD after Wave 178 ships: **AD-745** (two new top-level ADs; one consumed forward marker).

**Wave 177 in flight (2026-05-19):**
- AD-733c-5-4 — HXI per-agent perception badges (parent AD-733c-5; no GH issue per AD-722c-3). Extends `usePerceptionModeStore` with `perAgent: Record<string, PerceptionMode>` populated from `GET /api/perception/mode` `per_agent` field (already shipped by AD-733c-5 backend Wave 176). `CameraLiveIndicator.tsx` renders per-agent compact badges (`EZRI:ENG` / `ATLAS:AMB` format) when `perAgent` has ≥ 2 entries; falls back to the legacy single MODE badge otherwise. `PerceptionLivePanel.tsx` adds a per-agent MODE table beneath the existing preset buttons. +3 vitest, zero new pytest. Zero new pip / npm deps. 0-line license diff. **Status: shipped.**
- AD-733c-7-5 — HXI Silero VAD browser integration (parent AD-733c-7; no GH issue per AD-722c-3). Three new browser modules: `ui/src/audio/silero-vad.ts` (lazy-loads `onnxruntime-web` via the indirect-string-variable pattern from `wakeWord.ts:268-289` — static import FORBIDDEN), `ui/src/audio/voiceActivity.ts` (mic-tap + 30ms chunked detection at 16 kHz + debounce by `vad_min_speech_duration_ms` + POST to existing `/api/perception/voice-activity` from AD-733c-7 backend). `CameraLiveIndicator.tsx` SPEECH badge flashes on speech event (1.5s decay), hidden when `vad_engagement_enabled=false`. App.tsx arms/disarms the loop in sync with the snapshot toggle. Privacy invariant: audio bytes NEVER leave the browser — regression test asserts fetch body contains ONLY `{agent?, source}`. New `getUserMedia({audio: true})` stream — no shared `useMicStream()` exists in codebase (`wakeWord.ts` uses SpeechRecognition transcript API; `useCameraStream` is explicit audio:false). +5 vitest, zero new pytest. Zero new pip / npm deps. 0-line license diff. **Status: shipped.**
- AD-742c-6 — HXI camera multiplexer (parent AD-742c; no GH issue per AD-722c-3). New SIBLING Zustand slice `useCameraMultiplexerStore` (NOT a merger with `useCameraStore` — separate endpoints + lifecycles; SRP wins). Tracks `bindings: Record<string, string>` from `GET /api/perception/cameras` + `devices: MediaDeviceInfo[]` from `navigator.mediaDevices.enumerateDevices`. `useCameraStream` EXTENDED (not rewritten — preserves BF-301/302/305 invariants) with optional `deviceId` kwarg on `startCameraStream`, module-level `_activeDeviceId` + `Map<deviceId, MediaStream>` alongside the existing `_stream`, and `_computeAgentIds()` deriving the form field from multiplexer bindings at capture time. `PerceptionLivePanel.tsx` adds CAMERA BINDINGS section (collapsible per HXI #5) with per-agent device dropdown + clear-binding stroke-X. `CameraLiveIndicator.tsx` gains CAMS:N label when ≥ 2 devices bound. AD-731 invariant preserved — `agent_ids` is a STRING list, never bytes. +6 vitest (spec called for +4; the 2 extra cover toggle round-trip + solo-Captain regression on BF-301/302/305 invariant family), zero new pytest. Zero new pip / npm deps. 0-line license diff. **Status: shipped.**

**Wave 176 in flight (2026-05-19):**
- AD-743 — Adaptive conversational pacing in 1:1 DMs (closes #662). New `[FOLLOW_UP delay_seconds reason]` bracket marker (extends AD-728d / AD-730-3 family) parsed in new `step_4d_follow_up_parse` of `DmReplyPipeline` (inserted between `step_4c_image_gen_parse` and `step_4b_dm_outbound_parse` — name adjusted from prompt's `step_5_follow_up_parse` because `step_5_episodic_store` already occupied that slot). New `ConversationPacingScheduler` runtime service owns per-(agent_id, conversation_id) `asyncio.Task` refs; schedules synthesized user-turn `IntentMessage` carrying `from: "pacing_scheduler"` after delay. Two-budget rate limit (AD-728c precedent): per-active-conversation cap (default 2) + hourly per-agent cap (default 6); budgets are NOT additive. Cancelled on Captain interruption (new DM cancels pending follow-up via `agent_chat` top-of-handler hook). `AvatarsConfig.pacing_enabled=False` default-off transitional gate; all cap values hot-reload per BF-308 (scheduler reads `_get_pacing_config()` on every schedule call). New file `cognitive/dm/pacing_scheduler.py`. AD-733c-1 force_describe + AD-733c-2 note_dm_activity continue to fire on synthesized user-turn (perception stays alive during multi-beat). +12 pytest. Zero new deps. 0-line license diff. Forward markers: AD-743-1 (Captain-silence trigger), AD-743-2 (same-tick multi-message), AD-743-3 (correction-driven budget reset). **Status: shipped.**
- AD-733c-5 — Per-agent perception engagement (closes #676). Promotes `PerceptionModeController` from singleton to per-agent instances via new `PerceptionEngagementRegistry` (`perception/engagement_registry.py`). New `PerceptionProfile` dataclass on `CrewProfile` (after `PeerPerceptionProfile`): `engagement_enabled=True` + `initial_mode="ambient"` + `camera_device_id=""` (reserved for AD-742c). Legacy profile JSON without `perception` block backward-compat (defaults). `startup/finalize.py` per-agent loop runs AFTER the existing singleton wiring; back-compat pointer `runtime.perception_mode_controller` is repointed to the primary controller (Counselor `e1` if present, else first registered). `routers/agents.py:agent_chat` note_dm_activity callsite routes through registry. `routers/perception.py` GET /mode response extended with `per_agent: {agent_id: mode}` for HXI rendering; POST /engage accepts optional `agent` (agent_id or callsign) and 404 honest-degrades on unknown agent. `startup/shutdown.py` stops per-agent controllers. **Scope deviation from prompt**: UI changes (CameraLiveIndicator per-agent badges + usePerceptionEngagementStore Zustand slice + PerceptionLivePanel MODE table) deferred to forward marker AD-733c-5-4 (HXI per-agent badges) — backend acceptance criteria fully met; 3 vitest deferred with the UI work. observer.py / consumer.py per-agent threading also deferred to forward marker AD-733c-5-5 (per-agent budget enforcement) — current AD-733c-6 enforcement transitions the primary (Counselor) controller. +11 pytest. Zero new deps. 0-line license diff. Forward markers: AD-733c-5-1 (HXI editor), AD-733c-5-2 (hot-reload toggle), AD-733c-5-3 (federation sync), AD-733c-5-4 (HXI per-agent badges), AD-733c-5-5 (per-agent budget enforcement). **Status: shipped.**
- AD-733c-7 — Silero VAD secondary engagement trigger (closes #678). NEW TRIGGER (NOT a strategy plugin) feeding the per-agent `PerceptionModeController` via new `note_voice_activity()` method. Step-wise ramp (DORMANT→AMBIENT→ENGAGED) mirroring `note_dm_activity`; cooldown `VOICE_ACTIVITY_COOLDOWN_S=3.0` between PROGRAMMATIC (1s) and WAKE_WORD (5s). New `POST /api/perception/voice-activity` endpoint (mirrors AD-733c-3 `/engage` shape) — body JSON only, no audio. 503 honest-degrade when `vad_engagement_enabled=False`. Routes through AD-733c-5 registry when `agent` specified; 404 on unknown agent. New `PerceptionConfig.vad_engagement_enabled=False` (default-off transitional gate) + `vad_min_speech_duration_ms=400` (browser-side debounce floor). New `scripts/silero-vad-fetch.ps1` operator-pullable model download (mirrors `piper-voice-fetch.ps1` shape). Silero model NEVER committed (`data/*` gitignore rule already covers `data/silero-vad/`). New `THIRD_PARTY_LICENSES.md` Silero VAD entry (MIT). **Scope deviation from prompt**: browser-side `silero-vad.ts` / `voiceActivity.ts` + CameraLiveIndicator SPEECH indicator + 5 vitest deferred to forward marker AD-733c-7-5 (HXI VAD integration) — backend acceptance fully met. +10 pytest. Zero new pip / npm deps (`onnxruntime-web` already in `optionalDependencies`). 0-line diff on `pyproject.toml` / `package.json` / `package-lock.json` / `LICENSE`. +1 entry on `THIRD_PARTY_LICENSES.md`. 0-line diff on `.gitignore` (existing `data/*` rule already covers Silero dir). Forward markers: AD-733c-7-1 (browser-pause getUserMedia), -2 (multi-mic), -3 (diarization), -4 (VAD-driven wake-word mute), -5 (HXI VAD integration). **Status: shipped.**
- AD-742c — Per-agent camera selection (closes #671). Wires `CrewProfile.perception.camera_device_id` (introduced by AD-733c-5) into the actual capture path. `routers/perception.py:upload_camera_frame` accepts new optional `agent_ids` comma-separated form field; threads into `IntentMessage.params["bound_agent_ids"]: list[str]`. `VisionConsumer._handle` early branch: when `bound_agent_ids` present, restricts WM fan-out + episodic anchor to that set (intersected with registered observers); when absent, legacy fan-out-to-all preserved (back-compat). New `GET /api/perception/cameras` returns persisted bindings dict; `POST /api/perception/cameras/binding` accepts `{agent_id, device_id}` and persists via `ProfileStore.update`; 404 honest-degrade on unknown agent. AD-731 invariant rigorously preserved — bytes still flow as SHA refs; source-scan test re-runs on `routers/perception.py`; `bound_agent_ids` is a STRING list, never bytes. AD-541b BF-311 anchor `agent_ids_json` restricted to bound set when present. **Scope deviation from prompt**: UI components (CameraMultiplexer Zustand slice + useCameraStream multi-deviceId refactor + PerceptionLivePanel CAMERA BINDINGS table + 4 vitest) deferred to forward marker AD-742c-6 (HXI camera multiplexer integration) — backend acceptance fully met. +10 pytest. Zero new deps. 0-line license diff. Forward markers: AD-742c-1 (screen capture), -2 (federation), -3 (RTSP), -4 (audio device), -5 (permissions), -6 (HXI camera multiplexer integration). **Status: shipped.**

**Wave 175 in flight (2026-05-18):**
- AD-733c-6 — Engaged-mode vision LLM call budget enforcement (closes #677). Extends AD-742e telemetry into enforcement. New `_maybe_enforce_budget(session_id)` method called from `_record_vision_call` after the counter increment: when mode is ENGAGED and total session calls >= cap OR total today calls >= cap, the consumer synchronously calls `controller.transition_to(Mode.AMBIENT, trigger="budget_exhausted")` and logs a once-per-session WARNING. Three new `PerceptionConfig` fields (`engaged_budget_enforcement=True`, `engaged_call_cap_per_session=200`, `engaged_call_cap_per_day=2000`) — all hot-reload per BF-308 (cap dial mid-session, on/off without restart). New `_budget_cap_notified_sessions: set[str]` (cleared on session change so subsequent sessions get their own one-shot WARNING). `get_budget_snapshot()` extended with `cap_per_session` + `cap_per_day` + `enforcement_enabled` + `cap_reached_session` + `cap_reached_day` fields; `session_ceiling_estimate` retained for AD-742e backcompat but now maps to `cap_per_session` (default 200 vs AD-742e heuristic 120). `mode_controller.Transition.trigger` docstring extended to enumerate `"budget_exhausted"`. `VisionBudgetBadge.tsx` reads `cap_per_session` + `cap_reached_session` + `enforcement_enabled` — color scale shifted: green <80%, orange 80-99%, red >=100%; dim grey override when enforcement_enabled=false. Anti-deadlock confirmed: `transition_to` is synchronous; safe to call while holding `_describe_lock` (no async acquisition, no re-entry). `_maybe_enforce_budget` is defense-in-depth — if perception config or controller is missing, silently skips (counter behavior preserved). +9 pytest in `tests/test_ad733c6_engaged_budget_enforcement.py` (under-cap no-transition, session-cap hit drops, day-cap hit drops, enforcement-disabled no-transition, ambient-mode no-transition, WARNING rate-limited per session, session-change resets notification, snapshot exposes caps, hot-reload cap change effective on next call). +3 vitest in `VisionBudgetBadge.test.tsx` (orange via cap_per_session, red via cap_reached_session, dim via enforcement_enabled=false); 6 existing AD-742e color-scheme vitests updated to the new green/orange/red palette. UI bundle: `index-DPxUl-OZ.js`. Zero new pip / npm deps. 0-line diff on all 5 license files. Forward markers filed: AD-733c-6-1 (daily-counter restart persistence), AD-733c-6-2 (WardRoom cap-hit notification).

- AD-742d — Pluggable VisionSupervisor strategies (closes #672). Four new `SupervisorStrategy` implementations alongside `PerceptualHashStrategy`: `MotionStrategy` (per-pixel L1 diff after 32x32 grayscale downscale; catches small motion that aHash blurs), `SceneChangeStrategy` (16-bin HSV histogram chi-square-style distance on 64x64 downscale; catches lighting shifts + new objects entering frame), `NeverDescribeStrategy` (drops every frame — cost-tight deployments, describe only on force/DM-receive paths), `AlwaysAdmitStrategy` (admits every frame — debug + test scaffolding). All four implement the BF-308 setters (`set_min_interval_seconds`, `set_novelty_threshold`, `set_baseline_max_age_seconds`) so hot-reload of cap values works uniformly; null strategies accept-and-ignore. New `STRATEGY_REGISTRY` (name -> class) + `build_strategy()` resolver with honest-degrade-to-aHash on unknown name. New `PerceptionConfig.vision_supervisor_strategy` field (default `"ahash"` — current behavior preserved bit-for-bit) + Pydantic validator restricting to the five names. New non-hot-reload FieldDescriptor (strategy swap orphans previous baseline state — restart required per GATE 1 §3). `VisionConsumer.__init__` accepts `supervisor_strategy_name` kwarg defaulting to `"ahash"`; `startup/finalize.py` threads `vision_supervisor_strategy` through. New shared `_load_pil_image` helper (separate from `_ahash_jpeg_bytes`). Tier-2 honest-degrade: every strategy allows the first frame and then throttles on decode failure; never raises. +12 pytest in `tests/test_ad742d_pluggable_supervisor.py` (registry contents, Protocol conformance, build_strategy resolution, unknown-name fallback with WARNING, motion admits/throttles/diff-detects, scene_change lighting-shift, never/always determinism, config validator rejects unknown, consumer init uses configured strategy). Real PIL JPEG fixtures over BytesIO; no MagicMock at strategy boundary (BF-287). Zero new pip / npm deps. 0-line diff on all 5 license files. Forward markers filed: AD-742d-1 (CLIP-embedding semantic strategy), AD-742d-2 (per-session strategy override).

- AD-742f — Per-agent vision working memory persistence across restart (closes #674). New `src/probos/perception/wm_store.py` ships `WorkingMemoryStore` (stdlib `sqlite3`, module-level write lock, schema with `agent_id` + `timestamp` + `attachment_ref` + `description` + `novelty_score` + `subject_identity` + `session_id` columns — NO BLOB columns per AD-731 invariant). `VisionWorkingMemory.__init__` now accepts optional `store` + `agent_id` kwargs: when both are present, the ring auto-hydrates from disk and every `append` mirrors to SQLite (best-effort, honest-degrades on lock/disk failure with WARNING). `consumer.py` factory gains `set_working_memory_store(store)` module setter + `_WM_STORE` module global; `get_or_create_working_memory` threads the store through; `reset_working_memories_for_tests` clears the handle. `startup/finalize.py` constructs `WorkingMemoryStore(data_dir / "perception_wm.db")` BEFORE `VisionConsumer` construction so observer registration sees a wired store; gated on new `PerceptionConfig.wm_persistence_enabled` (default True, hot-reload — toggles WRITE path only). New `FieldDescriptor("perception.wm_persistence_enabled")` in Perception settings section. Tier-2 honest-degrade: unwritable path → `store.available=False` → WM operates as in-memory ring (legacy behavior bit-for-bit preserved when persistence disabled or store unavailable). AD-541b chroma episodic_memory unchanged (long-term anchored episodes still go through that path; WM persistence is a separate per-agent narrower store). +10 pytest in `tests/test_ad742f_wm_persistence.py` (schema init, append+load roundtrip, capacity cap on load, eviction-beyond-capacity through WM, per-agent isolation on clear, honest-degrade on unwritable path, in-memory-only when store=None creates no .db, AD-731 invariant source-check NO BLOB columns, factory threads store, factory reset clears handle). Zero new pip / npm deps. 0-line diff on all 5 license files. Forward marker filed: AD-742f-1 (federation cross-host WM sync), AD-742f-2 (TTL-based pruning).

**Wave 174 in flight (2026-05-18):**
- AD-742e — Vision LLM call budget telemetry in HXI status badge (closes #673). `VisionConsumer` gains in-memory per-tier counters (`_budget_calls_session` + `_budget_calls_today` keyed by `vision` / `vision_fast`) and a `_budget_last_call_at` monotonic timestamp. New `_record_vision_call(tier, session_id)` helper resets the session counter on session change and the today counter on UTC date rollover; it's invoked from `_describe` immediately after a successful `llm_client.complete` call so the recorded tier is the resolved AD-742a `describe_tier`. New public `get_budget_snapshot()` method returns `{session_id, calls_this_session, calls_today, total_session, total_today, session_ceiling_estimate, next_allowed_in_seconds}`. Session ceiling heuristic = `proactive_max_emissions * 40` (forward marker AD-742e-1 for SQLite persistence + tuned ceiling). `_process` sets `_budget_current_session_id` when session changes so the right bucket fires. New `GET /api/perception/budget` under `require_crew_scope` returns the snapshot + `consumer_wired` flag. v1 in-memory only (forward marker AD-742e-1 for SQLite persistence). New `<VisionBudgetBadge />` component (`ui/src/components/perception/VisionBudgetBadge.tsx`) polls the endpoint every 5s, renders nothing when `total_session == 0` (HXI Principle #5 progressive disclosure), shows `Vis N/M` with amber (<80%), dim-red (80-100%), bright-red (>100%) color scale; hover-title shows per-tier breakdown + today + next-allowed. Mounted in `DecisionSurface.tsx` immediately after the Routing Entropy span. HXI Principle #3 honored — no emoji, color + text only. +8 pytest (initial state, increment, per-tier tracking, session reset, date rollover, snapshot shape, API endpoint wired/unwired). +6 vitest (hidden at zero, renders at N/M, amber/dim-red/bright-red boundaries, hover-title content). UI bundle: `index-CfjDvOSd.js`. UI gate per BF-279/AD-738b: `npx vitest run` 757/758 pass; `npm run build` exit 0. Zero new pip deps. Zero new npm deps. Forward marker filed: AD-742e-1 (SQLite persistence for vision call log + daily roll-up across restart). Tracker: 14138 → 14145 (+7 net; one documented flake `test_auto_commit_after_debounce` per dispatch baseline).

- AD-742b — Face-embedding identity recognition (closes #670). Replaces AD-733b's per-session LLM identity prompt with a local face-embedding model. New `src/probos/perception/identity.py` ships `IdentityResolver` (MTCNN face detection + InceptionResnetV1 512-d embedding via facenet-pytorch MIT) + `_cosine_distance` helper. Lazy-loaded models (one process-wide instance); thread offloaded via `loop.run_in_executor` since MTCNN/Resnet are sync + CPU-bound. Two new `PerceptionConfig` fields (`identity_match_threshold=0.6`, `identity_resolver_enabled=True`); `captain_avatar_ref` retained but deprecated. `VisionConsumer._resolve_subject_identity` first tries the resolver path (`is_enrolled` gated); falls back to the AD-733b LLM-prompt path only when `identity_resolver_enabled=False` AND `captain_avatar_ref` is set — preserves the legacy code path for CI/test envs without facenet-pytorch. `startup/finalize.py` constructs the resolver next to `VisionConsumer` when perception is enabled; on import failure, logs WARNING and falls back to legacy path. Three new API endpoints under `require_crew_scope`: `POST /api/perception/identity/enroll` (multipart, 10 MB cap, 422 on no-face-detected, embedding written via atomic .tmp→rename), `DELETE /api/perception/identity`, `GET /api/perception/identity` (returns `{enrolled, resolver_wired, model_id}` — no embedding leaked). **Privacy invariants**: reference photo bytes discarded after enrollment (the 2 KB embedding is the only persistent artifact); `data/captain_identity.json` gitignored (explicit + `data/*` wildcard); embedding stored locally — NOT in AttachmentStore (AD-731 invariant preserved: that's about RPC bus payloads, not local lifecycle artifacts); operator opt-out via DELETE endpoint OR `identity_resolver_enabled=False`. One new pip dep: `facenet-pytorch>=2.5` (MIT verified via PEP 639 classifier; pretrained VGGFace2 weights Apache-2.0). `THIRD_PARTY_LICENSES.md` stamped. +19 pytest in `tests/test_ad742b_face_embedding_identity.py` (enrollment, persistence, revoke, no-face raises, resolve captain/other/unknown, threshold operator-tunable, config defaults, cosine_distance helper edge cases, privacy invariant — reference bytes never leak to disk, VisionConsumer integration — resolver path used when enrolled, fallback to unknown when not enrolled). Tracker: 14119 → 14138.

- AD-742a — `vision_fast` LLM tier (closes #669). Splits the single AD-732 `vision` tier into `vision`/`vision_fast` so per-frame supervisor describes can use a small VLM (default: `moondream`, Apache 2.0, ~400-800ms via Ollama) while narrative + identity stay on the 27B `vision` tier. Five new `CognitiveConfig` fields (`llm_base_url_vision_fast`, `llm_api_key_vision_fast`, `llm_model_vision_fast`, `llm_timeout_vision_fast`, `llm_api_format_vision_fast`); all default None (honest-degrade). Eight-guard catalog audit (15 sites): `_LLM_TIERS` extended to 7 tiers; `_TIER_ORDER` unchanged (BF-269 — vision tiers MUST NOT fall back to text); `tier_config()` 8 dict-maps all carry `vision_fast`; `is_vision_tier_configured` branch added; ModelRouter bypass extended (BF-273); fallback chain `vision_fast -> vision` ONLY (BF-269); health probe short-circuits when `llm_model_vision_fast` unset; cache shape-based bypass (BF-272 — unchanged); 5 hardcoded `("fast","standard","deep","vision")` tuples in `__main__.py` (3) + `experience/commands/commands_llm.py` (2) refactored to import `_LLM_TIERS` per AD-732 lesson #1. `_cmd_doctor` + `_create_llm_client` now skip unconfigured optional tiers (vision_fast/compute_use/image_gen) instead of flagging them unreachable. New `PerceptionConfig.vision_fast_tier` field (default `"vision_fast"`); `VisionConsumer.__init__` takes `vision_fast_tier`; `_describe` routes to `vision_fast` when `is_vision_tier_configured(cfg, "vision_fast")`, else falls back to `vision_tier`. `startup/finalize.py` wires through new field. Settings registry gains 3 FieldDescriptors. `config/system.yaml` commented-out opt-in block. +13 pytest in `tests/test_ad742a_vision_fast_tier.py` (tier registered, TIER_ORDER unchanged, config fields, tier_config maps, is_vision_tier_configured branch, ModelRouter bypass, fallback chain via source-scan, health probe short-circuit, PerceptionConfig field, VisionConsumer routes to fast when configured, VisionConsumer falls back when fast unconfigured, source-scan regression for tier-tuple duplication). 0 new pip deps. 0 new npm deps. `THIRD_PARTY_LICENSES.md` adds moondream attribution (Apache 2.0, operator-pullable via `ollama pull moondream`). Forward markers filed: AD-742a-1 (A/B moondream vs qwen2-vl:2b). Tracker: 14111 → 14119.

**Wave 172 in flight (2026-05-18):**
- AD-733c-3 — Wake-word → engage endpoint + UI fire-and-forget (part of #675). New `POST /api/perception/engage` accepts `{agent?, phrase?, source: "wake_word"|"manual"}` and calls `controller.note_wake_word()`. The controller's `note_wake_word` signature is rewritten from `None` to `tuple[bool, str]` returning `(transitioned, reason)` where `reason` is `"transitioned" | "refreshed" | "cooldown" | "blocked"`. New `WAKE_WORD_COOLDOWN_S = 5.0` floor (separate from the 1s `PROGRAMMATIC_COOLDOWN_S`) prevents UI flap when the detector fires multiple times during the same utterance. New `_last_wake_word_at` tracker on the controller (initialized to 0 so the first wake event always succeeds). HXI: `IntentSurface.tsx:onWake` callback now fires a fire-and-forget `void fetch('/api/perception/engage', ...)` BEFORE the chat submit, ONLY for agent-surface wakes (system-surface "computer ..." does not imply engagement with any agent's perception). Failure surfaces as `console.warn` only — the chat path proceeds regardless. Endpoint behind `require_crew_scope`. Body fields are informational (logged + journalled); the controller only needs the side effect. +4 pytest in `tests/test_ad733c3_engage_endpoint.py` (ambient→engaged + transitioned=True, 5s cooldown returns transitioned=False+reason=cooldown, already-engaged refresh returns reason=refreshed, invalid source returns 400). +3 vitest in `ui/src/__tests__/IntentSurface.engage.test.tsx` (agent wake fires engage once, system wake does NOT fire, fetch failure swallowed). UI bundle: `index-C8-ybnnU.js`. Zero new pip deps, zero new npm deps. Tracker: 14099 → 14103.

- AD-733c-2 — PerceptionModeController (part of #675). New `src/probos/perception/mode_controller.py` ships `Mode` Enum (DORMANT/AMBIENT/ENGAGED), `ModePreset` frozen dataclass, `PRESETS` public mapping, `Transition` ring-buffer row, and `PerceptionModeController` class. `transition_to(mode, *, trigger="manual")` pushes the preset bundle to `consumer._supervisor._strategy` via BF-308 setters (`set_min_interval_seconds`, `set_novelty_threshold`, `set_baseline_max_age_seconds`); idempotent (same-mode = no-op + DEBUG log); 1s programmatic cooldown (`PROGRAMMATIC_COOLDOWN_S`) bypassed by manual trigger. Three engagement hooks: `note_dm_activity()` step-wise ramp (DORMANT→AMBIENT→ENGAGED, one step per call; ENGAGED only refreshes `last_dm_activity_at`); `note_high_novelty_event()` (AMBIENT→ENGAGED only — observer wired); `note_wake_word()` stub (rewritten by AD-733c-3). Timestamps use `time.time()` throughout (Captain Required override — operator-facing `/api/perception/mode` GET would surface meaningless monotonic values otherwise). `start()`/`stop()` lifecycle holds an `asyncio.create_task` reference, catches CancelledError, re-raises per async-discipline rule; `_run()` body is a 30s tick stub (AD-733c-4 fills with idle drop-back). `routers/perception.py` gains `GET /api/perception/mode` (returns `{mode, since, last_dm_activity, presets, transitions}`) and `POST /api/perception/mode {mode}` (manual override; trigger="manual" bypasses cooldown); both behind `require_crew_scope`. `routers/agents.py:agent_chat` extends the AD-733c-1 force-describe block with a `controller.note_dm_activity()` call so the conversational tempo drives mode transitions. `perception/observer.py` calls `controller.note_high_novelty_event()` after a successful proactive DM dispatch — AMBIENT→ENGAGED on the "you sat down" trigger. `startup/finalize.py` wires the controller next to VisionConsumer with `initial_mode=AMBIENT` + `await controller.start()`; `startup/shutdown.py` mirrors the `recording_reaper` pattern with `await runtime.perception_mode_controller.stop()`. New `ui/src/store/usePerceptionModeStore.ts` Zustand slice exports `PerceptionMode` / `PerceptionTransition` / `PerceptionPreset` types + `refresh()` + `setMode(mode)` actions. `CameraLiveIndicator.tsx` adds a stroke-bordered Mode badge after the CAMERA LIVE span (amber DORMANT/AMBIENT/ENGAGED text, HXI Principle #3 — no emoji). `PerceptionLivePanel.tsx` gets a MODE section above vision-config warnings with three text buttons + last-3-transitions list. AD-731 invariant extended: `test_ad731_invariant_no_inline_base64_in_perception_modules` now scans `mode_controller.py`. AD-541b anchored episodes still written on every force-describe path (no change to `_anchor_episode`). +13 pytest in `tests/test_ad733c2_mode_controller.py` (initial state, preset push via real PerceptualHashStrategy, same-mode no-op, programmatic cooldown blocks, manual override bypasses, three note_* hooks, watchdog start/stop clean, GET endpoint, POST endpoint, AD-731 invariant scan). +6 vitest in `CameraLiveIndicator.modeBadge.test.tsx` (3) + `PerceptionLivePanel.modeSection.test.tsx` (3). UI bundle: `index-CUG-925p.js`. Zero new pip deps, zero new npm deps. Tracker: 14086 → 14099.

- AD-733c-1 — DM-receive force describe of latest captured frame (part of #675). `VisionConsumer._handle` now records `(sha, captured_at)` per-session AND globally BEFORE the supervisor gate so dropped/throttled frames still update the cache. New `VisionConsumer.force_describe_current_frame(session_id=None, *, timeout_s=4.0) -> str | None` looks up the cached SHA, constructs a synthetic `IntentMessage` with `force=True` (BF-302 bypasses supervisor), runs `_process` under `asyncio.wait_for(timeout_s)`, and returns the description from the just-written WM entry. Tier-2 honest-degrade: timeout / no cached frame / LLM error → returns None, WARNING log, DM still proceeds. New `_reset_latest_frame_cache_for_tests(consumer)` test helper. `routers/agents.py:agent_chat` calls `force_describe_current_frame(timeout_s=4.0)` immediately BEFORE the AD-733a `render_for_prompt()` scene-block injection, gated on new `PerceptionConfig.dm_force_describe_enabled` (default True). BF-304 single-flight lock keeps cost bounded when DMs arrive rapidly. AD-731 invariant preserved (SHA refs only, no inline bytes). AD-541b anchored episode still written by the existing `_process()` path (force=True does not bypass `_anchor_episode`). +6 pytest in `tests/test_ad733c1_force_describe.py` (cache populated before supervisor admission, force-describe per-session lookup, global fallback, empty-cache None, timeout honest-degrade, agent_chat DM-hook calls force-describe with timeout=4.0). Zero new pip deps, zero new npm deps. Tracker: 14080 → 14086.

**Wave 171 in flight (2026-05-17):**
- BF-298 — Settings parent/child toggle UX + perception status badge. UI-only. `ui/src/components/settings/SettingsMain.tsx` `FieldRow` gains optional `disabled` + `disabledReason` props threaded through all five input branches (bool/enum/int/float/text) with `aria-disabled`, `title` tooltip, `opacity:0.4`, `cursor:not-allowed`. Section render loop computes `disabled` for the `perception` section by reading the master `perception.enabled` from draft (falling back to snapshot) — every non-master field in the section becomes inert when master is OFF; the master toggle itself stays clickable so the operator can flip it on. `ui/src/components/settings/sections/PerceptionLivePanel.tsx` adds a `perception-status-badge` row above the existing header that reads `subsystem: OFF` (dim) / `subsystem: ON · 0 modalities active` (engineering orange) / `subsystem: ON · camera live` (amber) based on snapshot + live `useCameraStore.active`. HXI Design Principle #3 honored: no emoji anywhere — uppercase mono text + stroke-only colors. +6 vitest in `ui/src/components/settings/__tests__/PerceptionParentChild.test.tsx` (master OFF aria-disabled child, master OFF disabled number input, master ON master-stays-clickable, master ON all-children-enabled, draft-flip-re-enables-without-APPLY, 3-state status-badge). UI bundle: `index-BTChzXer.js`. Zero new pip deps, zero new npm deps. Tracker: vitest 726 → 732.

- AD-733b — ProactiveVisionObserver + identity hook (closes #666). New `src/probos/perception/observer.py` ships `ProactiveVisionObserver` + `ProactiveBudget`. Two triggers in v1: `scene_introduction` (fires once per camera session on the first WM observation, bypassing the novelty threshold) and `high_novelty` (novelty >= 0.50, bounded by `max_emissions_per_session=3` AND `min_dwell_seconds=30s`). The observer does NOT compose user-facing reply text — it sends a `[SYSTEM-INITIATED: ...]` synthesized user-turn to the agent so the agent's own LLM composes the visible message using its voice profile. **Identity hook** in `VisionConsumer._process` calls the vision LLM with the Captain reference avatar SHA + the live frame on the first frame per session and parses the one-word response (`captain` | `other` | `unknown`), populating `VisionObservation.subject_identity`. One extra vision LLM call per session — Captain authorized the cost. Five new `PerceptionConfig` fields (`captain_avatar_ref` empty default, `proactive_observer_enabled`, `proactive_max_emissions`, `proactive_dwell_seconds`, `proactive_novelty_threshold`). `startup/finalize.py` wires the observer onto the AD-733a consumer when both `perception.enabled` and `proactive_observer_enabled` are True. AD-731 invariant preserved (identity hook reads BOTH images through `AttachmentStore.read(sha)`; bus message stays ref-only). +10 pytest in `tests/test_ad733b_proactive_observer.py` (scene-intro fires once, low-novelty blocks, dwell blocks, budget exhaustion blocks, session reset clears state, identity resolves captain when configured, identity unknown when no reference, proactive_observer_enabled=False propagates as config-level guard). Forward markers cited: AD-742a (#669), AD-742b (#670), AD-742c (#671), AD-742d (#672), AD-742e (#673), AD-742f (#674). Tracker: 14032 → 14042.

- AD-733a — Vision consumer + supervisor + working memory + DM context injection (closes #665). Three new modules under `src/probos/perception/`: `supervisor.py` ships pluggable `SupervisorStrategy` Protocol + default `PerceptualHashStrategy` (temporal throttle floor + 64-bit aHash diff, Pillow-only, pure Python); `working_memory.py` per-agent `VisionWorkingMemory` ring buffer (default capacity 8) whose `render_for_prompt` emits a `--- Current Visual Context ---` block (BF-294 confabulation guard: empty buffer renders an explicit "no current visual data" sentinel with "Do NOT describe what you cannot see"); `consumer.py` runtime-owned `VisionConsumer` subscribes to `vision_observation`, runs the supervisor gate, calls the AD-732 vision tier via `build_multimodal_messages` (BF-268 OpenAI-shape resolver), writes to every registered observer's WM, and anchors an `Episode` at `importance=6` with `AnchorFrame(channel="perception", trigger_type="vision_described")`. Five new `PerceptionConfig` fields (`vision_consumer_enabled` / `vision_min_interval_seconds` / `vision_novelty_threshold` / `working_memory_capacity` / `vision_tier`). `startup/finalize.py` wiring iterates `runtime.registry.all()` per BF-287 (NEVER `_agents`); only crew agents with `callsign_registry.get_profile(...).vision_capable=True` are registered as observers; `runtime.vision_consumer` exposed as the public attribute. `routers/agents.py` prepends `render_for_prompt()` ahead of the bus send (mirroring AD-725's `targeted_recall_block` pattern), gated on `runtime.config.perception.enabled` so disabling the subsystem cleanly removes the block. **NeuralCompanion three-tier pattern (Source/Supervisor/Consumer) absorbed under MIT — architecture only, no code copied.** AD-731 invariant rigorously enforced: frames stay as SHA refs end-to-end; new source-scan test `test_ad731_invariant_no_inline_base64_in_perception_modules` asserts ZERO `b64encode` / `base64.b64` / `blob_b64` in any new perception module. **Eight-guard catalog NOT retriggered** — reuses the single AD-732 `vision` tier; AD-742a (#669) splits into `vision_fast`/`vision_deep` later. +19 pytest in `tests/test_ad733a_vision_consumer.py` (6 supervisor, 4 WM, 5 consumer, 3 integration, 1 AD-731 source scan). +1 pytest in `tests/test_wave171_acceptance.py` — Captain's acceptance test (`test_captain_holds_glass_ezri_describes_it`) drives the full end-to-end path: frame → AttachmentStore → bus → supervisor → vision LLM → working memory → DM scene-block injection → importance=6 episode. Zero new pip deps (Pillow already resident), zero new npm deps, zero license diff. Forward markers referenced in DECISIONS.md: AD-742a (#669), AD-742d (#672), AD-742f (#674). Tracker: 14012 → 14032.

**Wave 170 in flight (2026-05-17):**
- AD-733 — Camera streaming v1 — frame ingestion + `vision_observation` IntentDescriptor + Perception Settings section (closes #641 umbrella; AD-733a/-b/-1/-2 filed as new issues #665/#666/#667/#668). New `PerceptionConfig` + nested `CameraStreamConfig` blocks on `SystemConfig` (default-OFF on both `enabled` and `camera.enabled` — privacy-first posture). New `src/probos/perception/__init__.py` registers `VISION_OBSERVATION_DESCRIPTOR` (non-destructive sensor intent, `requires_consensus=False`, `tier="domain"`) and calls `insert_section(...)` to add the Perception section to the AD-741 registry (Perception & Voice domain). New `POST /api/perception/camera/frame` multipart endpoint reuses `_validate_and_store_attachment` (AD-720/AD-731 SHA-256 keyed bytes via `AttachmentStore.write`), enforces per-session token-bucket rate limit (`camera_max_fps_server` default 4, 429 + `Retry-After`), `frame_max_size_bytes` cap (default 512 KB, 413), 12-byte minimum (400), broadcasts `IntentMessage(intent="vision_observation", params={"attachment_ref": <sha>, "mime": "image/jpeg", "captured_at": <unix>, "source": "camera", "session_id": <opaque>})` on the bus. AD-731 invariant rigorously enforced: source-scan test `test_router_source_has_no_inline_blob_patterns` asserts `routers/perception.py` contains NO `b64encode` / `base64.b64` / `b64decode` / `blob_b64`. AD-541b anchored episode written on first frame per session per boot (importance=8, `AnchorFrame(channel="perception", trigger_type="camera_stream_began")`), Tier-2 honest-degrade on episodic store failure. v1 has NO LLM consumer for `vision_observation` (intentional — proves wire shape without committing to inference cadence); AD-733a forward marker adds the 1-Hz tick batcher + consumer. New `ui/src/hooks/useCameraStream.ts`: `startCameraStream` runs `navigator.mediaDevices.getUserMedia` + offscreen `<canvas>` + `setInterval` capture → `toBlob('image/jpeg', q=0.6)` → multipart POST; downsamples longest-edge to `frame_max_dimension` (512 default); `stopCameraStream` calls `track.stop()` on every track; 503 response stops stream + surfaces error; 429/413 dropped silently (next interval tick retries). New persistent `CameraLiveIndicator` rendered top-right of every HXI view when `useCameraStore.active === true` (inline SVG red dot + animated pulse + REVOKE button per HXI #3 — no emoji). `App.tsx` adds `beforeunload` handler that calls `stopCameraStream` unconditionally. New `PerceptionLivePanel` rendered inside the AD-741 SettingsMain when the Perception section is selected: live START/STOP button (toggles `startCameraStream` directly — camera is live, not draft), honest-degrade banner when `cognitive.llm_base_url_vision` is empty, HTTPS warning on non-localhost non-HTTPS, frames-sent counter. +13 pytest in `tests/test_ad733_perception_config.py` (3), `tests/test_ad733_intent_descriptor.py` (2), `tests/test_ad733_frame_endpoint.py` (8 — including AD-731 source-scan + AD-541b anchor assertion). +5 vitest in `ui/src/components/perception/__tests__/CameraLiveIndicator.test.tsx` (2) + `ui/src/hooks/__tests__/useCameraStream.test.ts` (3). Real `SystemConfig()` + real `FilesystemAttachmentStore(tmp_path)` per BF-287; `_FakeMediaStream` + `_FakeTrack` dataclass stubs for getUserMedia per BF-286. 0 new pip deps. 0 new npm deps — all browser-native APIs (`getUserMedia`, `<canvas>`, `Blob`). License-clean. Forward markers filed as new GitHub issues per AD-722c-3 TECHNICAL trigger format: AD-733a (#665 LLM consumer + tick batcher), AD-733b (#666 ObserverAgent), AD-733-1 (#667 AttachmentStore retention reaper for camera frames), AD-733-2 (#668 multi-camera + screen capture). UI build: `index-Cq1Q7Rbf.js` → `index-bhtBkOzv.js`. Tracker: 13999 → 14012.

- AD-741 — Settings / Control Panel HXI shell (new AD; no GH issue). New `/api/config` endpoints (GET snapshot + section registry + CSRF token; GET /yaml scrubbed; POST validate + atomic YAML write). New `src/probos/settings/section_registry.py` single-source-of-truth registry shipping 10 wired sections across 4 domains (System, LLM Tiers, Memory, Voice, Avatars, Ward Room, Federation, Channels, Cloud Pickers, Tools) — Perception slot reserved for AD-733. Secret-field rule (regex `(?i)(secret|token|password|api_key|private_key)`) enforced at three layers (GET redaction with `secret_present` map, YAML scrub to `"<redacted>"`, POST rejection with 400 `secret_field_readonly`). `runtime.config_path` attribute added (set during `_load_config_with_fallback` in `__main__.py`); 503 `config_path_unavailable` when None. Single-consume CSRF token (5-min TTL) issued on GET, required on POST. Hot-reload v1 = uniformly restart-required; forward marker AD-741-1 for per-field hot paths. New HXI overlay `SettingsPanel` (sidebar grouped by canonical domain order + draft buffer + APPLY-↵ flow + read-only VIEW YAML modal + "Advanced configuration" affordance) wired into TopNav. New `useSettingsStore` Zustand slice keeps the giant `useStore.ts` untouched. Pre-flight grep correction applied: per-tier LLM fields live on `cognitive.*` not `system.*`; channels phantom fields `webhook_url`/`url` dropped from v1 (not in real `DiscordConfig`/`SlackConfig`/`WebhookConfig` — wired the real `command_prefix`/`mention_required`/`default_thread_ts` instead, plus secret-flagged `token`/`bot_token`/`signing_secret`/`shared_secret`). HXI Design Principle #3: every glyph is inline stroke SVG (`strokeWidth: 1.5`, `strokeLinecap: round`), no emoji; amber `#f0b060` active / dim `#666680` inactive. +19 pytest in `tests/test_ad741_config_api.py` (9), `tests/test_ad741_section_registry.py` (4), `tests/test_ad741_secret_redaction.py` (4), `tests/test_ad741_integration.py` (2). +10 vitest in `ui/src/components/settings/__tests__/SettingsPanel.test.tsx` (7) + `SettingsSidebar.test.tsx` (3). 0 new pip deps (`pyyaml` resident). 0 new npm deps. Forward markers AD-741-1 (per-field hot reload) / -2 (MCP server add/remove) / -3 (YAML diff preview) / -4 (restart-in-place) / -5 (multi-Captain auth + audit) / -6 (raw YAML editor with Pydantic validate-on-save) / -7 (per-agent deep-link to Crew Roster) filed. Tracker: 13980 → 13999.


- AD-720c — Cloud file picker (OAuth-bound source, Google Drive v1) (closes #551). New `src/probos/cloud_pickers/` module exports `OAuthTokenBundle` + `CsrfStateStore` (tokens.py), `CloudPickerProvider` Protocol + `ProviderError`/`ReauthorizationRequired` (provider.py), `GoogleDriveProvider` v1 working backend, and `OneDriveProvider`/`DropboxProvider` stubs (forward markers AD-720c-1/-2). New `src/probos/routers/cloud_pickers.py` exposes 4 endpoints under `/api/cloud-pickers/{provider}` (POST start, GET callback, GET files, POST attach). OAuth tokens persisted exclusively via AD-706f `CredentialVault` as `OAuthTokenBundle.model_dump_json()` under ref shape `cloud_provider:{provider}:{captain_id}` with `CredentialScope()` empty-frozenset (captain-only per credentials.py:40-58); the AD-706f invariant is preserved (NO new credential store). Google consent URL includes `access_type=offline&prompt=consent` so a refresh_token is issued on every authorization. Refresh-on-401: `_with_refresh_retry` helper attempts a single `grant_type=refresh_token` exchange on 401, preserves the original refresh_token when the response omits one (Google rotates rarely), retries the original request exactly once, and surfaces `ReauthorizationRequired` (→ 401 `reauthorization_required` + vault entry deleted) on refresh failure or second 401. PKCE skipped per OAuth 2.1 §1.5 — ProbOS is a confidential client with server-stored `client_secret`; in-memory CSRF state guard (32-byte url-safe token, single-consume, default 5-min TTL) backs the callback. AD-731 invariant preserved: file bytes flow through the shared `_validate_and_store_attachment` → `AttachmentStore.write(sha, blob, mime)` chain (the same defense-in-depth path used by `POST /api/chat/attachments` and the multipart endpoint); the HTTP response carries only `{attachment_id, mime, size_bytes, filename}` — never raw bytes. Honest-degrade matrix: 503 `feature_disabled` (master switch off), 503 `credential_vault_unavailable` (vault precondition unmet), 503 `provider_disabled` / `provider_not_configured`, 401 `oauth_not_authorized` / `reauthorization_required`, 413 `file_too_large`, 403 `invalid_state_token`. New `CloudPickersConfig` + per-provider `CloudPickerProviderConfig` on `ProbOSConfig` (default-OFF master switch + per-provider default-OFF + BYOC operator-supplied client_id/client_secret/redirect_uri). New `ui/src/components/CloudPicker.tsx` modal: provider selector (only enabled providers render), authorize button opens auth_url in popup + listens for `oauth_complete` postMessage, file list with search + pagination, file click POSTs `/attach` and invokes `onAttached({attachment_id,...})`. HXI Design Principle #3 honored — inline stroke SVG icons, no emoji. +22 pytest in `tests/test_ad720c_provider_protocol.py` (3) + `tests/test_ad720c_google_drive.py` (7, httpx.MockTransport injected via `http_client_factory`) + `tests/test_ad720c_state_store.py` (4, TTL + single-consume + provider-mismatch + purge) + `tests/test_ad720c_endpoints.py` (8, real `SystemConfig()` + real `EncryptedFileCredentialVault` per BF-287; covers feature/vault/provider 503 matrix, /start happy path, /callback CSRF rejection + bundle persistence roundtrip, /files 401 honest-degrade, /attach AD-731 invariant assertion). +7 vitest in `ui/src/components/__tests__/CloudPicker.test.tsx` (provider filter, modal open/close, authorize popup + postMessage flow, attach happy path, 503 banner, 401 reauth banner, close button). UI build: `index-DqmvgWw2.js`. Zero new pip deps, zero new npm deps — `httpx` already resident from Wave 162, `cryptography` already resident from Wave 166; OAuth state uses stdlib `secrets` + `urllib.parse`. Forward markers filed with TECHNICAL triggers per AD-722c-3: AD-720c-1 OneDrive (trigger = operator demand after Google Drive exercised end-to-end ≥1 wave), AD-720c-2 Dropbox (same trigger). Tracker: 13923 → 13951 (+22 from this AD; other +6 came in with the Wave 168 cluster-1 commits at origin/main).

**Wave 167 in flight (2026-05-17):**
- AD-721i-2 — VRoid Studio CLI alternative backend evaluation (closes #543; **REJECTED**). Research-only deliverable; zero code, zero deps, zero tests. New `docs/research/vroid-cli-evaluation.md` records the disposition: VRoid Studio is GUI-only with no documented headless / CLI mode, distributed under a Pixiv-controlled proprietary EULA (not OSI-recognized), Windows + macOS only (no Linux), and the GUI export is not byte-deterministic. Any one of those three constraints (no CLI, proprietary, Linux-incompatible) would be sufficient to reject as the OSS-default renderer backend. Together they make VRoid a non-starter. The existing AD-721i Blender + saturday06 VRM-Addon backend stays as v1. Operator-elected VRoid output remains viable as a non-default path via AD-721h upload UI or AD-721g `_baselines/` directory — both consume operator-installed bytes and impose no license claim on the produced VRMs. No forward marker filed; re-evaluation only triggers if Pixiv publishes a CLI or open-source release.
- AD-720b — Chat tool attach: in-chat capability grants for BrowserTool / MCP (closes #550). Scope clarification per Architect pre-flight: Wave 167 dispatch brief said "attach a tool output (browser session screenshot, MCP resource)" but issue #550 body says "attach AD-706 BrowserTool / AD-449 MCP tools to a chat surface as scoped capability grants." Built per the issue body — capability grants — because the screenshot-attach path already works today (BrowserTool writes screenshots to AttachmentStore via `tools/browser/compute_use.py:174-176` and they ride existing `attachment_ids`). New `POST /api/chat/tool-grant` endpoint reuses `ToolPermissionStore.issue_grant` (AD-423a/b/c) so a grant issued via chat is indistinguishable on disk from one issued via the `/tool-access grant` shell slash-command — both have `issued_by="captain"`. Defense-in-depth chain: agent existence (404), permission enum validation (422 with valid list), tool registry check via `tool_registry.get()` (404 `tool_not_found`; pass-1 nit corrected: real API is `.get()` returning None, NOT `.has()`), permission store availability (503). `ToolPermission` imported from `probos.tools.protocol` (canonical source). Source-scan regression test asserts the canonical import. New `ChatToolGrantRequest` Pydantic model with `duration_hours ≥ 0 ≤ 720` and `reason ≤ 500 chars` validators. New string event `tool_grant_issued` with `source: "chat"`. Audit-failure honest-degrade: event-emit exception logged at WARNING but grant still returned (Tier-2). HXI: `IntentSurface.tsx` handleSubmit recognizes `/grant <agent_id> <tool_id> <permission> [hours]` BEFORE the normal "send DM" path; on match, POSTs to the endpoint and renders the response as a system-styled inline message ("Granted BrowserTool read to e1 (expires in 2h)"); the slash-command itself is NOT sent as a chat message; 422/error surfaces inline + preserves the typed text in the composer so the Captain can correct. +11 pytest in `tests/test_ad720b_chat_tool_grant.py` (happy + expiry, happy + null expiry, agent missing 404, invalid permission 422 with valid list, tool not in registry 404, registry-absent passthrough, store missing 503, duration too long 422, reason too long 422, event-emit failure does not block, source-scan asserts canonical import). +4 vitest in `ui/src/__tests__/IntentSurface.toolGrant.test.tsx` (POSTs with duration_hours=2, POSTs with duration_hours=null, 422 preserves typed text, success renders system-styled message). UI build: `index-BTcSysUH.js` → `index-DzUHsZVI.js`. Real `ToolPermissionStore()` in-memory + real `ToolRegistry`-shape stub + real `AgentRegistry`-shape stub per BF-287. Zero new deps. v1 does NOT persist the system-styled inline message to ward-room threads — that's a forward marker. Tracker: 13900 → 13911.
- AD-721h — Browser-based VRM upload UI (closes #535). New multipart endpoint `POST /api/agent/{agent_id}/appearance/vrm` lets the Captain drag a custom `.vrm` into the HXI. Defense-in-depth: glTF binary magic-byte check (`blob[:4] == b"glTF"`) BEFORE storage; size cap mirrors `cfg.avatars.max_vrm_size_bytes`; minimum-size gate (12 bytes); `target.resolve().relative_to(avatars_dir.resolve())` path-traversal guard even though `registry.get` already constrained the agent_id. Dual-write per AD-731 invariant: content-addressed `AttachmentStore.write(sha, blob, "model/gltf-binary")` AND atomic named cache via `os.replace(<tmp>, <avatars_dir>/<agent_id>.vrm)`. `ProfileStore.vrm_url` updated so the next read picks it up without a re-render. New string event `appearance_vrm_uploaded`. HXI: `AgentProfilePanel.tsx` adds "Upload VRM" button + hidden `<input type="file" accept=".vrm,application/octet-stream,model/gltf-binary">` next to "Design avatar"; click triggers the file picker; selection POSTs multipart; success refreshes profile (so CrewVRM re-loads); 413/415 surface as inline error reason on the button's `title` attribute (no toast spam). v1 validates magic bytes only — full VRM 1.0 schema validation deferred to forward marker. +8 pytest in `tests/test_ad721h_vrm_upload.py` (happy path with dual-write + ProfileStore update; avatars disabled 503; agent missing 404; too_large 413; too_small 400; missing-magic 415 with disk-state assertion that nothing landed on failure; AttachmentStore-vs-named-cache byte parity; last-write-wins overwrite with no leaked .tmp). +4 vitest in `ui/src/__tests__/AgentProfilePanel.uploadVRM.test.tsx` (button-clicks-input, multipart POST happy path, 413 inline error, 415 inline error). UI build: `index-1THkGO2n.js` → `index-BTcSysUH.js`. Real `FilesystemAttachmentStore` + real `tmp_path` avatars_dir per BF-287. Zero new deps. Tracker: 13892 → 13900.
- AD-721g — Per-tier baseline VRMs (closes #534). New `BaselineVRMManifest` Pydantic block on `AvatarsConfig` with four bare-filename slots (`ensign` / `lieutenant` / `commander` / `senior`); all default `""` so ProbOS still boots with zero config. New `src/probos/avatars/baseline_resolver.py` (pure-mapping `resolve_baseline_vrm_filename` + filesystem-checking `resolve_baseline_vrm_path`) enforces bare-filename hygiene — `/`, `\\`, `..` rejected with a logged warning; resolved targets are `.resolve()`-checked to stay under `avatars_dir`. Appearance read path in `routers/agents.py` inserts a baseline fallback step between AD-721d D8 cache synthesis and the parametric capsule: when no `vrm_url` is set, the resolver maps `Rank.from_trust(trust_score)` to the manifest filename and synthesises `_baselines/<filename>` on the response. License-clean by construction: NO avatar bytes ship in the repo (AD-721i-1 CC0/MIT/Apache/BSD/CC-BY whitelist; operators install their own files under `<avatars_dir>/_baselines/`). v1 keys on rank only — department-aware baselines deferred. +9 pytest in `tests/test_ad721g_baseline_vrms.py` covering empty manifest, populated + file present, populated + file missing, slash rejection (with logged warning), `..` rejection, backslash rejection, ensign-trust → ensign-entry mapping, senior-trust → senior-entry mapping, and read-path integration (real `AvatarsConfig` / real `BaselineVRMManifest` per BF-287; agent_type `ad721g_test_agent` so the test doesn't collide with seed-profile vrm_urls). Zero new deps. Tracker: 13883 → 13892.
- AD-721d-3 — Visual avatar preview before DSL persistence (closes #619). New `POST /api/agent/{agent_id}/appearance/preview` endpoint renders an unpersisted `AvatarDSL` via the existing `BlenderRenderer` (AD-721i; introduces no new subprocess call site — BF-280 boundary preserved) and returns a SHA-256 `AttachmentStore` ref (AD-731 invariant: VRM bytes through `AttachmentStore.write(sha, blob, mime)`, never inlined). Endpoint does NOT persist, does NOT consume an iteration slot, does NOT touch the canonical `<avatars_dir>/<agent_id>.vrm` cache. Honest-degrades to 503 (`renderer_unavailable` / `blender_not_found`) or 502 (`render_failed`) per existing AD-721i error taxonomy; the HXI keeps the parametric capsule fallback. Reuses `_resolve_avatars_dir` for both `avatars_dir` and `dsl_drafts_dir` resolution (matches `avatar_agents.py` precedent). New `PreviewAppearanceRequest` Pydantic model. New event-type string `appearance_preview_rendered`. `model/gltf-binary` MIME added to `_MIME_TO_EXT` in `FilesystemAttachmentStore` (single source of truth for MIME→ext; VRM is glTF-binary). HXI: `CrewAvatarPopout.tsx` gains new props (`previewVrmUrl`, `onRenderPreview`, `previewInFlight`, `previewError`); new stroke-based SVG "Render preview" button (eye glyph; no emoji per HXI Design Principle #3). When `previewVrmUrl` is set, the existing `<CrewVRM>` swaps to that URL (existing absolute-URL pass-through at `CrewVRM.tsx:252`). `AgentProfilePanel.tsx` adds the three state hooks and wires `onRenderPreview` to POST the proposed DSL and stash the resulting `/api/chat/attachments/<sha>` URL; preview state cleared on approve/reject/close. +8 pytest in `tests/test_ad721d_3_avatar_preview.py` (happy path; avatars disabled; agent missing; invalid DSL 422; renderer disabled 503; BlenderNotFoundError 503; BlenderRenderError 502; real `FilesystemAttachmentStore` round-trip per BF-287). +3 vitest in `ui/src/__tests__/CrewAvatarPopout.preview.test.tsx` (click invokes callback; swap pane on previewVrmUrl; disabled + error inline during in-flight). UI build: `index-1THkGO2n.js`. Real `AvatarsConfig()` / `AttachmentsConfig()` per BF-287; renderer monkey-patched at the source module so the in-function `from probos.avatars.blender_renderer import BlenderRenderer` resolves the stub. Tracker: 13875 → 13883.


- AD-721i-1 — License-audited starter asset pack (closes #542). v1 ships **zero asset bytes** — only the audit infrastructure. New `data/avatar-assets/MANIFEST.md` audit ledger with markdown-table schema (name / source_url / license / version / sha256 / attribution / disposition); 4 RESEARCH candidates (Quaternius CC0 humanoid + hair + outfit, Khronos glTF-Sample-Assets CC0, KayKit CC0, Poly Haven CC0) and 4 REJECTED entries with documented reasons (MakeHuman AGPL-3.0 propagates copyleft; Mixamo Adobe TOS; Ready Player Me proprietary; VRoid Studio per-file metadata leak). Zero APPROVED rows — every candidate stays RESEARCH until Captain ruling flips it. New `src/probos/avatars/asset_manifest.py` exposes `AssetManifestEntry` NamedTuple + `AssetManifest.load/approved/by_disposition` parser + `validate_license` whitelist hard-coded to `{CC0, CC0-1.0, MIT, Apache-2.0, Apache 2.0, BSD, BSD-2-Clause, BSD-3-Clause, CC-BY-4.0, CC-BY}`; anything outside (GPL/AGPL/CC-BY-SA/CC-BY-NC/proprietary) returns False. Parser is lenient — skips rows with invalid disposition + logs warning, returns empty manifest on missing file (honest-degrade, never raises). New `scripts/avatar-assets-fetch.ps1` mirrors `piper-voice-fetch.ps1` shape: parses MANIFEST.md, filters to APPROVED, downloads each via `Invoke-WebRequest` to `data/avatar-assets/_<category>/<name>.<ext>`, verifies SHA-256 against the manifest (mismatch → delete + non-zero exit), writes attribution to `ATTRIBUTION.txt` (one line per asset for CC-BY compliance). v1 manifest has zero APPROVED rows so the script is a no-op with an informative "0 assets approved" message. `.gitignore` rules appended: `data/avatar-assets/_base_meshes/`, `_hair/`, `_outfits/`, `_materials/`, `ATTRIBUTION.txt` (manifest itself is tracked — it's the audit ledger). New `docs/development/avatar-assets.md` documents propose / fetch / revoke workflows and the license policy. +6 pytest in `tests/test_ad721i_1_asset_manifest.py`: v1 manifest parses; v1 has zero APPROVED rows (guard against accidental promotion); every row's disposition is valid; whitelist accepts all CC0/MIT/Apache/BSD/CC-BY shapes; whitelist rejects GPL/AGPL/CC-BY-SA/CC-BY-NC/proprietary/empty; synthetic mixed-disposition manifest filters correctly. AD-731 invariant n/a (asset bytes flow through filesystem to Blender, not bus). Full gate: 13869 → 13875 (incl. 1 known dreaming flake outside this wave per dispatch).

- AD-718e — Multi-language voice selection (closes #526). New `VoiceProfile.language` field (default `"en"`) in `src/probos/crew_profile.py`. `__post_init__` strips whitespace, normalises empty string to `"en"` BEFORE the `_LANGUAGE_RE = re.compile(r"^[a-z]{2,3}([_-][A-Za-z0-9]{2,8})?$")` validation (backward-compat — legacy on-disk profiles deserialize cleanly). `from_dict` accepts `language` when present; missing key defaults to `"en"` via the dataclass default. `to_dict` includes the field via `asdict`. `ui/src/audio/voice.ts` adds a `_resolveVoiceByLanguage(lang)` helper that splits the tag on `_`/`-` and matches against `SpeechSynthesisVoice.lang.startsWith(prefix)` (case-insensitive); the resolution ladder is now `named ?? langMatch ?? findPreferredVoice()` so the profile's preferred language family wins over the en fallback. `VoiceProfile` TS interface gains optional `language?: string`. `ProfileInfoTab.tsx` gets a new `<select data-testid="ad718e-lang-filter">` dropdown above the voice picker; distinct lang codes are extracted from the loaded piper catalog and rendered with an "All" option. The voice picker filters `availableVoices` by the selected lang code (split on `_`/`-`). `scripts/piper-voice-fetch.ps1` catalog expanded with 18 new entries across es/fr/de/it/nl/pt (sourced from `huggingface.co/rhasspy/piper-voices` — Apache-2.0/MIT; failed downloads honest-degrade in the existing per-voice try/catch). +8 pytest in `tests/test_ad718e_voice_language.py` (default `en`; empty → `en`; whitespace strip; invalid char rejection; accepts `es/fr/de/it/nl/pt/en-US/pt_BR`; `to_dict` includes `language`; `from_dict` missing-key backward-compat; CrewProfile roundtrip). +3 vitest in `ui/src/audio/__tests__/voice.language.test.ts` (es voice picked when available; falls back to en when language voice missing; en default when language undefined). +2 vitest in `ui/src/__tests__/ProfileInfoTab.langFilter.test.tsx` (filter dropdown lists distinct lang codes; selecting es filters the voice list). Real `VoiceProfile()` per BF-287; `vi.resetModules` per test to clear the cached voice. Full gate: 13861 → 13869 pytest; vitest 657 → 667. UI build green.

- AD-721b-1a — ffmpeg-backed audio format conversion for client-captured audio (closes #663). New helpers in `src/probos/avatars/rhubarb_backend.py`: `_resolve_ffmpeg_binary(configured: str) -> Path | None` mirrors `_resolve_binary_path` (Windows `.exe` fallback; empty / missing configured path returns None), and `_convert_to_wav(audio_path, ffmpeg_binary, *, timeout_seconds=30.0) -> Path | None` builds the `-y -i ... -ac 1 -ar 22050 -acodec pcm_s16le <tempfile>` arg-list, dispatches `subprocess.Popen` through `loop.run_in_executor` (BF-280), writes to `tempfile.NamedTemporaryFile(suffix='.wav', delete=False)` (BF-282 — never captures binary on stdout), unlinks the temp on TimeoutExpired / non-zero exit / empty-output, returns the temp Path on success (caller MUST `unlink(missing_ok=True)` in finally). `generate_visemes` gains keyword-only `ffmpeg_binary_path: str | None = None`; when the audio suffix is not in `_SUPPORTED_SUFFIXES` and ffmpeg is resolvable, transcodes first and rebinds `audio_path` to the temp WAV; the rhubarb invocation is extracted into a private `_run_rhubarb` helper so `generate_visemes` can wrap it in `try/finally` that unlinks the converted temp. If ffmpeg is missing OR conversion fails, the BF-292 honest-degrade contract is preserved (`generate_visemes` returns `[]`). One new `LipSyncConfig.ffmpeg_binary_path` field (`tools/ffmpeg/ffmpeg` — operator-provided binary; LGPL-2.1+ / GPL-2+ stays at the operator boundary because `/tools/` is gitignored, same pattern as rhubarb + piper). Both `routers/avatars.py` callsites (`/api/avatars/lipsync` and the TTS reuse path) updated to thread `ffmpeg_binary_path=cfg.ffmpeg_binary_path`. AD-731 invariant preserved (audio bytes flow through AttachmentStore SHA-256 refs in the existing pipeline; the temp WAV is local-only and short-lived). +9 pytest in `tests/test_ad721b_1a_ffmpeg_conversion.py`: 3 cover `_resolve_ffmpeg_binary` (missing / present / Windows .exe fallback), 3 cover `_convert_to_wav` (success creates tempfile + asserts `-i` and last-positional output path; timeout returns None + kills + unlinks; non-zero exit unlinks); 2 cover `generate_visemes` integration (ffmpeg+rhubarb chain unlinks converted temp; missing ffmpeg honest-degrades); 1 config sanity. Real `LipSyncConfig()` per BF-287; `_FakePopen` records args + emulates `communicate()` / `kill()` per BF-286. Full gate: 13852 → 13861.

- AD-706b — Browser session video recording + retention policy (closes #517). `BrowserSession.start()` now opens the Playwright `BrowserContext` with `record_video_dir=<recording_dir>/<session_id>` when `BrowserToolConfig.recording_enabled` is True (default-OFF transitional gate per Wave 10 convention #14); v1 keeps recordings on disk as Playwright's native `.webm` (no ffmpeg transcode — AD-706b-1 forward marker). `BrowserSession.stop()` emits `BROWSER_RECORDING_STOPPED` after `_context.close()` finalizes the file (with `size_bytes` summed across `*.webm` in the subdir) OR `BROWSER_RECORDING_FAILED` on close error — Tier-2 throughout, never raises. Four new EventType values (`BROWSER_RECORDING_STARTED`/`STOPPED`/`EXPIRED`/`FAILED`). Five new `BrowserToolConfig` fields: `recording_enabled` (False), `recording_dir` (`data/browser-sessions`), `recording_retention_days` (7 ge=1 le=365), `recording_reaper_interval_seconds` (3600 ge=60 le=86400), `recording_max_size_mb_per_session` (500 ge=10 le=5000). New `src/probos/tools/browser/recording_reaper.py` exposes `RecordingReaper` with `start()/stop()/reap_once()`: per-iteration sweep walks the recording root, deletes `*.webm` past `retention_days * 86400`, enforces the per-session size cap by removing oldest-first until under the bytes ceiling, removes empty session subdirs, and emits `BROWSER_RECORDING_EXPIRED` per delete with reason `retention` or `size_cap`. Async loop holds task reference per Async Discipline; CancelledError re-raised after cleanup; blocking filesystem work dispatched via `loop.run_in_executor`. `BrowserSession` ctor accepts an optional `emit_event` callable so recording lifecycle events surface without reaching across BrowserTool. Wiring split: synchronous `_wire_browser_tool` declares `runtime.recording_reaper = None`; new async `_start_recording_reaper` (called from `finalize_startup`) constructs + awaits `reaper.start()` when recording is enabled. `runtime.stop()` cleanup added in `startup/shutdown.py` (verified at `runtime.py:2227` — public async shutdown hook; `runtime.shutdown()` is NOT a method on the runtime class). New admin endpoints in `routers/browser_recordings.py`: `GET /api/browser/recordings` (list), `GET /api/browser/recordings/{session_id}/{filename}` (FileResponse), `DELETE /api/browser/recordings/{session_id}` (rmtree). All three behind `require_crew_scope`; path-traversal rejected via `.resolve()` prefix check. AD-731 invariant preserved (recordings stay on disk; AttachmentStore promotion forward-marked at AD-706b-2). +9 pytest in `tests/test_ad706b_session_recording.py` (real `BrowserToolConfig()` + handcrafted `_FakeBrowser`/`_FakeContext`/`_FakePlaywright` chain; recording dir parameterised via `tmp_path`; per BF-287). Full gate: 13843 → 13852.

- AD-706a — Captain-watch MJPEG streaming bridge (closes #516). New `src/probos/routers/browser_stream.py` exposes `GET /api/browser/sessions/{session_id}/stream` as `multipart/x-mixed-replace` MJPEG; the HXI consumer renders it in an ordinary `<img>` tag, zero new JS, zero new deps. `BrowserSession.get_streaming_url()` returns `/api/browser/sessions/{sid}/stream` when `BrowserToolConfig.streaming_enabled` is True (default-OFF transitional gate per Wave 10 convention #14). Four new `BrowserToolConfig` fields: `streaming_enabled` (False), `streaming_fps` (4, ge=1 le=15), `streaming_jpeg_quality` (60, ge=20 le=95), `streaming_max_concurrent_viewers` (4, ge=1 le=16). New public viewer-slot API on `BrowserTool` (`acquire_viewer_slot`/`release_viewer_slot`/`active_viewers`) backed by an `asyncio.Lock` — the streaming router does NOT touch `_active_viewers` directly (Demeter; no private member patching). 503 + `Retry-After: 5` when the cap is exhausted; 404 when the session is unknown. Three new EventType values (`BROWSER_STREAM_OPENED`/`BROWSER_STREAM_CLOSED`/`BROWSER_STREAM_FRAME_DROPPED`); OPENED + CLOSED are emitted around every viewer lifecycle (CancelledError re-raised after the CLOSED emit). `require_crew_scope` in `routers/auth.py` extended with a `Request` first param and a `?token=` query-param fallback so `<img src>` can carry the token; empty-string token is rejected explicitly (regression-protected). New `ui/src/components/browser/BrowserStreamPanel.tsx` renders a stroke-based SVG glyph when `streamingUrl` is null and a plain `<img>` otherwise (HXI Design Principle #3 — no emoji); appends `?token=` only when a non-empty token is provided. AD-731 invariant preserved (JPEG frames are ephemeral on-wire bytes, never stored — explicit in DECISIONS entry). +11 pytest in `tests/test_ad706a_browser_streaming.py` (real `SystemConfig()` + `BrowserToolConfig()` per BF-287; `_FakePage` raises after N frames so `TestClient` doesn't deadlock on the infinite generator) + 4 vitest in `ui/src/__tests__/BrowserStreamPanel.test.tsx`. Full gate: 13832 → 13843.

- AD-706f — Browser Tool credential vault (closes #521). New `src/probos/tools/browser/credentials.py` exports `CredentialScope` + `CredentialMetadata` frozen dataclasses, `CredentialVault` Protocol, `EncryptedFileCredentialVault` v1 backend, `_derive_kek(crew_scope_token)` helper, and `action_fill_credential` action. KEK derived from `AuthConfig.crew_scope_token` via stdlib `hashlib.scrypt` (n=2**14, r=8, p=1, dklen=32, salt=b"probos-credential-vault-v1"). Values encrypted via `cryptography.fernet.Fernet` (Apache-2.0/BSD dual — verified `License-Expression: Apache-2.0 OR BSD-3-Clause`; `cryptography>=42` added to `pyproject.toml`). One new pip dep this wave. Per-credential `CredentialScope`: `allowed_agent_ids` (empty set = Captain-only), `allowed_domains` (fnmatch against page host), `expires_at` (Unix timestamp, None = no expiry). `materialize_to_temp` writes plaintext to `tempfile.mkstemp` and returns the Path — CALLER MUST UNLINK in finally (contract documented). Five new EventType values inserted after `BROWSER_EVAL_JS_EXECUTED`: `CREDENTIAL_STORED`, `CREDENTIAL_READ`, `CREDENTIAL_READ_DENIED`, `CREDENTIAL_DELETED`, `CREDENTIAL_FILL_REQUESTED`. New nested `CredentialVaultConfig` Pydantic model under `BrowserToolConfig.credential_vault` (default-OFF transitional gate, `backend="file"` v1, `file_path="data/credential_vault.json"`, `max_credentials=100` ge=1 le=10000, `require_https_for_fill=True`). New `action_fill_credential(session, params, *, runtime, emit_event)` action: validates selector + credential_ref, honest-degrades if vault is None, blocks http:// when require_https_for_fill, checks `scope.allowed_domains` via fnmatch against the page host BEFORE decryption, calls `vault.read(ref, requesting_agent_id=agent_id)`, then `await page.fill(selector, value)`. Tier-3 always (Captain ACK every credential read). Late-bind handler registration in `actions.py` parallel to AD-706c-2's compute_use_click pattern; tool.py `dispatch_action` flow special-cases `compute_use_click` and `fill_credential` like AD-706c-1's `verify` (kwargs runtime + emit_event); `agent_id` from session `_agent_id` merged into params at dispatch boundary. Startup wiring in `startup/finalize.py:_wire_browser_tool` adds two-phase init: `runtime.credential_vault = None` declared next to BrowserTool construction; constructed only when `cfg.credential_vault.enabled` AND `cfg.auth.crew_scope_token` is non-empty (both preconditions logged with operator remediation on mismatch). Empty crew_scope_token at vault ctor raises `RuntimeError` with explicit operator remediation message. Three-tier hardening: empty allowed_agent_ids = Captain-only, expired = None, scope mismatch = None + `CREDENTIAL_READ_DENIED` event. `list_refs()` returns `CredentialMetadata` with NO value field (regression-protected via test). Atomic write via tmp+rename (mirrors AD-720d-2.1 pattern). RLock for concurrent access; async API dispatches blocking I/O through `asyncio.get_running_loop().run_in_executor`. AD-731 invariant n/a (credentials are short strings, not blobs); AD-706e `upload_file.credential_ref` hook remains forward-compatible (the vault is the consumer). +15 pytest in tests/test_ad706f_credential_vault.py: default-off, ctor rejection, KEK determinism, roundtrip, scope denies unauthorized, expiry enforced, restart persistence, materialize_to_temp + caller unlink, delete, list_refs no-values regression, action honest-degrade when vault None, https required, domain mismatch, happy path with event emission, always-tier-3 classification. Real `EncryptedFileCredentialVault` against `tmp_path` per BF-287. Full gate: 13816 → 13832 passed (20 skipped). `THIRD_PARTY_LICENSES.md` updated with cryptography entry.

- AD-706e — Browser Tool action vocabulary v2 (closes #520). Seven new action verbs in `src/probos/tools/browser/actions.py`: `drag` (tier 2, tier 3 on tier-3 hosts; supports `from_selector`/`to_selector` and index fallback), `key_combo` (tier 2; tier 3 for destructive combos Control+W/Control+Q/Alt+F4/Control+Shift+W via new `_KEY_COMBO_TIER_3_PATTERNS` frozenset), `mouse_move` (tier 1, silent observation — added to `silent` set), `mouse_button` (tier 2, validates `button` ∈ {left/right/middle} and `action` ∈ {down/up/click}), `upload_file` (always tier 3; forward-compatible `credential_ref` hook degrades to `skipped_reason="credential_vault_unavailable"` until AD-706f lands), `download` (tier 2; tier 3 for executable suffixes `.exe`/`.dll`/`.dmg`/`.msi` via new `_DOWNLOAD_TIER_3_SUFFIXES` tuple), `eval_js` (always tier 3, script length capped at 4096 chars via new `_EVAL_JS_MAX_SCRIPT_LEN` constant, result serialised via json.dumps(default=str)). Per-verb short-circuit branches in `classify_action` (vs set-membership) so AD-706f's `fill_credential` is a single new branch with no merge conflict on a literal. AD-706e is NO-OP for `compute_use_click` (owned by AD-706c-2) and `fill_credential` (owned by AD-706f). Three new EventType values (`BROWSER_FILE_UPLOAD_REQUESTED`/`BROWSER_DOWNLOAD_REQUESTED`/`BROWSER_EVAL_JS_EXECUTED`) emitted from BrowserTool layer post-dispatch (alongside existing per-action `BROWSER_ACTION_EXECUTED`). `classify_action_with_llm` unchanged — its existing `rule_tier >= 3` short-circuit handles the always-tier-3 verbs. +23 pytest in tests/test_ad706e_action_vocab_v2.py: happy + error path per verb (14) + 9 classification rules + _HANDLERS registration. Real `BrowserToolConfig()` + `_FakePage`/`_FakeMouse`/`_FakeKeyboard` dataclass stubs per BF-287. Full gate: 13794 → 13816 passed (1 known dreaming flake outside this wave per dispatch).

- AD-706c-2 — Coordinate-aware `compute_use` BrowserTool tier for DOM-less surfaces (closes #643). New `compute_use` LLM tier (fifth peer of fast/standard/deep/vision in `_LLM_TIERS`). New `_TIER_ORDER: tuple[str, ...] = ("fast", "standard", "deep")` promoted to module-level constant; vision + compute_use deliberately excluded (BF-269). ModelRouter bypass extended for compute_use (BF-273). `is_vision_tier_configured` extended to recognize `"compute_use"` tier (REUSE — no new helper, no new honest-degrade constants per BF-274 lesson). Seven new `CognitiveConfig` fields (`llm_base_url_compute_use`, `llm_api_key_compute_use`, `llm_model_compute_use`, `llm_timeout_compute_use`, `llm_api_format_compute_use`, `llm_temperature_compute_use`, `llm_top_p_compute_use`, `llm_max_tokens_compute_use`); `tier_config()` map extended. New file `src/probos/tools/browser/compute_use.py` exports `action_compute_use_click(session, params, *, runtime, emit_event)`: validates intent, captures screenshot via `page.screenshot()`, stores via `AttachmentStore.write` keyed by SHA-256 (AD-731 invariant), builds OpenAI-shape multimodal request via `build_multimodal_messages`, calls `runtime.llm_client.complete(LLMRequest(tier="compute_use", ...))` (no cache per BF-272, no fallback chain per BF-269), parses `{x, y, confidence}` JSON, runs AD-706c-1 `action_verify` handshake (Guard #9 — reuses existing primitive, never forks), then either `page.mouse.click(x, y)` + emit `BROWSER_COMPUTE_USE_CLICK_EXECUTED` or aborts with `BROWSER_COMPUTE_USE_CLICK_ABORTED`. Late-bind registration: `_HANDLERS["compute_use_click"] = action_compute_use_click` appended after `action_verify` to avoid actions.py ↔ compute_use.py circular import. `classify_action` extended with `compute_use_click → 3` short-circuit at top of ladder (Captain ACK required every call); AD-706e's later additive entries can stack without re-shaping this branch. `classify_action_with_llm` already short-circuits when `rule_tier >= 3` — no change. Guard #10 trust budget: two new `BrowserSession` attributes (`_compute_use_consecutive_autonomous`, `_compute_use_total_calls`) + public properties + `note_compute_use_call()` / `note_captain_ack()` methods (LoD-compliant); two new `BrowserToolConfig` fields (`compute_use_max_consecutive_autonomous_actions` default 5 ge=0 le=20, `compute_use_max_per_session` default 50 ge=0 le=500). Four new EventType values (`BROWSER_COMPUTE_USE_CLICK_PROPOSED/VERIFIED/ABORTED/EXECUTED`). REUSES `VISION_UNCONFIGURED_MESSAGE` from `cognitive/vision_dispatch.py` for honest-degrade (BF-274 — one module owns these strings). Tier-2 throughout: every failure mode returns `{ok: False, skipped_reason, ...}`, never raises. AD-731 source-scan asserts no `b64encode`/`base64.b64`. BF-287 enforced: tests use real `SystemConfig()` + dataclass fakes at substrate boundary (no MagicMock at config / page / store / LLM client). +16 pytest tests + 4 existing tests adapted for the fifth peer tier (test_bf069_llm_health, test_per_tier_llm, test_ad732_vision_tier). Full gate: 13778 → 13794.

**Wave 164 shipped (2026-05-16):**
- AD-728c — Agent-initiated render self-check with contextual rate limits (closes #660). Flips AD-728's `agent_initiated_stub` trigger from hard-reject to a gated, two-budget rate-limited path. Default-OFF transitional gate `cfg.avatars.render_self_check_enabled` (False). When enabled, REUSES AD-728's `verify_render_coherence` callsite (no duplication of the vision-LLM call). New `_agent_initiated_rate_check` helper inside `avatars/render_verification.py` implements two-budget logic: per-active-conversation budget (default 2, scope `render_self_check_conv:<last_reply_ts>`) applies INSTEAD OF the hourly budget (default 3, scope `render_self_check_hour`) when the agent is in an active conversation (default 600s since `last_reply_emitted_at`). Budgets are NEVER additive — the per-conversation budget is the override while engaged, not an add-on (Test 5 explicitly enforces). Four new `AvatarsConfig` fields: `render_self_check_enabled` (default False), `render_self_check_max_per_hour_per_agent` (default 3, ge=0), `render_self_check_max_per_active_conversation` (default 2, ge=0), `render_self_check_active_window_seconds` (default 600, ge=0). New async `CognitiveAgent.check_own_render(reason: str | None = None)` method ("look in the mirror") folds every outcome (coherent, divergent, rate-limited, honest-degrade) into the agent's own working memory via `AgentWorkingMemory.record_observation(...)` so the agent's next LLM call sees the mirror result; rationale documented inline — SENSORIUM_REGISTRY is class-level static dispatch metadata, NOT a runtime mailbox. Cost discipline preserved on the event-bus side: agent-initiated coherent calls still emit NOTHING (only divergent calls emit `RENDER_DIVERGENCE_OBSERVED`); the agent-private working-memory entry is the only additional surface. `_last_reply_emitted_at(runtime, agent_id)` lookup goes through public `runtime.registry.get(agent_id)` API per BF-287 — never `registry.agents`. AD-728's existing `test_agent_initiated_stub_hard_rejected` test renamed to `test_agent_initiated_stub_default_off_preserves_baseline` and updated to assert that with `render_self_check_enabled=False` the baseline `"agent_initiated_disabled"` honest-degrade is preserved exactly. No new event types (AD-728c reuses `RENDER_DIVERGENCE_OBSERVED`). AD-731 invariant preserved (source-scan asserts no `b64encode`/`base64.b64` in `render_verification.py`). Tier-2 throughout — `check_own_render` never raises; internal errors `logger.warning(..., exc_info=True)` and fold an `internal_error` observation. Test scaffolding uses real `SystemConfig()` fixtures + hand-rolled `@dataclass` agent stub (NOT `MagicMock(spec=CognitiveAgent)`) per BF-287. Three forward markers filed with TECHNICAL triggers per AD-722c-3: AD-728c-1 (per-conversation budget reset on Captain-acknowledged correction), AD-728c-2 (Counselor mediation of self-check requests), AD-728c-3 (`VisionLLMRateLimit._windows` per-conversation bucket GC). +12 pytest tests; full gate 13750 → 13762.

**Wave 163 in flight (2026-05-15):**
- AD-728 — Vision-LLM render-coherence mirror function (closes #586). New `src/probos/avatars/render_verification.py` with `RenderCoherenceResult` dataclass + module-level `verify_render_coherence(runtime, agent_id, trigger, ...)` function. REUSES AD-722a-1's `VisionLLMRateLimit` (scope `render_verification`) and `is_render_phrased()` AD-727 rule #8 phrasing helper. Three triggers: `captain_command` (new `/verify-render <agent_id>` slash command in `experience/shell.py`), `divergence_followup` (gated by `cfg.avatars.render_verification_followup_enabled`, default False), `agent_initiated_stub` (path exists, hard-rejected). Default-OFF gate `AvatarsConfig.render_verification_enabled`; per-agent hourly cap `render_verification_max_per_hour_per_agent` (default 3, ge=0). New `EventType.RENDER_DIVERGENCE_OBSERVED`. Phrasing rejection re-prompts ONCE with explicit renderer-subject constraint; second failure drops the analog description with `skipped_reason="phrasing_rejected"`. Tier-2 throughout (every failure mode returns a result with `skipped_reason` set). Cost discipline: coherent observations are NOT logged. AD-731 invariant preserved (image bytes flow through AttachmentStore SHA-256 refs via `build_multimodal_messages`; module source-scan asserts no `b64encode`/`base64.b64`). AD-727 rule #1 source-scan asserts no `trust_network`/`hebbian` token in the module. +15 pytest tests.
- AD-729 — Peer avatar perception governance contract (closes #587). New `src/probos/avatars/peer_perception.py` exports `ObservationRegister` enum (OPERATIONAL / PERSONAL), `PeerObservation` frozen dataclass, async `observe_peer(...)` capability surface, async `request_permission(...)` speak-freely protocol, sync `composite_impressions_for(...)` helper, `register_permission_listener(...)` and `reset_state()` test hooks. Four mechanical floors enforced: (1) reputation/routing read-only (source-scan regression test asserts zero `trust_network`/`hebbian`/`mesh.routing` imports), (2) privacy opt-out via observed's `CrewProfile.peer_perception.enabled`, (3) backend-render-only path, (4) cross-federation honest-degrade (`federation_review_required`). Eight hard gates evaluated in order. New `PeerPerceptionProfile` dataclass added to `crew_profile.py` (default `enabled=True, certified=False`) with `to_dict`/`from_dict` roundtrip wired into `CrewProfile` JSON serialisation. 5 new EventType values (`PEER_OBSERVATION_RECORDED/DECLINED/PERMISSION_REQUESTED/GRANTED/DENIED`) inserted after `RENDER_DIVERGENCE_OBSERVED`. 3 new `AvatarsConfig` fields (`peer_perception_enabled` default-OFF, `peer_observation_decay_seconds` default 7-day, `peer_observation_max_per_pair_per_thread` default 1 ge=0). Records-store persistence via existing `records_store.write_entry(...)` API (artifact type `peer_observation`, tagged, classification `ship`). Permission grants: in-module store, single-use, 5-minute TTL; default deny-silent listener. AD-731 invariant preserved (textual payloads only; module source-scan asserts no `b64encode`/`base64.b64`). Tier-2 throughout. Real `AgentRegistry`-shape fixture in tests per BF-287 (no MagicMock at substrate boundary). +18 pytest tests.
- AD-722a-6 — Cross-agent intent-vs-presentation divergence observation (closes #615). New async `observe_peer_divergence(runtime, observer_id, observed_id, *, register, permission_grant_id, thread_id, window_seconds)` in `avatars/peer_perception.py`. Consumes AD-722a-1's `runtime.divergence_history` ring buffers and routes the resulting observation through AD-729's `observe_peer(...)` so the eight governance gates apply uniformly. Three pre-delegation hard gates: (1) new `AvatarsConfig.cross_agent_divergence_observation_enabled` default-OFF, (2) AD-722a-1's `vision_intent_divergence_enabled` upstream gate, (3) at least one history entry inside `window_seconds` (default 24h). New pure-template `_format_divergence_summary(...)` emits flat OPERATIONAL phrasing (count, dominant intent emotion, mean magnitude) — no LLM call, no embeddings. New `EventType.CROSS_AGENT_DIVERGENCE_OBSERVED` emitted only after the AD-729 governance layer records the observation. Dual default-OFF preserved: both `peer_perception_enabled` AND `cross_agent_divergence_observation_enabled` must be True before the surface fires. AD-731 invariant preserved (textual summary only; module + runtime payload source-scans). +10 pytest tests.
- AD-729b — Peer-observation conduct training module (closes #589). New `src/probos/cognitive/peer_observation_training.py` exports `load_module(path)`, `grade_module(*, module, responses)`, `peer_observation_graduation_gate(*, profile, qualification_config)`, async `set_peer_observation_certified(*, runtime, agent_id, value, reason)`. Deterministic weighted-rubric grade (NOT LLM in v1; AD-729b-2 forward marker filed). New YAML scaffold `config/manuals/peer_observation_conduct.yaml` with six AD-729b sections (theory, register_identification, phrasing_practice, permission_protocol, pattern_recognition (placeholder until AD-729c corpus available), final_assessment with 0.8 pass threshold and per-section rubric weights). Two new EventType values (`PEER_OBSERVATION_CERTIFIED`, `PEER_OBSERVATION_CERTIFICATION_REVOKED`). Two new `QualificationConfig` fields: `peer_observation_module_path` (default `config/manuals/peer_observation_conduct.yaml`), `peer_observation_certification_required` (default-OFF; flips True after AD-729a Standing Orders ship). `peer_observation_graduation_gate` is the integration hook for Boot Camp / Qualification consumers (actual `crew_development/boot_camp.py` graduation-pipeline wire-up deferred — gate is exported and unit-tested in isolation). `set_peer_observation_certified` mirrors AD-720d-2.1 `set_vision_capable` shape. Tier-2 throughout (parse failures honest-degrade to False; never raises). +8 pytest tests with real `SystemConfig()` fixtures loading the actual YAML scaffold.
- AD-729c — Counselor pattern-monitoring for peer-observation conduct (closes #590). New `src/probos/cognitive/peer_observation_monitor.py` exports `PatternFinding` dataclass, `PeerObservationPatternDetector` Protocol, seven concrete detectors (`FrequencyDriftDetector`, `RegisterDriftDetector`, `CascadeSignalDetector`, `StaticImpressionDetector`, `PermissionDenialPatternDetector`, `SycophancyPatternDetector`, `PrivilegedTierLeakageDetector`), `default_detectors(...)` factory, `aggregate_health_metrics(...)` privacy-preserving counts helper, and `PeerObservationMonitor` orchestrator. Fixed 60-second cadence pinned at module-level constant `_MONITOR_INTERVAL_SECONDS=60` (AD-504 phantom dropped; configurable cadence filed as AD-729c-1 forward marker). Three-tier escalation per `(detector, observer_id)` pair: Tier 1 first finding → `PEER_OBSERVATION_INTERVENTION_TIER_1` event (private coaching), Tier 2 persistence → revoke certification (calls injected `revoke_certification` callable; typically `set_peer_observation_certified(...False)`) + Tier-2 event, Tier 3 persistence post-recert → Tier-3 event (bridge alert surfaces via existing AD-635 alert API; concrete wiring deferred). State persists across runtime restarts via sidecar JSON at `<data_dir>/peer_observation_intervention_state.json` (atomic-write tmp+rename pattern mirroring AD-720d-2.1). 4 new EventType values (`PEER_OBSERVATION_PATTERN_FLAGGED`, `*_INTERVENTION_TIER_1/2/3`). Counselor-own-conduct enforced: module never invokes the AD-729 capability surface (source-scan regression Test 21). Trust read-only: SycophancyPatternDetector reads via injected callable, never writes (source-scan Test 20). +23 pytest tests covering all seven detectors (positive + negative each), three-tier escalation cascade, aggregate metrics correctness, cadence pinning, sidecar restart roundtrip. Tier-2 throughout.
- AD-729d — Peer-observation reinforcement loop FORWARD MARKER (closes #591 as the marker filing; the build is a future wave). Documentation-only Wave 163 deliverable. Reinforcement creates an optimization gradient that peer perception alone does not; the AD-729 mechanical constraint #1 (read-only with respect to trust) prevents trust drift, but reinforcement is the natural channel where that pressure could re-enter the system through DSL drift. Five TECHNICAL triggers per AD-722c-3 (NOT calendar dates) — all must hit before the marker advances to a build: (A) operational maturity (`PEER_OBSERVATION_RECORDED` count ≥100 across ≥3 pairs over a 2-quarter window); (B) conduct stability (zero `PEER_OBSERVATION_INTERVENTION_TIER_3` events in the same window); (C) AD-729a Standing Orders shipped AND content includes a reinforcement-specific section reviewed by Counselor; (D) AD-729b module YAML includes reinforcement content AND ≥3 officers passed the extended module; (E) Captain explicit ruling at design stage documented in DECISIONS.md. Open design questions captured in the DECISIONS entry (proposals-vs-direct DSL mutation, mentor-mentee scope, decay semantics, clinical-role boundary). No code, no tests, no config — pure documentation housekeeping.
- AD-721d-2c — HXI Counselor-mediation button (closes #658). Adds the UI surface for the AD-721d-2 server-side mediation endpoint (shipped Wave 162). New props on `CrewAvatarPopout.tsx`: `onMediateRevision?: (note) => Promise<{refined_hint?, proposal_iteration?, error?}>` and `counselorOnline?: boolean`. New inline-SVG "Mediate" button (two circles + bridge glyph; stroke-based per HXI Design Principle #3, no emoji) rendered between the revision counter and the existing submit button when ALL of: prop callback present, counselor online, revision note non-empty. Click invokes the callback then renders the refined hint inline ("Counselor refined: ...") with an iteration chip ("(iter N)"); on success the refined text populates the textarea so the Captain can review/edit before submitting via the existing submit-revision path. On error, a stroke-based error surface is shown and the Captain's original hint is preserved (NOT clobbered). All local state (mediating, mediateRefined, mediateError, mediateIteration) lives inside CrewAvatarPopout; no store mutations. AgentProfilePanel wiring not in this AD (parent integration of the callback + counselor-online lookup is a sub-AD; the new props default to undefined so the existing wiring is byte-compatible). +4 vitest tests covering: visible-when-conditions-met, hidden-when-offline, happy-path-renders-refined-panel, error-path-preserves-original-hint. Vitest: 644 → 648 passed. `npm run build` green; bundle hash `index-a4x_HPw3.js` → `index-cAfin0aS.js`. No backend changes; existing pytest 13715 baseline preserved.
- AD-719b — Copilot-style left rail + Agents nav (closes #547). New self-contained `ui/src/components/leftrail/LeftRail.tsx` exports `LeftRail` plus `LeftRailAgent` / `LeftRailThread` / `LeftRailProps` types. Default OFF via localStorage flag `hxi_left_rail_enabled` (returns null until parent flips). Two sections: online agents (filtered to `status==='online'`) and recent threads. Collapse toggle persists `hxi_left_rail_collapsed` to localStorage (width 240px ↔ 56px). Progressive disclosure per HXI Design Principle #5: first-time users (visit count <10) see max 5 agents + 3 threads; veteran users see max 12 + 8. Visit count tracked in localStorage `hxi_visit_count`. Three inline-SVG stroke-based glyphs (agents, threads, collapse) per HXI Design Principle #3 — no emoji. Pure presentational — data flows in via props; parent wires zustand stores. `onSelectAgent` / `onSelectThread` callbacks fire on click. Component is built but NOT yet imported by App.tsx (parent integration deferred to AD-719b-parent-wire forward marker); bundle hash unchanged because Vite tree-shakes the unreferenced module. +5 vitest tests: default-OFF returns null, enabled renders both sections, click-agent fires callback, click-thread fires callback, collapse toggle persists state and updates width. Vitest: 648 → 653.
- AD-719a — Persistent multi-agent chat threads under WardRoom (closes #546). Wave 163 ships the architectural contract — `thread_mode='multi_agent'` marker on existing `WardRoomThread`, the `create_multi_agent_thread(service, *, channel_id, captain_id, title, body, mentioned_agent_ids)` helper, and the participants-trailer protocol. Architectural decisions (Captain ruling 2026-05-15): (1) YES, agents observe other agents' messages mid-thread when they have already been @-mentioned in the thread; (2) NO, agents do NOT observe messages in threads they were never @-mentioned in (cross-thread observation out of scope); (3) Captain messages are always the seed (agent-to-agent without Captain prompt deferred to AD-719a-2). New `src/probos/ward_room/multi_agent.py` exports `MULTI_AGENT_THREAD_MODE` constant, `create_multi_agent_thread`, `format_participant_trailer`, `parse_participants`, `is_participant`, `cross_agent_visibility` helpers. Participants encoded in thread body via `@participants: id1,id2` trailer; recovered via regex. AD-719 transient @-mention fan-out wire-up deferred to forward marker AD-719a-wire (the helper is the seam the future wire-up consumes). UI delta minimal: new "MULTI" badge with stroke-based two-circle-bridge glyph in `WardRoomThreadList.tsx` for `thread_mode==='multi_agent'` rows; store type extended `'inform' | 'discuss' | 'action' | 'multi_agent'`. +6 pytest tests (helper roundtrip, participants trailer, visibility rules, Captain-as-author, AD-731 invariant source-scan). +4 vitest tests (badge renders for multi_agent, hidden for discuss, mixed-mode list, click selects thread). Vitest: 653 → 657. Bundle hash `index-cAfin0aS.js` → `index-hKNByK6W.js`. Full pytest gate: 13730 passed.
- AD-739 — Captain Card data model + render pipeline (closes #649, v1 contract). Operator self-card always-in-context across all CognitiveAgent prompts. System-maintained — NOT agent-self-edited. New `src/probos/captain_card/card.py` exports the `CaptainCard` Pydantic model (identity, voice, active-context, preferences max-10, recent_corrections max-3, avatar_ref, version, updated_at), `CorrectionRef` model, `default_captain_card()` bootstrap, `load_card(path)` / `save_card(card, path)` JSON-sidecar persistence (atomic temp+replace, tier-2 degrade), and `render_card_for_prompt(card, *, max_tokens=500)` pure-template renderer. Renderer truncates `preferences`/`recent_corrections` from the tail when over budget while preserving identity fields. AD-588/589/592 confabulation-guard via `_CAPABILITY_GAP_RE` from `probos.cognitive.decomposer`: any rendered line matching the capability-gap regex is dropped with a logged warning. AD-731 invariant: `avatar_ref` Pydantic validator enforces SHA-256 hex format (64 chars); ValueError raised on non-hash strings; module source-scan asserts no `b64encode`/`base64.b64`. Four new `CognitiveConfig` fields: `captain_card_enabled: bool = True` (default-ON; benign anchor), `captain_card_path: str = "captain_card.json"`, `captain_card_max_tokens: int = 500` (ge=100, le=1500), `captain_card_refresh_min_interval_seconds: int = 3600` (ge=60). Prompt-builder injection point and Dreaming-loop integration deferred to forward markers AD-739-prompt-wire and AD-739-dreaming-wire (the model + renderer are the contract; consumer wiring is mechanical). +10 pytest tests: bootstrap default, roundtrip persistence, render-within-budget, truncation preserves identity, capability-gap-line stripped, avatar_ref SHA-256 validation, avatar_ref None allowed, AD-731 source-scan, CognitiveConfig defaults, package public surface. Real `SystemConfig()` fixtures per BF-287.
- AD-706d — LLM-driven Browser Tool tier classifier (closes #519). New `src/probos/tools/browser/llm_classifier.py` exports `classify_action_with_llm(runtime, rule_tier, action, ...)` companion to the existing rule-based `classify_action` (at `tools/browser/actions.py:550`, UNCHANGED — int tier 1/2/3 contract preserved). Default OFF via `BrowserToolConfig.llm_classifier_enabled`. Critical safety property: LLM can only UPGRADE tier (1→2→3), never DOWNGRADE — short-circuits without calling LLM when rule_tier >= 3. REUSES AD-722a-1's `VisionLLMRateLimit` under new scope `browser_action_classifier` (no fork to `LLMCallRateLimit`; the class is already generalized via cross-module use). In-memory cache keyed by `(action, url[:80], element_text[:120], page_title[:80])` with configurable TTL. Strict single-word output parsing: maps `auto_run`/`ack_required`/`destructive` (plus synonyms) to int 1/2/3; any other string honest-degrades. Four new `BrowserToolConfig` fields: `llm_classifier_enabled` (default OFF), `llm_classifier_tier` (default `fast`), `llm_classifier_max_per_hour` (default 60, ge=0), `llm_classifier_cache_ttl_seconds` (default 300, ge=0). Sync entry-point: classifier reads `runtime.llm_client.complete_sync` when present; runtimes that only expose async `complete` honest-degrade. AD-731 invariant n/a (text-only classifier). +10 pytest tests: disabled preserves rule tier, destructive short-circuits, LLM upgrades 1→2, LLM cannot downgrade 2→1, LLM failure degrades, malformed output degrades, unknown enum degrades, rate-limit exhaustion degrades, cache hit reuses prior tier, source-scan asserts `classify_action` unchanged + companion exists + VisionLLMRateLimit reused.

**Wave 162 in flight (2026-05-15):**
- AD-722b-1a — MagicMock(spec=SystemConfig) cleanup in tests, removed routers/auth.py:43 isinstance guard (closes #657). 7 sites migrated to real `SystemConfig()` (conftest fixture + 6 individual tests). 3 additional test helpers in test_ad722_avatar_telemetry / test_ad722b_websocket_push / test_ad722b4_fleet_telemetry received `cfg.auth = AuthConfig()` to preserve empty-token=auth-disabled contract after guard removal. Net test count unchanged.
- AD-729a — Peer-observation Standing Orders extension (closes #588). New `config/standing_orders/peer_observation.md` with 5 sections verbatim from Captain ruling 2026-05-10. Cross-references appended to `ship.md` and `counselor.md`. +7 pytest tests verifying section phrases + cross-reference. Unblocks AD-729 capability AD.
- AD-720d-2.1 — Captain vision-capability approval flow (closes #645). Three new endpoints `/api/agent/{id}/vision-capability/{propose,approve,history}`; new `CallsignRegistry.set_vision_capable(agent_id, value, *, reason)` resolves agent_id→agent_type via bound AgentRegistry then flips registry profile dict; new `src/probos/avatars/vision_proposal_history.py` sidecar (RLock + atomic write, mirrors AD-721d-4 pattern); two new EventType enum values (`VISION_CAPABILITY_PROPOSED`, `VISION_CAPABILITY_RESOLVED`); new `AvatarsConfig.vision_proposal_history_path`. +8 pytest tests. AD-731 invariant preserved (no image bytes in this code path).
- AD-706c-1 — Browser Tool visual verify via vision tier (closes #642). New `verify(expectation: str)` action on BrowserTool's 10-action vocab (now 11). Screenshot captured by Playwright → stored via `AttachmentStore.write` keyed by SHA-256 (AD-731 invariant preserved) → vision LLM call via `build_multimodal_messages` + `llm_client.complete(tier="vision")` → parsed JSON `{ok, observation}`. Tier-2 honest-degrade throughout (vision unconfigured / call error / parse error → ok=None, skipped_reason set, never raises). New `EventType.BROWSER_VERIFY_OBSERVED`. Classified tier-1 (no Captain ACK). BrowserTool constructor gained optional `runtime` parameter; wired from `startup/finalize.py`. +10 pytest tests.
- AD-722a-1 — Vision-LLM intent-vs-render divergence detector (closes #610). New `src/probos/avatars/vision_intent_divergence.py` with `VisionIntentDivergenceDetector` + reusable `VisionLLMRateLimit` (class-level shared store, scope-keyed; AD-722e-2 wave-step #6 inherits it) + `is_render_phrased()` AD-727 rule #8 phrasing-regex helper. Default-OFF flag `AvatarsConfig.vision_intent_divergence_enabled`; per-agent hourly cap `vision_intent_divergence_max_per_hour_per_agent` (AD-728 ceiling alignment). Detector constructed at runtime startup; `DivergenceDetector` callsite wiring deferred (Section 2) until AD-721i backend renderer ref-lookup is stable. New `EventType.VISION_INTENT_DIVERGENCE_OBSERVED`. AD-731 invariant preserved (refs only, no inline bytes). +10 pytest tests.
- AD-722e-2 — Vision-LLM self-render coherence verifier (closes #644). New `src/probos/cognitive/self_render_verify.py` with `SelfRenderVerifier` REUSING AD-722a-1's `VisionLLMRateLimit` (scope `self_render_verify`) and `is_render_phrased()` (AD-727 rule #8). Default-OFF flag `AvatarsConfig.self_render_verify_enabled`; per-agent hourly cap (AD-728 ceiling alignment). Verifier constructed at runtime startup; `self_perception.py` callsite wiring deferred (Section 2) until AD-721i ships. New `EventType.SELF_RENDER_COHERENCE_OBSERVED`. AD-727 rule #1 enforced (READ-ONLY — verifier module has zero trust/hebbian imports, verified by source-scan regression test). AD-731 invariant preserved. +9 pytest tests.
- AD-722a-2 — Chain-path divergence detection at compose-step emit (closes #611). New canonical `CognitiveAgent.mark_chain_output_emitted(output_text, *, audience, intent_self_tag, applied_modulation_rules)` hook + `chain_divergence_buffer_for(audience)` accessor. Per-audience ring buffer (maxlen=8) prevents cross-channel surface pollution (AD-727 addendum h). Wired from chain compose consumer at `cognitive_agent.py:2934` (Phase 2b). Uses the pure `compute_divergence` function; reads `chain_result["audience" | "intent_self_tag" | "applied_modulation_rules"]` (defaulted to `sensorium` / None / None — no new chain_result fields invented). New `EventType.DIVERGENCE_OBSERVED_CHAIN` emitted with `path_tag="chain"` when magnitude > 0. AD-722a DM-path behavior unchanged. +10 pytest tests.
- AD-721d-2 — Counselor-mediated avatar revision (closes #618). New `mediate_appearance_revision` intent + IntentDescriptor on CounselorAgent; new handler `_mediate_appearance_revision(target_agent_id, captain_hint)` refines via standard-tier LLM then forwards to target agent's AD-721d-1 `propose_appearance(captain_note=...)` path. New API endpoint `POST /api/agent/{agent_id}/appearance/mediate` uses `intent_bus.send(IntentMessage(target_agent_id=...))` per pass-1 fix (NOT broadcast). New `MediateAppearanceRevision` Pydantic model. New `EventType.APPEARANCE_REVISION_MEDIATED`. +8 pytest tests. HXI surface deferred to forward marker AD-721d-2c.
- AD-720a-1 — PDF / DOCX / XLSX document text extraction (closes #562). Three new permissive deps: `pypdf>=4.0` (BSD-3-Clause), `python-docx>=1.1` (MIT), `openpyxl>=3.1` (MIT) — all verified at install time via `pip show`. New helpers `_extract_pdf` (page-by-page, max 100 pages), `_extract_docx` (paragraphs + table cells), `_extract_xlsx` (sheet-by-sheet, max 10k rows). Dispatch table in `extract_text`. Tier-2 (parser exceptions bubble as ValueError). Default-OFF gate `AttachmentsConfig.pdf_extraction_enabled` (already plumbed through chat/agents routers; extended `vision_dispatch.py` PDF gate to cover DOCX + XLSX MIMEs too). +12 pytest tests (real `SystemConfig()` per AD-722b-1a; fixtures built in-memory via pypdf.PdfWriter / Document / Workbook). AD-731 invariant preserved (helpers operate on `bytes` already-resolved from AttachmentStore).
- AD-722b-5 — Federation cross-mesh telemetry push (LOCAL-MESH PORTION ONLY) (closes #602). New `src/probos/federation/telemetry_relay.py` with `FederationTelemetryRelay` + `PeerTelemetrySubscription`. Local plumbing only — subscription registration, per-peer outbound rate-limit (default 10 fps), agent_id filtering, frame dispatch through a pluggable `_emit_to_peer` callback. Federation hop FORWARD-MARKED as AD-722b-5a — `FederationBridge` exposes only `forward_intent` (single-shot RPC) per Section 0 pre-flight gate; streaming primitive does not yet exist. `set_emit_callback` is the hookup point for the future bridge wiring. +8 pytest tests covering subscription, multicast, rate-limit cap + window recovery, unregister, custom callback contract.

**Wave 161 in flight (2026-05-15):**
- AD-730-2-1 — Image-budget tracker JSON sidecar persistence (+5 pytest tests; closes #656). New `src/probos/attachments/image_budget_store.py` (`load`/`save` module functions, atomic temp-file + `os.replace`). Runtime boot loads from `<data_dir>/image_budget.json` (configurable via `AttachmentsConfig.image_budget_path`). `ImagePolicyEnforcer.check_budget` persists on append AND on prune. Tier-2 throughout — disk failure logs and degrades; in-memory tracker remains authoritative. AD-731 invariant untouched (BUDGET tracker only — image bytes still flow through AttachmentStore SHA-256 refs). Forward markers AD-730-2-1a (write throttle) + AD-730-2-1b (ConnectionFactory backend swap) filed.
- AD-721d-4 — Avatar proposal-history JSON sidecar persistence (+5 pytest tests; closes #620, #623 dup). New `proposal_history.configure(path)` loads + binds the on-disk sidecar; `_persist_locked()` rewrites after every mutation (`append`, `clear`, `reset_all`). Path resolves to `AvatarsConfig.proposal_history_path` or defaults to `<data_dir>/proposal_history.json`. 5 existing public function signatures unchanged. Tier-2 throughout (disk failure logs + degrades). AD-721d-1 module-level dict + RLock unchanged. Forward markers AD-721d-4a (`ConnectionFactory` migration) + AD-721d-4b (periodic compaction) filed.
- AD-723a-2 — WR branch consumer-side sensorium dispatch migration (+6 pytest tests; closes #625). New `_WR_SELF_WRAPPED_KEYS: ClassVar[tuple[str, ...]] = ()` (sibling of `_DM_SELF_WRAPPED_KEYS`). WR branch of `_build_user_message` invokes `_dispatch_sensorium_async(SensoriumPath.WR_ONESHOT, observation)` inside Tier-2 try/except, mirrors AD-723a-1 DM path. Byte-parity preserved with empty selector (current registry shape). AD-723a-1 DM-branch tests all still green. Forward marker AD-723a-2a (populate `_WR_SELF_WRAPPED_KEYS` with first real consumer) filed.
- AD-722b-1 — Crew-scope auth substrate for telemetry surfaces (+8 pytest tests; closes #598). New `src/probos/routers/auth.py` (`require_crew_scope` HTTP `Depends`, `verify_ws_token` pre-accept WS gate, `hmac.compare_digest` constant-time compare). New `AuthConfig` Pydantic model with `crew_scope_token: str = ""` (default-OFF — empty disables auth, preserves backward compat for single-operator HXI installs). Applied to 4 endpoints: HTTP `GET /avatar-telemetry`, HTTP `GET /avatar-telemetry/history`, per-agent WS `/avatar-telemetry-stream`, fleet WS `/avatar-telemetry/stream`. v1 is substrate — this is the FIRST auth dep in the codebase, becomes template for future waves. `_configured_token` defensively skips non-str values (MagicMock fixtures stay backward-compat). Forward markers AD-722b-1a (multi-Captain per-crew tokens) + AD-722b-1b (apply to remaining read endpoints) + AD-722b-1c (federation JWT) + AD-722b-1d (token rotation/TTL) filed.
- AD-722b-4a — HXI fleet-hook integration (+4 vitest tests; closes #655). `useFleetAvatarTelemetry` now wired into `CognitiveCanvas.tsx` (single call, opens once at mount). New `useStore.avatarTelemetry: Map<string, Record<string, unknown>>` + `setAvatarTelemetryFrame(agent_id, type, payload)` action handles 4 frame types: snapshot replaces, diff after snapshot shallow-merges, diff before snapshot is dropped, ping no-op, error logs once. Per-agent `SelfImageTab` WS unchanged (AD-722b-4b forward marker covers migration). HXI Canvas tooltips/bloom/raycasting intact (mock-stubbed canvas children in test, structural integration confirmed). Bundle hash: `index-BDgoocuQ.js` → `index-D0tUvFeA.js`. Forward markers AD-722b-4b (migrate SelfImageTab consumer) + AD-722b-4c (canvas-side selectors `useAgentEmotion`/`useAgentWorkingState`) filed.

**Wave 160 in flight (2026-05-14):**
- AD-726 — DM post-LLM cleanup pipeline extracted to DmReplyPipeline (+12 pytest tests; closes #584 partial — pre-LLM prep deferred to AD-726a/b/c forward markers). `agent_chat` shrinks 574→~305 lines. AD-722c-3 (#654) folded — BEP standing rule bullet for technical-not-commercial forward-marker language.
- AD-723a-3 — SensoriumEntry gains injection_zone + wrapper metadata (+7 pytest tests; closes #626). Backward-compatible — both fields default None. Dispatcher applies wrapper to string outputs only (dict-return contract unchanged). `_DM_SELF_WRAPPED_KEYS` still the v1 selector; per-entry migration deferred to AD-723a-3a.
- AD-722a-4 — Auto-correction loop on high-magnitude divergence (+9 pytest tests; closes #613). Default OFF. Re-modulates prosody only — response_text never rewritten. Per-utterance budget (1 correction). `DivergenceResult` gains `corrected: bool` field. `runtime.divergence_corrections` sibling map populated by `apply_divergence_check`, cleared at reply-entry by `DmReplyPipeline.step_1_sanity_gate_retry` (NOT step_7 — TTS reads slot post-reply). `apply_voice_modulation` gains keyword-only `noise_scale_factor` / `length_scale_factor` with default-1.0 no-op preserved.
- AD-730-2 — Multi-image DM policy (+9 pytest tests; closes #632). Hard cap (8, 413), in-place PIL downscale to 1024px bounding box (Tier-2; AD-731 invariant preserved via NEW refs), per-Captain rolling 24h budget (50, 429 with Retry-After). No new pip deps (Pillow 12.2.0 already in venv).
- AD-722b-4 — Fleet avatar telemetry stream (+6 pytest +3 vitest tests; closes #601). New WS at `/api/agent/avatar-telemetry/stream` fans out per-agent publish loops over one connection. Every frame carries explicit `agent_id` field. Per-agent endpoint preserved. `fleet_stream_enabled` default-ON. HXI hook stub `useFleetAvatarTelemetry` ships; per-agent store migration deferred to AD-722b-4a.

**Wave 159 in flight (2026-05-14):**
- AD-722c — Avatar telemetry JSONL history + query endpoint (+6 pytest tests; closes #569). New `src/probos/avatars/telemetry_history.py` `TelemetryHistoryWriter` (append-only JSONL, per-agent lock, executor-backed write, agent_id boundary sanitizer). `AvatarTelemetryConfig.history_enabled`/`history_retention_days`/`history_dir` added with `>= 1` validator on retention. Writer constructed in `runtime.py` next to the AvatarEventBus and exposed as `runtime.avatar_telemetry_history`. `_publish_loop` (both initial-send and per-interval send) appends snapshots best-effort — log-and-degrade, never blocks the WS publish. New `GET /api/agent/{id}/avatar-telemetry/history?limit=&since=` endpoint clamps limit to `[1, 1000]` and returns `{"agent_id", "rows": []}` when the feature is off. Forward markers AD-722c-1 (size-based rotation) + AD-722c-2 (`TelemetryHistoryStore` Protocol for commercial overlay backend swap) filed. Known dreaming flake (`test_nl_to_dream_cycle_changes_weights`, environmental Chroma boot under serial) noise-only.
- AD-722d — Auto-write significant telemetry events to Ship's Records (+5 pytest tests; closes #570). New `src/probos/avatars/records_writer.py` `TelemetryRecordsWriter` — three v1 events (`emotion_divergence_high`, `working_state_transition_to_blocked`, `sustained_silence`), priority pick (divergence > blocked > silence), per-agent throttle (default 3600 s, in-memory, resets on restart by design), Tier-2 log-and-degrade. `AvatarTelemetryConfig` gains `records_auto_write_enabled=False` (Captain opt-in), `records_throttle_seconds=3600`, `records_significant_events` list (default-factory; unknown names silently dropped), `sustained_silence_seconds=1800` — validators bound all three. Two-phase wiring in `runtime.py`: attribute declared as `None` next to AvatarEventBus, real `TelemetryRecordsWriter` constructed after `self._records_store = cog.records_store` finalize line. `_publish_loop` initial + per-interval hooks observe AFTER the AD-722c history append. Tests use `_FakeRecordsStore` stub — no git subprocess. Forward markers AD-722d-1 (operator-defined event classifiers) + AD-722d-2 (Records-side dedup/aggregation) filed.
- AD-722b-3 — Fine-grained snapshot-diff for WS push (+6 pytest tests + 1 Vitest test; closes #600). New `src/probos/avatars/snapshot_diff.py` `compute_diff(prev, next, threshold, skip_fields)` with `last_observed_at` in `DEFAULT_SKIP_FIELDS`. New `AvatarTelemetryConfig` fields: `ws_diff_enabled=True`, `ws_diff_threshold=0.05`, `ws_full_snapshot_every_n=10` (with validators). `_publish_loop` now tracks per-connection `last_sent_snap_dict` + `tick_count`; emits `{"type":"snapshot", ...flat}` on first frame / every Nth tick / fallback, otherwise `{"type":"diff","agent_id":...,"changed":{...}}`. Empty diffs are suppressed entirely (zero bytes on the wire). Frontend `SelfImageTab.tsx` `onmessage` merges diff frames into a closure-scoped `lastSnapshot` and strips the `type` field on snapshot frames for shape parity with the GET endpoint. The shared `_ws_endpoint_runtime` fixture explicitly disables diffing so the AD-722b legacy tests keep their one-frame-per-wake semantics. Forward markers AD-722b-3a (RFC 6902 JSON-Patch payload) + AD-722b-3b (server-side `SubscriberState` Protocol for fan-out broker) filed.
- AD-720e + AD-738e-2 — Audio attachment playback + Refs-trailer standing rule (+5 pytest tests + 3 Vitest tests; closes #566, #653). `AttachmentsConfig.allowed_mime_types` defaults extended with `audio/mpeg`, `audio/mp4`, `audio/ogg`. `attachments/mime.py._SIGNATURES` registers MP3 sync bytes (4 variants) + MP4 ftyp brands (3 variants) + Ogg `OggS`; `_ANY_OF` extends to include `audio/mpeg` + `audio/mp4` so the existing any-of branch validates them. `IntentSurface.tsx` attachment preview gains middle audio branch rendering `<audio controls preload="metadata">` between the image and file-icon branches. `WardRoomThreadDetail.tsx` paste handler accepts audio MIMEs (chip-only render — playback delivered through IntentSurface, scope-collapse per AD). AD-731 SHA-ref invariant preserved (audio bytes flow through AttachmentStore exactly like images). Transcription explicitly deferred (AD-705a forward marker). BUILDER-EXECUTION-PLAN standing rule for orphan sub-AD Refs-trailer added per #653; DECISIONS AD-738e-1 forward-marker AD-738e-2 (noise_w / sentence_silence) renumbered to AD-738e-2-prosody so #653 owns the AD-738e-2 slot. Forward markers AD-720e-1 (drop-zone visual feedback) + AD-720e-2 (waveform thumbnail) + AD-720e-3 (inline player in WR/Profile chips) filed.
- AD-725 — Targeted sub-intent dispatch on DM one-shot path (+11 pytest tests; closes #583). New `src/probos/cognitive/dm_targeted_lookup.py` `LookupDispatcher` + `SubintentClassifier` Protocol + v1 `RegexSubintentClassifier` (episodic → codebase → knowledge → oracle → none ladder). New `DmTargetedLookupConfig` (default `enabled=False`, `timeout_ms=500`, per-store enables, `max_lookup_chars=1500`, classifier-tier validator with `{regex, embedding}`). Wired into `routers/agents.py::agent_chat` BEFORE the vision-pipeline branch. Result prepended to `message_text` as `--- Targeted Recall (<type>) ---` block immediately before IntentMessage build. Four hard contracts: (1) at most one lookup per turn, (2) read-only, (3) hard `asyncio.wait_for` timeout, (4) no `intent_bus` broadcast. Defensive dispatch — missing methods log INFO and degrade to `""`. Test #10 explicitly verifies zero `trust_network`/`intent_bus`/`hebbian_router`/`consensus_engine` mock calls (firewall regression test). Forward markers AD-725-1 (sensorium-path registration) + AD-725-2 (embedding classifier) + AD-725-3 (per-agent sub-intent vocabulary) + AD-725-4 (multi-store fan-out) + AD-725-5 (LRU cache) + AD-725-6 (Episode dataclass stringify) filed.

**Wave 158 in flight (2026-05-13):**
- AD-737a — Divergence-detector hygiene (+3 pytest tests; closes #648). Hoisted `import dataclasses` to module-top; collapsed the two-pass `parse_intent_self_tag` re-parse in `apply_divergence_check` (palette fetched first, single parse); documented the test-fake contract for `runtime.profile_store` / `divergence_results` / `divergence_history` in the function docstring.
- AD-738a — Orchestrator commit-count audit + voice.ts test gate (+2 Vitest tests; closes #650). `Format-Gate2` now prints a COMMIT-COUNT AUDIT line comparing the wave-plan expectation vs HEAD's unpushed commits (audit trail only — never blocks push). `_resetTtsStatusForTests` gated behind `import.meta.env.MODE === 'test'` so production callers are inert. Renumbered Wave-157 forward markers AD-738a/b/c/d → AD-738f/g/h/i atomically in `docs/development/roadmap.md` and the AD-738 closure block.
- AD-738b — Per-wave UI gate codifies `npm run build` requirement (closes #651, BF-279 root cause). Standing rule added to `BUILDER-EXECUTION-PLAN.md` and surfaced in `wave-orchestrator.ps1:Format-BuildDispatch` so every UI-touching prompt's per-commit gate runs `npx vitest run` AND `npm run build` (Vitest skips `tsc -b` strict; BF-279 was 32h of stale-bundle drift across three waves).
- AD-738c — rhubarb→Oculus viseme mapping polish (+4 pytest tests + 1 Vitest test; closes #652). Duration-aware Preston-Blair → Oculus routing: long `B` frames (>80 ms) now route to `ih` (full vowel) instead of `kk` (stop consonant default). Consonant residuals in `VISEME_TARGETS` bumped from 0.10–0.20 → 0.20–0.30 so stop consonants are visible above the perceptual blend baseline. AD-721b-3 (#561 whisper.cpp WASM tiny.en) remains the long-term proper fix.
- AD-738e-1 — Per-emotion Piper prosody overrides (+7 pytest tests + 2 Vitest tests). Additive on top of AD-738e global defaults; concerned (slower + expressive), excited (faster + more variation), formal (drier). Custom AD-737 emotions resolve to v1 parent server-side via the new public `resolve_emotion_to_v1` alias before reaching the chat response. PiperBackend merges overrides per-call (no instance mutation).

**Wave 157 in flight (2026-05-13):**
- AD-738 — Server-streamed TTS via Piper (+26 pytest tests + 6 Vitest tests + 1 regression Vitest; closes forward marker AD-721b-2.3). MIT-licensed operator-provided binary at `tools/piper/piper(.exe)` + MIT-licensed default voice model `en_US-amy-medium.onnx[.json]` at `tools/piper/voices/`. New `src/probos/audio/tts/` module with `TTSBackend` Protocol + `PiperBackend` subprocess wrapper + `NullBackend` honest-degrade. New `TTSConfig` Pydantic model (default `backend = "browser"` — zero behaviour change for operators who don't install Piper). New `GET /api/avatars/tts/status` probe endpoint + `POST /api/avatars/tts` synthesis endpoint (AD-731 ref-shape invariant — audio bytes flow through AttachmentStore as SHA-256 refs, never inline). Browser `speakResponse` caches the one-time status probe in module-level state and skips POST entirely when `backend != "piper"` (load-bearing zero-HTTP-per-utterance guarantee). On piper happy path, browser plays via `<audio>` element + injects visemes into `useLipSyncCapture` via new `injectLipSyncFrames` setter. AD-735 volume + AD-737 emotion modulation apply to BOTH paths. Honest-degrade chain preserves `SpeechSynthesisUtterance` fallback at three tiers. Forward markers AD-738a (per-agent voice selection), AD-738b (GPU TTS backend eval — Kokoro/StyleTTS2), AD-738c (server-side voice modulation), AD-738d (TTS text caching) filed.

**Wave 156 in flight (2026-05-13):**
- AD-735 — Per-agent volume slider in `ProfileInfoTab.tsx` (+5 Vitest tests; closes #527). Backend chain (`VoiceProfile.volume`, `SetVoiceProfileRequest`, `PUT /api/agents/{id}/voice-profile`, `voice.ts` playback) was already shipped under AD-718; this AD exposed the UI slider with inline SVG speaker glyph (HXI Design Principle #3) and percent display. Mirrors Pitch/Rate `onMouseUp`/`onTouchEnd` persistence semantics. No backend, deps, or wire-shape changes.
- AD-736 — Mic-permission UX polish (+8 Vitest tests; closes #558). New `MicPermissionState` enum (`pending` / `granted` / `denied` / `unavailable`) + `onMicPermissionState` listener API in `wakeWord.ts`. Pre-flight `navigator.mediaDevices.enumerateDevices` hardware probe distinguishes "no audio device" from "permission denied"; `audio-capture` SR error mapped to `unavailable`. New `MicPermissionHint.tsx` HXI overlay mounts at `App.tsx` root; renders only on `denied`/`unavailable`. Dismissal sticky via `localStorage[hxi_mic_hint_dismissed]`. Inline SVG mic glyph (HXI Design Principle #3) — no emoji. AD-705 wake-word algorithm + AD-731 attachment invariant unchanged.
- AD-737 — Per-agent custom emotion taxonomy v2 (+8 pytest tests; closes #612). New `EmotionProfile` dataclass + `CrewProfile.custom_emotions` field (max 8 entries, must not collide with the v1 fixed 8). `parse_intent_self_tag` accepts custom names; `apply_voice_modulation` resolves through `inherits`, composes additive ±0.15 deltas, and emits BOTH the parent `intent_X` rule (so `compute_divergence`'s `startswith('intent_')` filter preserves scoring) and a `custom_X` observability tag. `apply_divergence_check` pre-resolves the custom name to its v1 parent for scoring, then restores the custom name on the `DivergenceResult` for observability. Prompt builder dynamically renders v1 + custom names from `profile_store`. Parent-equivalence pinned: zero-shift custom emotion inheriting `concerned` scores `match_score=1.0` identical to `concerned`. `EmotionalIntent`, `INTENT_EXPECTED_RULES`, `_REQUIRED_INTENT_EMOTIONS`, manifest, AD-731 invariant all unchanged.

**Wave 155 in flight (2026-05-12):**
- AD-721b-1 — Server-side rhubarb-lip-sync backend (+16 pytest tests; closes #559). MIT-licensed operator-provided binary at `tools/rhubarb/`; honest-degrade to AD-721b v1 heuristic when binary absent. Section 0.5 extends `AttachmentsConfig.allowed_mime_types`, `attachments/mime.py._SIGNATURES`, and `attachments/filesystem_store.py._MIME_TO_EXT` for `audio/webm` and `audio/wav`. `POST /api/avatars/lipsync` endpoint accepts AD-720 sha256 attachment refs and returns Oculus-mapped viseme schedule.
- AD-721b-2 — Browser-side real-audio capture infrastructure (+8 Vitest tests; closes #560). Always-on capture via `MediaStreamAudioDestinationNode` + `MediaRecorder`; honest-degrade end-to-end on the server (capture-fail → server-fail → empty frames → CrewVRM falls back to v1 heuristic → AD-721 D5 amplitude). AD-731 invariant honored: captured bytes upload via `/api/chat/attachments/multipart` as a sha256 ref; the lipsync request body carries only the hash, never inline base64. Most browsers today do not route SpeechSynthesis through Web Audio — the infrastructure ships ahead of upstream browser routability.

**Wave 154 in flight (2026-05-12):**
- AD-719c + AD-718d-1 — HXI polish: @-picker keyboard nav (↑/↓/Tab) + ModulationIndicator pulse overlay (+6 Vitest tests; closes #548, #553).
- AD-730-1-1 — WardRoomThreadDetail drag/drop + paste-image attachment (+3 Vitest tests; closes #646; #647 closed as duplicate pre-flight).
- AD-720d-1 — Multi-image batch + per-attachment timing; AttachmentsConfig.multi_image_warn_threshold soft warning (+5 pytest tests; closes #563).
- AD-724-1 + AD-724-2 + AD-724-5 — DM sanity gate hardening: one-shot retry on rejection, stdlib SequenceMatcher fuzzy repetition, shared `apply_dm_sanity` helper lifted into proactive WR/chain paths (+12 pytest tests; closes #627, #628, #629).

**Recent eras (archived):**
- [Era I — Genesis](progress-era-1-genesis.md)
- [Era II — Emergence](progress-era-2-emergence.md)
- [Era III — Product](progress-era-3-product.md)
- [Era IV — Evolution](progress-era-4-evolution.md)
- [Era V — Unification](progress-era-5-unification.md)

----

## Era VI — Commercial Bridge (Waves 105-128, May 2026)

### What landed (this era)

| Wave | AD/BF | Summary |
|------|-------|---------|
| 105 | AD-641g v1 | NATS cognitive chain pipeline foundation (publish-side, opt-in) |
| 106 | BF #423 | knowledge/store git-recovery test coverage (+6 tests) |
| 107 | AD-694a | extract `/api/ontology/graph` edge logic to `ontology/graph_snapshot.py` |
| 108 | BF #426 | `KnowledgeGraphView` TRUST mode O(n²) edge cap |
| 109 | BF #425 / AD-696 | wave-plan status drift reconciliation + convention docs |
| 110 | Nit #427 | `TrainingAgent` redundant `_runtime` override removed |
| 111 | AD-697 v1 | commercial overlay extension-point registry |
| 112 | AD-641g-1 | chain NATS consumer foundation (subscriber side, opt-in) |
| 113 | AD-697-1 + AD-698 | overlay seam validation: HXI badge + pre-intent authorization hook |
| 114 | BF #465 | roadmap reconciliation — flip 27 shipped `(planned, OSS)` tags |
| 115 | AD-443 (drift) | mobility protocol — already shipped |
| 116 | AD-482 (drift) | self-improvement pipeline — already shipped |
| 117 | AD-529 + AD-568 | comm contagion firewall + source governance |
| 118 | AD-660b | `/api/causal-templates` router |
| 119 | AD-572d-i + AD-573e-i | interruptible-wait + `recent_for_agent` infra |
| 120 | AD-539c-i + AD-539d-i | active gap remediation + federated aggregation |
| 121 | AD-509b/c/d + AD-507b | boot-camp continuations + curriculum progression |
| 122 | AD-511b + AD-511d | protective disengagement + boundary-probing detection |
| 123 | AD-522b + AD-522c | Cp/Cpk capability indices + graduated-response zone mapping |
| 124 | AD-583f/g (drift); AD-574c-i partial | observable state already shipped; DM convergence partial-deferred |
| 125 | AD-686c + AD-647d | `oracle.semantic_stats()` + chain CONSULT checkpoint store |
| 126 | AD-473 + AD-474 | PWA Web Push subscription registry + voice STT/TTS substrate |
| 127 | priority-4 cleanup combo | AD-509e/507c/507d/511c/511e/522d/522e/660c/660d |
| 128 | BF #466 | xdist stabilization: `-n 16 --dist=loadfile` |
| BF | BF #467 | distinguish leading @callsign (DM) from embedded mention (broadcast) |

**Net new tests this era:** ~145 pytest + ~5 vitest. Issue count: 37 closed.

### What's open (forcing-function deferrals)

| AD | Forcing function |
|----|------------------|
| AD-574c-ii | DM conversation convergence (full ProfileChatTab refactor — substrate now ready) |
| AD-641g-1-1 | flip executor to `await` ANALYZE results from NATS (depends on AD-641g-1 consumer in production use) |

----

## Era VI — Closing Notes

- **Commercial overlay seam is live.** `runtime.commercial_overlay_loaded` and `/api/system/extensions` expose registry state. `pre_intent_authorization` hook fires on every `IntentBus.broadcast`; default-empty registry means zero overhead.
- **No open OSS issues.** Pre-public-release tracker stays clean unless a regression or a new commercial-tagged AD lands.
- **Roadmap drift convention:** `(SHIPPED, OSS)` for landed entries; `(planned, OSS)` for pending. Reconciliation runs on demand (tracked in BF #465 / AD-696).


