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
Updated
Mirrored verbatim from
utopia-models/knowledgewi/decisions/ADR-9839-the-plan-issue-is-the-only-record.mdby the allowlisted publish pipeline. Edit it there, not here.
artifact_type: adr
adr_id: ADR-9839
adr_status: accepted
status: active
decision_date: 2026-10-23
supersedes:
superseded_by:
issue: 9839
subprocess: process
references:
- wi/work/specs/plan-405-session-context-wiring/spec.md
- wi/decisions/ADR-9837-plan-start-loads-one-project.md
- wi/decisions/ADR-9838-docs-follow-code-through-claude-code-action-and-a-close-out-hook.md
- _process/steps/2-loop.md
- _process/steps/plan-start-frame.md
- _infra/scripts/file-issue.sh
- utopia-models/fleet-plugins (fleet-doctrine 0.94.0 scripts/plan-open.sh, plan-stamp.sh, plan-loop.sh, skills/plan-continue)
- utopia-models/brain-agent (skills/plan-continue → skills/plan-continue-registry)ADR-9839 — The Plan issue is the only record of a planning session; one loop hook reads the card
Status note (2026-10-31): when this was decided the plan registry still ran beside the Plan issue (see Decision and Consequences, "until P7"). ADR-10109 (decided 2026-10-25) completed that retirement, and from then on the Plan issue is the only record.
Context
A planning session left its record in the plan registry (wi/work/plans/entries/ + a document, written by hooks
and CLIs on whichever box planned). Measured for PLAN-405 (2026-10-22): 190 plans in 60 days, 54% archived, 48
archived plans had no issue at all, 42 in flight — a second tracker beside the board, and the dashboard could not
see a planning session from the board. P2 (ADR-9837) gave every /plan-start session a state file
~/.claude/state/plan-issue/<sid> with project, mode and recipe; this phase fills in the issue.
The step-2 loop was read by three hooks (385 lines): posttooluse-ask-loop.sh (research-then-ask after an answer),
posttooluse-step2-exit.sh (the exit click, plus a marker for the Stop gate) and pretooluse-exit-card-guard.sh
(an exit option never shares a card with content), backed by stop-no-permission-ask.sh rule 2, which blocked a
planning turn that ended without a card. Tyler, 2026-09-29: "The stop hook gate gets annoying honestly, want a
different solution."
Measured 2026-10-23 on cloud@novaserver: createProjectV2Field works with the fleet token (fields Mode,
Recipe, Last active created); POST /orgs/utopia-models/issue-types also works (type Plan created — the
spec's note that it 404s for lack of admin:org no longer holds); GraphQL budget 5,000/h.
Options considered
- Do nothing — keep the registry as the record. Rejected: it is the second tracker the spec removes, and the dashboard (P5) cannot read a planning session from the board.
- A Plan issue filed through
file-issue.sh— Rejected:file-issue.sh(183 KB) validates TASKS (lane, spec path, chain shape, collision check) and files nothing without them; a Plan issue has no spec yet when it is opened. Its field ids are recorded there; the writer is a separate CLI. - A GitHub Discussion or a Projects draft item — Rejected: drafts cannot carry comments, sub-issues or a
Closes, and no reader on the fleet reads discussions. - Integrate existing: one issue per session in
utopia-models/knowledge, the board's own fields, the plugin's hooks — CHOSEN.
Decision
- Board fields, ADDED (Portfolio and Sub-Project untouched):
Modesingle-select (interactive, dispatch),Recipetext,Last activetext (ISO-8601). Ids recorded in_infra/scripts/file-issue.sh;plan-open.shresolves them live by name and refreshes~/.claude/state/board-fields.jsonon each box it runs on. plan-open.sh open(fleet-doctrine 0.94.0), the first action of /plan-start after LOAD: an issue inutopia-models/knowledgewith typePlan(best-effort) and labelplanning(what every reader keys on), body = brief +project: · mode: · recipe:+ the spec path; on the board Portfolio · Mode · Recipe · Last active, and Laneagent+ Status Todo, because laneagentis routed by no sweeper (sweep-dispatch.shpredicate 2) and is reporteddead-laneonly when another issue is blocked on it — a Plan issue is never dispatched (and since #9963 theplanninglabel is itself a sweeper HOLD_LABEL: a Plan issue opened with no fields set had a blank, routable Lane and was dispatched). It publishes<project>/work/specs/<issue#>-<slug>/{spec,plan}.mdskeletons throughplan-registry-publish.sh(an ephemeral worktree, never a commit in the shared clone) and merges issue, repo, item_node_id,loop: open, last_stamp_epoch into the state file.plan-stamp.sh: async on UserPromptSubmit, PostToolUse and Stop (a Stop-only stamp goes dark on an interrupt). One[ -f ]for every session without a Plan issue. The slot is CLAIMED (last_stamp_epochwritten under flock) BEFORE the GraphQL write, so concurrent fires cannot stampede. 180 s → about 25 writes an hour for 4 sessions × 150 turns.plan-loop.shreplaces the three hooks: PreToolUse denies the mixed exit card; Post/PostFailure, while the loop is open (the state file withoutloop: closed, or the old ask-loop flag under a recordedinteractive), injects research-then-ask on a non-exit answer, and on an exit writesloop: closedand says file (or produce) now.stop-docs-sync.sh(ADR-9838) already readsloop: closed.- Stop rule 2 is deleted. Rule 1 (the permission-ask) and its guards stay.
/plan-continue [issue#]is a plugin skill:plan-open.sh resumereads Mode · Recipe · Portfolio off the board item, runs the same LOAD as /plan-start, and printsplan.mdand the last comment — no registry read. Parked = labelparkedwith the condition in the body; archived = closed as not planned. The registry loader is renamed/plan-continue-registryin brain-agent (a same-named user skill shadows the plugin skill, measured in ADR-9837) and kept for plans that predate their Plan issue, until P7.- Side by side: the registry is still written by every hook that wrote it; nothing in it is deleted here.
Corrections preserved, and the ones retired
Preserved in plan-loop.sh (and graded in fleet-doctrine test/hook-integrity.sh G4/S4/S5, test/ask-loop.sh):
- 🔴 read
tool_response.answersand NOTHING else —questionsechoes the exit (the live 2.1.241 false-positive control); - 🔴 the exit predicate is
fileANDstep 3(or "produce now"), never an equality — the typed sentence and the click share it; "looks good" / "nice" / "yes" are not it; - 🔴 the click never advances the step — it writes no registry reference;
- 🔴 the Post side never denies and never blocks; every failure path is silent, exit 0;
- 🔴 a card that returns
{}, or fails other than by the model's ownInputValidationError, CLOSES the loop and writes the.no-asktombstone (#9522); a "no questions" tombstone silences the hook; - 🔴 the old flag counts only under a RECORDED
interactive(the injector's predicate, #8499); - 🔴 an exit lives only on a single-question card whose other options stay in the loop; the deny never rewrites the card;
onFailure: blockkept; - 🔴 research lands before the next card (case 2026-10-20-answer-then-wait-for-research-before-asking) — the injected text is unchanged.
Changed: the state-file path opens the loop in EITHER mode (case 2026-10-17-research-and-questions-are-step-2-in-both-modes — the old flag was interactive-only); a "produce now" press now says so once instead of emitting nothing.
Retired with Stop rule 2 (knowledge#5344) — each was a correction to that rule, so none has a subject left:
the step derived from the registry on every fire (never a sidecar); the entry: id binding and its degraded-path
notice (#5749, #5775); the three terminal statuses Archived · Parked · Executed (#7119 — still enforced in the
injector and the artifact advisory); the recorded mode outranking the document's **Mode:** header (#8499); the
30-minute bound on the exit marker and the step2-exit/<sid> marker itself (#6932); the ⟳/fork-number loop marker
in the block reason (#6588 — still in the injector). Kept from that file: the research-in-flight stand-down
(it applies to rule 1 too), the 8-block fail-safe guards, dedupe and staleness.
Consequences
- A planning session is on the board the moment its three answers land, with a
Last activeno older than 180 s while it works — P5's dashboard reads that instead of the registry'sseen:. - Nothing forces a card at the end of a planning turn any more; the order to ask comes from
plan-loop.shafter each answer and from the per-turn injector. If turns start ending on reports mid-loop, fix the injection, not a gate. - Two records exist side by side until P7 (registry retirement, after 7 days of dashboard parity).
ADR-9838 — Docs follow code through claude-code-action on PRs and a close-out Stop hook, from one map
Mirrored from the fleet knowledge base: wi/decisions/ADR-9838-docs-follow-code-through-claude-code-action-and-a-close-out-hook.md
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