RELEASE
planning/2.8.0/plans/DO-O_PLAN.md
sha256 54bb3980ea839602 · 33056 bytes ·
original held in the private archive
# DO-O — THE LIVING OVERSEER (DIRECTOR UPLINK) · Opus Diagnose/Plan (Protocol 8, stage 1)
> **Branch:** `dev` @ `cce83d2` (verified HEAD). **Version now:** APP_VERSION `2.7.0`, CACHE `robco-terminal-v2.7.0-r43`, **1972 tests / 162 suites** (last = Suite 161). DO-O adds **Suite 162**.
> **This is a PLAN ONLY.** No code was edited. Planning/ is gitignored — nothing here is committed.
> **Owner-approved redesign** under the Protocol 25 site-wide Design-Overhaul exception; the concrete design is the approved `planning/mockups/nv-machine-mockup.html` **DIRECTOR UPLINK** panel + `nv-machine-rationale.md §3`.
---
## 0. What DO-O is (one paragraph)
Turn the Comm-Link (the `.col-right` chat panel) from a plain blue text box into the mockup's **DIRECTOR UPLINK** — a diegetic, *living* Overseer presence: a phosphor **oscilloscope** (`<canvas>`) whose waveform visibly reacts to the real AI/chat lifecycle (calm sine when LISTENING, jagged spikes while ESTABLISHING-LINK/thinking, pulsing while TRANSMITTING, a flat NO-CARRIER trace with no key / AI off), a **SIGNAL · ENCRYPTION · VOX** status strip, the transcript restyled as `> `-prefixed terminal lines, and the existing OVERSEER/TERMINAL pill + TRANSMIT controls beneath it. **It is a presentation layer over the existing chat pipeline** — `appendToChat()` / `transmitMessage()` are *reskinned and hooked*, never forked (Protocol 22). All per-game flavor (colour, relay name, carrier line, state tags, blip bank) is read from `GAME_DEFS[ctx].identity` (Protocol 38) so FO3/FO4 re-skin later with zero feature-code change.
---
## 1. Confirmed anchors (files / functions / selectors — read & verified this session)
| Concern | Location | Notes |
|---|---|---|
| Chat render + typewriter | `appendToChat(text, sender, isHistoryLoad)` — `js/ui-core.js:3793` | `sender` ∈ user/sys/ai. AI branch runs a `setTimeout` typewriter; **reduced-motion + `isHistoryLoad` skip it** (instant `innerHTML`). This is where SPEAKING (start) → LISTENING (end) hooks go. |
| Chat mount | `#chatDisplay` — `index.html:2441` (aria-live polite, aria-atomic false) | Keep as-is. Scope + status strip inserted **above** it. |
| Comm column shell | `.col-right > .panel.chat-panel` — `index.html:2417`–`2642` | Header "> ROBCO COMM-LINK" (`index.html:2429`), PURGE LOGS, image upload, `#chatInput` (2477), `#modeHintPopup` (2490), `#modePill` (2502), tactical dashboard (2517), `#transmitBtn` (2639). |
| AI request lifecycle | `transmitMessage(overrideText)` — `js/api.js:1770` | Order: native-router early-return (1792) → `isFeatureEnabled('aiChat')` gate (1803) → **no-key** gate (1813) → **THINKING window**: `body.classList.add('thermal-load')` + `startThermalLoad()` (1830–1831) → success appends `'ai'` (2000) → **`finally`** resets btn + `stopThermalLoad()` (2108–2116). Error/abort/429/5xx branches (2043–2106) append `'sys'`, never `'ai'`. |
| Feature gate | `window.isFeatureEnabled('aiChat')` (fail-open) | NO-CARRIER trigger. |
| Key read | `_commGet('geminiKey','robco_gemini_key')` — `js/api.js:12` | NO-CARRIER trigger. UI-side use `MetaStore.get('robco_gemini_key')`. |
| Mode pill (B1) | `_renderModePill()`/`toggleInputMode()`/`_updateModeHint()` — `js/ui-core.js:2907/2928/2945`; `submitCommandInput()`/`transmitTerminal()` — `js/api.js:1445/1372` | TERMINAL routes to `transmitTerminal` (no AI, no thermal) → scope simply stays at rest. Keep pill; restyle only. |
| Identity data | `GAME_DEFS[ctx].identity` — `js/state.js:890` (FNV) / `1058` (FO3) / `1237` (FO4 `designOnly:true`); accessor `getIdentity(ctx)` — `js/state.js:1319` (fails safe to FNV) | Has `persona.blipBank`, `voice.loading` (NV: `['ESTABLISHING DIRECTOR LINK…','DECRYPTING TELEMETRY…','CARRIER SYNC…']`), `motionTexture`, `audio`. **No Director-UPLINK flavor strings yet → add an `identity.overseer` sub-block (§4).** |
| Runtime observers | `AmbientRuntime.register({states,tier,onTick,onEnter,onExit})` — `js/runtime.js:103`; states `OFF/COLD_BOOT/READY/ACTIVE/IDLE/STANDBY/SHUTDOWN`; `immersionAllows(tier)` fail-open (`73`). `onEnter/onExit` are **not** tier-gated; only `onTick` is. A3 precedent: `_wireAmbientExperiences()` — `js/ui-core.js:1112`. | Register the Overseer's pause-on-standby + dial-gated life here. |
| Boot wiring | boot phase block `js/ui-core.js:1648`–1667 (`_wireAmbientExperiences()` @1659, `initAmbientRuntime()` @1660) | Add `initOverseerScope()` here (register observer + set initial resting state + arm rAF). |
| Bezel nav sync | `_syncBezelNav(subsystem)` — `js/ui-core.js:2029` (runs on **every** subsystem change: hotkey / click / `#go=` / AI auto-expand); `selectSubsystem('uplink')` — `2085` scrolls+focuses `#chatInput`. | Extend to set `document.body.dataset.subsystem` for the **mobile** view toggle (§5). |
| Grid / layout | `.main-grid` — `css/terminal.css:103` (mobile `minmax(0,1fr)` single col; desktop `380px 1fr` @ `2297`, gated `(min-width:1000px) and (hover:hover) and (pointer:fine)`). `.col-right` has **no `data-tab`** → **always visible on mobile** (`.panel:not([data-tab]){display:block}` @ `832`) = the infinite-scroll problem. Mobile bezel is `position:fixed` bottom dock (`761`), `.container.machine{padding-bottom:100px}` (784). | §5 mobile. |
| Reduced-motion | global `@media (prefers-reduced-motion: reduce){ * { animation-duration:.01ms; animation-iteration-count:1 } }` — `css/terminal.css:3183` | CSS keyframes auto-neutralized. The **canvas rAF must self-gate** on `matchMedia('(prefers-reduced-motion: reduce)')` (JS, like `appendToChat` @3803). |
| `--bezel-wire` token (DO-N) | phosphor default, amber override under `[data-game='FNV']` (`css/terminal.css` DO-N block) | Reuse as the scope trace colour → per-game with **zero JS ctx branch** (Protocol 38). |
**Decision — no new served JS file.** All Overseer code lands in **`js/ui-core.js`** (co-located with `appendToChat` + the existing `AmbientRuntime` observers + `switchTab`/`selectSubsystem`/mode-pill). This avoids a new `sw.js` ASSETS entry, a new `index.html` script tag, new Suite 56 boot-order guards, and load-order risk — and matches the A3 ambient-experiences precedent. `api.js` calls one thin global (`window.setOverseerState?.(…)`) — the api→ui boundary stays a function call (Protocol 23).
---
## 2. The oscilloscope component
**Element:** a single `<canvas id="overseerScope" aria-hidden="true">` inside a new `.scope-wrap`, inserted at the **top of `.chat-panel`** (above `#chatDisplay`), under a restyled `.ovs-head` (lamp + "DIRECTOR UPLINK" + relay line) and above a `.scope-meta` status strip. Decorative → `aria-hidden` (the transcript keeps the polite live region; screen-reader UX is unchanged).
**Render model** (ported from the mockup `drawScope()`/`scopeLoop()`, `nv-machine-mockup.html:1495`–1560, into a small module object `OverseerScope` in ui-core.js): DPR-aware sizing, a faint graticule, one amber trace whose per-x `y` is computed differently per state. Trace colour = `getComputedStyle(document.documentElement).getPropertyValue('--bezel-wire')` (read once per state-change/game-change, cached) → FNV amber, FO3/FO4 phosphor, **no JS colour branch**.
**Visual STATES** (module var `_scopeState`, plus tag text from `identity.overseer.states`, §4):
| State | Trigger (exact code event) | Waveform | Tag (NV) |
|---|---|---|---|
| `listening` (IDLE/LISTENING) | Resting state when a key is present, `aiChat` enabled, `navigator.onLine`. Set on boot, after `finally` (if state was `thinking`), on typewriter completion, on carrier refresh. | slow warm sine + rare scratch | `[ LISTENING ]` |
| `thinking` (ESTABLISHING-LINK) | `transmitMessage` **after** the native-router/flag/key gates pass, at the `thermal-load` add (`api.js:1830`) → `setOverseerState('thinking')`. Also each retry. | jagged random spikes | `[ ESTABLISHING DIRECTOR LINK ]` |
| `speaking` (TRANSMITTING/RECEIVING) | `appendToChat(text,'ai')` when **`!isHistoryLoad`** — set at typewriter **start** (and in the reduced-motion/instant branch). | pulsing envelope | `[ TRANSMITTING ]` |
| `disabled` / `no-carrier` | Resting refresh finds **no key** OR `isFeatureEnabled('aiChat') === false`. | flat low-amplitude line | `[ NO CARRIER ]` |
| `offline` | Resting refresh finds `navigator.onLine === false`. | flat line | `[ OFFLINE ]` |
**Transitions are deterministic (no reliance on thermal alone):**
- `speaking` → `listening` at the typewriter **completion callback** (`appendToChat`, end of `typeWriter()`); and immediately in the reduced-motion/`isHistoryLoad`-false instant branch.
- `transmitMessage` **`finally`**: reset to resting **only if `getOverseerState()==='thinking'`** (request ended with no AI reply — error/abort/429/native/missing-narrative). If it is `speaking`, leave it — the typewriter callback owns the reset. (Critical: `finally` runs *after* `appendToChat('ai')` has kicked off the async typewriter, so a blind reset would truncate SPEAKING.)
- Resting state (`listening` vs `disabled` vs `offline`) is computed by a **pure helper `_overseerRestState({hasKey, aiEnabled, online})`** — vm-testable (§9), no canvas dependency.
**Animation gating (`_scopeShouldAnimate()`), re-checked each frame:**
`!reducedMotion && immersionAllows('balanced') && _runtimeAwake && !document.hidden`.
- True → compute + draw + `requestAnimationFrame(loop)`.
- False → draw **one static frame** for the current state and **stop** the loop; re-arm on the wake signals (`visibilitychange` visible, runtime `onExit` STANDBY/OFF, immersion-dial change, and any `setOverseerState()`).
- `setOverseerState()` **always draws one frame** immediately (so reduced-motion / minimal-dial users still see the correct resting/thinking/speaking frame), then arms the loop iff `_scopeShouldAnimate()`.
**Runtime observer (registered in `initOverseerScope`, A3 pattern):**
`AmbientRuntime.register({ id:'overseer-scope', states:['STANDBY','SHUTDOWN','OFF'], onEnter: pause+flat-frame (sets _runtimeAwake=false), onExit: resume (sets _runtimeAwake=true, re-arm) })`. `onEnter/onExit` are **not** tier-gated (always pause on real power-down). The **dial** gate lives in `_scopeShouldAnimate()` via `immersionAllows('balanced')` — at Minimal immersion the scope shows a static resting frame (matches "balanced-tier observer" intent) without a bespoke carve-out.
**Idle-life blips (OPTIONAL — see Q4).** If included: a low-rate (`cadenceMs ~30–45s`), `tier:'balanced'`, `states:['ACTIVE','IDLE']` observer `onTick` that emits one line from `getIdentity().persona.blipBank` via **`appendToChat(blip,'sys', /*isHistoryLoad*/ true)`** — the `true` flag means it renders but is **never pushed to `chatHistory`/`robco_chat`** (so it doesn't bloat the saved log or reload as fake history). Reduced-motion/minimal → the tier gate already suppresses it. Recommend shipping it **behind Q4's answer**; core DO-O works without it.
---
## 3. The diegetic chat presence (reskin — Protocol 22, do NOT fork)
- **Header** `.ovs-head`: pulsing amber lamp + `DIRECTOR UPLINK` + right-aligned `identity.overseer.relay` (NV: `LUCKY 38 RELAY · 0.417 MHz`). Replaces the plain "> ROBCO COMM-LINK" `<h2>` (keep PURGE LOGS, restyled).
- **Status strip** `.scope-meta`: `identity.overseer.signalStrip` (NV: `SIGNAL ▂▄▆_ 72% · ENCRYPTION: TRI-NODE · VOX MODULE: NOT INSTALLED (OPT-IN)`). VOX stays an honestly-labeled deferred opt-in — **no VOX/TTS is built in DO-O.**
- **Transcript styling:** `#chatDisplay` is unchanged structurally; CSS restyles `.msg-user`/`.msg-ai`/`.msg-sys` inside the UPLINK panel as `> `-prefixed terminal lines (user = phosphor, ai = amber/`--bezel-wire`, sys = dim). The `> ` prefix for user lines already exists in `appendToChat` (`> ${displayUserText}`); AI lines get the amber treatment via CSS only. **No change to escaping/`escapeAndFormat` (XSS contract intact).**
- **Input row:** `#modePill`, `#chatInput`, `#modeHintPopup`, `#transmitBtn`, image upload, and the whole tactical dashboard stay functionally identical; amber/`--bezel-wire` restyle only.
---
## 4. Per-game identity consumption (Protocol 38 / DO-K)
Add an **`identity.overseer`** sub-block to `GAME_DEFS[ctx].identity` (the sanctioned per-game design-data home, DO-K) for **all three** entries + FO4:
```
overseer: {
title: 'DIRECTOR UPLINK', // panel heading
relay: 'LUCKY 38 RELAY · 0.417 MHz', // FNV; FO3 'GNR RELAY …'; FO4 stub
signalStrip: 'SIGNAL ▂▄▆_ 72% · ENCRYPTION: TRI-NODE · VOX MODULE: NOT INSTALLED (OPT-IN)',
states: { listening:'[ LISTENING ]', thinking:'[ ESTABLISHING DIRECTOR LINK ]',
speaking:'[ TRANSMITTING ]', disabled:'[ NO CARRIER ]', offline:'[ OFFLINE ]' },
greeting: '▸ EVENING, COURIER. The Director is on the wire. Transmit when ready.' // optional boot line
}
```
- FNV populated richly (Lucky-38/Mojave fiction from the mockup). FO3 = sensible Vault-Tec/GNR stub. FO4 = stub that **validates** (keeps `designOnly:true` reachability rules intact).
- Feature code reads `getIdentity().overseer` with a **generic fallback** object (so a game without the block never borrows another game's fiction — Protocol UI-7). `getIdentity()` already fails safe to FNV; add a literal generic default in ui-core.js for the missing-`overseer` case (`{title:'COMM UPLINK', relay:'CARRIER LINK', signalStrip:'SIGNAL — · ENCRYPTION: TRI-NODE', states:{…generic…}}`).
- **Colour is NOT in identity** — it stays `--bezel-wire` (CSS `[data-game]`), so there is no JS colour branch.
- **Loading lines:** reuse existing `identity.voice.loading` for the transcript "thinking" leader lines (`> DECRYPTING TELEMETRY…`) that appear during `thinking` and are removed when the reply lands — mirrors the mockup `THINK_LINES`. (Already present per game; nothing to add there.)
- **Schema note:** Suite 157 validates `identity` completeness → its contract must be **extended** to require `overseer` on FNV/FO3/FO4 (add to the DO-K completeness assertion), or DO-O adds its own coverage in Suite 162 and leaves 157 as-is. Recommend **extending Suite 157** minimally (one `overseer`-present assertion per game) + full field coverage in Suite 162.
---
## 5. Layout — desktop / mobile (Protocol 10, 17; 0i/0g)
**Desktop (≥1000px, hover+fine):** the comm column (`.col-right`, currently the wide `1fr` track) already renders permanently under the glass — the Director is *always present* (mockup §5 "permanent column"). DO-O inserts the scope/head/strip at the top of `.chat-panel`; `#chatDisplay` keeps `flex-grow:1; overflow-y:auto` so the transcript scrolls internally and the input stays pinned. **No change to the `380px 1fr` track widths** (that's DO-N/layout territory and risky under Protocol 25 — see Q1). UPLINK `[4]` keeps its current desktop behavior (pulse + focus input).
**Mobile (<1000px) — the fix for the infinite scroll (0i):** introduce a root signal `document.body.dataset.subsystem` written by `_syncBezelNav()` (already the single choke point for every subsystem change). CSS in the existing `@media (max-width:999.98px)` block then makes UPLINK a **self-contained view**:
- `body[data-subsystem="uplink"]`: `.col-left{display:none}`; `.col-right{display:flex; flex-direction:column}` bounded to the viewport minus casing/bezel; `#chatDisplay{flex:1; overflow-y:auto}` → transcript scrolls **inside** the view, no page-length growth.
- `body:not([data-subsystem="uplink"])`: `.col-right{display:none}` (kills the always-on comm column that caused the scroll) and a compact **carrier strip** `.carrier-strip` (mini CSS-keyframe waveform + `[ LISTENING ]`, `onclick=selectSubsystem('uplink')`) is shown pinned at the top of the glass — the Overseer never fully leaves (mockup §3). The strip's `.cs-state` mirrors `_scopeState`'s tag via `setOverseerState()`.
- The mini-wave animation is pure CSS keyframes → auto-neutralized under reduced-motion.
- `selectSubsystem('uplink')` on mobile now shows the view (via the attribute) + focuses `#chatInput`; on desktop the attribute has no display effect (desktop @media doesn't hide either column) → **desktop unchanged.**
**Carrier-strip markup:** one small element added once inside `.glass-frame` (or top of `.main-grid`), shown only via the mobile media query. It is **not** `position:fixed` (avoids the DO-N fixed-descendant-vs-transformed-casing caveat from Suite 160) — it sits at the top of the scrollable glass.
**Known DO-N interaction to respect:** three ambient body states (idle-dim / night / shutdown) put `filter/transform` on `.container.machine`, which makes it the containing block for `position:fixed` descendants (Suite 160 note). The carrier strip is **not** fixed, so it's unaffected. The scope canvas is inside normal flow — also unaffected.
---
## 6. PROTOCOL-8 PLAN AUDIT — every entry path × state × edge (intended behavior)
| # | Entry path / state | Intended DO-O behavior |
|---|---|---|
| E1 | **Cold boot, key present, AI on, online** | `initOverseerScope()` → resting `listening`; rAF animates if dial≥balanced & motion on, else static frame. Greeting/history render in transcript unchanged. |
| E2 | **Cold boot, no key** | resting `disabled` / `[ NO CARRIER ]`, flat trace. App fully usable; typing in OVERSEER mode → existing no-key `sys` message; scope stays `disabled`. |
| E3 | **Cold boot, `aiChat` kill-switched off** | resting `disabled`; `transmitMessage` early-returns at the flag gate → scope never enters `thinking`. Fail-open: if the flag read is unavailable/throws, `isFeatureEnabled` returns true → treated as enabled (Protocol 33). |
| E4 | **Offline (navigator.onLine false)** | resting `offline` / `[ OFFLINE ]`. Recomputed on `online`/`offline` window events → flips to `listening`/`disabled` live. |
| E5 | **Reload with saved chat history** | `appendToChat(...,'ai', isHistoryLoad=true)` for each restored line → **does NOT set `speaking`** (guarded on `!isHistoryLoad`); scope stays at computed resting state. |
| E6 | **Send in OVERSEER mode (idle→request)** | native-router miss → flag+key pass → `thinking` at thermal add → (leader lines from `voice.loading`) → reply `appendToChat('ai')` → `speaking` (typewriter) → `listening` at completion. |
| E7 | **Send that hits native router** (e.g. `[THREAT]`) | `transmitMessage` returns before thermal → scope **unchanged** (stays resting). Correct: deterministic command, not a Director transmission. |
| E8 | **Send in TERMINAL mode** | `submitCommandInput` → `transmitTerminal` (no AI, no thermal) → scope **unchanged** (resting). Pill green; Overseer present but quiet. |
| E9 | **`/` or `@` one-off override** | routing resolved by `_resolveCommandInput` (unchanged). `@`→overseer path behaves like E6; `/`→terminal path like E8. Scope follows whichever pipeline actually runs. |
| E10 | **Error / abort / 429 / 5xx** | branches append `'sys'`, never `'ai'` → scope still `thinking` at `finally` → reset to resting (`listening`/`disabled`/`offline`). Retry (5xx/429): thermal restarts → `thinking` again. |
| E11 | **Reply with modal but no narrative** | no `appendToChat('ai')` → `thinking` at `finally` → reset to resting. |
| E12 | **Image upload (VATS scan)** | thermal path → `thinking` (scope) alongside the existing `.vats-scanning` overlay; reply → `speaking` → `listening`. |
| E13 | **Reduced-motion ON** | rAF never schedules; `setOverseerState` draws exactly one static frame per state. Mini-wave / tag-blink / SWEEP CSS keyframes neutralized by the global block. Typewriter already instant (existing `appendToChat` behavior). |
| E14 | **Immersion dial = Minimal** | `_scopeShouldAnimate()` false → static resting/thinking/speaking frames (no continuous animation). Balanced/Full → animates. Live re-evaluated (dial change re-arms). |
| E15 | **Runtime STANDBY / SHUTDOWN / OFF** | observer `onEnter` → pause rAF + flat frame; `onExit` (WAKE→ACTIVE) → resume + resting. Works with the A3 shutdown/standby experiences (no conflict — different concerns). |
| E16 | **Tab hidden (visibilitychange)** | rAF paused; resumes visible. (Battery/perf.) |
| E17 | **Desktop vs mobile** | Desktop: Director column always present, UPLINK pulses+focuses. Mobile: UPLINK = self-contained view; other subsystems show carrier strip; no infinite scroll. Verified 360/412/desktop. |
| E18 | **Mode pill toggled mid-session** | pill restyle only; scope state machine is pipeline-driven, independent of pill → no special handling. |
| E19 | **Game switch (FNV→FO3) or FO4 designOnly** | `getIdentity().overseer` re-read on `onGameContextChange` (which reloads) → per-game strings; `--bezel-wire` via `[data-game]` → per-game colour. FO4 unreachable at runtime (designOnly) but its `overseer` block validates. Missing-block game → generic fallback (no borrowed fiction). |
| E20 | **PWA installed / offline / standalone** | Only CSS + a canvas + hooks in existing served files change; no SW logic, no manifest, no new document/route → installable/offline/standalone intact. `#go=comm` deep-link still routes (unchanged `SHORTCUT_ROUTES.comm`, now also sets `data-subsystem` via `_syncBezelNav('uplink')`). |
| E21 | **No campaign-state write** | Scope state is a transient module var; idle-blips use `isHistoryLoad=true` (no persist). No `saveState`/`robco_v8`/`state.*` write anywhere in DO-O. (If Q3's optional scope-off pref is adopted, it is a **device** pref in `META_MANIFEST` + Suite 134 — not campaign state.) |
---
## 7. Exact change set (files · rationale)
1. **`index.html`** (`.chat-panel`, ~2417–2445): restructure the header into `.ovs-head` (lamp + DIRECTOR UPLINK + relay span) + insert `.scope-wrap > canvas#overseerScope[aria-hidden] + .scope-tag#scopeTag + .scope-meta#scopeStrip` above `#chatDisplay`; add one `.carrier-strip` element (mobile) inside `.glass-frame`. Keep every existing control/id. *Served file → cache bump.*
2. **`css/terminal.css`**: `.ovs-head/.scope-wrap/#overseerScope/.scope-tag/.scope-meta/.carrier-strip/.mini-wave` (ported + adapted from mockup §3 CSS, `nv-machine-mockup.html:288`–439); amber trace via `--bezel-wire`; UPLINK-scoped `.msg-*` restyle; mobile `body[data-subsystem="uplink"]` view rules + carrier-strip visibility in the existing `@media (max-width:999.98px)` block; desktop unchanged. Tag-blink/mini-wave are CSS keyframes (reduced-motion-safe). *Served file → cache bump.*
3. **`js/ui-core.js`**: add the `OverseerScope` object — `initOverseerScope()`, `setOverseerState(s)`, `getOverseerState()`, `_overseerRestState({hasKey,aiEnabled,online})` (pure), `refreshOverseerCarrier()`, `_scopeShouldAnimate()`, `drawScope()`/`_scopeLoop()`, the runtime observer, and (optional) the idle-blip observer. Hook **`appendToChat`** (SPEAKING at AI typewriter start; LISTENING at completion; guarded `!isHistoryLoad`). Extend **`_syncBezelNav`** to set `document.body.dataset.subsystem`. Call `initOverseerScope()` + `refreshOverseerCarrier()` in the boot block (~1659) and after key save/validation; add `online`/`offline`/`visibilitychange` listeners. *Served file → cache bump.*
4. **`js/api.js`**: two thin calls — `window.setOverseerState?.('thinking')` at the thermal-load start (`~1830`); in `finally` (`~2108`) `if (window.getOverseerState?.() === 'thinking') window.setOverseerState('listening'→refresh)`. Optionally call `refreshOverseerCarrier()` after key save in `saveApiKey*`. *Served file → cache bump.*
5. **`js/state.js`**: add `identity.overseer` to FNV/FO3/FO4 identity blocks. *Served file → cache bump.*
6. **Cache bump** `sw.js`: `robco-terminal-v2.7.0-r43` → **`-r44`** (served files changed; APP_VERSION stays 2.7.0 — this is `[Unreleased]` Design-Overhaul work, MINOR user-facing feature; bump `APP_VERSION`→`2.8.0`? **No** — the whole DO program has been landing under `2.7.0`/`[Unreleased]` per the changelog; keep `2.7.0`, rev cache. See Q5.)
7. **Docs** (Protocol 2): `CHANGELOG.md` `[Unreleased]`, `ARCHITECTURE.md` (Overseer/DIRECTOR-UPLINK section + identity.overseer), `README.md` currency.
8. **Tests** (§9) + Protocol 2a count sync everywhere.
---
## 8. Protocol touchpoints
- **Protocol UI-10 (Overseer Presence)** — **DO-O adopts it.** This plan defines the standard: the Overseer is a canvas presence over the existing chat, driven by the real AI lifecycle + runtime, game-agnostic via `identity.overseer`, reduced-motion-degrading, PWA-safe, zero campaign write. Add the protocol text to `CLAUDE.md` (owner-approved batch) alongside this unit.
- **Protocol UI-9 (Motion-Verb Grammar)** — scope animation is a JS-gated rAF (not a CSS verb), but the mini-wave/tag-blink are plain CSS keyframes caught by the global reduced-motion block (the UI-9 "reduced-motion-for-free" property). No bespoke carve-out.
- **Protocol UI-7 (Device Chrome)** — flavor text via identity + `[data-game]`, colour via `--bezel-wire`, never a JS ctx branch. PWA-intact guardrail (E20).
- **Protocol 38 (game-agnostic)** — all per-game strings from `getIdentity().overseer`; static guard: no `'Lucky 38'`/`'0.417'`/`'Mojave'` literal in ui-core.js/api.js.
- **Protocol 22 (extend, don't fork)** — reskin `appendToChat`/comm panel; reuse `AmbientRuntime`, `_syncBezelNav`, `openModal`; idle-blips reuse `appendToChat(...,true)`. No `renderChatV2`/second pipeline.
- **Protocol 23/24 (boundaries / AI determinism)** — api→ui via a global hook; AI contract (`_validateTriNode`, Tri-Node) untouched; scope is decorative and never a state source.
- **Protocol 10/17 (UI verification / mobile baseline)** — render at 360/412/desktop; `render-check.mjs`; ≥16px inputs, ≥28px targets, no h-overflow.
- **Protocol 25 (UX stability)** — owner-approved Design-Overhaul exception; same panel location; TERMINAL/OVERSEER unchanged; DIRECTORY fallback + real labels preserved.
- **Protocol 4** — **not triggered** (no new campaign-state field). `test.html` KNOWN_KEYS/import contract **unchanged** → no `test.html` edit required (Protocol 40). If Q3's device pref is adopted → `META_MANIFEST` + Suite 134 (device-pref, not Protocol 4).
---
## 9. Test plan (Suite 162 — both runners at exact parity, Protocol 15)
New **Suite 162 — "DO-O: the living Overseer (DIRECTOR UPLINK)"** in `tests/robco-diagnostics.js` **and** `.ps1` (identical suites/titles/per-suite counts). Mix of static source guards (Protocol 20) + one vm behavioral test:
1. `#overseerScope` canvas exists in index.html **and is `aria-hidden`** (decorative; transcript keeps aria-live).
2. `.scope-meta`/status-strip + `.ovs-head` (DIRECTOR UPLINK) present; `#chatDisplay` still aria-live polite (unregressed).
3. `setOverseerState`/`getOverseerState`/`_overseerRestState`/`initOverseerScope` defined in ui-core.js; state constants include listening/thinking/speaking/disabled/offline.
4. Wiring: `transmitMessage` sets `thinking` at the thermal window; `finally` resets only from `thinking`; `appendToChat` sets `speaking`/`listening` guarded on `!isHistoryLoad`.
5. Reduced-motion: `_scopeShouldAnimate` reads `matchMedia('(prefers-reduced-motion: reduce)')` and gates the rAF (no unconditional `requestAnimationFrame`).
6. Runtime observer registered for `['STANDBY','SHUTDOWN','OFF']` with onEnter/onExit; dial gate via `immersionAllows('balanced')`.
7. Protocol 38: **no** `Lucky 38`/`0.417`/`Mojave` literal in ui-core.js/api.js; scope strings sourced from `getIdentity().overseer`; generic fallback present.
8. `identity.overseer` block validates on FNV **and** FO3 **and** FO4 (title/relay/signalStrip/states) — extends the DO-K identity contract.
9. Mobile: `_syncBezelNav` sets `document.body.dataset.subsystem`; CSS has `body[data-subsystem="uplink"]` view rules + `.carrier-strip`; carrier strip is **not** `position:fixed` (Suite 160 caveat).
10. Save boundary: **no** `saveState(`/`robco_v8`/`state.` write inside the Overseer code (grep-guard); idle-blip path (if present) uses `appendToChat(…, true)`.
11. **Behavioral (Node vm, Suite 133/150 technique):** load the pure `_overseerRestState()` + tag lookup and assert the truth table — {key,on,online}→listening; {no key}→disabled; {flag off}→disabled; {offline}→offline — and that tags come from a per-game `overseer.states` map (feed a mock identity). (Canvas rAF can't run in vm → static-guarded; the live Playwright boot-smoke/render-check exercises the real canvas.)
**Count sync (Protocol 2a):** update the hardcoded count everywhere in the CLAUDE.md/RULES.md/README/ARCHITECTURE/CHANGELOG-Unreleased-header/both runners' per-suite comments/`test.html Suites:` marker table. New total = `1972 + N(162)`. Run the `Select-String` drift scan before commit.
**Playwright (Protocol 10/36a, run outside gate:fast):** `render-check.mjs` at 360/412 (no h-overflow, no focus-zoom on the UPLINK view + carrier strip); boot-smoke (zero console errors, canvas present, no black screen). **Escape-ratchet (Protocol 36b):** if the live send-reaction can't be asserted statically, add a boot-smoke assertion that `setOverseerState('thinking')` flips `#scopeTag` text.
---
## 10. Verification steps (stage-2 must actually perform)
1. Run at **360 / 412 / ≥1000px**: `document.documentElement.scrollWidth === innerWidth` at all three; UPLINK mobile view self-contained (transcript scrolls internally, page does not grow); carrier strip present in other subsystems and tappable → UPLINK.
2. **Real send** (valid key): watch `#scopeTag` go `[ LISTENING ]` → `[ ESTABLISHING DIRECTOR LINK ]` → `[ TRANSMITTING ]` → `[ LISTENING ]`; waveform visibly changes per state.
3. **No-key** and **kill-switch-off**: scope reads `[ NO CARRIER ]`, flat trace, app fully usable.
4. **Reduced-motion** (`prefers-reduced-motion`) and **Minimal dial**: static frame per state, no continuous animation; still legible.
5. **Standby/shutdown → wake**: scope pauses then resumes.
6. **PWA**: install prompt still works; offline load still boots; standalone launch shows the Overseer.
7. Full gate green (lint 0-warn, prettier, both runners at parity, boot-smoke, render-check, a11y baseline, test.html audit).
---
## 11. Summary + OPEN DESIGN QUESTIONS (for the owner — I did NOT use AskUserQuestion)
**Summary.** DO-O reskins the existing Comm-Link into the mockup's DIRECTOR UPLINK: a `<canvas>` oscilloscope whose waveform is driven by named states hooked to the *real* AI lifecycle (thinking at the thermal-load window, speaking during the typewriter, listening at rest, no-carrier when key/flag absent, offline when the browser is), a per-game status strip + relay header sourced from a new `identity.overseer` block (Protocol 38, `--bezel-wire` colour via `[data-game]`), the transcript restyled as terminal lines, and a mobile carrier-strip + self-contained UPLINK view that kills the infinite scroll. All in existing served files (no new JS module), zero campaign writes, reduced-motion + dial + standby gated, PWA-intact. Adopts Protocol UI-10. ~11–15 tests in Suite 162, both runners, plus render-check/boot-smoke. Cache → r44.
**Open questions:**
1. **Desktop column arrangement.** The mockup shows the Director as a *fixed ~390px right* column with subsystems taking the wide track; the **shipped** DO-N desktop grid is the reverse (`380px` left panels, Comm-Link the wide `1fr` right). DO-O reskins in place and does **not** re-swap the track widths (out-of-scope/risky). Ship in-place now and revisit widths in a later layout unit — agreed? Or do you want the 390px-Director swap folded into DO-O?
2. **UPLINK `[4]` on desktop.** Keep current behavior (pulse the always-visible column + focus input), matching rationale Q4? Or make desktop `[4]` open a full UPLINK view? (Recommend: keep current — the Director is already always visible on desktop.)
3. **Optional "disable oscilloscope" device pref** (accessibility/perf, `META_MANIFEST` + Suite 134)? Reduced-motion already yields a static frame, so I lean **no pref** for DO-O to keep surface minimal — confirm.
4. **Idle-life blips** (unprompted templated `identity.persona.blipBank` lines in the transcript, non-persisted, tier-gated). Include in DO-O, or defer to DO-W? They add "living" feel but also unprompted transcript noise. (Recommend: include, low-rate, behind the dial.)
5. **Version/cache.** Keep `APP_VERSION 2.7.0` under `[Unreleased]` and bump only the cache (`-r44`), consistent with every prior DO-* unit — correct? (No `APP_VERSION` bump unless you want to cut 2.8.0.)
6. **VOX line.** Confirm VOX stays a *labeled, non-functional* "NOT INSTALLED (OPT-IN)" chip in DO-O (no TTS built) — the opt-in speech module is a later unit.
7. **`identity.overseer` schema in Suite 157.** OK to extend the DO-K identity-completeness assertion to require the new `overseer` block on all three games (keeps one source of truth), with full field coverage in Suite 162?
STAMP · generated for RELEASE v2.8.5 commit 06e5180 (06e51801b38a) · archive input-tree hash c07fbfbdd2e1ddeb · 754 files · no wall-clock timestamp (regenerates identically when nothing changed).