# Registry metadata for the Job page — design

## Problem

`Job.tsx`'s Registry Metadata panel (`SLUG/TOPIC/CREATED/EXEC_MODE/SCHEDULER/SEAT/BASE_BRANCH/LAND_MODE`)
always renders `—`. `control-api`'s `/stream` snapshot payload never carries these fields — they don't
exist anywhere in the run's registry pointer (`~/.harness/runs/<runId>.json`) or in `makeRunSummary`'s
output. This isn't a UI bug; the backend never captured the data.

## What's actually deducible

`Job.tsx`'s field list (`slug, topic, created, exec_mode, scheduler, seat, base_branch, land_mode`) matches
the `meta` record schema older brainstorm-authored session files write as line 1 of
`docs/plans/YYYY-MM-DD-<slug>.jsonl` — but the schema is mid-transition. Checked all 7 session files
present in this repo: 6 (including today's `2026-07-04-runplan-dep-provisioning.jsonl`) carry the full
`session-state/v1` shape with `topic/created/exec_mode/scheduler/land_mode/base_branch`; 1
(`2026-07-02-preset-tmpfile-cleanup.jsonl`) already uses the brainstorm skill's current, slimmer schema —
`{slug, base_branch, gate0_mode, preset}` only. **`seat` has never been a `meta` field in either schema** —
it lives per-task in the run's journal (`runstate/<slug>.jsonl` lines carry `binding.seat`, e.g. `"coder"`),
because a run can bind different seats to different tasks; there is no single run-level "seat".

So the real sourcing, field by field:
- `slug`, `created` — always available from the registry pointer itself (`pointer.slug`, `pointer.ts`),
  independent of whether a plan session file exists at all. No `plan.meta` dependency needed.
- `base_branch` — present in `plan.meta` under **both** schemas (it's one of the slim schema's 4 fields
  too). Reliably available whenever a plan session file exists at all.
- `topic`, `exec_mode`, `scheduler`, `land_mode` — present in `plan.meta` only for plans using the full
  schema; absent for plans using the slim schema or no session file. Correctly render as `—` when absent;
  never guessed.
- `seat` — not a meta field under either schema. Deduced instead from the run's own journal: the seat
  bound to the most recently-active task (`runRecord`'s already-loaded task map, same data
  `makeRunSummary` reads today for `currentTask`).

`src/runner.js`'s `loadPlan()` (line ~759) already parses the session file and exposes it as `plan.meta` —
a live object already sitting in memory at `runnerContext.plan.meta` at the exact point
`writeRunRegistryPointer()` is called (`src/runner.js:111`, inside `run()`).

## Approaches

### Approach A: lazy match at read time
`control-api.js` globs `<repoRoot>/docs/plans/*-<slug>.jsonl` on every `/stream` request, parses line 1.

| Dimension | Assessment |
|---|---|
| Robustness | Ambiguous glob match if slug appears in >1 dated file (reruns, renamed plans); breaks silently if the file moves/gets archived |
| Long-term | Couples control-api's request path to a doc-authoring convention that's free to change |
| Performance | Extra fs glob + read + parse on every snapshot request |
| Reversibility | two-way door, but adds a permanent runtime dependency on `docs/plans/` layout |

**Weakness:** matches a filesystem convention instead of the data the run itself already has in memory.

### Approach B: capture once, at run start (recommended)
Extend `writeRunRegistryPointer()`'s payload with the whitelisted `meta` fields, filled from
`runnerContext.plan.meta` at the exact call site that already runs once per run start.

| Dimension | Assessment |
|---|---|
| Robustness | No glob/ambiguity; a run not launched via a plan session file (`plan.meta` absent) just gets absent fields, never a wrong match |
| Long-term | Pointer file is already the single per-run registry record; this is additive, no new moving part |
| Performance | Zero extra I/O — data is already in memory at write time |
| Reversibility | two-way door — additive JSON fields, no migration |

**Recommended: Approach B** — reuses data already in scope at the one place a run's registry record is
written, adds no new read path, and degrades to "field absent" (never "wrong field") for runs without a
plan session file.

## Design

### 1. `src/runner.js` — `writeRunRegistryPointer(pointer)` (line ~182)

Accept an optional `meta: Record<string, unknown>` on the input `pointer` object. Persist a **whitelisted**
subset into the on-disk JSON — do not persist arbitrary `plan.meta` verbatim (it carries operational fields
like `preflight`/`refresh_how` that aren't registry metadata and may vary release to release):

```
META_REGISTRY_FIELDS = ['topic', 'created', 'exec_mode', 'scheduler', 'base_branch', 'land_mode']
```

Note: no `seat` here — it is never a `meta` field (see below). For each key in `META_REGISTRY_FIELDS`
present in `pointer.meta`, copy it onto the written payload under that same key. Absent keys are simply
omitted (not written as `null`) — `makeRunSummary` and `Job.tsx`'s `mergeRegistry` already treat "key
absent" as "no update," so omission is the correct absent-data representation, not an extra
null-handling path. Always also write `created` from `pointer.ts` (the pointer's own write timestamp,
sliced to a date — `meta.created` is a bare `YYYY-MM-DD`, so normalize the fallback to match) when
`meta.created` is absent — this makes `created` reliably present even for slim-schema or session-file-less
runs.

### 2. `src/runner.js` — call site (line ~111, inside `run()`)

Pass `meta: runnerContext.plan.meta || {}` alongside the existing fields in the
`writeRunRegistryPointer(...)` call.

### 3. `src/control-api.js` — `makeRunSummary(runRecord, supervised)` (line ~64)

Add a `registry` object to the returned summary, built from `runRecord.pointer`:
`{ slug: pointer.slug, created: pointer.created || pointer.ts, ...pick(pointer, META_REGISTRY_FIELDS
without 'created'), seat: <deduced, see below> }`.

**`seat`**: derive from the same task map `makeRunSummary` already reads for `currentTask` — take
`binding.seat` off any task record that has one (in practice near-constant across a run's tasks, since the
plan skill authors every task as `"coder"`; reviewer/fixer seats are runtime-added, not authored). Absent
if the run has no task bindings yet.

Reuse the whitelist constant — export it from `runner.js` or duplicate the literal array (closed enum,
changes together with `writeRunRegistryPointer`).

### 4. `src/control-api.js` — `/stream` snapshot handler (line ~217, inside the `includeSnapshot` block)

Add `registry: summary.registry` to the per-run snapshot `payload` object sent via `sseFrame`. `Job.tsx`'s
`mergeRegistry()` already spreads `toObject(payload.registry)` (`Job.tsx:110`) — no client-side change
needed.

### 5. Tests

- `test/runner-integration.sh` (or wherever `writeRunRegistryPointer` is unit-covered): assert a pointer
  written with `meta: {topic: 'x', exec_mode: 'ship', ...}` persists exactly those whitelisted keys, that
  `created` falls back to the pointer's own `ts` when `meta.created` is absent, and that a pointer written
  with no `meta` (or a `meta` missing some keys) omits the missing keys rather than writing
  `null`/`undefined`.
- `test/control-api.test.*` (existing suite): assert `makeRunSummary` includes `registry.slug` and
  `registry.created` always, the other whitelisted fields when present on the pointer, and `registry.seat`
  derived from the task map's most recent binding; assert the `/stream` snapshot frame's payload carries
  `registry`.
- Web side: no test change required (`Job.tsx`/`job.test.ts` already cover `payload.registry` merge).

## Out of scope

- Plans using the brainstorm skill's current slim `meta` schema (`slug, base_branch, gate0_mode, preset`
  only, e.g. `docs/plans/2026-07-02-preset-tmpfile-cleanup.jsonl`) will show `slug`, `created`, `base_branch`
  and `seat` (all independently sourced) with `topic`/`exec_mode`/`scheduler`/`land_mode` as `—`. This is
  correct, not a bug — the slim schema genuinely does not capture that data anywhere. Re-adding those
  fields to the brainstorm skill's session-file schema is a separate decision, out of scope here.
- Runs with no plan session file at all: same degraded set (`slug`/`created`/`seat` only).
- Backfilling registry metadata for already-completed/historical pointer files on disk — out of scope,
  affects only runs started after this ships.
