# Mission Control — Data Contract

`mission-control.html` (the program board) and `dashboard.html` (the generic per-phase taskgraph view) are **fully data-driven**. They contain no per-phase code. To add, update, or remove a phase you only touch JSON — never the HTML.

Two file types drive everything:

1. **`phases-manifest.json`** — one file. The list of phases and their high-level status.
2. **`phase-<id>-orchestration-state.json`** — one per phase. The self-contained taskgraph + live status.

The orchestrator owns both. As long as it emits these shapes, the dashboards stay correct with zero code changes.

---

## 1. `phases-manifest.json`

```jsonc
{
  "project": "Korti",
  "generated_at": "2026-06-05T17:30:00Z",        // ISO-8601 UTC
  "dashboard_url": "dashboard.html",
  "phases": [
    {
      "id": "2d",                                  // unique; used in dashboard.html?phase=2d
      "name": "Profile · Invite Gate · Export · BYOK",
      "group": "Phase 2 · Capture & Compilation",  // cards are grouped by this label
      "order": 25,                                 // sort order across the whole board
      "status": "running",                         // running | completed | not_started
      "state_file": "phase-2d-orchestration-state.json",  // null if no taskgraph yet
      "summary": { "total": 10, "completed": 9, "in_flight": 0,
                   "pending": 1, "failed": 0, "blocked": 0 },  // optional cache; see note
      "pct": 90,                                   // optional cache
      "overall_status": "awaiting_operator_smoke", // optional; copied from the state file
      "started_at": "2026-06-04T22:00:00Z",
      "updated_at": "2026-06-05T04:50:00Z",
      "note": "..."                                // shown on cards that have no state_file
    }
  ]
}
```

Field notes:

- **`status`** drives the card's visual theme: `running` (cyan, animated top-scan, clickable), `completed` (green), `not_started` (dim dashed, planned).
- **`state_file: null`** is valid — use it for phases that are planned (Phase 3+) or that shipped before telemetry existed (1A/1B). The card renders but isn't clickable; put a one-liner in `note`.
- **`summary` / `pct` are an optional cache** for the board so it doesn't have to open every state file. If you omit them, set them to `null` and the board falls back to `pct = 100 if completed else 0`. Keeping them fresh (the orchestrator can recompute on each write) is what makes the program-completion bar accurate. **When present, every count must equal the value derived from the phase's `tasks` — the orchestrator recomputes it on every write and `validate-orchestration-state.mjs` asserts it. A summary that drifts from the task statuses is a bug, not a cache.**
- **`group`** is free text; phases with the same `group` render under one heading, in `order`.

---

## 2. `phase-<id>-orchestration-state.json`  (the important one)

This is the **single source of truth** for a phase. It must be *self-contained*: the generic dashboard parses no markdown — every field it needs to draw the graph lives here.

```jsonc
{
  "phase": "2d",                          // bare id (no "phase-" prefix; both tolerated)
  "name": "Profile · Invite Gate · …",    // optional; manifest name wins if absent
  "overall_status": "running",            // running | completed | shipped | blocked | failed | <free text>
  "started_at": "2026-06-04T22:00:00Z",   // drives the mission clock
  "updated_at": "2026-06-05T04:50:00Z",
  "current_wave": 3,
  "concurrency_cap": 3,
  "summary": { "total": 10, "completed": 4, "in_flight": 2,
               "pending": 4, "failed": 0, "blocked": 0 },
  "tasks": {
    "T-01": {
      "title": "Platform bundle: migrations 015 + 016 + ProfileSnapshot",  // REQUIRED
      "status": "merged",                 // REQUIRED — see status vocabulary below
      "role": "opus",                     // REQUIRED — opus | sonnet | haiku | manual
      "wave": 1,                          // REQUIRED — integer; sets the column
      "depends_on": [],                   // REQUIRED — array of task ids (edges point here)
      "github_issue": 207,                // optional (alias: "issue")
      "pr": 217,                          // optional (alias: "pr_number")
      "commit": "c58dede",                // optional
      "agent": "korti-worker-opus@T-01",  // optional — shown in the agent roster
      "started_at": "2026-06-04T22:15:00Z",  // optional — drives the live elapsed timer
      "completed_at": "2026-06-04T22:45:00Z",// optional — drives duration
      "attempt": 1,                       // optional
      "error": null,                      // optional — string puts the node in a failed banner
      "note": "Reviewer PASS; CI 7/7 green."  // optional (alias: "notes")
    }
  }
}
```

### The four required task fields

`title`, `status`, `role`, `wave`, `depends_on`. If any are missing the dashboard falls back (title→id, role→sonnet, wave→1, depends_on→[]), but the graph won't be meaningful. **Emit all five.**

### How the graph is drawn

- **Columns = `wave`.** Tasks are placed left-to-right by wave number.
- **Edges = `depends_on`.** Every id in a task's `depends_on` becomes an arrow from that upstream task into this one. This array is the *only* thing that defines connections — there is no separate edges list. Dangling ids (not present in `tasks`) are silently dropped.
- **Critical path** is computed automatically as the longest dependency chain. No field needed.
- **Waves can be derived** from `depends_on` if you ever omit `wave` (longest-path depth), but emitting `wave` explicitly is preferred so the layout matches your intended schedule.

### Status vocabulary

The dashboard normalizes many spellings into five visual states:

| Visual state | Accepted `status` values |
|---|---|
| **completed** (green) | `merged`, `completed`, `done`, `verified`, `shipped`, `pass`, `passed` |
| **in_flight** (cyan, animated) | `in_flight`, `in_progress`, `running`, `active`, `needs-review`, `needs-human-verification` |
| **failed** (red) | `failed`, `error` |
| **blocked** (amber) | `blocked` |
| **pending** (dim dashed) | anything else, e.g. `pending`, `queued` |

Pick any spelling in a column; they all map to the same look. Prefer one consistent vocabulary per project.

### Roles

`role` themes the node tag and the agent roster. Use `manual` for human/operator tasks — they get a purple accent and a 🧑 marker. Anything containing "manual", "human", "operator", or `n/a` is coerced to `manual`; otherwise opus/sonnet/haiku are matched by substring, defaulting to `sonnet`.

---

## Conformance — validate before you trust it

The state file feeds two consumers that both act on it blindly: the dashboard renders it, and the orchestration coordinator reads it to decide what's eligible to dispatch next. A drifted state file is therefore not cosmetic — it misleads the machine. Because every transition is a manual `Write`, "remember to update it" is not enough on its own; pair the discipline with a reconciler.

`validate-orchestration-state.mjs` (shipped in this kit, copied into `docs/phases/`) is that backstop. Run it after every state write and from the project's pre-push hook:

```bash
node docs/phases/validate-orchestration-state.mjs docs/phases/phase-<id>-orchestration-state.json
# or every running phase at once:
node docs/phases/validate-orchestration-state.mjs --all --dir docs/phases
```

It enforces:

- **Schema completeness** — every task carries the five required fields.
- **Derived summary** — recomputes the six counts from `tasks` and fails on any mismatch (`summary` is a projection, never hand-authored).
- **DAG sanity** — no task is `complete` while a dependency isn't.
- **Manifest sync** — the manifest's cached summary matches the state file (warning).
- **Ground-truth reconcile** *(the load-bearing check)* — for any task still `pending`/`blocked`, it checks GitHub for an open PR or a remote branch targeting it and flags *dispatched-but-not-recorded* drift. This catches what a derived-summary check cannot: a task whose `status` is wrong but whose summary is internally consistent with that wrong status. Degrades to a skip when `gh`/network are unavailable, so offline structural validation still works.

Exit `0` = clean; exit `1` = drift (fix the file before reporting status or dispatching). This is what makes "single source of truth" true instead of aspirational.

## Operating the boards

Both pages fetch live JSON, so they must be **served over HTTP** (browsers block `fetch()` of local files on `file://`):

```bash
cd docs/phases
python3 -m http.server 8000
# then open:
#   http://localhost:8000/mission-control.html      (program board)
#   http://localhost:8000/dashboard.html?phase=2d   (one phase)
```

- `mission-control.html` refreshes the manifest every **20s**.
- `dashboard.html` refreshes the active phase's state file every **15s** and flashes any node whose status changed.
- `dashboard.html` auto-selects the first `running` phase when opened without `?phase=`; the header dropdown switches phases without a reload.

## Adding a new phase

1. Orchestrator writes `phase-<id>-orchestration-state.json` in the shape above.
2. Add an entry to `phases-manifest.json` (`id`, `name`, `group`, `order`, `status`, `state_file`).

That's it — both dashboards pick it up on their next refresh. No HTML edits, ever.

## Legacy / per-phase snapshot dashboards

The standalone files `phase-2a-dashboard.html` … `phase-2e-dashboard.html` embed their own data snapshot and work offline (`file://`). They predate this generic system and are kept as-is. New work should use the manifest-driven `dashboard.html`; the per-phase files can be regenerated or retired at will.
