# Feature Spec: Feature Focus View UX Redesign

> Purpose: Redesign the `/feature/{feature_id}` page to make live feature development status immediately scannable, drill-down accessible, and phase-aware — replacing the current single-column scroll with a structured two-pane, tabbed layout.

**Version:** 1.3
**Date:** 2026-03-14
**Status:** Outstanding
**Roadmap Mapping:** M46-A (Focus View Core Redesign), M46-B (Focus View Mission-Control Enhancements)
**Depends On:** M45 (dashboard advanced UX, complete)

---

## 0. Problem Statement

The current feature focus view (`/feature/{feature_id}`) has the following structural deficiencies:

| Problem                                         | Root Cause                                                                    | Impact                                                                       |
| ----------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Single long vertical scroll                     | All sections stacked in one `<aside>` without layout separation               | Users cannot scan at a glance — must scroll to understand state              |
| Diff viewer embedded in same column as metadata | `DiffViewer` rendered inside the narrow `detailPanel` column                  | Wide diffs are squeezed; side-by-side diffs unusable                         |
| No visual hierarchy between sections            | Every section is equal weight (same `.section` class)                         | Users cannot tell what is urgent vs. informational                           |
| Phase-agnostic layout                           | Layout does not change based on current phase                                 | A `building` view and `ready_to_merge` view look identical in structure      |
| Review actions buried at the bottom             | `stickyReviewFooter` only sticky within the panel scroll                      | For long-diff features, reviewer must scroll past diff to reach approve/deny |
| Metadata repetition                             | Feature ID, phase, status appear in header AND panel header                   | Cognitive redundancy, wasted vertical space                                  |
| `DetailPanel` component is 1130+ lines          | All rendering logic collapsed into one component                              | Difficult to maintain, test, and extend                                      |
| No "what is happening right now" signal         | Activity state, current agent role, time-in-phase all buried in metadata list | Operators cannot triage at a glance                                          |

---

## 1. Design Goals

### 1.1 Primary Jobs

This redesign optimizes for three ordered jobs:

1. **Operator job:** Understand the current state of a feature run in under 5 seconds without scrolling.
2. **Reviewer job:** Make an approve/deny/request-changes decision with full context visible, without hunting for the action buttons.
3. **Diagnostics job:** Drill down into plan, diff, gates, QA, runtime, and history in focused dedicated panels — not a scroll maze.

### 1.2 UX Principles

1. **Status first:** The hero block answers "what is this, what phase is it in, is it healthy?" before any detail is visible.
2. **Phase-aware defaults:** The active tab defaults to the most relevant content for the current phase (e.g., Gates for `blocked`, Changes for `building`, Review for `ready_to_merge`).
3. **Actions always reachable:** Review actions (approve / deny / request changes) are pinned — never buried below content.
4. **Progressive disclosure:** Deep diagnostics (runtime inspector, history log, checkpoint comparison) are accessible but not in the primary viewport.
5. **Content width matches content type:** Diffs, plan trees, and QA maps get the full viewport width. Metadata and actions stay in a fixed sidebar.

### 1.3 Non-Goals

- Redesigning the Board view (`/`) — out of scope (except the Merge Queue card, which surfaces queue position on the focus view only).
- Changing any existing backend API response shape (additive new endpoints are in scope for Track B features, and additive metadata is allowed when required for Track A lazy-loading or tab visibility).
- Implementing drag-and-drop or resizable panes.

### 1.4 Visual System Contract

The focus view must align with the current Aequitas-inspired dashboard styling already established in `packages/web-dashboard/src/app/globals.css` and `packages/web-dashboard/src/styles/dashboard.module.css`.

- Use the shared light-surface visual system: white and light-blue panels, slate text, soft borders, and the existing page background gradient.
- Use the existing semantic palette: teal as the primary brand/action color, gold/orange for emphasis and warnings, red for destructive or blocked states.
- Do not reintroduce the older dark "ops console" palette for new focus-view components unless a specific embedded tool requires it.
- Embedded tooling must visually match the page: Monaco diff/editor instances use the light theme; diff containers and overlays use the shared surface tokens.
- New focus-view CSS must consume shared dashboard tokens rather than defining page-local colors.

### 1.5 Delivery Tracks

This spec is split into two delivery tracks to keep the core focus-view redesign from being blocked by optional analytics and mission-control features.

**Track A: Core Focus View Redesign**

- Two-pane layout, hero block, diagnosis panel, tabbed content, pinned actions
- Review/runtime/history/transcript capability preservation from the current `review-workspace.tsx`
- Lazy-load cleanup, responsive behavior, accessibility, and decomposition of `detail-panel.tsx`

**Track B: Mission-Control Enhancements**

- Transcript polish, code evolution scrubber, collision radar, token-usage burn analytics, genealogy, merge queue, role performance, flakiness summaries, annotations, and command palette
- New additive API endpoints required by those advanced features

Track A is the required deliverable for M46-A. Track B is a follow-on deliverable for M46-B.

---

## 2. Layout Architecture

### 2.1 Top-Level Structure

The page uses a **two-zone layout**:

```
┌─────────────────────────────────────────────────────────────────┐
│  FOCUS HEADER  (back nav, feature ID, breadcrumb)               │
├──────────────┬──────────────────────────────────────────────────┤
│              │  TAB BAR                                         │
│  CONTEXT     │──────────────────────────────────────────────────┤
│  SIDEBAR     │                                                  │
│  (fixed      │  TAB CONTENT PANEL                               │
│   width,     │  (scrollable, full remaining width)              │
│   scrollable)│                                                  │
│              │                                                  │
│  ─────────── │                                                  │
│  REVIEW      │                                                  │
│  ACTIONS     │                                                  │
│  (pinned     │                                                  │
│   bottom)    │                                                  │
└──────────────┴──────────────────────────────────────────────────┘
```

**Context Sidebar** — 280px fixed width, `position: sticky`, full viewport height. Contains:

- Hero status block
- Agent pipeline stepper
- Phase metadata (branch, time-in-phase, activity state)
- Usage panel (collapsed by default)
- Unresolved dependencies list (if any)
- PR status card (if PR exists)
- Review actions (pinned to sidebar bottom for `ready_to_merge`, `building`, `qa` phases)

The sidebar is priority-ordered. The first viewport must fit, in order: hero block, diagnosis panel (if present), pinned actions or human input, and pipeline stepper. Lower-priority content such as usage, dependencies, and PR status may collapse behind summaries if vertical space is constrained.

**Tab Content Panel** — fills remaining width. Contains all deep-detail sections behind tabs.

### 2.2 Hero Status Block

The top of the sidebar shows a compact, always-visible hero block:

```
┌────────────────────────────┐
│  feature-id-slug           │  ← feature_id (truncated with tooltip)
│  ● BUILDING  [active]      │  ← phase badge + activity_state badge
│  In phase: 4m 32s          │  ← time-in-phase, live updating
│  Builder running           │  ← current active role
│  Branch: feat/my-feature   │  ← branch name
└────────────────────────────┘
```

- Phase badge uses the existing phase color variables (`--color-building`, etc.)
- Activity state badge (`active`, `waiting_input`, `blocked`, `idle`) uses existing badge classes
- Time-in-phase updates every 30 seconds (not every second — avoids unnecessary re-renders)
- "Live activity" pulse indicator (`.inlinePulse`) shown when `activity_state === 'active'`
- If `phase === 'blocked'`, the hero block renders in `--color-blocked` accent and shows `status_reason` inline

### 2.3 Tab Definitions

| Tab Key      | Label      | Icon | Visible When             | Default For Phases |
| ------------ | ---------- | ---- | ------------------------ | ------------------ |
| `overview`   | Overview   | `◎`  | Always                   | `planning`         |
| `plan`       | Plan       | `📋` | `plan !== null`          | —                  |
| `changes`    | Changes    | `⌥`  | `shouldShowDiff`         | `building`         |
| `gates`      | Gates & QA | `✓`  | `shouldShowGates`        | `qa`, `blocked`    |
| `review`     | Review     | `◈`  | `ready_to_merge`, `qa`   | `ready_to_merge`   |
| `runtime`    | Runtime    | `⚡` | Always                   | —                  |
| `history`    | History    | `↺`  | Always                   | `merged`           |
| `transcript` | Transcript | `💬` | `log_entries.length > 0` | —                  |

Tab visibility is computed from the same conditions already used in `DetailPanel` (`shouldShowPlan`, `shouldShowDiff`, `shouldShowGates`). No new logic is required.

The active tab at page load is determined by the feature's current phase (see "Default For Phases" column). If a phase maps to no tab (e.g., `paused_budget`), fall back to `overview`.

---

## 3. Tab Content Specifications

### 3.1 Overview Tab

Replaces the current fragmented metadata sections. Consolidates into scannable cards:

**Sections (stacked, no scroll needed for most features):**

1. **Verification Signals** — `VerificationSignals` component, unchanged.
2. **Plan Revision Timeline** — `PlanRevisionTimeline`, collapsed by default if no revisions.
3. **Risk Annotations** — `PlanRiskAnnotations`, collapsed by default unless `ready_to_merge`.
4. **Blocked State Banner** — visible only when `phase === 'blocked'`; shows `status_reason` and retry button inline.
5. **Merged State Summary** — visible only when `phase === 'merged'`; shows PR link and merge commit.
6. **Budget Banner** — visible only when `status === 'paused_budget'`.

### 3.2 Plan Tab

Dedicated full-width view:

1. **Plan Scope Tree** — `PlanScopeTree` at full content width.
2. **Plan Viewer** — `PlanViewer` below the tree.

No other content competes for horizontal space. This is important because plan trees benefit from wide rendering.

### 3.3 Changes Tab

Dedicated full-width diff view:

1. **DiffViewer** — `DiffViewer` rendered at full content panel width.
2. No sidebar competition. Wide diffs render correctly.

For `ready_to_merge` and `qa` phases, if the `ReviewWorkspace` is currently shown, the Changes tab hosts the Monaco diff editor component from `review-workspace.tsx` rather than the `diff2html` viewer. The tab-based layout replaces the current `showReviewWorkspace` boolean branch.

### 3.4 Gates & QA Tab

1. **Review Brief Panel** — `ReviewBriefPanel` at top (for `qa`, `ready_to_merge`).
2. **Gate Results** — `GateResults`.
3. **Gate Step Drilldown** — `GateStepDrilldown`.
4. **QA Coverage Map** — `QaCoverageMap`.
5. **Evidence Viewer** — `EvidenceViewer`.

All components already exist. Tab simply groups them.

### 3.5 Review Tab

Visible only for `ready_to_merge` and `qa` phases. Consolidates the review actions form:

1. **Review Brief Panel** (repeated from Gates & QA for convenient co-location with action form).
2. **Review actions form** — review message textarea, approval token input, merge strategy select.
3. **Action buttons** — Approve (green), Deny (red), Request Changes.

This tab is the default for `ready_to_merge`. The review actions are no longer buried at the bottom of a long scroll.

**Note:** The review actions are also pinned in the sidebar bottom for quick access without navigating to the Review tab. The sidebar version is a compact form (just the three action buttons); the full form with textarea/token/strategy is on the Review tab.

The compact sidebar actions and the full Review tab form share one source of truth for `reviewMessage`, `approvalToken`, and `mergeStrategy`. Navigating between the compact actions and the full form must not reset draft state.

### 3.6 Runtime Tab

Replaces the runtime-oriented sections currently split across `DetailPanel` and `review-workspace.tsx`:

1. **Execution Runtime Badge** — interactive execution pill, optionally paired with headless/attached runtime state when available.
2. **Runtime Inspector Panel** — `RuntimeInspectorPanel` component (existing, unchanged).
3. **Agent Session Status** — current role, per-role session IDs, active marker, copy affordances.
4. **Directed Agent Console** — preserved from `review-workspace.tsx`; allows sending a message to the active runtime session when available.
5. **Activity Timeline** — existing runtime/activity view and role/validity filtering preserved.
6. **Checkpoints Section** — visible only when `showCheckpointTimeline`. Contains the tab-within-tab UI (Timeline / Snapshot Diff / Compare) from the current `DetailPanel`. This sub-navigation is preserved exactly.

By moving to a dedicated tab, the checkpoint compare grid (which uses a two-column layout) can render at full content width rather than being crammed into the detail panel.

### 3.7 History Tab

1. **Log List** — lazy-loaded, same as current `<details>` section.
2. **Raw Agent Output** — preserved from `review-workspace.tsx`, including policy-disabled states and file selection.
3. Default for `merged` phase so merge summary is the first thing visible.

### 3.8 Transcript Tab

The transcript tab is part of Track A in its base form, because the current review workspace already exposes live role-tagged activity that would otherwise be lost in the redesign.

1. **Role-grouped transcript view** — live or loaded entries grouped by consecutive role turns.
2. **Follow / pause behavior** — preserve the current "Pause Follow / Resume Follow" interaction for actively streaming runs.
3. **Actor and role identity** — preserve actor labels and timestamps.
4. **Advanced markdown rendering, raw-log shortcuts, and enhanced syntax highlighting** are Track B polish, not Track A requirements.

---

## 4. Sidebar Review Actions (Pinned)

For phases `ready_to_merge`, `building`, and `qa`, the sidebar renders a pinned action strip at its bottom:

```
┌────────────────────────────┐
│  [Approve]  [Deny]  [⋯]   │  ← ready_to_merge
└────────────────────────────┘

┌────────────────────────────┐
│  [Request Changes]         │  ← building / qa
└────────────────────────────┘
```

- **`ready_to_merge`:** Three buttons: Approve (primary green), Deny (danger red), and an overflow menu (⋯) for Request Changes. Clicking Approve or Deny opens the existing `ConfirmationModal`.
- **`building` / `qa`:** Single "Request Changes" button that opens the same modal.
- Clicking any action button that requires the full form navigates to the Review tab and scrolls to the form.
- For `waiting_input`, the pinned strip shows the `HumanInputPanel` widget instead.
- For all other phases, the pinned strip is absent.

This ensures the most important action is always one click away regardless of which tab is active.

If an action requires inputs that are not currently satisfied in compact mode:

- `Approve` without an approval token navigates to the Review tab and focuses the approval-token input.
- `Deny` without a reason navigates to the Review tab and focuses the review-message textarea.
- `Request Changes` may execute from compact mode using the shared draft state or a default message, but its behavior must be defined consistently across phases.

---

## 5. Health Diagnosis System

A feature is not always simply "healthy" or "blocked." There is a spectrum: hard blocks, degraded runs, stalled activity, human-input waits, CI failures, and dependency blocks. The current page surfaces none of this proactively — `status_reason` is buried in the metadata list, gate failures require navigating to the Gates tab, and lock contention is invisible.

This section specifies a client-side health diagnosis layer that derives a structured diagnosis from available `FeatureDetail` and `FeaturesIndex` data, and surfaces it in a `DiagnosisPanel` placed in the sidebar immediately below the hero block.

### 5.1 Taxonomy of Unhealthy States

| Severity   | Label    | Definition                                                                               |
| ---------- | -------- | ---------------------------------------------------------------------------------------- |
| `blocked`  | BLOCKED  | Hard stop — feature cannot progress without intervention                                 |
| `degraded` | DEGRADED | Feature is running but has active failures or warnings that may prevent phase completion |
| `stalled`  | STALLED  | Feature is not progressing; no recent activity detected                                  |
| `waiting`  | WAITING  | Feature is paused pending human input (not an error, but requires action)                |
| `healthy`  | —        | No diagnosis panel rendered                                                              |

Severity is exclusive and priority-ordered. If multiple signals apply, the highest-severity wins for the `primary_cause`. All other signals are `secondary_causes`.

### 5.2 Signal-to-Diagnosis Mapping

The following signals from `FeatureSummary`, `GateRunEvidence`, `FeaturesIndex`, and `FeatureCheckpoint[]` drive the diagnosis:

| Signal                                                                | Severity   | Cause Code               | Human Label                            | Suggested Action                                           | Links To                  |
| --------------------------------------------------------------------- | ---------- | ------------------------ | -------------------------------------- | ---------------------------------------------------------- | ------------------------- |
| `phase === 'blocked'`                                                 | `blocked`  | `PHASE_BLOCKED`          | "Feature is blocked"                   | Review the block reason below; use Retry or send a message | Overview tab              |
| `activity_state === 'blocked'`                                        | `blocked`  | `ACTIVITY_BLOCKED`       | "Agent process blocked"                | Check runtime logs for deadlock or error                   | Runtime tab               |
| `activity_state === 'exited'` AND phase not `merged`                  | `blocked`  | `AGENT_EXITED`           | "Agent exited unexpectedly"            | Review history for last known state; consider retry        | History tab               |
| `status === 'paused_budget'`                                          | `blocked`  | `BUDGET_EXHAUSTED`       | "Token budget exhausted"               | Increase budget in policy or request manual continuation   | Overview tab              |
| `FeaturesIndex.dep_blocked` contains this feature_id                  | `blocked`  | `DEPENDENCY_BLOCK`       | "Blocked on unresolved dependencies"   | List shown inline; navigate to dependent feature           | Overview tab              |
| `gate_evidence.overall !== 'pass'`                                    | `degraded` | `GATE_FAILURE`           | "Gate checks failing"                  | Review failing steps below                                 | Gates & QA tab            |
| `gate_retry_count >= 2`                                               | `degraded` | `REPEATED_GATE_FAILURES` | "Gates failing repeatedly (N retries)" | Check for flaky tests or systemic issue                    | Gates & QA tab            |
| `pr.ci_status === 'failed'`                                           | `degraded` | `CI_FAILURE`             | "CI pipeline failing"                  | Review CI logs on PR                                       | Review tab                |
| `pr.has_conflicts === true`                                           | `degraded` | `MERGE_CONFLICTS`        | "PR has merge conflicts"               | Manual conflict resolution required                        | Review tab                |
| `pr.pending_review_threads > 0`                                       | `degraded` | `OPEN_REVIEW_THREADS`    | "N unresolved review threads"          | Address reviewer comments                                  | Review tab                |
| `review_brief.unresolved_questions.length > 0`                        | `degraded` | `UNRESOLVED_QUESTIONS`   | "N open questions identified by QA"    | Review questions in Gates & QA                             | Gates & QA tab            |
| `checkpoints` contains entries where `validation_status !== 'valid'`  | `degraded` | `CHECKPOINT_VIOLATIONS`  | "Checkpoint validation failures"       | Review violations in Runtime tab                           | Runtime tab               |
| `activity_state === 'idle'` AND `activity_last_event_at` > 10 min ago | `stalled`  | `ACTIVITY_STALLED`       | "No activity detected for N minutes"   | Check if agent is still running; consider retry            | Runtime tab               |
| `FeaturesIndex.lock_leases` has stale lease for this feature          | `stalled`  | `STALE_LOCK`             | "Stale lock detected"                  | Lock may be preventing progress; may auto-expire           | Overview tab              |
| `activity_state === 'waiting_input'`                                  | `waiting`  | `WAITING_INPUT`          | "Waiting for human input"              | Respond to the agent prompt below                          | Sidebar (HumanInputPanel) |

### 5.3 `deriveHealthDiagnosis()` Function

A pure function, no side effects, no async. Lives in `lib/health-diagnosis.ts`.

```typescript
export type DiagnosisSeverity = 'blocked' | 'degraded' | 'stalled' | 'waiting' | 'healthy';

export interface DiagnosisCause {
  code: string;
  label: string;
  detail?: string; // e.g. status_reason text, retry count, dep list
  link_tab?: TabKey; // tab to navigate to for details
  action_hint?: string; // one-sentence suggested action
}

export interface HealthDiagnosis {
  severity: DiagnosisSeverity;
  primary: DiagnosisCause;
  secondary: DiagnosisCause[];
}

export function deriveHealthDiagnosis(
  feature: FeatureSummary,
  gateEvidence: GateRunEvidence | null | undefined,
  featuresIndex: FeaturesIndex | null,
  checkpoints: FeatureCheckpoint[],
): HealthDiagnosis | null; // returns null when healthy
```

**Evaluation order** (first match wins for `primary`):

1. `BUDGET_EXHAUSTED` — `feature.status === 'paused_budget'`
2. `PHASE_BLOCKED` — `feature.phase === 'blocked'`
3. `AGENT_EXITED` — `feature.activity_state === 'exited'` AND phase not `merged`
4. `ACTIVITY_BLOCKED` — `feature.activity_state === 'blocked'`
5. `DEPENDENCY_BLOCK` — `featuresIndex.dep_blocked` contains this feature_id
6. `GATE_FAILURE` — `gateEvidence?.overall` is not `'pass'` and not null
7. `CI_FAILURE` — `feature.pr?.ci_status === 'failed'`
8. `MERGE_CONFLICTS` — `feature.pr?.has_conflicts === true`
9. `REPEATED_GATE_FAILURES` — `feature.gate_retry_count >= 2`
10. `CHECKPOINT_VIOLATIONS` — any checkpoint has `validation_status !== 'valid'`
11. `OPEN_REVIEW_THREADS` — `feature.pr?.pending_review_threads > 0`
12. `UNRESOLVED_QUESTIONS` — checked only if `review_brief` is passed; treated as secondary unless it is the only signal
13. `ACTIVITY_STALLED` — `activity_state === 'idle'` AND `activity_last_event_at` is > 10 minutes ago (configurable constant)
14. `STALE_LOCK` — `featuresIndex.lock_leases` has entry for this feature with `stale: true`
15. `WAITING_INPUT` — `feature.activity_state === 'waiting_input'`

All matching signals below the first are collected as `secondary`. The function returns `null` (healthy) if no signals match.

The 10-minute idle threshold is a named constant `STALL_THRESHOLD_MS = 10 * 60 * 1000` in the same file.

### 5.4 `DiagnosisPanel` Component

**Location in layout:** Sidebar, between `HeroBlock` and `AgentPipelineStepper`. Rendered only when `deriveHealthDiagnosis()` returns non-null.

```
┌────────────────────────────────────────┐
│  ⛔ BLOCKED                             │  ← severity icon + label, phase-severity color
│                                        │
│  Feature is blocked                    │  ← primary.label
│  "Planner rejected: schema mismatch    │  ← primary.detail (status_reason, truncated)
│   in plan step 3"                      │     with expand toggle for long text
│                                        │
│  → See Overview                        │  ← link_tab navigation (tab switch, not href)
│                                        │
│  Also:                                 │  ← secondary causes (collapsed by default,
│    · Gates failing (2 retries)         │     expandable with "Also: N more" toggle)
│    · CI pipeline failing               │
└────────────────────────────────────────┘
```

**Visual design:**

- Border-left 3px in severity color (`--color-blocked` for blocked, `--color-building` orange for degraded, muted yellow for stalled, muted blue for waiting)
- Background: subtle tint of severity color at 8% opacity
- Icon: `⛔` blocked, `⚠` degraded, `⏸` stalled, `⏳` waiting
- `primary.detail` is truncated at 120 characters with an inline expand toggle ("show more / show less")
- `link_tab` renders as a small text link that calls `setActiveTab(link_tab)` — no page navigation
- Secondary causes are collapsed by default behind a "Also: N more issues" toggle; expanded as a bullet list

**Gate failure detail enrichment:**
When `code === 'GATE_FAILURE'`, the panel additionally renders the first failing gate step inline:

```
Gates failing (1 of 3 steps failed)
Step: "npm run test:coverage"
  Last output: "Error: Coverage threshold not met…"  ← output_tail[last]
  → See Gates & QA
```

This is the most critical diagnostic data for a blocked build — surfacing the actual error output from `GateStepResult.output_tail` in the sidebar means the operator does not have to navigate to the Gates tab just to see why the gate failed.

**Dependency block detail:**
When `code === 'DEPENDENCY_BLOCK'`, the panel lists the unresolved dependency feature IDs inline as links:

```
Blocked on unresolved dependencies:
  · feat/auth-tokens  →  (link to /feature/feat/auth-tokens)
  · feat/db-schema    →  (link to /feature/feat/db-schema)
```

### 5.5 Hero Block Integration

When `diagnosis.severity` is not `healthy`, the hero block's phase badge gains a secondary indicator:

```
● BUILDING  ⚠          ← phase badge + severity icon inline
```

This is a single character addition to the existing badge — no layout change. The severity color applies to the warning icon, not the phase badge (phase badge color is always phase-based, not health-based, to preserve the existing color-coding convention).

### 5.6 Data Availability and Lazy Loading

`deriveHealthDiagnosis()` is called whenever `FeatureDetail` loads. It does not require any additional API calls beyond what is already fetched on page load. The `FeaturesIndex` data comes from the existing `/api/status` call. `GateRunEvidence` is part of `FeatureDetail.gate_evidence`.

`review_brief.unresolved_questions` is a lazy-loaded field (fetched on Gates & QA tab activation). If not yet loaded, the `UNRESOLVED_QUESTIONS` signal is simply absent from the diagnosis — the panel re-evaluates when the field becomes available.

The diagnosis panel must degrade gracefully when optional fields are unavailable. "Healthy" must never be inferred from missing lazy data; it means "no active signals from currently-available data."

---

## 6. Component Decomposition

The current `detail-panel.tsx` (~1130 lines) must be split. The new structure:

| New File                                        | Responsibility                                                                                                 | Approx. Lines |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------- |
| `components/focus/FocusLayout.tsx`              | Two-zone layout shell, tab state management                                                                    | ~120          |
| `components/focus/FocusHeader.tsx`              | Back nav + feature ID breadcrumb                                                                               | ~40           |
| `components/focus/ContextSidebar.tsx`           | Hero block + pipeline stepper + metadata + pinned actions                                                      | ~220          |
| `components/focus/HeroBlock.tsx`                | Feature ID, phase badge, activity state, time-in-phase                                                         | ~80           |
| `components/focus/TabBar.tsx`                   | Tab definitions, active tab, visibility logic                                                                  | ~90           |
| `components/focus/tabs/OverviewTab.tsx`         | Verification signals, timeline, risk, blocked/merged banners                                                   | ~100          |
| `components/focus/tabs/PlanTab.tsx`             | Scope tree + plan viewer                                                                                       | ~40           |
| `components/focus/tabs/ChangesTab.tsx`          | DiffViewer or Monaco diff by phase                                                                             | ~60           |
| `components/focus/tabs/GatesQaTab.tsx`          | Review brief + gate results + coverage + evidence                                                              | ~80           |
| `components/focus/tabs/ReviewTab.tsx`           | Full review form + actions                                                                                     | ~120          |
| `components/focus/tabs/RuntimeTab.tsx`          | Runtime inspector + session status + directed console + checkpoints section                                    | ~140          |
| `components/focus/tabs/HistoryTab.tsx`          | Log list + raw provider output                                                                                 | ~90           |
| `components/focus/PinnedActions.tsx`            | Phase-aware pinned action strip                                                                                | ~100          |
| `components/focus/DiagnosisPanel.tsx`           | Health diagnosis panel with primary/secondary causes and gate step detail                                      | ~150          |
| `lib/health-diagnosis.ts`                       | `deriveHealthDiagnosis()` pure function + all types                                                            | ~120          |
| `components/focus/tabs/TranscriptTab.tsx`       | Agent transcript preserving live role-tagged activity; advanced rendering is Track B                           | ~120          |
| `components/focus/CheckpointScrubber.tsx`       | Timeline slider for scrubbing through checkpoint diffs chronologically                                         | ~140          |
| `components/focus/CollisionRadar.tsx`           | Directory treemap showing cross-feature file collision risk                                                    | ~200          |
| `components/focus/UsageBurnChart.tsx`           | Token-usage-over-time sparkline with projected final usage and optional derived dollar overlay                 | ~130          |
| `components/focus/GenealogyTree.tsx`            | Revision ancestry tree with reason labels                                                                      | ~120          |
| `components/focus/GateFlakinessSummary.tsx`     | Per-gate-step sparkline of pass/fail history with flakiness score                                              | ~150          |
| `components/focus/AnnotatableDiffViewer.tsx`    | Monaco-based diff with click-to-annotate hunk comments                                                         | ~200          |
| `components/CommandPalette.tsx`                 | Global Cmd+K overlay with fuzzy feature search and action dispatch                                             | ~220          |
| `components/focus/AgentRolePerformanceCard.tsx` | Per-role metrics card comparing this run to historical provider averages                                       | ~100          |
| `components/focus/MergeQueueCard.tsx`           | This feature's position in the merge queue with dependency blockers                                            | ~110          |
| `lib/collision-radar.ts`                        | `deriveCollisionRisk()` pure function mapping file paths to risk scores across active features                 | ~80           |
| `lib/usage-burn.ts`                             | `projectUsageBurnRate()` pure function extrapolating token usage/time from current spend and provider averages | ~80           |

The existing component files (`plan-viewer.tsx`, `diff-viewer.tsx`, `gate-results.tsx`, etc.) are **not changed** where practical — they are consumed by the new tab components. `detail-panel.tsx` is retired and replaced by `FocusLayout.tsx`.

`review-workspace.tsx` is retired only after all of its operator-critical capabilities are preserved:

- Monaco diff inspector
- Review action form
- Live agent log stream / transcript
- Directed agent console
- Session status panel
- Runtime activity timeline filters
- Raw provider output access
- Mobile inspector flow

The `showReviewWorkspace` boolean branch in `page.tsx` is eliminated only after feature parity is achieved.

---

## 7. Page Route Changes (`app/feature/[id]/page.tsx`)

The page component currently renders either `<ReviewWorkspace>` or `<aside className={styles.detailPanel}>` based on a boolean. This branch is replaced:

```tsx
// Before (two separate rendering paths)
{showReviewWorkspace ? <ReviewWorkspace ... /> : <aside><DetailPanel .../></aside>}

// After (single path, phase-aware via tab defaults)
<FocusLayout
  detail={detail}
  project={project}
  statusPayload={payload}
  hasActiveRuntimeSession={hasActiveRuntimeSession}
/>
```

`FocusLayout` reads the phase and selects the correct default tab internally. No branching at the page level.

---

## 8. CSS Changes

New CSS classes needed in `dashboard.module.css`:

| Class             | Purpose                                                                                    |
| ----------------- | ------------------------------------------------------------------------------------------ |
| `.focusTwoPane`   | Two-zone grid layout: `grid-template-columns: 280px 1fr`                                   |
| `.contextSidebar` | Fixed-width sidebar: `position: sticky; top: 0; height: 100vh; overflow-y: auto`           |
| `.heroBlock`      | Hero status container: padding, border-bottom                                              |
| `.heroPhaseBadge` | Large phase badge in hero: 0.85rem, phase color variable                                   |
| `.heroFeatureId`  | Feature ID in hero: 1rem bold, truncation with tooltip                                     |
| `.heroMeta`       | Secondary metadata lines in hero: 0.75rem muted                                            |
| `.sidebarDivider` | Horizontal rule between sidebar sections                                                   |
| `.pinnedActions`  | Sticky bottom strip in sidebar: `position: sticky; bottom: 0; background: var(--bg-panel)` |
| `.tabBar`         | Horizontal tab row: flex, border-bottom                                                    |
| `.tabItem`        | Individual tab button: 0.78rem, uppercase, padding                                         |
| `.tabItemActive`  | Active tab: bottom border 2px phase-colored, primary text color                            |
| `.tabContent`     | Tab content area: `overflow-y: auto; padding: 1rem 1.5rem`                                 |

| `.diagnosisPanel` | Diagnosis container: border-left 3px severity color, 8% tint background, padding |
| `.diagnosisSeverityLabel` | "BLOCKED / DEGRADED / STALLED / WAITING" label: 0.7rem uppercase, severity color |
| `.diagnosisPrimary` | Primary cause text: 0.82rem, primary text color |
| `.diagnosisDetail` | Detail/reason text: 0.78rem muted, truncated with expand toggle |
| `.diagnosisLink` | Tab-switch link: 0.75rem, underline, phase-accent color |
| `.diagnosisSecondary` | Secondary causes list: 0.75rem muted, bullet list |
| `.diagnosisGateStep` | Gate step failure inline block: monospace, 0.72rem, slightly indented |

Responsive behavior:

- At viewports < 900px: sidebar collapses to a horizontal strip above the tab bar (phase badge, activity state, and pinned actions only). Metadata moves into the Overview tab.
- At viewports < 600px: sidebar strip becomes a single-line status bar. Tab bar becomes horizontally scrollable.

Visual token requirements:

- New focus-view classes must use the shared dashboard light-theme tokens already defined in `globals.css` and `dashboard.module.css`.
- `--bg-panel`, `--bg-card`, `--bg-elevated`, `--text-primary`, `--text-muted`, `--border-weak`, `--border-strong`, and the phase color variables are the default inputs for new components.
- Primary action buttons use the shared teal CTA treatment. Destructive actions use the shared red semantic styling. Warning/attention surfaces use the shared gold treatment.
- New diagnosis and phase badges must pass contrast checks against light surfaces.
- Monaco and diff containers must remain on the light theme to match the page.

This section supersedes the earlier assumption that "existing CSS variables and component-level classes are unchanged." The visual system has already moved to the Aequitas-aligned light palette and the focus view must follow it.

---

## 9. Data Fetching

No required backend contract changes for Track A beyond additive support where noted. The loading model is clarified to separate first-paint data from tab-lazy data.

### 9.1 First-Paint vs Lazy-Load Contract

**Required for first paint:**

- `FeatureDetail`
- `DashboardStatusPayload`
- Phase/status/activity/role metadata needed for hero + diagnosis

**Sidebar-lazy or expansion-lazy:**

- Usage details and budget/conversion policy, unless usage is promoted into the hero summary
- PR details beyond a compact summary

**Tab-lazy:**

- Review brief
- QA test index
- History log
- Raw provider output
- Checkpoint diff/compare payloads
- Runtime event timeline details

The focus view must not eagerly fetch every optional dataset on mount.

Specifically:

- `OverviewTab` triggers no additional fetches (all data available in `FeatureDetail`).
- `GatesQaTab` triggers the `review-brief` fetch on first activation.
- `HistoryTab` triggers the `log` fetch on first activation.
- `RuntimeTab` triggers the runtime/activity fetch on first activation; SSE stream for checkpoints starts only when Runtime tab is active and `execution_mode === 'interactive'`.
- `HistoryTab` and `TranscriptTab` may share a common underlying log source, but must not double-fetch it.
- If transcript visibility requires a cheap presence signal, an additive `has_transcript` or equivalent metadata field may be introduced.

This is a behavioral improvement over the current implementation where all lazy fetches fire simultaneously on mount.

New endpoints required by Track B features are listed in §9.2 below.

### 9.2 New API Endpoints (Track B Features)

| Method | Path                               | Returns                                                 | Required By                                  |
| ------ | ---------------------------------- | ------------------------------------------------------- | -------------------------------------------- |
| GET    | `/api/features/[id]/files`         | `string[]` (touched file paths)                         | Collision Radar                              |
| GET    | `/api/features/[id]/gate-history`  | `GateRunEvidence[]` (all past retry runs)               | Gate Flakiness Analyzer                      |
| GET    | `/api/features/[id]/genealogy`     | `{ feature_id, revision_of, revision_reason, phase }[]` | Genealogy Tree                               |
| GET    | `/api/features/[id]/worker-events` | `WorkerEventEntry[]`                                    | Per-Role Performance Cards                   |
| GET    | `/api/analytics/provider`          | `ProviderAnalytics[]`                                   | Usage Burn Chart, Per-Role Performance Cards |
| GET    | `/api/merge-queue`                 | `MergeQueueEntry[]`                                     | Merge Queue Card                             |

All new endpoints follow the existing `{ ok, data/error }` envelope. They are additive — no existing endpoint shape changes.

---

## 10. Accessibility Requirements

- All tabs are keyboard navigable: arrow keys move between tabs, Enter activates.
- Tab panels have `role="tabpanel"` and `aria-labelledby` pointing to the corresponding tab.
- The pinned action strip buttons have descriptive `aria-label` attributes (e.g., `aria-label="Approve feature for merge"`).
- Phase badges use `role="status"` and `aria-label="Phase: building"` (text, not color alone).
- Hero time-in-phase uses `aria-live="polite"` for 30-second updates.
- All interactive elements are reachable and operable with keyboard alone.
- Sticky sidebars, sticky action regions, and mobile overlays must not create keyboard traps or hidden focus targets.
- Long transcript/log content, diagnosis expanders, and command surfaces must preserve visible focus treatment in the light theme.

---

## 11. Risks, Side Effects, and Tradeoffs

### 11.1 Known Side Effects To Manage

- Tabs improve scanability but reduce "single-scroll discoverability." Default-tab selection and in-page cross-links matter more.
- Lazy loading reduces first-paint cost but introduces first-open latency and asynchronous content pop-in.
- A sticky sidebar plus sticky pinned actions plus mobile inspector overlays can create nested-scroll and viewport-height bugs on small screens.
- Moving from the old dark ops-console look to the shared light Aequitas styling improves consistency but reduces some of the ambient severity/high-contrast feel of the previous interface.
- Advanced transcript rendering increases trust and security risk if raw agent content is treated as safe HTML or safe Markdown without sanitization.

### 11.2 Tradeoffs To Call Out Explicitly

- Full replacement of `detail-panel.tsx` and `review-workspace.tsx` is cleaner architecturally, but higher risk than incremental extraction unless parity is tested aggressively.
- "Existing components unchanged" is aspirational. Several existing panels were designed for the old narrow-column layout and may need adaptation once placed into full-width tabs.
- A fixed 280px sidebar is simple, but information density must be managed with explicit collapse rules or the "5 second scan" goal will fail.
- Track B features are valuable, but must not be allowed to delay Track A completion.

---

## 12. Success Criteria

| Criterion                                                                              | How Verified                                                                                                                    |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Operator can determine phase, activity state, and current agent role without scrolling | Manual review of `building` phase screenshot at 1440px                                                                          |
| Reviewer can reach Approve/Deny buttons without scrolling for any diff length          | Manual review with a 500-line diff feature at 1440px                                                                            |
| Wide diff renders without horizontal scrollbar at > 900px viewport                     | Automated: render DiffViewer in ChangesTab with 120-char lines, assert no overflow                                              |
| Default tab matches current phase                                                      | Unit test: `getDefaultTab(phase)` for all 7 phases                                                                              |
| All existing components unchanged (no regressions)                                     | `npm run test` passes at ≥90% thresholds                                                                                        |
| Lazy fetches fire on tab activation, not page mount                                    | Unit test: mock API calls, assert call counts per tab activation sequence                                                       |
| Responsive: primary flow usable at 360px                                               | Manual check + Vitest snapshot at 360px breakpoint                                                                              |
| `deriveHealthDiagnosis()` returns correct severity for all 15 signal types             | Unit tests: one test per signal, plus priority-ordering tests (higher severity wins)                                            |
| `DiagnosisPanel` hidden when feature is healthy                                        | Unit test: render with healthy feature, assert panel absent from DOM                                                            |
| `DiagnosisPanel` shows `output_tail` for GATE_FAILURE primary                          | Unit test: render with failed gate evidence, assert step name and last output line visible                                      |
| `DiagnosisPanel` link_tab calls `setActiveTab`, not page navigation                    | Unit test: click "See Gates & QA", assert tab state updated, no `window.location` change                                        |
| Secondary causes collapsed by default, expandable                                      | Unit test: render with 3 signals, assert secondary list hidden; click toggle, assert visible                                    |
| Hero badge severity icon present when diagnosis non-null                               | Unit test: render blocked feature, assert warning icon in hero badge                                                            |
| STALL_THRESHOLD_MS constant exported and used in diagnosis                             | Static: import check ensures threshold is not hardcoded inline                                                                  |
| Focus view uses shared Aequitas light-theme tokens                                     | Visual regression / snapshot: no dark-panel fallback classes used in new focus components                                       |
| Track A preserves current review workspace capabilities before retirement              | Regression tests cover live log stream, send-message flow, session status, raw output access, and mobile inspector availability |
| Zero ESLint warnings                                                                   | `npm run lint`                                                                                                                  |
| TypeScript strict mode passes                                                          | `npm run typecheck`                                                                                                             |

---

## 13. Migration Path

### Track A: Core Focus View Redesign

1. Create `lib/health-diagnosis.ts` with `deriveHealthDiagnosis()` and all types. Write unit tests first.
2. Create `components/focus/` directory with the Track A component files.
3. Build `DiagnosisPanel` consuming `deriveHealthDiagnosis()`.
4. Build `FocusLayout`, `ContextSidebar`, `HeroBlock`, `TabBar`, and the core tab components.
5. Preserve current `review-workspace.tsx` capabilities inside the new tab model before deleting anything.
6. Add new CSS classes to `dashboard.module.css` using the shared Aequitas-aligned tokens.
7. Update `app/feature/[id]/page.tsx` to render `<FocusLayout>` instead of the current branch.
8. Add unit tests for tab logic, pinned action rendering, capability preservation, and lazy fetch gating.
9. Delete `detail-panel.tsx` and `review-workspace.tsx` only after parity tests pass.
10. Run `npm run build`, `npm run test`, and visual checks at desktop/mobile breakpoints.

Do **not** modify `detail-panel.tsx` incrementally. The decomposition is a full replacement. Incremental edits to a 1130-line component introduce more risk than a clean cut.

### Track B: Mission-Control Enhancements

11. Implement new API endpoints (§9.2) in the Next.js route handlers.
12. Build Track B pure lib functions first (`collision-radar.ts`, `usage-burn.ts`), then leaf components, then tab integrations.
13. Add `CommandPalette` to the root layout (`app/layout.tsx`) so it is available on all pages.
14. Wire `CollisionRadar`, `BurnRateChart`, `GenealogyTree`, `AgentRolePerformanceCard`, `MergeQueueCard`, and Track B transcript/scrubber enhancements into the appropriate tabs and sidebar locations.
15. Run full test suite and `npm run build`.

---

## 14. Out of Scope

- Animated tab transitions.
- Persisting the user's active tab across navigation (could be added later via localStorage).
- Redesigning the Board (`/`) view (Merge Queue card appears on focus view only).
- Changes to the control-plane backend or MCP tool contracts — new endpoints are in the Next.js API layer only.
- Real-time multi-user collaboration beyond presence indicators.

---

---

# Track B: Mission-Control Enhancements

The following features extend the focus view beyond its current data surface. They are explicitly Track B deliverables and must not block Track A completion.

---

## §A: Agent Transcript View

### Purpose

Surface the actual text output of each agent role (planner, builder, QA) as a readable conversation transcript, not a flat log. This answers the question developers most want answered when watching an AI agent: "what was it thinking?"

### User Job

Operator/reviewer wants to understand why the builder agent wrote specific code, why the planner chose a particular approach, or why QA flagged a test — without reading raw log files.

### Data Sources

`FeatureDetail.log_entries: AgentLogEntry[]` is already fetched on page load. Each entry has:

- `actor` — the agent instance identifier
- `role` — `'planner' | 'builder' | 'qa' | 'orchestrator'`
- `text` — the raw text output of that agent turn
- `timestamp` — ISO string

`RawLogFileMeta[]` (new lazy endpoint `GET /api/features/[id]/log-files`) provides metadata for raw log files per role, enabling a "download raw log" link per role group. This fetch is on-demand, not on tab activation.

### Component: `TranscriptTab.tsx`

````
┌──────────────────────────────────────────────────────┐
│  TRANSCRIPT                                          │
│                                                      │
│  [All] [Planner] [Builder] [QA] [Orchestrator]       │  ← role filter pills
│                                                      │
│  ┌──────────────────────────────────────┐            │
│  │ 🟦 PLANNER  · 14:03:21              │ ↓ raw log  │
│  │                                      │            │
│  │  I'll break this into 3 steps:       │            │
│  │  1. Add the route handler            │            │
│  │  2. Update the schema               │            │
│  │  3. Write tests for edge cases       │            │
│  └──────────────────────────────────────┘            │
│                                                      │
│  ┌──────────────────────────────────────┐            │
│  │ 🟧 BUILDER  · 14:05:44              │ ↓ raw log  │
│  │                                      │            │
│  │  ```typescript                       │            │
│  │  export async function handler() {   │  ← syntax  │
│  │    ...                               │    highlighted│
│  │  }                                   │            │
│  │  ```                                 │            │
│  └──────────────────────────────────────┘            │
└──────────────────────────────────────────────────────┘
````

**Rendering rules:**

- Entries are grouped into consecutive same-role "turns." A new bubble starts when the role changes.
- Each bubble has a role-colored left border (using phase color variables: planner=blue, builder=orange, qa=gold, orchestrator=muted).
- Text is rendered as Markdown. Code fences (` ``` `) are syntax-highlighted using the same Monaco tokenizer already available in the project (no additional dependency).
- Long turns (> 40 lines) are collapsed to 20 lines with a "Show N more lines" inline toggle.
- Role filter pills filter the visible turns by role. Default: All.
- Each role group header has a "↓ raw log" link that triggers a lazy fetch of `RawLogFileMeta` for that role and initiates a file download.
- If `log_entries` is empty, a placeholder is shown: "No transcript available — logs are written once the agent completes its first turn."

### Tab Placement

`transcript` tab. Visible when `log_entries.length > 0`. Not a default tab for any phase — users navigate to it deliberately.

### Implementation Notes

- No new API endpoint required for the transcript itself. `log_entries` is in `FeatureDetail`.
- The `RawLogFileMeta` endpoint is optional and fetched only on "↓ raw log" click, never eagerly.
- Markdown rendering: use an existing lightweight parser (e.g., `marked` or similar) — check if one is already in `package.json` before adding a dependency.

---

## §B: Code Evolution Scrubber ("Time Machine")

### Purpose

Let the user scrub through the evolution of the codebase checkpoint-by-checkpoint, watching the diff update as they move through time. Transforms the static diff into an interactive story of how the code was written.

### User Job

Operator wants to understand the sequence of decisions the builder made, or find the exact checkpoint where a specific change was introduced. Currently requires manually selecting from a dropdown and waiting for a fetch each time.

### Data Sources

`FeatureDetail.checkpoints: FeatureCheckpoint[]` — already available. Each checkpoint has `diff_snapshot: string` (the full diff at that point in time), `timestamp`, `files_changed[]`, `validation_status`, and `violations[]`.

No new API endpoint required for the basic scrubber. `diff_snapshot` is already on the checkpoint object.

For the advanced "Play" mode, diffs are pre-loaded from `checkpoint.diff_snapshot` — no per-step network requests needed.

### Component: `CheckpointScrubber.tsx`

Placed inside `RuntimeTab.tsx`, replacing the current dropdown-based checkpoint selector.

```
┌──────────────────────────────────────────────────────────┐
│  CODE EVOLUTION  · 12 checkpoints                        │
│                                                          │
│  ◀  ▶  ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━○  [Play ▷]     │
│  cp-001              cp-006         cp-012               │
│  14:01               14:22          15:03                │
│                                                          │
│  Checkpoint 6 of 12  · 14:22:18                          │
│  ✓ valid  · 4 files changed                              │
│                                                          │
│  ┌────────────────────────────────────────────────────┐  │
│  │  [Diff content renders here at full width]         │  │
│  └────────────────────────────────────────────────────┘  │
│                                                          │
│  File filter: [All files ▾]                              │
└──────────────────────────────────────────────────────────┘
```

**Interaction:**

- **Scrubber slider:** `<input type="range">` mapped to checkpoint index. Moving it updates the displayed diff instantly (all diffs are already in memory from `checkpoints[]`).
- **◀ / ▶ buttons:** Step one checkpoint backward/forward.
- **Play ▷:** Auto-advances one checkpoint per 1.2 seconds. Button toggles to "Pause ⏸" while playing.
- **Checkpoint metadata strip:** Shows checkpoint ID, timestamp, validation status badge, files-changed count for the currently-selected checkpoint.
- **Violations inline:** If `violations.length > 0`, a red banner appears below the metadata strip listing them.
- **File filter dropdown:** Filters the rendered diff to a single file path from `files_changed[]`. "All files" is the default.
- Diff is rendered by `DiffViewer` (existing component), passed the `diff_snapshot` of the selected checkpoint.

**Threshold:** Only rendered when `checkpoints.length >= 2`. With 0–1 checkpoints, the existing single-snapshot view is shown (no scrubber).

---

## §C: File Collision Radar

### Purpose

Show the operator which files this feature is touching and highlight where other concurrently-active features are editing the same paths — before a merge conflict actually occurs. This is an early warning system unique to a multi-agent orchestrator dashboard.

### User Job

Operator running multiple parallel features wants to know which ones are on a collision course before they hit a merge conflict that blocks the queue.

### Data Sources

- `GET /api/features/[id]/files` → `string[]` — list of file paths touched by this feature (derived server-side from the feature's diff; trivial to compute).
- `DashboardStatusPayload.features: FeatureSummary[]` — already fetched on page load; provides the list of all active feature IDs.
- For each other active feature: `GET /api/features/[otherId]/files` → `string[]` — fetched lazily on Collision Radar tab activation, in parallel for all active features. Results are cached in component state for the session.

### Pure Function: `lib/collision-radar.ts`

```typescript
export interface CollisionEntry {
  path: string;
  depth: number; // directory depth (0 = root, 1 = src/, etc.)
  competing_features: string[]; // other feature IDs touching this path
  risk: 'high' | 'medium' | 'low';
}

export function deriveCollisionRisk(
  thisFeatureFiles: string[],
  otherFeatures: Record<string, string[]>, // featureId → file list
): CollisionEntry[];
```

Risk scoring:

- `high` — exact file match with ≥1 other active feature
- `medium` — same directory (one level up) with ≥1 other active feature
- `low` — same top-level package/module with ≥1 other active feature

### Component: `CollisionRadar.tsx`

Placed in the **Overview tab**, below Verification Signals. Hidden when no collisions are detected (returns empty array from `deriveCollisionRisk()`).

```
┌───────────────────────────────────────────────────────────┐
│  FILE COLLISION RADAR                                     │
│  2 high-risk · 3 medium-risk conflicts detected           │
│                                                           │
│  src/                                                     │
│    auth/                                                  │
│      ▓▓▓▓ login.ts        HIGH · feat/auth-tokens        │  ← red
│      ▒▒▒▒ session.ts      MED  · feat/db-schema          │  ← amber
│    api/                                                   │
│      ░░░░ routes.ts        LOW  · feat/route-cleanup      │  ← muted
│                                                           │
│  [Show all N files ▾]                                     │
└───────────────────────────────────────────────────────────┘
```

- Renders as an indented file tree (not a treemap — simpler, more readable at typical file counts).
- Each file shows: path, risk badge, competing feature IDs as links to `/feature/{id}`.
- Only `high` and `medium` entries shown by default. "Show all N files" toggle reveals `low`.
- If no active features share files: panel is hidden entirely.
- Loading state: shows skeleton rows while parallel file-list fetches complete.

---

## §D: Predictive Token Usage Burn Chart

### Purpose

Answer "how much token usage will this feature consume in total, and is it on track?" Dollar cost is secondary and only shown when an optional token-to-dollar conversion is configured in `policy.yaml`.

### User Job

Operator managing a token budget wants to know before a feature exceeds its limit. Team lead wants to understand usage variance across features. If a dollar conversion is configured, the same panel may also show estimated dollar impact as a derived secondary metric.

### Data Sources

- `FeatureDetail.cost: CostSummary` — current usage (`tokens_used`, optional `estimated_cost_usd`, `recorded_at`)
- `WorkerEventEntry[]` from `GET /api/features/[id]/worker-events` — per-iteration timing and request counts, enabling a usage-over-time curve
- `ProviderAnalytics[]` from `GET /api/analytics/provider` — historical `avg_tokens_used` and `avg_duration_ms` per provider/model, used as the projection baseline
- Budget policy from existing `GET /api/policy/budget`, which should expose `per_feature_limit_tokens` and optional `token_cost_conversion`
- Optional token-to-dollar conversion from `policy.yaml`, exposed via the policy endpoint when configured

### Policy Configuration

Primary budgeting is token-based.

`policy.yaml` may optionally define:

```yaml
budget:
  per_feature_limit_tokens: 250000
  token_cost_conversion:
    usd_per_1k_tokens: 0.35
```

Rules:

- `per_feature_limit_tokens` is the primary budget threshold used by the focus view.
- `token_cost_conversion` is optional.
- If `token_cost_conversion` is absent, the UI shows token usage only and does not invent or require dollar estimates.
- If `token_cost_conversion` is present, the UI may show derived USD as a secondary line or tooltip.
- If both token and dollar limits are present, token limit remains the primary budget bar and enforcement visualization; dollar values remain informational unless policy explicitly says otherwise.

### Pure Function: `lib/usage-burn.ts`

```typescript
export interface UsageBurnProjection {
  current_tokens_used: number;
  projected_final_tokens: number;
  projected_completion_at: string | null; // ISO timestamp estimate
  budget_limit_tokens: number | null;
  will_exceed_budget: boolean;
  confidence: 'high' | 'medium' | 'low'; // based on data availability
  usage_points: { timestamp: string; tokens_used: number }[]; // historical curve
  derived_usd?: {
    current_estimated_usd: number;
    projected_final_estimated_usd: number;
    usd_per_1k_tokens: number;
  } | null;
}

export function projectUsageBurnRate(
  cost: CostSummary,
  workerEvents: WorkerEventEntry[],
  providerBaseline: ProviderAnalytics | null,
  budgetLimitTokens: number | null,
  tokenCostConversion: { usd_per_1k_tokens: number } | null,
  phase: string,
): UsageBurnProjection;
```

Confidence is `high` when `workerEvents.length >= 5`, `medium` when 2–4, `low` when < 2 (falls back to provider baseline only).

### Component: `UsageBurnChart.tsx`

Placed in the **sidebar usage panel** — replaces the existing `FeatureCostPanel` with an expanded version. Basic token totals remain; derived dollar values are secondary and only rendered when conversion is configured. The chart appears below them, collapsed by default.

```
┌────────────────────────────────┐
│  USAGE                         │
│  42k of 200k tokens  (21%)     │
│  ████░░░░░░░░░░░░ budget bar  │
│  ≈ $0.42 of ≈ $2.00 est.       │  ← optional, only if conversion configured
│                                │
│  ▼ Projection                  │  ← expand toggle
│  ·····  ────────────────────── │
│       ↑ now       ↑ projected  │
│  Projected final: ~180k tokens │
│  Projected est.: ~$1.80        │  ← optional, only if conversion configured
│  Est. completion: ~47 min      │
│  ⚠ Trending 1.4× historical avg│
└────────────────────────────────┘
```

- Sparkline chart: x-axis = time (from `usage_points`), y-axis = cumulative tokens used. A dashed projection line extends from the current point to the estimated final usage.
- Budget threshold rendered against token usage.
- `will_exceed_budget === true` → budget bar turns red, warning badge shown.
- If `confidence === 'low'`: projection shown with a disclaimer: "Estimate based on provider average — insufficient data for this run."
- If token-to-dollar conversion exists, render derived USD as a secondary label only. It must never replace the token metric as the primary budget visualization.
- Chart rendered with a minimal SVG (no chart library dependency; simple polyline).

---

## §E: Feature Genealogy Tree

### Purpose

Show the complete revision history of a feature — parent revisions, the reasons they were abandoned, and which succeeded — as a visual lineage tree. This is a novel artifact of AI-driven development with no human-workflow equivalent.

### User Job

Reviewer or team lead wants to understand how many attempts it took to produce this feature and why previous versions failed, without reading through a flat history log.

### Data Sources

`GET /api/features/[id]/genealogy` → `GenealogyNode[]`:

```typescript
interface GenealogyNode {
  feature_id: string;
  phase: string;
  status: string;
  revision_of: string | null;
  revision_reason: string | null;
  plan_version: number;
  last_updated: string;
}
```

Server derives this by traversing `revision_of` links across the features index. The response is a flat array; the client builds the tree structure.

### Component: `GenealogyTree.tsx`

Placed in **Overview tab**, collapsed by default. Not shown when `revision_of === null` and no other features reference this feature's ID as their `revision_of`.

```
┌───────────────────────────────────────────────────────────┐
│  REVISION HISTORY  · 3 attempts                           │
│                                                           │
│  feat/my-feature [v1] ──(blocked: schema mismatch)──┐    │
│                                                      ↓    │
│  feat/my-feature [v2] ──(blocked: gate failure)─────┐    │
│                                                      ↓    │
│  feat/my-feature [v3] ← YOU ARE HERE                      │
│  [ready_to_merge]                                         │
└───────────────────────────────────────────────────────────┘
```

- Each node is a pill: feature ID + plan version + phase badge.
- Edges are labeled with `revision_reason` (truncated to 40 chars with tooltip).
- Current feature is highlighted.
- Clicking a past-revision node navigates to `/feature/{revision_id}`.
- If `revision_of === null` and no children: component not rendered (hidden).

---

## §F: Gate Flakiness Analyzer

### Purpose

Distinguish between tests that consistently fail (systemic code problem) and tests that flip between pass/fail across retries (flaky infrastructure). Currently impossible to see from a single gate run snapshot.

### User Job

Operator sees `gate_retry_count = 4` and needs to know: should I fix the code or fix the test infrastructure?

### Data Sources

`GET /api/features/[id]/gate-history` → `GateRunEvidence[]` — all past gate run attempts for this feature in chronological order. Server reads all gate evidence files from `.aop/features/{id}/evidence/`.

### Component: `GateFlakinessSummary.tsx`

Placed in **Gates & QA tab**, above `GateResults`. Visible only when `gate_retry_count >= 1`.

```
┌───────────────────────────────────────────────────────────┐
│  GATE HISTORY  · 4 attempts                               │
│                                                           │
│  Step                    Runs  Pass  Flakiness           │
│  ─────────────────────────────────────────────────────── │
│  npm run lint            ●●●●  4/4   0%     ✓ stable     │
│  npm run typecheck       ●●●●  4/4   0%     ✓ stable     │
│  npm run test:coverage   ●✗●✗  2/4   50%    ⚠ flaky      │
│  npm run validate:arch   ✗✗✗✗  0/4   0%     ✗ failing    │
│                                                           │
│  ● = pass   ✗ = fail  (chronological, left to right)     │
└───────────────────────────────────────────────────────────┘
```

- Each row shows: step name, mini pass/fail sparkline (colored dots, chronological), pass count/total, flakiness percentage.
- Flakiness % = (number of outcome flips / total runs − 1). A step that goes ●✗●✗ flips 3 times out of 3 transitions = 100% flaky.
- Labels: `✓ stable` (0% flaky, all pass), `⚠ flaky` (> 25% flaky), `✗ failing` (0% pass across all runs), `⚡ recovering` (last run passes, earlier runs fail).
- Clicking a step row expands `output_tail` from that step's most recent failure inline.

---

## §G: Diff Annotation for Reviewers

### Purpose

Allow reviewers to leave inline comments on specific diff hunks, exactly as in GitHub code review. Eliminates the current constraint of writing all review feedback into a single textarea with no connection to the code being discussed.

### User Job

Reviewer at `ready_to_merge` wants to say "this line specifically needs to handle the null case" — attached to the actual line, not floating in a text box.

### Data Sources

Client-side only: annotations are accumulated in component state as the reviewer comments on hunks, then bundled with the `review.request_changes` action payload.

New type (client-side only):

```typescript
interface DiffAnnotation {
  file_path: string;
  hunk_header: string; // diff hunk header line (e.g., "@@ -12,4 +12,6 @@")
  line_number: number; // line number in the modified file
  comment: string;
}
```

The `review.request_changes` action body gains an optional `annotations: DiffAnnotation[]` field. The control-plane receives this as part of the existing review action tool call — no schema changes are needed if the annotations are embedded in the review message as structured text (see implementation note below).

### Component: `AnnotatableDiffViewer.tsx`

Replaces `DiffViewer` in `ChangesTab.tsx` for `ready_to_merge` and `qa` phases. For other phases, `DiffViewer` continues to be used unchanged.

```
┌──────────────────────────────────────────────────────────┐
│  src/auth/login.ts                                       │
│  ─────────────────────────────────────────────────────── │
│  @@ -12,4 +12,6 @@                           [+ Comment] │  ← hover reveals
│   function login(user: string) {                         │
│  +  if (!user) throw new Error('missing user');  [+]     │  ← line-level button
│     return db.find(user);                                │
│   }                                                      │
│                                                          │
│  ┌─────────────────────────────────────┐                 │
│  │ 💬 Your comment on line 14:         │                 │
│  │ [What happens if user is empty str?]│                 │
│  │ [Save]  [Cancel]                    │                 │
│  └─────────────────────────────────────┘                 │
│                                                          │
│  📎 3 annotations attached to this review                │
└──────────────────────────────────────────────────────────┘
```

- Each diff line gains a `[+]` comment button on hover (not shown by default to avoid visual noise).
- Clicking `[+]` opens an inline comment popover below that line.
- Saved annotations accumulate in state and are shown as a count badge at the top of the diff: "📎 N annotations."
- When the reviewer submits via `RequestChanges`, the annotations are serialized and prepended to the review message in a structured format:

  ```
  [ANNOTATIONS]
  src/auth/login.ts:14 — What happens if user is empty str?
  src/auth/login.ts:22 — This should be async
  [/ANNOTATIONS]

  {reviewer's free-text message}
  ```

  This requires no backend schema changes — the control-plane receives it as message text.

**Implementation note:** The `AnnotatableDiffViewer` uses `diff2html` for rendering (same as existing `DiffViewer`) with a thin React overlay for the comment buttons. Monaco Editor is not required for this feature.

---

## §H: Command Palette

### Purpose

A keyboard-driven global overlay (Cmd+K / Ctrl+K) that lets power operators navigate between features, switch tabs, and trigger actions without using the mouse.

### User Job

Operator managing 15 concurrent features wants to jump to a specific feature, check its gate status, and approve it — without clicking through the board each time.

### Data Sources

- `DashboardStatusPayload.features: FeatureSummary[]` — all active feature IDs and phases (already fetched).
- Current tab state (from `FocusLayout` context) — for "Switch to [tab]" commands.

No new API endpoints required.

### Component: `CommandPalette.tsx`

Mounted at the root layout level (`app/layout.tsx`) so it is available on every page in the dashboard.

```
┌─────────────────────────────────────────────────────┐
│  ⌘K  > feat/my-feature_                             │  ← fuzzy search input
│  ─────────────────────────────────────────────────── │
│  FEATURES                                            │
│    ● feat/auth-tokens          building   → Open     │
│    ● feat/db-schema            qa         → Open     │
│    ◎ feat/my-feature           ready      → Open     │
│                                                      │
│  ACTIONS  (current feature)                          │
│    Approve feature             ↵                     │
│    Deny feature                                      │
│    Request Changes                                   │
│                                                      │
│  NAVIGATION                                          │
│    Switch to Changes tab                             │
│    Switch to Gates & QA tab                          │
│    Go to Board                                       │
└─────────────────────────────────────────────────────┘
```

**Keyboard behaviour:**

- `Cmd+K` / `Ctrl+K` — open palette
- `Escape` — close
- `↑` / `↓` — move selection
- `Enter` — execute selected command
- Typing filters all sections by fuzzy match against feature IDs, command names, and tab labels

**Command categories:**

1. **Features** — all active features from `DashboardStatusPayload.features`, fuzzy-matched against input. Selecting navigates to `/feature/{id}`.
2. **Actions** — phase-appropriate actions for the currently-viewed feature (Approve, Deny, Request Changes, Retry, Send Message). Only shown when on a `/feature/{id}` page. Executing an action calls the same handler as the button in the sidebar.
3. **Navigation** — Switch to [tab name] for each visible tab on the current feature; Go to Board.

**Implementation:**

- Global `keydown` listener registered in `useEffect` at layout level, cleaned up on unmount.
- Fuzzy matching: simple substring scoring (no dependency — implement inline, < 30 lines).
- Palette is a `position: fixed` overlay with a backdrop. Focus is trapped inside while open (`Tab` cycles through results).
- `aria-role="combobox"` with `aria-expanded`, `aria-activedescendant` for screen reader compatibility.

---

## §I: Per-Role Agent Performance Cards

### Purpose

Show how each agent role (planner, builder, QA) performed in this run compared to historical averages for the same provider/model — catching anomalous runs before they become failures.

### User Job

Operator sees a feature that has been in the `building` phase for 40 minutes and wants to know: is this normal for this provider, or is the builder stuck?

### Data Sources

- `GET /api/features/[id]/worker-events` → `WorkerEventEntry[]` — per-iteration timing and counts, filterable by `role`.
- `GET /api/analytics/provider` → `ProviderAnalytics[]` — historical averages by provider + model.
- `FeatureSummary.cluster: AgentSessionCluster` — session IDs per role.

### Component: `AgentRolePerformanceCard.tsx`

Three instances rendered in **Overview tab** (one per role: planner, builder, qa). Each card is compact — collapsed by default, expandable.

```
┌────────────────────────────────────────────────────────┐
│  🟧 BUILDER  · claude-sonnet-4-6                       │
│  ─────────────────────────────────────────────────────  │
│  Runtime:     12m 04s   (avg: 8m 22s)   ↑ 1.4×        │
│  Requests:    47         (avg: 31)       ↑ 1.5×        │
│  Patches:     8          (avg: 4)        ↑ 2.0×        │
│                                                        │
│  ████░░░  vs historical average                        │
│  ⚠ Running slower and making more requests than average│
└────────────────────────────────────────────────────────┘
```

- Metrics shown: total elapsed time, total request count, patch count, plan submission count (where relevant per role).
- Each metric shows actual value, historical average in parentheses, and a ratio badge.
- Ratio badges: `↑ Nx` in amber if > 1.5× average, `↑ Nx` in red if > 2.5× average, `✓` in green if within 0.75–1.25× average.
- A subtle horizontal bar chart compares this run to the historical average visually.
- Summary sentence: "Running slower and making more requests than average" / "On track" / "Ahead of historical average."
- Card is hidden for roles that have not yet started (`role_status === 'ready'`).

---

## §J: Merge Queue Position

### Purpose

Show where this feature sits in the merge sequence, what is blocking it from merging ahead of other features, and what it is blocking — without navigating to the board.

### User Job

Reviewer approving a `ready_to_merge` feature wants to know: "If I approve this now, when will it actually merge? What's ahead of it?"

### Data Sources

`GET /api/merge-queue` → `MergeQueueEntry[]`:

```typescript
interface MergeQueueEntry {
  feature_id: string;
  position: number; // 1-indexed position in the queue
  phase: string;
  estimated_merge_at: string | null;
  blocked_by: string[]; // feature IDs that must merge first
  blocks: string[]; // feature IDs waiting on this one
  lock_holder: boolean; // whether this feature currently holds the merge lock
}
```

Server derives this from `FeaturesIndex.dep_blocked`, `lock_leases`, and the ordered `active` feature list.

### Component: `MergeQueueCard.tsx`

Placed in the **sidebar**, below the PR status card. Visible for all non-`merged` phases. Hidden if queue has only one entry and no dependencies.

```
┌────────────────────────────────┐
│  MERGE QUEUE  · Position #3   │
│                                │
│  ① feat/auth-tokens  [rtm]   │  ← ahead (links)
│  ② feat/db-schema    [qa]    │
│  ③ YOU ARE HERE      [rtm]   │  ← current feature highlighted
│  ④ feat/route-fix    [bldg]  │  ← behind (dimmed)
│                                │
│  Waiting on: feat/auth-tokens  │  ← dep-block if applicable
│  Est. merge window: ~2h 15m   │  ← based on avg phase durations
└────────────────────────────────┘
```

- Queue positions are rendered as a compact vertical list (max 5 shown; "N more" collapse for longer queues).
- Features ahead of current are shown normally; features behind are dimmed.
- Current feature is bold with a `YOU ARE HERE` label.
- `Waiting on` row appears when `blocked_by` is non-empty.
- `Est. merge window` is omitted when `confidence` is too low (insufficient historical data).
- Each feature ID in the list is a link to `/feature/{id}`.

---

## Track B: New API Endpoint Contracts

All new endpoints follow the existing Next.js API route pattern in `packages/web-dashboard/src/app/api/`. Response envelope: `{ ok: true, data: T } | { ok: false, error: string }`.

### `GET /api/features/[id]/files`

Returns the sorted list of file paths modified by this feature's current diff.

```typescript
// Response: string[]
// e.g. ["src/auth/login.ts", "src/auth/session.ts", "test/auth.spec.ts"]
```

Implementation: parse the diff from `FeatureDetail.diff` using the same diff parser already used in `aop-client.ts`. Extract `+++ b/<path>` lines. No filesystem access required beyond what already exists.

### `GET /api/features/[id]/gate-history`

Returns all past gate run evidence for this feature, in chronological order.

```typescript
// Response: GateRunEvidence[]
```

Implementation: reads all `evidence/gate_run_*.json` files from `.aop/features/{id}/evidence/` and returns them sorted by `finished_at`.

### `GET /api/features/[id]/genealogy`

Returns the revision chain for this feature (ancestors and descendants).

```typescript
// Response: GenealogyNode[]
interface GenealogyNode {
  feature_id: string;
  phase: string;
  status: string;
  revision_of: string | null;
  revision_reason: string | null;
  plan_version: number;
  last_updated: string;
}
```

Implementation: traverse `revision_of` links from the features index. Maximum depth: 20 (guard against cycles).

### `GET /api/features/[id]/worker-events`

Returns the worker event ledger for this feature.

```typescript
// Response: WorkerEventEntry[]
```

Implementation: reads from the existing worker event ledger file (same source as the runtime inspector). Filtered to entries with `feature_id === id`.

### `GET /api/analytics/provider`

Returns historical performance analytics aggregated by provider + model.

```typescript
// Response: ProviderAnalytics[]
```

Implementation: aggregates `WorkerEventEntry` data across all features in the index. Can be cached with a 5-minute TTL since this is aggregate historical data.

### `GET /api/merge-queue`

Returns the ordered merge queue with dependency information.

```typescript
// Response: MergeQueueEntry[]
```

Implementation: derived from `FeaturesIndex.active`, `dep_blocked`, and `lock_leases`. Order: lock holder first, then dep-unblocked features in index order, then dep-blocked features last.

---

## Track B: Success Criteria

| Feature                 | Criterion                                                            | How Verified                                                                                                                                               |
| ----------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Agent Transcript        | Entries grouped by role into turns; code fences syntax-highlighted   | Unit test: render 10 entries alternating planner/builder, assert 5 bubbles; assert `<code>` block in builder turn                                          |
| Agent Transcript        | Role filter pills hide turns from other roles                        | Unit test: select "Planner" filter, assert builder turns absent from DOM                                                                                   |
| Code Evolution Scrubber | Slider position updates displayed diff without network request       | Unit test: render scrubber with 5 checkpoints, change slider to index 3, assert `DiffViewer` receives `checkpoints[3].diff_snapshot`, assert 0 fetch calls |
| Code Evolution Scrubber | Play mode advances one checkpoint per 1.2s                           | Unit test: mock timers, click Play, advance 2.4s, assert index incremented by 2                                                                            |
| File Collision Radar    | Hidden when no files overlap with other active features              | Unit test: `deriveCollisionRisk(thisFiles, {})` returns `[]`; assert panel absent from DOM                                                                 |
| File Collision Radar    | High-risk files shown first; low-risk collapsed by default           | Unit test: render with 1 high + 2 low risk entries, assert low hidden; click toggle, assert visible                                                        |
| Usage Burn Chart        | `will_exceed_budget === true` turns token budget bar red             | Unit test: `projectUsageBurnRate` with projected tokens > token budget, assert red class on bar                                                            |
| Usage Burn Chart        | `confidence === 'low'` shows disclaimer                              | Unit test: render with 0 worker events, assert disclaimer text present                                                                                     |
| Usage Burn Chart        | Derived USD hidden when token-to-dollar conversion is not configured | Unit test: render with `tokenCostConversion = null`, assert no dollar labels shown                                                                         |
| Genealogy Tree          | Hidden when `revision_of === null` and no children                   | Unit test: render with isolated feature, assert tree absent                                                                                                |
| Genealogy Tree          | Nodes link to correct `/feature/{id}` paths                          | Unit test: click ancestor node, assert navigation to correct URL                                                                                           |
| Gate Flakiness          | Flakiness % correct for alternating pass/fail pattern                | Unit test: 4 runs [pass, fail, pass, fail] = 3 flips / 3 transitions = 100%                                                                                |
| Gate Flakiness          | Stable steps labeled `✓ stable`, flaky labeled `⚠ flaky`             | Unit test: assert label class for each pattern                                                                                                             |
| Diff Annotation         | Annotations serialized as structured text in review message          | Unit test: submit request_changes with 2 annotations, assert message contains `[ANNOTATIONS]` block                                                        |
| Diff Annotation         | `[+]` button only visible on hover                                   | Snapshot test: assert button has `opacity: 0` by default, `opacity: 1` on hover (CSS class)                                                                |
| Command Palette         | Opens on Cmd+K, closes on Escape                                     | Unit test: dispatch `keydown` with metaKey+k, assert palette visible; dispatch Escape, assert hidden                                                       |
| Command Palette         | Feature list filtered by fuzzy match                                 | Unit test: type "auth", assert only features containing "auth" in ID are shown                                                                             |
| Command Palette         | Action commands only shown on `/feature/{id}` pages                  | Unit test: render palette on board page, assert ACTIONS section absent                                                                                     |
| Per-Role Cards          | Cards hidden for roles with `role_status === 'ready'`                | Unit test: render with all roles ready, assert no cards rendered                                                                                           |
| Per-Role Cards          | Ratio > 2.5× shows red badge                                         | Unit test: `elapsed_ms` = 3× provider average, assert red ratio badge                                                                                      |
| Merge Queue             | `YOU ARE HERE` label on current feature                              | Unit test: render queue with current feature at position 3, assert label at index 2                                                                        |
| Merge Queue             | Hidden when queue has one entry and no dependencies                  | Unit test: render with single-entry queue and `blocked_by: []`, assert card absent                                                                         |
