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 orch installs 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> and orch worktree prune are its lifecycle. Promote is fail-closed on conflicts and never pushes.
  • A task can carry standing context and a list of acceptance_criteria; each criterion is rendered into the worker's prompt as an unchecked box and must come back through verify_report marked met or not. A worker that never reports leaves them all standing as unmet on the board — deliberately.
  • When clarify_before_spawn is on, spawning is gated on orch ledger check passing, unless the call sets force.
  • 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_spawn says, because the cost is the worker's scope; force is 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.