# HANDOFF — 2026-09-20 (ON-AIR armable signed-out shipped + pushed; Good Dog ONE-test ruling) ## Branch / Commit State `main` HEAD = **`0c55645` — feat(pills): ON-AIR armable signed-out; Start face reads "Sign In" while armed signed-out (Good Dog 2026-09-20 ruling)** — **PUSHED** to `origin/main` (`5a1993b..0c55645`, non-force). Working tree **clean** (all 6 units of the change committed). This is a continuation of the 2026-09-18 drag-reorder + 2026-09-20 radio-exclusive-pill work: the ON-AIR setter could only light while `IsConnected`, so a signed-out creator found a **greyed pill = dead end**. The ruling ships the armable signed-out pill. ## ✅ What shipped this session (commit 0c55645, pushed) - **ON-AIR pill is intent, not account reality** — `OnAirPillOn` setter is **armable signed-out** (greyed/disabled-until-`IsConnected` gate REMOVED; a pill that could not light offline was the dead end the guard existed to forbid). REC-OR-live radio exclusivity is untouched — arming one clears the other; the radio constraint lives at the pills. - **Start button face is reality-aware**: `PrimaryStartButtonLabel` = `"Start"` while `IsConnected || !OnAirPillOn`, else **`"Sign In"`** when an ON-AIR pill is armed signed-out. Clicking Start while the ON-AIR pill is armed signed-out routes to **sign-in**, not a dead-end go-live (no dead ends — creator ruling; see ai.md §Streaming pills). - **One Good Dog test** (`ytLive.Tests/PillRadioTests.cs`): `PillRadioTests.Arming_One_Output_ Pill_Clears_The_Other` (radio-exclusive) **+** the signed-out armable-pill headless test (`RealAppHost` + temp DB, no browser). - **ai.md** updated in the SAME commit: tooltip wording + the armable-signed-out ruling (2026-09-20) recorded; stale "greyed/disabled until `IsConnected`" fact replaced. ## Deferred / queued (recorded in TASKS.md, NOT folded into this change) - **TASK 37 — defaults vs current layout split**: DB holds static brandable `default` layout; the `current` layout carries the build-id and only loads when it matches `BuildStamp.Id` — enables one-click revert to factory defaults. Files: `TASKS/task-37-defaults-vs-current.md`. - **Manual take on webcam-between-sources order** (cross-type z-order fix `94e114b` already committed/pushed): relaunch, live view, drag Web Resource-0 below the webcam, restart, expect it pinned. Closes the bug with the builder before any further streaming work. - **Build-id-on-exit** demand: still queued for a later slice (do not fold in). ## Critical working rules (failure cap) - **Good Dog = ONE integration test per change.** Do not pile up multiple tests for one guard. - **Scope lock:** declare the exact file list BEFORE editing; run `scripts/scope-check.sh` with the declared list before commit. No "while I'm here" edits. - **Run from WSL** always uses the Windows dotnet host (`/mnt/c/Program Files/dotnet/dotnet.exe`) with **quoted** paths — a path containing a space left unquoted was the #1 tool-spin cause this session. Test filter runs go through `dotnet vstest --filter …` (Windows DLL path), NOT `dotnet test` (WSL re-downloads the windowsdesktop packs + double-restores). - **Never type the username in paths.** Canonical root is `$(pwd)`. Byte-verify every edit anchor (node `fs` on `path.join(R, p)`) — the phantom-tree reads wasted this whole session. - **ai.md + code in the SAME commit**; rewrite HANDOFF.md at session end or interruption. ## Where the machine facts live - `%APPDATA%\ytLive\ytLive.db` = real layout DB (`layout.db` is a 0-byte legacy file). sqlite3 at `/root/android-sdk/platform-tools/sqlite3` (or the SDK copy) for DB checks. - ffmpeg/ffprobe: `/mnt/c/Program Files/Krita (x64)/bin/` with Windows paths. - `ai.md` §Local recording (TASK 18, ≈lines 605-640) = the authoritative pill/state-model doc (record-OR-live, armable signed-out, Start-button-face).