ADR-9840 — The OCC reads Plan issues into the registry's row shape, behind `PLANS_SOURCE`, and the in-flight plans are backfilled as Plan issues
Mirrored from the fleet knowledge base: wi/decisions/ADR-9840-the-dashboard-reads-plan-issues-behind-a-flag.md
Updated
Mirrored verbatim from
utopia-models/knowledgewi/decisions/ADR-9840-the-dashboard-reads-plan-issues-behind-a-flag.mdby the allowlisted publish pipeline. Edit it there, not here.
artifact_type: adr
adr_id: ADR-9840
adr_status: accepted
status: active
decision_date: 2026-10-23
decision-makers: [cloud]
consulted: []
informed: [agent, tyler-youk]
supersedes:
superseded_by: ADR-9930 (the `PLANS_SOURCE` default only — production reads the board)
issue: 9840
subprocess: process
references:
- wi/work/specs/plan-405-session-context-wiring/spec.md
- wi/decisions/ADR-9839-the-plan-issue-is-the-only-record.md
- wi/work/reference/occ/planning.md
- _infra/scripts/plan-issue-backfill.py
- utopia-models/utopia#594 (apps/web/lib/plan-registry.ts, lib/occ-registry.ts, app/v1/board/route.ts, packages/occ-ui PlanCells.tsx · planStatus.ts, scripts/plans-parity.ts)ADR-9840 — The OCC reads Plan issues into the registry's row shape, behind PLANS_SOURCE, and the in-flight plans are backfilled as Plan issues
Context and Problem Statement
ADR-9839 made the Plan issue (label planning, opened by /plan-start) the only record of a planning session.
The OCC's Planning and Execution sections still read the plan registry (wi/work/plans/entries/). Before the
registry can be retired (P7), the dashboard has to render the same plans from the board, and the two views have
to be compared side by side. On 2026-10-23 the registry held 385 entries, and 26 were in flight on the live
/v1/board (17 Dispatched · 5 Parked · 4 Planning; the spec counted 42 on 2026-10-22).
Considered Options
- Do nothing. The dashboard keeps reading the registry. Rejected: the spec's acceptance requires the dashboard to read Plan issues, and P7 cannot start without a parity measurement.
- A new board-specific component tree and payload. Rejected: two renderers for one surface, and every
honesty rule in
planStatus.ts(declared vs derived, the empties) would have to be written twice. - Cut over without a flag. Rejected: there would be no side-by-side period and no rollback short of a revert.
- Integrate existing (chosen): a second reader that turns Plan issues into the same
PlanEntrythe registry reader produces.PLANS_SOURCE=board|registry(defaultregistry) picks the reader in/v1/board. Every component and every derivation stays as it is.
Decision
- The mapping (
planEntryFromIssue, utopiaapps/web/lib/plan-registry.ts):- id:
registry: PLAN-Nin the body, else#<issue>. - created:
created:in the body, else the issue'screatedAtin America/Denver (the zonecreated_dateuses). - dispatched: the first comment that opens with
▶. Adispatched yyyy-mm-dd HH:MMinside it wins over the comment's own time. - status: label
parked→ Parked · closed NOT_PLANNED → Archived · closed otherwise → Executed · open with Features or a▶→ Dispatched · otherwise Planning. - Features: the Plan issue's sub-issues plus the refs on a
Features:body line. - plan ↗:
Document:in the body, else theSpec:path thatplan-open.shwrites. - artifact: every claude.ai artifact URL in the body.
- mode · portfolio: the board fields, with the body's meta line as the fallback.
- Last active: feeds both
lastSeenand a newlastActive.
- id:
- Live: a row is tagged
● livewhen its Plan issue'sLast activeis at most 10 minutes old.plan-stamp.shwrites it at most once per 180 s while a session works, so a working session cannot fall behind the line. Only board rows carrylastActive, so the registry view is unchanged. - Dropped, named instead of hidden: steps 1–2 and their condition sub-line (
brief:,agent:,asked:), the loop counter (stepcycle:), the claude PID (pid:) and the declared phase count (phases:). No Plan-issue writer records them. A board Planning row renders—for its step, never a guessed one. - Backfill (
_infra/scripts/plan-issue-backfill.py): one Plan issue per in-flight plan, usingliveStatus()mirrored from the OCC. It carries the meta line,registry:,created:,Document:,Artifact:,Features:and## Parked. Its board fields are Portfolio · Mode · Recipe (the entry'ssubprocess:) · Last active (the entry'sseen:), plus Laneagentand Status Todo, asplan-open.shsets them. The script is idempotent onregistry: PLAN-N. It ran 2026-10-23: #9887–#9914, 26 issues. - Features are LISTED, not re-parented, in the backfill. A sub-issue link would place each chain under
stuck-pr-sweep.yml's Feature-autoclose (a Parked plan whose Features are all closed would be closed as complete) and under sweep-dispatch predicate 3, which changes 26 live chains for no reading benefit. The reader still accepts sub-issues, which are the native link for Plan issues filed from now on.
Consequences
scripts/plans-parity.tsin utopia is the instrument for the 7-day side-by-side period before P7. Run it asGITHUB_TOKEN=$(gh auth token) npx tsx --tsconfig apps/web/tsconfig.json apps/web/scripts/plans-parity.ts.- No writer posts a
▶on a Plan issue at dispatch yet.dispatch.shstamps the registry'supdated_dateonly, and none of the 26 in-flight entries carries one, so both views read "dispatched —" today. A plan dispatched from now on shows its dispatch date on the board only once something posts that comment. That writer belongs with P7, beside the registry writes it replaces. - A session on a backfilled plan stamps
Last activeonly after/plan-continue <issue#>binds it. Until then the registry'sseen:keeps moving and the board's does not. This is theupdateddifference the parity report lists for three plans. - Plans that finished before 2026-10-23 have no Plan issue. The board view's Executed and Archived groups start empty and fill from here on.
ADR-9839 — The Plan issue is the only record of a planning session; one loop hook reads the card
Mirrored from the fleet knowledge base: wi/decisions/ADR-9839-the-plan-issue-is-the-only-record.md
ADR-9841 — `claude --bg` as the dispatch runtime: adopt with limits
Mirrored from the fleet knowledge base: wi/decisions/ADR-9841-claude-bg-dispatch-pilot.md