The control plane is where the maintainer works: the /plane page pins a chat column beside the panels, and an orchestrator thread the person keeps talking to answers questions about the fleet and acts on it
Accepted 2026-09-19, 21:05 PDT, with three layout decisions the maintainer took at acceptance, closing the two questions the plan left to the check-in. The chat column defaults to 400 px wide. The person can drag the divider to resize it, and the chosen width is remembered per person in the browser's localStorage — a per-viewer convenience, never shared state, and the page renders correctly when storage is empty or blocked. Below the wide breakpoint the chat folds behind a floating button like the one the app already shows. The re-evaluation: the record was accepted to put the maintainer's conversation beside the tables it acts on — the existing web chat through the one fenced door under the person's own grants — and the three decisions are layout only, changing no thread, no preset and no fence, so that reasoning stands as written; what could regress is the difficulty map's fifth entry, the column starving the panels, watched at unit one's screenshots and revisable by amendment as What would change our mind already allows. Acceptance comes at the layout check-in ahead of unit five, so unit five's cost receipt and the unit-one screenshots land on this record as a dated amendment rather than at the flip.
The ask. Decide (the maintainer, before the plan's first unit is seeded): the /plane page becomes two columns — the panels on one side, a pinned chat on the other — and the chat is one long-lived orchestrator thread per person: a web thread bound to an orchestrator preset that holds the operator's projection plus the fleet's read commands, the plan seed, the merge and stop acts under the person's own grants, and the receipts and issue tools — so that every orchestration act a maintainer does today from a terminal is a sentence typed there, answered with the same cards the Slack door shows. Written for an engineer who knows the plane's tables, the operator door and the web chat. Success criteria:
/planerenders two columns: the panels (runs, units, pull requests, and the windows and health panels as record 0064's U12 lands them; record 0065's standing on the pipeline rows) on one side and the person's orchestrator thread pinned on the other — the existing web chat, not a second chat implementation.- The orchestrator thread is one per person, long-lived: a follow-up tomorrow continues today's thread with its transcript, and the thread survives a deploy the way any run thread does (record 0051's owner rule; the warm unit is the same store the run threads ride).
- A question about the fleet — what is running, which unit is idle, which pull request is merge-ready and nobody's — is answered from the plane's tables, never from the model's memory: the preset's read tools are the projections the panels paint, and an answer cites the row it read.
- A write typed in the thread — a plan seed, a merge, a stop, an admission or release move — runs through the same door the Slack channel runs, fenced exactly as that door fences it: bound by the operator, authorized by the policy table against the person's own grants, confirmed in proportion to its blast radius (records 0039, 0044, 0057). No new authority: the thread can do what its person can do, nothing a service actor could.
- The page speaks the twelve user nouns and no others — thread, run, agent, pipeline, unit, round, budget, follow-up, verdict, outcome, card, pull request (record 0066) — in the panels, the chat's cards and the preset's own prose instructions.
- The plane's decider and the thread divide the work by input kind: the decider (record 0064's unit 3, its unit 9 moves) acts on rules over facts it already holds; the thread acts on a person's words and never re-implements a rule the decider owns.
TL;DR
The plane's tables shipped, but the maintainer does not work there: every orchestration act of the last day — seeding plan units, re-posting asks, verifying presets, reading runs and friction, merging approved pull requests, stopping runs, enqueuing a merge queue, filing issues with run evidence, repairing handed-back threads, pacing admissions to about twenty pull requests per release, draining the fleet for a deploy, handing the release merge to a person, posting receipts, keeping a resume ledger — was done from a terminal session over the MCP tools and gh, with the /plane page open in another window as a read-only reference and the web chat on /threads as a third, unconnected surface. The bet: put the conversation where the tables are. The /plane page becomes two columns — the panels on one side, a pinned chat on the other — and the chat is one long-lived orchestrator thread per person, bound to an orchestrator preset that reads the fleet from the plane's projections and writes through the one fenced door under the person's own grants. Every act above becomes a sentence typed beside the tables it acts on, answered with the same cards the Slack door shows; a question is answered from the rows, never from memory; the thread survives deploys like any run thread. It costs one page layout, one preset, the door's existing fences extended to the acts that today have no command (the merge, the queue, the release handoff), and the long thread's tail economics. Doing nothing keeps the control plane split across three windows and one terminal, where the acts that matter most — the merge, the drain, the handoff — run over gh outside every fence the registry holds.
Today at b19caf8a
Every claim verified at head. The day of acts above is the maintainer's own report of one release day; the code claims are pinned below.
| Claim | Proof at b19caf8a |
|---|---|
/plane is routed and draws read-only panels — Runs, Units, Pull requests — with no chat | web/src/routes.ts line 32 routes GET /plane; web/src/pages/PlanePage.vue panel headings at lines 101, 156 and 190; its imports (lines 1 to 19) name no chat component; the windows and health panels are record 0064's U12, not yet built |
The web chat is its own page at /threads, unconnected to the plane | web/src/routes.ts lines 14 and 15 route /threads and /threads/:id to HomePage.vue — record 0043's chat, with its composer and rail (ChatComposer.vue, ConversationRail.vue) |
No orchestrator preset exists | src/core/budgets.ts line 135: LOOP_PRESETS is general, coding, review, research, explore, conductor; line 137 adds ship; nothing else |
| The fleet's read is already one command on three surfaces | src/core/commands/plane.ts lines 14 to 18: chat and CLI plane show and the plane_show MCP tool answer the same rows the /plane panel paints |
| A stop is a registry command with a declared blast radius; a merge is not a command at all | src/core/commands/runs.ts line 329 (runs.stop), its destructive annotation at line 339; no command under src/core/commands/ merges or enqueues a pull request — the merge, the queue and the release handoff ride gh from a terminal, outside the registry's fences and its record |
| The operator already holds a per-thread projection and a byte-capped tail that folds child reports first | src/core/dispatch/seed.ts line 306 (OPERATOR_TAIL_BYTES) and line 331 (operatorTail, dropping folded child reports before the person's own words); src/core/dispatch/operator.ts lines 573 to 587 read the thread session's tail first, then per-agent |
| The plane's decider is planned as a rules module, not a conversation | record 0064's plan: unit 3 (the decider, src/core/plane/decide.ts, behind plane.admission) and unit 9 (the moves) in its unit table |
| A web thread already survives the process | record 0043 (the conversation is the thread's records) and record 0051 (one owner for the thread's life); the state-survives-restarts invariant is AGENTS.md's sixth |
The shape
Two columns. The /plane page splits: the panels keep their side — the runs, units and pull-request tables as built, gaining record 0064's queue, windows and health panels and record 0065's standing rows as those plans land — and the other side pins a chat column. The chat is the existing web chat (HomePage.vue's composer, turns and cards) rendered into the column and bound to one thread; nothing about the chat's transport, seeds or cards is new. The panels and the chat share the page's one seed clock, so a row the chat cites is the row the panel paints.
One orchestrator thread per person. The pinned thread is a web thread keyed to the person, long-lived: opening /plane tomorrow continues today's conversation with its transcript, exactly as a run thread continues (record 0068). It is bound to an orchestrator preset — a capability profile like any other (record 0026) — that holds:
- the operator's projection and the plane's read commands (
plane show, the runs, units and pull-request reads), so a question about the fleet is answered from the tables; - the plan seed, the merge and stop acts, and the admission and release moves, each a registry command run under the person's own grants;
- the receipts and issue tools: deploy and feature receipts posted where they belong, issues filed with run evidence, and the resume ledger kept as the thread's own notes.
Answered with the same cards. The thread's answers are the cards and lines the Slack door already renders — a run's card, a unit line, a refusal with its reason — because the chat is a channel adapter behind the same seam (record 0043), not a new renderer.
Reads from the tables, never from memory. The preset's instructions and its tool set make the plane's projections the only source for a fleet fact: "which units are idle" runs the units read and cites its rows; the model never answers a standing question from its context alone. A stale answer is then a stale table — one bug class, visible on the panel beside the answer.
Writes through the one door. A sentence that means a write is bound by the operator to one registry command (record 0057), authorized against the person the dashboard session names (record 0042), and confirmed in proportion to its blast radius (record 0044): a stop asks once, a merge asks naming the pull request and its checks, a release handoff stays a person's click. The acts that today have no command — the merge, the merge-queue enqueue, the release handoff — gain registry commands first, so the door has something to bind; the thread never shells to gh and never gains a write the registry does not define.
The vocabulary is record 0066's. The page — panels, cards and the preset's prose alike — speaks the twelve nouns: thread, run, agent, pipeline, unit, round, budget, follow-up, verdict, outcome, card, pull request. The orchestrator thread is a thread; its turns are runs; the preset is an agent. No thirteenth noun rides in with the column.
The decider and the thread divide the work. Record 0064's decider admits, refuses, watches and moves on rules over facts the ledger object holds; it needs no words. The orchestrator thread acts on a person's words: it can ask the decider's tables, seed what the decider will admit, and take the moves the rules leave to a person — but it re-implements no admission rule, and the decider grows no conversational surface. When both could act — a stalled unit, say — the decider's move is the default and the thread's sentence is the override, on the record either way.
One trace: a release day, typed beside the tables
The hard case is the day the terminal currently owns end to end: pacing a release, draining the fleet, handing the merge to a person — while the deploy restarts the very process the conversation rides.
- The maintainer opens
/plane. The panels show nineteen merged pull requests since the last release; the orchestrator thread is where yesterday's conversation left it, its resume ledger the last assistant turn. Today: three windows and a terminal reconstruct this fromgh pr list, the MCP reads and a hand-kept ledger file. - "Pace admissions — one more pull request, then close coding for the release." The operator binds the sentence to the admission move; the policy table checks the person's grant; the blast radius asks once ("closes coding admission fleet-wide — go?"). The decider enforces the closed window from then on, by rule (record 0064's quiet window): the thread set the window; the decider holds it.
- "What is still live?" The preset runs the runs read and answers with the rows — two coding children and a review — citing the same table the left column paints. No answer comes from memory; scrolling the panel confirms the citation without leaving the page.
- The last unit merges; the panels read drained. "Hand the release to me." The thread posts the handoff card — the release pull request, the count, "this deploys production" — and calls no merge: the release merge stays a person's click, exactly as record 0064 shapes it.
- The person merges; the deploy rolls the bot. The orchestrator thread's transcript and notes are records in the store, so the restarted process seeds the same thread; the maintainer's next sentence — "post the deploy receipt and reopen admissions" — continues the conversation that watched the drain. Today this is the step where the terminal session's context dies with the ssh window.
- Tomorrow's first question — "what did we ship yesterday?" — is a follow-up in the same thread, answered from the receipts it posted and the tables it reads, not from a model's recollection.
The property: one page holds the whole day; every act is on the record as a bound command under the person's name; and the conversation outlives the deploy it orchestrated.
The difficulty map
- The orchestrator's authority (most consequential): the thread acts under the person's own grants — the dashboard session's identity (record 0042), the policy table's one question — never a service actor's standing credential. The merge and release commands must be born with policy rows, blast-radius annotations and a per-person record trail before the preset may hold them; a person without the merge grant has a thread that reads everything and merges nothing.
- Long-thread cost: one thread per person, kept for months, is exactly the shape the operator's tail cap and folding were built for (
OPERATOR_TAIL_BYTES, folded child reports dropped first; the session log and its compaction pointer, record 0035) — but the orchestrator's turns are denser in tables and receipts than a run thread's, so the fold rules need their own tuning and a measured receipt: what a month of release days costs in tail bytes and seed tokens, before the preset defaults on. - The merge act from a chat: today
gh pr mergeruns under the person's GitHub identity; the thread's merge must go through the same door with the same attribution — the registry command calling GitHub under an identity the person's grants and bindings resolve (record 0062's pattern), never the bot's App credential acting as nobody. Getting the attribution, the confirmation and the refusal (an unapproved pull request, a red check) right is the unit that earns or loses trust in the whole column. - The decider and the thread: record 0064's decider (unit 3, its unit 9 moves) acts on rules; the thread acts on a person's words. The boundary must be structural, not stylistic — the thread's tool set holds the moves the decider's spec leaves to a person, and no preset instruction re-states an admission rule — or the two will drift into deciding the same question differently on the same page.
- The two-column layout: the panels were designed for the full width; the pinned column costs them room, and the narrow layout must choose what folds. A visual call, gated on the maintainer's check-in, not an agent's taste.
Why not X
Why not keep the terminal and improve the MCP tools? The terminal session has no record, no fences and no survivor: the merge rides gh outside the registry, the resume ledger is a hand-kept file, and the context dies with the window. The gaps are structural, and the day's evidence is the list above.
Why not a command palette on the panels instead of a chat? A palette does the bound commands but not the conversation: the questions between the acts — what is still live, what did we ship — are the working memory of the day, and the thread's transcript is what survives the deploy. The operator door already turns sentences into commands; a palette would be a second binding path.
Why not one shared orchestrator thread for the org? Authority is personal: the thread acts under its person's grants, and two people's acts in one transcript would need per-turn identity the channel seam does not carry. Per person keeps the authority model one question; the fleet's shared state lives in the tables both people see.
Why not let the decider take the person's sentences too? The decider is a pure rules module inside a Durable Object (record 0064); giving it words means giving it a model, a budget and a conversation store — the whole thread, rebuilt in the wrong place.
Boundaries
Not changed: the panels' row semantics (records 0064 and 0065 own them), the operator's binding and fences (records 0039, 0044, 0057), the web chat's transport and cards (record 0043), the thread-ownership and survival rules (records 0051, 0068), the policy table's shape. Not owned here: the decider's conditions, windows and moves (record 0064's plan builds them); the merge door's GitHub mechanics beyond what the new commands need; the /threads page, which keeps its route and remains the general chat — the orchestrator thread is one thread pinned on one page, not a replacement rail. Compatibility: the page split is additive for a reader without the grants — the panels render as today and the column holds a read-only thread.
What would change our mind
- The column starves the panels. If the maintainer's check-in finds the two-column layout unusable below the wide breakpoint, the chat becomes a drawer the page pins open on demand, by amendment — the thread and preset are unchanged either way.
- The long thread does not pay. If the month's receipt shows the orchestrator thread's tail and seed costs dwarfing the terminal it replaced, the thread gains a harder fold or a periodic hand-rolled summary turn by amendment, never a silent truncation of the person's record.
- The merge attribution cannot be honest. If GitHub's credential model will not let the door merge as the person (not as the App), the merge act stays out of the preset and the thread hands a person the link — the trace's step four shape, applied one act wider.
Rollout
One plan, docs/plans/2026-09-19-004-feat-the-control-plane-chat-plan.md, in the same pull request as this record. Five units, each sized to one review: the two-column page pinning the existing web chat to the person's orchestrator thread (unit 1, small enough to land in a day; its visual choices gated on the maintainer's check-in); the orchestrator preset with the reads answered from the tables (unit 2); the write acts through the door, the merge and queue commands born with their fences (unit 3); the receipts, issues and the resume ledger as the thread's notes (unit 4); the long thread's economics and the deploy-survival receipt, and this record's acceptance (unit 5). Each unit binds its criteria to test ids in the plan's Verification Contract, [gap] until its unit merges.
Sources
Records 0026, 0039, 0042, 0043, 0044, 0051, 0057, 0062, 0064, 0065, 0066, 0068; docs/reference/specs/orchestration-plane.md; the maintainer's release-day report; the survey above, verified at b19caf8a.
Amended 2026-09-28 — ordinary questions use read sources in the same thread
The preset's read repertoire now includes the existing GitHub read tools and explicitly scoped MCP read sources for ordinary non-fleet questions. Fleet standing still comes only from plane_show, with the row cited. Other answers cite the GitHub path/ref or MCP server/tool/record and the source's time window, stating when freshness is unknown or evidence is partial or unavailable. The same thread session and actual requester remain in force; no write, run or Ship tool is added, and the operator, intake, dispatcher and coordinator are unchanged.
Re-evaluation: this record was accepted to put a durable personal conversation beside the tables it acts on, without another authority or write path. Refusing every question outside those tables made that conversation narrower than its working context. Adding existing read capabilities preserves the reasoning: GitHub keeps its installation-bounded read capability; MCP keeps the requester's org, current channel and own scopes and credentials. Omitted MCP agent lists remain general/research; orchestrator is an explicit opt-in even in self-serve tiers. Its MCP tool list admits only advertised non-destructive reads, after duplicate resolution. Remote annotations are not authorization: configured sources still need trusted endpoints and upstream read-only permissions. The fleet projection, session ownership and fenced command door do not change. Proof and the live citation check are in orchestration-plane.md item 12 and mcp-tools.md items 7 and 14.