# CI & Trains page — cross-project PR-flow observability

audience: AI coding agents first. Imperative, BLUF. No approved mockup exists — layout below IS
the reference; build in deck-ui idiom (Tailwind + `packages/deck-ui/src/tokens.css` custom
properties). New visual values land as tokens first.

## What this is

New route `/ci` — one screen answering, for EVERY project: what is queued to land, what train is
carrying it, what is blocking it, and how bad the land-lag is. Born from measured pain
(multideal 2026-07-18): gate = 35 min compute but 6+ h wall clock — per-PR serialized gates on
saturated runners. Remedy = merge trains (`integration/batch-train-N` branches, ONE gate per
combined tree). This page makes trains + queue amplification visible so the remedy is steerable
from Overdeck, not from `gh` CLI archaeology.

Cross-project is the point: every repo with PRs shows here, not just multideal.

## Hard rules

- **No fabricated numbers.** Every value traces to `ci` panel data. Metric without a source →
  labeled gap state ("not recorded — needs T3"), NEVER an invented number.
- **Componentization mandate (BLOCKING):** every UI element = named `@overdeck/deck-ui` component
  or existing `@platform-modules/*`. NO inline one-off markup in page files.
- Colocated `.test.tsx` per component; fixtures in `packages/deck-ui/src/fixtures/`.
- Both themes via tokens.
- **Mutating actions** journaled to actions.jsonl, allowlisted verbs only (AC-15 discipline).
- **GitHub API budget is a contract** (see Data budget). NEVER add an unbudgeted per-poll call.

## Train model (pin this, it is repo convention now)

- Train branch = `integration/batch-train-<N>`. Train PR = the open PR whose head is that branch.
- Members: parse PR body trailer line `Train-Members: #79 #74 #78 #72` (authoritative);
  fallback = `- #NN ` bullet lines in body (legacy, train 1 shape). Emit both sets merged, deduped.
- Train state machine (derive, do not invent):
  `building` (branch exists, no PR) → `gating` (PR open, latest run queued/in_progress) →
  `failed` (latest run completed/failure) → `rerunning` (run_attempt > 1 while in_progress) →
  `green` (latest run completed/success) → `merged` (PR merged).
- `infraFlag`: failed job whose log tail matches infra signatures
  (`ENOSPC|ENOTEMPTY|No space left|runner.*lost|The self-hosted runner.*lost communication`) —
  render as "infra, rerunnable", distinct from code-red. Log fetch is on-demand per failed
  train job only (budgeted).

## Data sources

REAL today — extend, do not fork:
- `collector/src/adapters/ghci.ts` — polls `/actions/runs` + `/actions/runners` per repo, emits
  `ci` panel `{repos:[{repo,queueDepth,runs,runners}]}` + failure items. Keep shape
  backward-compatible; ADD `prs`, `trains`, `lag` per repo (contract in plan T2/T3).
- Repos list: collector config (`collector/src/config.ts`), not the adapter DEFAULT. All active
  projects: `alexcodeplace/multideal`, platform, zync, press.zone, overdeck, mega-plan-harness —
  exact slugs from config owner; config is source of truth, spec does not hardcode.
- Actions: `collector/src/actions.ts` allowlist + journal. New verbs `ci.rerunFailed`,
  `ci.cancelRun` (plan T4). `open` exists.
- Live refresh: collector `/events` SSE via existing `useSseStream`.

GAPS (render honestly until landed):
- Gate compute-vs-wall amplification needs per-job timestamps (T3). Until then the
  Amplification widget shows "not recorded — needs T3".
- Train member auto-close on merge is a repo-side workflow, NOT this page. Show members as
  chips linking to PRs; no fabricated "closed" state.

## Data budget (contract)

`gh` authed = 5000 req/h. Poll interval 60s. Each poll selects enhanced-admission set `B`: at
most 5 consecutive configured repos from a persistent rotating cursor, wrapping once. Only repos
in `B` receive GitHub calls; reserve bounded run history, queued/in-progress counts, runners,
open-PR/ref snapshot, any due tenth-poll lag snapshot, and at most two exact train branch/OID run
queries per repo. Dispatch a due lag snapshot only after that repo's required fixed snapshots
succeed. Job detail is limited to two active trains per admitted repo. Failed-job log tails are
on-demand and cached by `jobId`. Jobs and logs consume only budget remaining after admitted
snapshots and exact train queries. Keep queue, train-run, and run-history completeness independent.

Emit every configured repo every poll. Repos outside `B` receive no calls: emit cached data when
available, otherwise empty collections/null metrics; mark completeness false and
`degraded: "budget"`. Advance cursor by `|B|` after every poll, including call failures. Count every
GitHub request before dispatch and NEVER exceed the global 40-call ceiling.

## Layout (reference — no mockup exists)

Top → bottom:
1. **Repo strip** — one `RepoCiTile` per repo: queue depth, runners busy/total, lands today,
   oldest-queued age (turns act-red > 30 min). Click = scope page to repo. "All" default.
2. **Trains rail** (primary surface) — `TrainCard` per non-merged train, newest first:
   member chips (#NN → PR link), `LaneBar` (gate jobs as colored segments:
   green/red/yellow-running/grey-queued, striped=infra-flag), state badge, blocking line
   ("Integration database lanes — ENOTEMPTY (infra)"), actions: Rerun failed (recommended when
   infraFlag), Cancel superseded, Open. Empty state: "No trains. Queue >2 green-intent PRs? →
   assemble a train" (copy only, no auto-assemble in v1).
3. **PR queue table** — `PrQueueTable`: all open PRs across scope. Columns: repo, #, title, age,
   checks rollup, train membership chip, `staleVsMain` → "needs rebase" chip. Sort: age desc
   default. Row expand = checks detail.
4. **Runner fleet** — `RunnerFleetStrip`: per box (name), slots with busy/idle + current job name
   + repo. All-busy + oldest-queued>15min → strip banner "head-of-line saturation".
5. **Amplification** — `AmplificationSpark` per repo: last 10 gated runs, compute-min vs wall-min
   bars; headline = median multiplier ("wall = 9× compute"). Gap state until T3.

## Inbox integration

New items (reconciler-resolved like existing ghci failures):
- Train failed with `infraFlag` → severity `act`, action `ci.rerunFailed` recommended.
- Train green > 10 min and unmerged → severity `act`, "train green — merge it", action `open`.
- Queue depth ≥ 4 with zero trains gating → severity `warn`, "queue building, no train".

## Explicitly NOT in v1

- Auto-assembling trains from the UI (repo-side mutation; controller work, post-v1).
- Per-PR "merge" button (safe-merge stays CLI/agent-side).
- Historical lag warehouse (ring buffer of last 50 gated runs per repo, in-memory + snapshot).
- GitHub webhooks (polling only, budget above).
