# Overdeck v1 Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use /ship (recommended) or /executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. Audience: AI coding agents first.

**Goal:** One local observability deck for the whole AI OS — Overview (scoreboards + urgent inbox + run forensics + widgets), Inbox (triaged actionables), Decisions (human-in-the-loop queue), Map (infra nodes), plus Plans/Agents/CI/Bots/Limits/Gates pages. Web app + collector daemon + (later wave) deck-tui. Approved mockups are the render oracle: `deck-overview-v2.html`, `deck-decisions-v3.html`, direction-C TUI (copies vendored under `docs/mockups/`).

**Architecture:** Three parts. (1) `collector/` — Bun daemon, one read-only adapter per existing tool (tools stay untouched and authoritative), normalizes into one schema, serves localhost HTTP + SSE. (2) `apps/web/` — Astro 6 + React 18 islands consuming published `@platform-modules/*` (mod-cms consumption pattern) + `packages/deck-ui` for every new component. (3) `collector/actions` — allowlisted action gateway (kill/re-run/answer/rotate); every action journaled. Existing backends are NEVER rewritten — adapters read their files/APIs/sockets as-is.

**Tech Stack:** Bun (collector, `bun test`), Astro 6 + React 18 + Tailwind v4 + `@platform-modules/{ui-primitives,ui-tokens,query-react,realtime-react}` from GitHub Packages, Playwright (webapp-testing toolkit), Rust (deck-tui, consumes `tui-kit` git dep from mega-plan-harness — see that repo's `docs/plans/2026-07-17-tui-kit-extraction.md`).

**Security invariants (every wave):** bind 127.0.0.1 only (tailnet exposure = explicit config, off by default); bearer token auth on collector (generated to `~/.config/overdeck/token`, mode 600); action gateway is default-deny allowlist; no secrets in repo; adapters read-only except the four allowlisted actions.

---

## Wave Plan

| Wave | Tasks | Scope | Safe to parallelize? |
|------|-------|-------|----------------------|
| 0 | S1, S2 | repo scaffold + tokens/theme/shell · collector skeleton | ✅ disjoint |
| 1 | A1, A2, A3, A4 | adapters: harness · systray · gh CI · prometheus | ✅ disjoint files |
| 2 | U1, U2, U3 | Overview · Inbox · Decisions pages + deck-ui components | U1 first, then ✅ |
| 3 | A5, A6, A7, A8, U4, U5 | adapters: agent-guard+cluster · gates · botmaster · **build/offload (A8)** · Map+remaining pages · **CI & Build page (U5)** | ✅ disjoint files |
| 4 | X1 | action gateway | single task |
| 5 | T1 | deck-tui | single task |
| 6 | N1 | alerter + morning digest | single task |
| 7 | V1 | end-to-end verify + GOLIVE audit | single task |

Land mode: local trunk commits per task (solo repo, no remote yet); when a remote is added, switch to PR-gated per global default.

## Normalized schema (the one seam every adapter feeds)

```ts
// collector/src/schema.ts — single source of truth, exported types + zod validators
type Project = { id: string; name: string; color: string /* auto OKLCH assign, override in config */ }
type Severity = "act" | "warn" | "info"
type Item = {            // inbox + decisions share this
  id: string;            // stable per source event (dedupe key)
  source: string;        // adapter id
  project?: string;      // Project.id
  severity: Severity;
  kind: "alert"|"ci"|"halt"|"decision"|"limit"|"gate"|"progress"|"build";
  title: string; detail: string; ts: string;
  actions: ActionRef[];  // ActionRef = { verb: string; args: Record<string,string>; label: string; recommended?: boolean }
  decision?: { question: string; options: { label: string; recommended?: boolean }[]; freeText: boolean; context: string; waitingSince: string };
}
type Panel = { id: string; ts: string; data: unknown /* per-panel typed payload, zod-validated */ }
```

Adapter contract: `poll(): Promise<{ items: Item[]; panels: Panel[] }>` + `interval` + `id`; throwing adapter → marked stale in UI (`age > 2×interval` = stale badge), NEVER crashes the collector. Endpoints: `GET /state` (all panels), `GET /items?kind=`, `GET /events` (SSE: item/panel deltas), `POST /actions/:verb` (wave 4), `POST /decisions/:id/answer` (proxies to source).

---

### Task S1: Web scaffold — theme, tokens, shell, project colors

**Wave:** 0 · **Blocks:** U1–U4 · **Blocked by:** —

**Contract:**
- pnpm workspace: `apps/web`, `packages/deck-ui`. `.npmrc` for `@platform-modules:registry=https://npm.pkg.github.com` (token via env, never committed).
- `apps/web`: Astro 6 + React islands + `ui-tokens` theme-engine. Dark default + light mode, toggle + system-follow; palette pinned from mockup v2 CSS vars (vendor mockups into `docs/mockups/` first — they are the oracle).
- Shell: `DashboardShell` (ui-primitives) with sidebar per mockup v2/v3: Overview · Inbox · Decisions · Map · [Operations] Plans · Agents · CI & Runners · Bots · [Health] Limits & Spend · Gates · Scoreboard · [System] Settings. Badge counts wired to `/items` in W2 (static until then).
- `packages/deck-ui/src/project-colors.ts`: 12-hue OKLCH wheel, assignment order `[0,4,8,2,6,10,1,5,9,3,7,11]` (max hue distance), second rotation shifts lightness/chroma (ΔE ≥ 20 vs first rotation — assert in test); manual override via collector config passthrough.
- Playwright configured (webapp-testing toolkit pattern).

**Acceptance:**
- Run: `pnpm -r typecheck && pnpm -r test && pnpm exec playwright test apps/web/tests/shell.spec.ts 2>&1 | tail -5`
- Expected: PASS — shell renders both themes, sidebar entries exact, project-color test: first 12 assignments pairwise hue-distance ≥ 2 wheel steps.

- [ ] Vendor approved mockups into `docs/mockups/`
- [ ] Scaffold workspace + shell + themes
- [ ] Implement + test project-colors
- [ ] Run acceptance → PASS
- [ ] Commit

### Task S2: Collector skeleton

**Wave:** 0 · **Blocks:** A1–A7, X1 · **Blocked by:** —

**Contract:**
- `collector/` Bun service: schema module (above), adapter registry, poll scheduler (per-adapter interval, jittered), in-memory state + `~/.local/state/overdeck/items.jsonl` append journal (restart-safe dedupe by `Item.id`), HTTP :4980 (127.0.0.1) + SSE, bearer token (auto-generate `~/.config/overdeck/token` mode 600), config `~/.config/overdeck/config.toml` (adapters on/off, intervals, project color overrides, tailnet bind opt-in).
- systemd user unit `packaging/overdeck-collector.service` (Restart=on-failure) + install script; NOT enabled by default.
- Fail-closed startup: unreadable config → exit with named error; missing token file → generate; port busy → exit (no auto-increment; deterministic addr for the web app).

**Acceptance:**
- Run: `cd collector && bun test 2>&1 | tail -5`
- Expected: PASS — fixture adapter round-trip: poll → `/state` + SSE delta; auth: missing or wrong token → 401; stale adapter marked after 2×interval.

- [ ] Write failing tests (fixture adapter)
- [ ] Implement skeleton
- [ ] Run acceptance → PASS
- [ ] Commit

### Task A1: Harness adapter (plans, runs, decisions, forensics)

**Wave:** 1 · **Blocks:** U1, U3 · **Blocked by:** S2

**Contract:**
- Source: mega-plan-harness control API — port from `${HARNESS_HOME}/control-api.port`, token per its auth. Endpoints consumed: `GET /runs`, `GET /runs/:id`, `GET /tasks/:id/*` (activity/prompt/transcript), decisions inbox list, `POST /tasks/:id/steer` + decision answer (proxied, wave-4 gated for steer; decision answers allowed here — they are the Decisions page's purpose).
- Emits: `Panel{id:"plans"}` (runs + waves + HALTs), `Panel{id:"forensics:<runId>"}` (per-run: KPI tile data, phase-attribution buckets, run-lanes segments, per-run table — computed from journal events; bucket taxonomy = Fixer/Implement/gate0/Review/Idle/Other exactly as `docs/reports/2026-07-16-harness-run-analysis.html`), `Item{kind:"halt"}` per unanswered HALT, `Item{kind:"decision"}` per pending decision (with `decision` block filled from journal context).
- Harness down → panels stale, items retained (journal), zero throw.

**Acceptance:**
- Run: `cd collector && bun test src/adapters/harness.test.ts 2>&1 | tail -5`
- Expected: PASS against recorded fixture responses (capture once from live API into `test/fixtures/harness/`): exact bucket sums for a fixture journal; decision Item carries options + context; steer/answer proxy hits mock with token.

- [ ] Capture fixtures from live control API
- [ ] Write failing tests, implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task A2: Systray-ai adapter (limits, spend, forecast)

**Wave:** 1 · **Blocks:** U1 · **Blocked by:** S2

**Contract:**
- Source: systray-ai `HealthStore` JSON snapshot file (locate via its config; read-only, tolerate atomic-rename races by retry-once).
- Emits: `Panel{id:"limits"}` (per-account: percent, status, spend, window) + linear-projection forecast `capEtaMinutes` from last N snapshots (persist ring buffer in collector state); `Item{kind:"limit", severity:"warn"}` when percent ≥ 75 or ETA < 60m (thresholds in config), auto-resolve when back under.
- Stale snapshot (`age_seconds > stale_after_s`) → panel stale flag, no false alerts.

**Acceptance:**
- Run: `cd collector && bun test src/adapters/systray.test.ts 2>&1 | tail -5` → PASS: fixture snapshots 60%→70%→78% over 30m → ETA within tolerance; stale → no Item.

- [ ] Fixtures + failing tests, implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task A3: GitHub CI adapter (runs, runners, queue)

**Wave:** 1 · **Blocks:** U1 · **Blocked by:** S2

**Contract:**
- Source: `gh api` (exec, reuse gh auth): runners + recent runs + queue for repos in config (default: multideal, platform-modules org runner repos). Respect rate budget: one poll cycle ≤ 4 API calls, interval ≥ 60s.
- Emits: `Panel{id:"ci"}` (per-repo latest runs, runner name/status/busy, queue depth); `Item{kind:"ci", severity:"act"}` on newly-failed run (dedupe by run id+attempt), auto-resolve on green re-run.

**Acceptance:**
- Run: `cd collector && bun test src/adapters/ghci.test.ts 2>&1 | tail -5` → PASS on recorded `gh` JSON fixtures; failure Item exactly once per failed attempt.

- [ ] Fixtures + failing tests, implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task A4: Prometheus adapter (host health)

**Wave:** 1 · **Blocks:** U1 · **Blocked by:** S2

**Contract:**
- Source: PromQL over 127.0.0.1:9090 — PSI cpu some, mem runway (node_exporter textfile `system_monitor` metrics), build.slice job count, PROCHOT/freq floor signal, disk free.
- Emits: `Panel{id:"host:<hostname>"}`; `Item{kind:"alert"}` only for: PROCHOT=1, mem runway < 30m, disk < 5% (mirrors netdata health.d thresholds — do not invent new ones).
- Prometheus down → stale panel, no items.

**Acceptance:**
- Run: `cd collector && bun test src/adapters/prometheus.test.ts 2>&1 | tail -5` → PASS on canned PromQL responses.

- [ ] Fixtures + failing tests, implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task U1: deck-ui core components + Overview page

**Wave:** 2 · **Blocks:** U2, U3, U4 · **Blocked by:** S1, A1–A4

**Contract:**
- `packages/deck-ui` components (each: typed props, no fetch inside — data via props; stories/fixtures per component; render oracle = mockup v2): `ScoreCard`, `InboxItem`, `LimitMeter`, `KpiTile`, `PhaseBars`, `RunLanes`, `RunTable`, `KvPanel`, `BotTable`, `SectionCard`, `StaleBadge`. Charts custom (divs/SVG per mockup) — NO chart library dep unless a later task proves need.
- Run-forensics set (`KpiTile`, `PhaseBars`, `RunLanes`, `RunTable`) renders `Panel{forensics}` payload 1:1 with the harness report visuals — THE shared implementation (harness report generator may adopt later; never re-implemented elsewhere).
- Overview page assembles per mockup v2: scoreboard strip (GOLIVE panels — parser adapter inline in collector reading `<repo>/GOLIVE.md` checkbox counts, repos from config), Needs-you-now (top-4 Items by severity/ts), Limits, forensics row, Bots/CI/Plans/Hosts&Gates cards (Bots card renders empty-state until A7). Data via `query-react` + SSE invalidation (`realtime-react` or native EventSource wrapper in deck-ui).
- GOLIVE parser: `Panel{id:"scoreboard"}` — works/total + 7-day trend (git log of GOLIVE.md checkbox flips) + churn flag (commits since last works-delta > threshold).

**Acceptance:**
- Run: `pnpm -r test && pnpm exec playwright test apps/web/tests/overview.spec.ts 2>&1 | tail -5`
- Expected: PASS — against a fixture collector (static JSON server): every mockup-v2 widget present with fixture values; dark AND light pass; stale adapter shows StaleBadge.

- [ ] Component fixtures + failing tests
- [ ] Implement components, then page
- [ ] Run acceptance → PASS
- [ ] Commit

### Task U2: Inbox page

**Wave:** 2 · **Blocks:** — · **Blocked by:** U1

**Contract:** full triage list per mockup B→v2: filter rail (kind counts), severity sort, project color tags, inline `ActionRef` buttons (disabled + tooltip "actions land in wave 4" until X1; decision answers ARE live via A1), snooze (local state, `Item` hidden until ts+duration), empty state = inbox-zero illustration + "nothing needs you".

**Acceptance:** `pnpm exec playwright test apps/web/tests/inbox.spec.ts` → PASS: filters, snooze round-trip, ordering, project colors match settings.

- [ ] Failing specs, implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task U3: Decisions page

**Wave:** 2 · **Blocks:** — · **Blocked by:** U1

**Contract:** per mockup v3 — collapsed `DecisionRow`s (project edge + tag, title, waiting-age with old-threshold color, inline option buttons + free-text field), click title → expand in place (full question, data block, context kvs, journal ref). Answer → `POST /decisions/:id/answer` → A1 proxies to harness inbox; optimistic UI + journal-confirmed state; failure → row error state, answer preserved in input. Sources = HALTs + decisions inbox (A1); gate sign-offs join in A6.

**Acceptance:** `pnpm exec playwright test apps/web/tests/decisions.spec.ts` → PASS: expand/collapse, option answer posts exact payload to mock, free-text answer, failure path keeps text.

- [ ] Failing specs, implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task A5: Cluster + agent-guard adapter

**Wave:** 3 · **Blocks:** U4 · **Blocked by:** S2

**Contract:**
- Sources (all read-only): agent-guard unix socket `/tmp/system-monitor/agent-guard.sock` (events, culprit candidates), `~/.claude/run/buildbox-watch/state`, `~/.claude/run/ci-fallback/{idle,pressure}`, `~/.cache/buildslot` queue dir, `rtk proxy ps` orphan scan (etime>30m ∧ pcpu>80 ∧ agent-class regex from ANNOYANCE_FATIGUE §7 — pin regex in config).
- Emits: `Panel{id:"cluster"}` (debian1 online/offline, autoscaler state, buildslot running/queued/p95), `Panel{id:"agents"}` (fleet by class, CPU, slices), `Item{kind:"alert", severity:"act"}` per orphan candidate with `ActionRef{verb:"reap"}`.

**Acceptance:** `cd collector && bun test src/adapters/cluster.test.ts` → PASS on fixture socket transcript + state files + ps output.

- [ ] Fixtures + failing tests, implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task A6: Gates adapter (security-gate + slopgate)

**Wave:** 3 · **Blocks:** U4 · **Blocked by:** S2

**Contract:** read `security-gate/prevent/confirmed.json` + precision records (paths in config); slopgate: iterate configured repos' `.slopgate/` ratchet baselines → per-repo debt count + 7-day delta (git log of baseline file). Emits `Panel{id:"gates"}`; `Item{kind:"gate"}` for unresolved prevent-band findings and `.warnignore` additions awaiting sign-off (→ Decisions page).

**Acceptance:** `cd collector && bun test src/adapters/gates.test.ts` → PASS on fixture trees.

- [ ] Fixtures + failing tests, implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task A7: Botmaster adapter

**Wave:** 3 · **Blocks:** U4 · **Blocked by:** S2

**Contract:** poll Botmaster's existing API routes (`/api/metrics`, `/api/bots/:id/logs` error counts; base URL + auth in config) — read-only, Botmaster untouched. Emits `Panel{id:"bots"}` (per-bot status/msgs24h/errors/cost) + `Item{kind:"alert"}` on bot down (dedupe, auto-resolve).

**Acceptance:** `cd collector && bun test src/adapters/botmaster.test.ts` → PASS on recorded API fixtures.

- [ ] Fixtures + failing tests, implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task A8: Build/offload adapter

**Wave:** 3 · **Blocks:** U5, X1 · **Blocked by:** S2, offload-robustness R6 (controller status + metrics endpoint)

**Contract:**
- Source (read-only): build controller `GET /status` + Prometheus metrics (offload-robustness R2/R6). NEVER parse `~/.claude/local-gate.log` or buildslot flock files as authoritative state — log = diagnostic payload only.
- Panels: `Panel{id:"offload-control"}` (desired/observed state, revision, fallback-lease, reconciler health, dispatch state, per-host capability); `Panel{id:"cluster-queue"}` (ONE global FIFO — ordered tickets, key/repo, owner pid+starttime, enqueue age, dispatch target, oldest/p95); `Panel{id:"remote-jobs"}` (per job: snapshot, stage, host, rc, pull bytes/duration, artifact-publication state); `Panel{id:"fleet"}` (per host: role, load/cores, running, slots, 24h, dispatch-accept, capability, cpu temp pkg/max/crit).
- Items (`kind:"build"`): `remote-idle-queue-stalled` (queue oldest > SLO ∧ builder capacity idle), `capability-missing` (repeated 126/127 same command+host), `fallback-lease-expired`, `artifact-publication-blocked` (snapshot/CAS mismatch — severity `act`, NEVER auto-promote), `controller-down`, `cpu-temp-critical` (host pkg temp ≥ crit ONLY — no warn-tier noise). Dedupe by incident key; auto-resolve on cleared invariant.
- Controller unavailable → panels stale + single `controller-down` Item. Do NOT synthesize green.

**Acceptance:** `cd collector && bun test src/adapters/offload.test.ts` → PASS on recorded controller `/status` + PromQL fixtures, INCLUDING a replay of the 2026-07-18 incident fixture (idle builder + stalled global queue + 127 storm) producing EXACTLY the `remote-idle-queue-stalled` + `capability-missing` Items with job links; controller-down fixture → one `controller-down` Item, all panels stale; normal-temp fixture emits NO temp Item, crit-temp fixture emits exactly one.

- [ ] Fixtures (incl. incident replay) + failing tests, implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task U4: Map page + remaining pages

**Wave:** 3 · **Blocks:** — · **Blocked by:** U1, A5–A7

**Contract:**
- Map per mockup E→v2: `MapCanvas` + `MapNode` (deck-ui, SVG edges, entity chips from panels: laptop, debian1, GitHub, Cloudflare) + inspector side panel (node kvs + ActionRefs). Node set derives from panels present — no hardcoded hosts.
- Remaining pages, each = filtered panel/table views reusing deck-ui (NO new one-off components): Plans (runs list → forensics per run), Agents (fleet + orphans), CI & Runners, Bots, Limits & Spend, Gates, Scoreboard (full history + churn callouts), Settings (project colors editor writing collector config via `POST /config/projects`, adapter status/intervals).

**Acceptance:** `pnpm exec playwright test 2>&1 | tail -5` → PASS full suite: map renders fixture nodes + inspector; every sidebar page non-empty against fixture collector; settings color override round-trips.

- [ ] Failing specs, implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task U5: CI & Build page + fleet components

**Wave:** 3 · **Blocks:** — · **Blocked by:** U1, A8

**Contract:**
- Render oracle = `docs/mockups/deck-ci-build-v1.html` (the pinned design — same tokens/shell/components as Overview; NO new one-off styling). Sidebar entry `CI & Runners` → `CI & Build`.
- New deck-ui components (typed props, no fetch inside, fixtures per component): `MachineCard` (load meter, running/slots/24h, dispatch-accept pill, capability probes); `MachineDetailModal` (per-core load grid, RAM + swap meters, network up/down, disk `df` table, CPU temp with a critical banner rendered ONLY when pkg ≥ crit; opens on card click, hover → summary popover); `ClusterQueue` (global FIFO table + dispatch target + spill state); `RemoteJobsTable`; `OffloadControl` (state-machine strip + kv rows). Reuse `InboxItem` for the incident feed, `KpiTile` for the offload KPIs.
- Fleet auto-scales: machine set derives from the `fleet` panel — NO hardcoded hosts; a joining host (debian2) renders from its panel entry with an `enrolling` state. Global-queue model: builds sit in one queue dispatched to the least-loaded eligible builder; laptop shown as workstation with spill state (spill engages only when all builders overloaded ∧ queue length > builder count — display only; policy is offload-robustness R4).
- Actions render from ActionRefs on panels/Items and call X1+ verbs (reconcile / drain / restore / quarantine / job-retry / recall-spill) — buttons inert until X1 lands.

**Acceptance:** `pnpm exec playwright test apps/web/tests/ci-build.spec.ts` → PASS: page renders both themes against fixture collector; fleet shows N machines from the panel incl. a joining host; clicking a `MachineCard` opens the detail modal showing per-core load + mem/swap/net/disk/temp; crit-temp fixture shows the banner, normal-temp fixture does NOT; every action button carries a valid X1 verb.

- [ ] Failing spec + component fixtures, implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task X1: Action gateway

**Wave:** 4 · **Blocks:** — · **Blocked by:** U2, A5, A8, offload-robustness R2 (controller transition API)

**Contract:**
- `POST /actions/:verb` — allowlist EXACTLY: `reap` (→ `reaper-ctl kill <pid> --escalate`), `ci-rerun` (→ `gh run rerun <id>`), `steer` (→ harness steer endpoint via A1), `snooze` (collector-local), PLUS the typed offload verbs that PROXY to the build controller transition API (offload-robustness R2): `box-drain`, `box-restore`, `host-quarantine`, `host-unquarantine`, `admission-reconcile`, `job-retry` (typed infra failure only), `ci-reconcile`, `recall-spill`. Anything else → 404. Args zod-validated per verb (pid: int + must appear in current orphan candidates; run id: must exist in ci panel; host/job: must exist in fleet/remote-jobs panel — no arbitrary exec, args NEVER concatenated into a shell string, spawn argv arrays only).
- Offload verbs MUST NOT shell out to edit buildslot files / config / GitHub labels. They call the controller API with `expectedRevision` (stale → 409, no-op), an idempotency key (replayed key → prior result, no re-exec), and a fail-closed audit write (audit-write fails → mutation fails). The controller — NOT the gateway — owns the mutation.
- Every action → append `~/.local/state/overdeck/actions.jsonl` `{ts, verb, args, requestedBy, result, rc}`; UI shows result toast; failure → Item stays with error note.
- Web: enable the inline buttons (U2 + U5) + confirm dialog on `reap`, `box-drain`, `host-quarantine`.

**Acceptance:** `cd collector && bun test src/actions.test.ts && pnpm exec playwright test apps/web/tests/actions.spec.ts` → PASS: non-allowlisted verb 404; pid not in candidates → 400; offload verb with stale `expectedRevision` → 409 no-op; replayed idempotency key → prior result, no second exec; audit-write failure → mutation refused; journal line exact; UI confirm→toast round-trip against mock executor + mock controller.

- [ ] Failing tests (deny paths FIRST), implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task T1: deck-tui

**Wave:** 5 · **Blocks:** — · **Blocked by:** U1, X1, and mega-plan-harness `2026-07-17-tui-kit-extraction.md` KV1

**Contract:**
- `tui/` Rust bin, deps: `tui-kit` (git dep on mega-plan-harness, `Theme::deck()`), reqwest against collector :4980. Views per direction-C mockup: tabs `1:overview 2:inbox 3:decisions 4:plans 5:agents 6:ci 7:limits`, `:` command palette (kit widget; commands = same ActionRef verbs + navigation), status bar (collector health, top severities). Read + act through collector ONLY (no direct source access — one seam).
- Keymap: `j/k` move, `Enter` expand/answer, `y/n` confirm, `?` help — kit overlay.

**Acceptance:** `cargo test --manifest-path tui/Cargo.toml 2>&1 | tail -5` → PASS: projection tests per view on fixture collector JSON; palette executes `reap` against mock gateway.

- [ ] Failing projection tests, implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task N1: Alerter + morning digest

**Wave:** 6 · **Blocks:** — · **Blocked by:** U1

**Contract:**
- Alert rules = severity `act` Items only, desktop notify via existing gdbus notifier pattern (system-monitor `notifier.py` conventions), per-item cooldown, quiet hours in config. Thresholds MUST come from config seeded by the notif-mining results (ANNOYANCE_FATIGUE §7 — `mine-all.prompt` output); no invented thresholds. If mining output absent → alerter ships disabled with a named config error telling how to enable.
- Digest: `GET /digest` + Overview banner at first visit of the day — one generated paragraph (template, not LLM): overnight runs, scoreboard deltas, new failures, waiting decisions.

**Acceptance:** `cd collector && bun test src/alerter.test.ts` → PASS: cooldown, quiet hours, digest string exact on fixture day.

- [ ] Failing tests, implement
- [ ] Run acceptance → PASS
- [ ] Commit

### Task V1: End-to-end verify + GOLIVE audit

**Wave:** 7 · **Blocked by:** all

- [ ] `pnpm -r typecheck && pnpm -r test && pnpm exec playwright test 2>&1 | tail -5` → PASS, zero warnings (`.warnignore` policy applies)
- [ ] `cd collector && bun test 2>&1 | tail -5` → PASS
- [ ] Live smoke on this machine: collector unit started, all adapters green or explainably stale; open web app; walk every sidebar page against LIVE data; answer one real (or staged) decision end-to-end; run one `reap --dry-run`-equivalent staged action.
- [ ] Flip `GOLIVE.md` ACs from observed behavior only; report works/total.

---

## Self-Review (done at authoring)

1. **Mockup coverage:** v2 Overview widgets → U1; Inbox → U2; v3 Decisions + project colors → U3 + S1; Map/E → U4; direction-C TUI → T1; forensics images 1–3 → A1 (data) + U1 (render), single implementation.
2. **Seams:** every adapter emits only `Item`/`Panel` (schema.ts is the sole contract); web + tui consume collector only; harness endpoints external-contract-pinned via dynwf-ux amendment; tui-kit via its K3 example contract.
3. **Reuse:** ui-primitives shell/table/toast/skeleton; ui-tokens themes; query-react; gh auth; existing notifier; tui-kit. New code = adapters, deck-ui components, pages, gateway — nothing existing rewritten.
4. **Waves:** same-wave tasks file-disjoint (adapters = separate files; U2/U3 after U1's components exist).
5. **CI/build control-plane (2026-07-18 offload review):** observability + action = A8 (adapter over the build controller) + U5 (CI & Build page, oracle `docs/mockups/deck-ci-build-v1.html`) + X1 typed infra verbs. The controller, cluster scheduler, and per-job workspaces are OUT of Overdeck scope — read-only boundary held — and live in `~/.claude/docs/plans/2026-07-18-offload-robustness.md` (R2/R3/R4). Overdeck reads controller status + Prometheus metrics (R6), NEVER log/flock files. Cross-plan deps pinned: A8 ← R6, X1+ ← R2. Answers the review's tabletop: today's incident (idle builder behind a stalled global queue, 127 storm, epoch races, wrong spill) is now both a panel and a typed action.
5. **P0.4 budget:** in `GOLIVE.md`.
