{"type":"meta","slug":"overdeck-build","topic":"Overdeck CI/Build control-plane: A8 build/offload adapter (reads the build-controller GET /status + Prometheus metrics), collector wiring, and the U5 CI & Build page + fleet components. Full contracts in docs/plans/2026-07-17-overdeck-v1.md Tasks A8 and U5; the external controller /status + metrics shape is pinned in ~/.claude/docs/plans/2026-07-18-offload-robustness.md Tasks R2 and R6. Base = overdeck-v1 landed on main at 61b814e.","created":"2026-07-18","exec_mode":"ship","scheduler":"dag-parallel","land_mode":"merge-to-main","base_branch":"main","gate0_mode":"strict","preset":"codex-cursor"}
{"type":"task","id":"a8","wave":1,"seat":"coder","tier":"medium","deps":[],"files":["collector/src/schema.ts","collector/src/adapters/offload.ts","collector/src/adapters/offload.test.ts","collector/test/fixtures/offload/"],"desc":"Read docs/plans/2026-07-17-overdeck-v1.md Task A8 (Build/offload adapter) for the full contract. STEP 1 — in collector/src/schema.ts add exactly one member \"build\" to the KindSchema z.enum (the collector package has NO exhaustive Record<Kind> consumer, verified, so widening the enum is safe here; do NOT touch the web app's separate Kind union in apps/web/src/lib/collector-types.ts — that is task u5's scope). STEP 2 — create collector/src/adapters/offload.ts exporting createOffloadAdapter(opts) implementing the Adapter contract in collector/src/adapter.ts (id \"offload\", interval, poll). Sources are READ-ONLY over HTTP on 127.0.0.1: the build controller GET /status plus its Prometheus metrics. The EXACT /status shape ({desired, observed, revision, lease, capacity, hosts}; per-host states available|draining|maintenance|restoring|degraded) and the metric series (cluster queue depth + oldest-age + p95; per-host running/slots/dispatch-accept; per-job stage; 126/127 counts by host+command; epoch/CAS-mismatch count; pull-back bytes+duration; fallback-lease state; per-host load, per-core %, mem, swap, net up/down, disk df, cpu temp pkg/max/crit) are pinned in ~/.claude/docs/plans/2026-07-18-offload-robustness.md Tasks R2 and R6 — READ that file by its absolute path BEFORE recording fixtures; do NOT invent the shape. NEVER parse ~/.claude/local-gate.log or buildslot flock files as authoritative state — the log is diagnostic payload only. Inject the fetch implementation via the FetchLike type exported from collector/src/adapter.ts so tests pass a mock — do NOT call global fetch directly; model this on collector/src/adapters/harness.ts (injected fetch) and collector/src/adapters/prometheus.ts (PromQL query shape). Emit 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 with 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). Emit Item{kind:\"build\"}: remote-idle-queue-stalled (queue oldest > SLO AND 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). Items that offer remediation carry ActionRef entries whose verb is an offload verb (box-drain, host-quarantine, job-retry, recall-spill, admission-reconcile, ci-reconcile) — ActionRef.verb is a free string in the schema, these are inert until task X1 lands, so emit them but do NOT add any verb enum/gate. Dedupe by incident key. DECLARATIVE RECONCILIATION CONTRACT — read collector/src/adapter.ts and collector/src/state.ts BEFORE writing any code: poll() returns this adapter's COMPLETE CURRENT snapshot; an item present means the condition holds NOW; an item absent from a SUCCESSFUL poll is auto-resolved by the reconciler (tombstoned). NEVER write emit-once / suppress / seen-set / already-reported logic — under this contract that is a BUG. controller-down is a FIRST-CLASS MODELLED observation, NOT a read failure: when the controller is unreachable the poll MUST still return a complete snapshot — panels marked stale per the state.ts staleness mechanism (read it) plus exactly one controller-down Item — and MUST NOT synthesize green. Reserve throw-to-retain for an OTHER, unexpected read/parse error on an otherwise-reachable source (a failed poll retains all prior state); NEVER swallow such an error and return a partial snapshot, which would falsely resolve live items. Do NOT edit collector/src/adapters/index.ts or collector/src/config.ts — registration is task w2's scope. Record fixtures into collector/test/fixtures/offload/. Acceptance: cd collector && bun test src/adapters/offload.test.ts — expect PASS 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; a controller-down fixture producing exactly one controller-down Item with all panels stale; a normal-temp fixture emitting NO temp Item and a crit-temp fixture emitting exactly one cpu-temp-critical Item. Commit only: collector/src/schema.ts, collector/src/adapters/offload.ts, collector/src/adapters/offload.test.ts, collector/test/fixtures/offload/.","requires_decision":null,"verify":["cd collector && bun test src/adapters/offload.test.ts"],"acceptance":["cd collector && bun test src/adapters/offload.test.ts — expect PASS 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; a controller-down fixture producing exactly one controller-down Item with all panels stale; a normal-temp fixture emitting NO temp Item and a crit-temp fixture emitting exactly one cpu-temp-critical Item"]}
{"type":"task","id":"w2","wave":2,"seat":"coder","tier":"medium","deps":["a8"],"files":["collector/src/adapters/index.ts","collector/src/config.ts","collector/test/adapters-wiring.test.ts"],"desc":"Register the offload adapter landed in task a8 into the collector. In collector/src/adapters/index.ts extend buildAdapters(config) to construct createOffloadAdapter following the EXACT pattern the existing botmaster/golive/ghci blocks already use (the isEnabled(id) guard, intervalOf(id), and settings(id) passthrough) — read the file first and copy its shape, do not invent a new registration mechanism; inject fetch exactly the way the botmaster block does (fetchImpl: fetch), and follow the file's existing DEFAULT_* constant convention for any default endpoint. In collector/src/config.ts extend AdapterConfigSchema with the per-adapter settings offload needs — controller base URL, Prometheus/metrics base URL, an optional bearer-token FILE path (never the secret itself), and an optional cpu-temp critical override — EVERY new field OPTIONAL, no secrets committed, no hardcoded absolute paths, matching the existing optional-field style with a short doc comment per field. Extend collector/test/adapters-wiring.test.ts so the expected adapter id list includes \"offload\" and so the offload adapter is proven to respect its config-disable flag (enabled:false omits it), mirroring the existing per-adapter disable assertions. Acceptance: cd collector && bun test test/adapters-wiring.test.ts — expect PASS. Commit only: collector/src/adapters/index.ts, collector/src/config.ts, collector/test/adapters-wiring.test.ts.","requires_decision":null,"verify":["cd collector && bun test test/adapters-wiring.test.ts"],"acceptance":["cd collector && bun test test/adapters-wiring.test.ts — expect PASS"]}
{"type":"task","id":"u5","wave":2,"seat":"coder","tier":"medium","deps":["a8"],"files":["apps/web/src/lib/collector-types.ts","apps/web/src/components/inbox/InboxFilterRail.tsx","apps/web/src/components/inbox/inbox-mappers.ts","apps/web/src/lib/sidebar-nav.ts","apps/web/src/pages/ci.astro","apps/web/src/components/ci/","packages/deck-ui/src/MachineCard.tsx","packages/deck-ui/src/MachineDetailModal.tsx","packages/deck-ui/src/ClusterQueue.tsx","packages/deck-ui/src/RemoteJobsTable.tsx","packages/deck-ui/src/OffloadControl.tsx","packages/deck-ui/src/index.ts","apps/web/tests/ci-build.spec.ts"],"desc":"Read docs/plans/2026-07-17-overdeck-v1.md Task U5 (CI & Build page + fleet components) for the full contract. The render oracle is docs/mockups/deck-ci-build-v1.html — match its tokens/shell/components exactly, add NO new one-off styling. Task a8 (a dep, already merged into your base) defines the offload panel shapes; read its committed collector/src/adapters/offload.ts plus collector/test/fixtures/offload/ for the EXACT panel data (offload-control, cluster-queue, remote-jobs, fleet) and build-kind Item shapes, and mirror those shapes in this page's fixtures — do NOT import collector source into the web app, keep the collector the seam. STEP 1 (keep gate0 typecheck green) — the web app has its OWN Kind union at apps/web/src/lib/collector-types.ts; add \"build\" to it, and because it drives exhaustive maps, ALSO add the \"build\" key to KIND_LABELS in apps/web/src/components/inbox/InboxFilterRail.tsx and to the ALL_KINDS literal in apps/web/src/components/inbox/inbox-mappers.ts. STEP 2 — rename the sidebar entry: in apps/web/src/lib/sidebar-nav.ts change the label 'CI & Runners' to 'CI & Build' (KEEP href '/ci' — no route churn), and upgrade the existing /ci page IN PLACE: apps/web/src/pages/ci.astro title/pageTitle to 'CI & Build', and rewrite its content under apps/web/src/components/ci/ into the CI & Build page per the oracle (fleet grid, global cluster queue, remote-jobs table, offload-control strip, and an incident feed of the build-kind Items). Do NOT create a parallel /ci-build route and do NOT touch the Overview mini-card at apps/web/src/components/overview/CiCard.tsx (out of scope). STEP 3 — add these deck-ui components (typed props, NO fetch inside, a co-located *.test.tsx per the existing deck-ui vitest pattern, exported from packages/deck-ui/src/index.ts following the existing barrel): 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 shows a summary popover); ClusterQueue (global FIFO table + dispatch target + spill state); RemoteJobsTable; OffloadControl (state-machine strip + kv rows). REUSE the existing InboxItem for the incident feed and KpiTile for the offload KPIs — do NOT reimplement them. Fleet auto-scales: the machine set derives from the fleet panel — NO hardcoded hosts; a joining host renders from its panel entry with an enrolling state; the laptop is shown as a workstation with a display-only spill state. Action buttons render from the ActionRefs on the panels/Items and carry the X1+ offload verbs (reconcile/drain/restore/quarantine/job-retry/recall-spill) but are INERT until X1 lands. All collector reads go through the same-origin proxy route apps/web/src/pages/api/collector/[...path].ts exactly as apps/web/src/pages/index.astro does — read that first; the bearer token is server-side only and must never reach client JS. Acceptance: pnpm exec playwright test apps/web/tests/ci-build.spec.ts — expect PASS: the page renders in both themes against the fixture collector; the fleet shows N machines from the panel including a joining host; clicking a MachineCard opens the detail modal showing per-core load + mem/swap/net/disk/temp; the crit-temp fixture shows the banner and the normal-temp fixture does NOT; every action button carries a valid X1 verb. Commit only: apps/web/src/lib/collector-types.ts, apps/web/src/components/inbox/InboxFilterRail.tsx, apps/web/src/components/inbox/inbox-mappers.ts, apps/web/src/lib/sidebar-nav.ts, apps/web/src/pages/ci.astro, apps/web/src/components/ci/, packages/deck-ui/src/MachineCard.tsx, packages/deck-ui/src/MachineCard.test.tsx, packages/deck-ui/src/MachineDetailModal.tsx, packages/deck-ui/src/MachineDetailModal.test.tsx, packages/deck-ui/src/ClusterQueue.tsx, packages/deck-ui/src/ClusterQueue.test.tsx, packages/deck-ui/src/RemoteJobsTable.tsx, packages/deck-ui/src/RemoteJobsTable.test.tsx, packages/deck-ui/src/OffloadControl.tsx, packages/deck-ui/src/OffloadControl.test.tsx, packages/deck-ui/src/index.ts, apps/web/tests/ci-build.spec.ts, apps/web/tests/fixtures/.","requires_decision":null,"verify":["pnpm exec playwright test apps/web/tests/ci-build.spec.ts"],"acceptance":["pnpm exec playwright test apps/web/tests/ci-build.spec.ts — expect PASS: the page renders in both themes against the fixture collector; the fleet shows N machines from the panel including a joining host; clicking a MachineCard opens the detail modal showing per-core load + mem/swap/net/disk/temp; the crit-temp fixture shows the banner and the normal-temp fixture does NOT; every action button carries a valid X1 verb"]}
