Chair and workers
One Chair per project, elected by arrival. A second interactive Chair is refused, and workers may not spawn workers.
What it is#
Two roles, and only two.
| Role | How it is set | May spawn workers? |
|---|---|---|
chair |
The first interactive orch <agent> seat in a project, opened with an empty task |
Yes — dispatch_worker, task_add, task_run |
worker |
Created by task_run or dispatch_worker, or any process with ORCH_ROLE=worker |
No — the attempt errors |
you, typing in the agent UI
|
Chair (one per git project)
/ | \
worker worker worker (own orch/<session> worktree each)
\ | /
msg_send / ask / verify_report
|
Chair
Election is by arrival, not by configuration. Open orch claude in a project and that seat is the Chair. Open a second interactive seat in the same project and Orchemax refuses it:
project already has Chair session <id> (<name>) — one Chair per project; spawn workers with orch task run, or pass --force-chair
--force-chair exists for the case the message names: you really do want two live Chairs. Ghost seats left behind by a killed terminal are cleared automatically, so you should rarely need it.
How many Chairs and workers your plan allows#
One Chair per project is the workshop law above. Separately, the plan caps how many open-agent Chairs and how many workers may run at once on a linked machine:
| Plan | Open Chairs (orch.agents.max) |
Concurrent workers (orch.agents.concurrent) |
|---|---|---|
| Free | 1 | 0 (a Chair only; workers start at Starter) |
| Starter | 2 | 2 |
| Professional | 5 | (plan concurrent) |
| Team | 10 | (plan concurrent) |
Chairs that only serve MCP (no open TUI) do not consume the worker meter.
Workers count against orch.agents.concurrent only.
Cheap-worker roster#
Optional in orch.yaml: agents.workers lists preferred {agent, model}
pairs (order = preference). The Chair should spawn from that list when the
task fits; empty list / placeholders = off and the Chair uses its own agent.
Live spawn still respects the concurrent worker cap. See
Configure agents.
Workers on other models#
- A worker can run a different CLI and a different model than the Chair. It addresses its peers by name over the same bus, in context-mode.
- Workers on external models go through Pi, which
orchinstalls and pins under machine-home the first time you dispatch one. See Configure agents. - Each worker runs in its own worktree, so what it reads and writes stays out of the Chair's window; only its own model is billed for that subtask.
- Tokens, bytes saved and hops per seat:
/app/board.
Fan-out from a worker is denied on both transports, with the same string:
dispatch_worker denied: ORCH_ROLE=worker — only the Chair may spawn (use orch msg to the Chair)
Why it exists#
A worker that can spawn workers is a fork bomb with a credit card. More quietly: it is also an accountability hole — nobody is left holding the thread that decides when the task is done. One Chair per project keeps a single place where work is handed out, a single inbox for questions, and a single verdict at the end.
Reporting is not fan-out, and the rule is explicit about it: a worker may call verify_report, because saying what you did is not the same as handing out more work.
Workers outlive the Chair's process#
A dispatched worker does not run inside the Chair's orch mcp process. dispatch_worker and task_run prepare the seat, the worktree and the log, then hand the run to a detached orch worker-run supervisor that launches the agent, seals the verification and writes the handoff. Killing the Chair — turn end, Ctrl-C, a crash — no longer leaves workers unsealed. The reply names the supervisor_pid and the detach path the OS allowed (breakaway, intermediary or setsid). On Windows, breakaway needs the Chair's own job object to allow it (JOB_OBJECT_LIMIT_BREAKAWAY_OK); without that flag neither breakaway nor intermediary gets the supervisor out, since intermediary stays inside the same job and dies with it.
The Chair still has to collect. task_wait blocks until the workers seal and returns every verdict at once:
dispatch_worker {agent, prompt, wait: false} once per worker, same turn
task_wait {all: true, timeout_sec: 300} -> status, exit, verification, handoff
Never end a turn with workers still running.
New in 0.5.0#
- A truthful start card. The Chair's start card is built from Orchemax's own records: sessions that died are shown as died, branches already merged are not listed, and pending ledger lines are shown, not counted.
- The improvement cycle. Every error, a worker's or the Chair's, becomes a row that moves detect → correct → improve (a gate) → memorize (a lesson) → learn (shown at the action) → repeat (verified no recurrence). A turn or a session cannot close with an unmemorized error, and a recurrence reopens it.
- The Chair is held to the same rules. Briefs without invariants-with-tests or an out-of-scope section are refused, repeated overrides of a gate must name the item that fixes it, and the land never blocks itself and waits for a quiet machine.
See Release 0.5.0.
Plan mode#
Risky work can be dispatched read-only first. dispatch_worker {agent, prompt, plan: true} (or orch dispatch --plan) runs the worker read-only. Claude Code refuses Write/Edit/MultiEdit/NotebookEdit at its PreToolUse hook with plan mode: read-only — return the plan in your final answer; cursor-agent has no pre-write hook, so for it (and any other CLI) the tree check below is the only enforcement. Sealing fails with plan: tree changed if the worker wrote anything anyway (a shell redirect, an unhooked CLI). The prompt gets a short template — goal, files to touch, steps, risks, tests — and the worker's final answer is captured as plan_text, returned in the reply and kept on the session row.
Once the plan looks right, dispatch the real work from it: dispatch_worker {agent, prompt, plan_from: "<planned-session-id>"} (or orch dispatch --plan-from <session>) injects that captured plan into the executing worker's prompt under "Plan approved by the Chair", instead of asking it to write a new one.
dispatch_worker {agent, prompt, plan: true, wait: true} -> plan_text, verification
dispatch_worker {agent, prompt, plan_from: "<session>"} -> executes with the approved plan
How it relates to the rest#
- A worker that needs a decision raises a blocking ask instead of guessing or spawning someone to decide for it.
- Each spawned worker gets an
orch/<session>git worktree by default;orch worktree status,orch worktree promote <session>andorch worktree pruneare its lifecycle. Promote is fail-closed on conflicts and never pushes. - A task can carry standing
contextand a list ofacceptance_criteria; each criterion is rendered into the worker's prompt as an unchecked box and must come back throughverify_reportmarked met or not. A worker that never reports leaves them all standing as unmet on the board — deliberately. - When
clarify_before_spawnis on, spawning is gated onorch ledger checkpassing, unless the call setsforce. - A spawn is refused when the ledger hands over a file that is in the working tree but not in HEAD: the worker's worktree is a checkout of HEAD, so it would not find that file. A path that exists nowhere is a file the work will create and says nothing, and a path git ignores never travels either way. This one runs whatever
clarify_before_spawnsays, because the cost is the worker's scope;forceis the escape.
Nothing to run here#
You elect a Chair by opening a seat, and you spawn a worker by asking the Chair for one in chat. orch dispatch --prompt "…" exists for scripts and CI, not for daily use.