Workers

Starting a worker with st worker start, writing its task file, a worker's obligations, waiting for it, reading and sending, a worker's name, and making an agent that is already running a worker (adopting).

A worker is a Claude Code, Codex, opencode or Pi session in a tmux window of its own that works for another agent (opencode and Pi from st 0.3.113). The coordinator starts a new one with st worker start, or adopts an agent that was already running. These commands are usually run by the coordinating agent, not by you. They need st 0.3.67 or later and the hooks (st hooks install); adopting needs st 0.3.70 or later.

Starting a worker

st worker start --agent claude --name api-tests --task api-tests.md
st worker start --agent codex --task -            # the task from stdin

--agent is claude, codex, opencode or pi.

What st worker start does:

  1. It copies the task file to ~/.sophonz/orch/<run>/<name>.md and adds the worker's obligations at the end (report with st worker done when finished).
  2. It opens a new window named after the worker in the coordinator's session and starts the agent there, in the coordinator pane's folder.
  3. Once the agent's input box is up, it types one line, "your task is in this file, read it and do it", through st's chat send. The file it names is a copy inside the project, which every agent reads without asking: in a checkout .git/st/<run>/, in a worktree .st/<run>/ (st adds /.st/ to the repository's .git/info/exclude, so git status stays clean). In a folder without git it names the file in ~/.sophonz/orch/ (st 0.3.102 or later). It never types over a permission prompt or another screen, and it checks the transcript to see the line went in.
  4. It reports success only after the hooks show the worker has started its turn.

If the line cannot be typed within 90 seconds (a trust or login screen in a folder the agent has not opened before), it fails and leaves the window open: get past the screen there, or close it with st worker stop <id> --yes. If the turn does not start within 30 seconds of the line going in, it fails too; that usually means the hooks are not installed.

More flags:

  • --name <name> the window's name and the task file's. Kept to lower-case letters, digits and -, cut at 24. Without it the task's first line names the worker, and a name already taken gets a number.
  • --split a pane beside the coordinator instead of a new window.
  • --cwd <dir> the folder the worker works in.
  • --owns "<paths>" and --worktree keep workers apart; see Tasks, ownership and worktrees.
  • --machine <machine> starts it on another machine of your account; see Workers on other machines.

Workers can start workers of their own, down to two levels below their run's coordinator, and the whole chain of agents handing work down, from the top coordinator to the deepest worker, is at most four levels. st counts both, across machines too, and refuses a worker that would go deeper ("too deep"). A team adopted as a worker is a run of its own, so its workers count from its lead. This is st 0.3.88 or later; st 0.3.87 and earlier counted two levels below the top coordinator, for everything.

Writing a task file

Write the task file so it can be worked from alone:

  • the target: files, component, command
  • the change to make
  • the constraints
  • what this worker may edit, and what other workers own
  • the evidence that it is done: a test or a command to run

A worker's obligations

  • When it is done, it checks the acceptance criteria and reports: st worker done --summary "<what it did, and how it checked>".
  • If it cannot finish, it says so: --outcome failed on st worker done, or a summary that starts with blocked: (read as failed). --outcome succeeded is the default (st 0.3.113 or later).
  • If it needs a decision, it asks in its answer and stops. The coordinator reads that answer and replies with st worker send. To keep working once it has the answer, it can ask with st worker question "<question>", which waits for the coordinator's st worker reply (below). When only a person should decide, it opens a decision gate instead.
  • With a worktree of its own, it commits its work on its branch before it reports.

The worker learns this from the end of its task file and from st skills get worker. Claude Code and Codex also get it from st plugin install; opencode and Pi have no plugin, so the task file's ending points them to st skills get worker (from st 0.4.0 also st --skill, the guide for that agent in one read).

Waiting, reading and sending

st worker wait --timeout 15m      # every worker of this pane
st worker wait %15 %16 --any      # as soon as one of them matches
st worker read %15                # its last answer and its done summary
st worker send %15 "Also cover the error path."
st worker send %15 --file brief.md   # a long brief, kept as a document
st worker list                    # this pane's workers
st orch history                   # what was handed to whom, and who finished
st worker stop %15 --yes          # close its pane

st worker wait [id…] waits until the workers are done or need input. With no ids it waits for every worker of this pane.

  • --any returns as soon as one worker matches.
  • --for done,waiting,idle which states to wait for. All three by default; a closed worker (exited) always counts.
  • --timeout 15m 15 minutes by default. Running out is not a failure: look at st worker list and wait again.
  • --check-in 3m comes back after that long with each worker's state, how long it has been so and its title, and leaves them running. It exits 2, as a timeout does (st 0.4.0 or later).

It exits 0 when they are done, 3 when one is waiting for input, and 2 on timeout. A worker that is idle counts as finished, and st worker wait says so next to it: read its answer.

A worker is in one of five states:

StateMeans
workingits agent is on a turn
waitingit is asking: a question, a permission, a gate
doneit reported with st worker done
idleits turn ended without st worker done
exitedits pane is gone
  • st worker read <id> prints the worker's last answer (what it wrote after the last prompt) and its done summary.
  • st worker send <id> "<text>" says more to it, through the same chat send, checked to have gone in. A wait after a send waits for the worker to start and finish a new turn, even if it still carries the done mark from earlier work (st 0.3.83 or later). It does not send when the agent in that pane is not your run's worker, which can happen with an old id after a restore; it prints your workers' ids instead, and --force sends anyway (st 0.3.90 or later).
  • st worker send <id> --file <file|-> ["<note>"] [--title T] sends a long brief: st keeps an unchanging copy of it and a document, and types one line into the worker that points at it. --doc <doc> sends a document this machine already keeps the same way (st 0.3.93 or later).
  • What could not be typed waits in an inbox. When a send cannot go in now (someone is typing in that box, or a question is on screen), it is not a failure: the work waits in the worker's inbox and st types a pointer there between its turns, [st] 2 messages for you -- st worker inbox. The same goes for a worker's done and idle news to its parent. st worker inbox shows what is owed to this pane and marks it taken; --peek only shows, --ack <id> or --ack-all marks without showing, and --wait waits until some comes (st 0.3.113 or later).
  • A question that waits for its answer. st worker question [<id>] "<question>" asks another pane (by default this worker's coordinator) and waits for the answer, 10 minutes by default (--timeout, up to 30m); it exits 2 when none comes, and --resume <question id> waits again. The pane asked answers with st worker reply <question id> "<answer>" (st 0.3.113 or later).
  • The parent is told. When a worker runs st worker done, or its turn ends without it, st types a line into the nearest parent still at work, between its turns: the worker is done, and st worker read it (st 0.3.92 or later). It counts as told once the parent's agent has taken it.
  • st orch history shows what was handed to whom and when, who finished and who was told; --run <run>, --all, --limit N, --json (st 0.3.92 or later).
  • st worker list shows this pane's workers: id, name, window, state and task. The workers under each worker show below it, as a tree; --depth 1 shows only this pane's own (st 0.3.90 or later). A team's lead whose workers are not shown has (team N) after it, and a worker whose coordinator pane is gone has (parent gone); one whose pane no longer exists is lost: adopt it again where it runs (st 0.3.97 or later). --all lists every worker, of every run, those on other machines included. st orch status is the same list. Before st 0.3.88 it listed the workers of this pane's run, whoever started them.
  • st worker stop <id> --yes closes the pane of a worker st started, in your run. It does not close an adopted one.

A worker's id is its tmux pane id (%15) or the name st worker list shows (api-tests); one on another machine has the machine in front (ci-mac/%15, ci-mac/api-tests). A name is looked for among this pane's workers and theirs first, then every worker on the machine. When two workers have the name, st does nothing and lists them; use the id. Names work in st 0.3.90 or later. st worker on its own prints the usage. Every orch and worker command takes --json. Flag values follow a space: --name api-tests. The full list is in the st command reference.

Handoffs are kept

A brief sent with st worker send <id> --file is not typed into the worker's window. st keeps it as a file and types one line that says where it is (st 0.3.93 or later):

A message from coordinator is in /path/to/project/.git/st/<run>/sent/20261021-025532-web.md ("Title", 4.1 KB). Read it and do it.
  • The copy is kept in ~/.sophonz/orch/<run>/sent/, named for when it was sent and to which worker: <YYYYMMDD-HHMMSS>-<worker>.md. The line names its copy inside the worker's project, as for a task (see Starting a worker). It is never written over, and the brief is a document on the machine as well.

  • A long brief does not fill the worker's input box or the terminal's scrollback, and its text never goes through a shell.

  • st orch history lists the run's handoffs, when, what kind, from whom to whom, and the title:

    10-22 02:49:01  done  terminal-go %12 → st-desktop %13  1. st set — PR #126…
  • The history also has every change to the tree: an agent adopted, released, renamed or given a role, with who asked and its parent before and after (st 0.3.98 or later).

  • The History tab beside the agent graph shows the same record, newest first, in the desktop app (0.1.39 or later) and in the web terminal (st 0.3.98 or later). Pick a card to see only its events. A line opens its handoff document, or the copy st kept when the document is gone or has changed.

A worker's name

A worker's name is a label. st worker start takes it from --name or the task's first line; an adopted agent gets its tmux window's title, written the same way. It does not say which machine the worker runs on: a window someone titled "Windows CI" makes a worker called windows-ci, and that can be an agent on a Mac. The machine is the part before the / in a remote id (ci-mac/%7), and nothing else.

To give a worker a name that says what it does (st 0.3.96 or later):

st worker rename %15 ios-tests
st worker rename ci-mac/windows-ci ios-ci   # ci-mac/%7 is ios-ci now (was windows-ci)
  • It takes every id form: %15, a name, ci-mac/%7, ci-mac/<name>, for workers st started and adopted ones alike.
  • The name is written the way --name is: lower case, and anything but letters, digits and - becomes -, so iOS CI is ios-ci. A name over 24 characters is refused, not cut.
  • A name another worker has is refused (case ignored), with that worker's id. So is a name that is one of the account's machines: its slug, name or hostname.
  • The run's records keep the new name, so it comes back after a tmux restart. An adopted worker takes its window's title again only when it is released and adopted again.
  • The tmux window keeps its title. The window is yours and can hold other panes, so from then on the name and the title can differ.

For a worker on another machine, see Renaming a worker there.

Making an agent you were talking to a worker (adopting)

st worker start starts a new agent with an empty context. If an agent has already been talking with you and has read the code, it can become another agent's worker as it is. Nothing is started again, so what it knows from the conversation stays. It may be in another session. This needs st 0.3.70 or later.

In the desktop app and a browser. In the sidebar, drag the window row of the agent that is to work and drop it on the window row of the agent that is to direct it. In the desktop app you can also drop an agent's card on another's in the agent graph. What the drop shows, where it is refused, how the sidebar looks afterwards and how to let a worker go are in Adopting an agent in the desktop app's guide. Desktop app 0.1.31 or later; the web terminal in a browser (desktop layout) does the same in its sidebar from st 0.3.86. A worker whose coordinator is gone shows ⚠ and Release there (desktop app 0.1.33 or later, the web terminal from st 0.3.88): let it go, or drag its row onto an agent to adopt it again.

On the phone. In a window list, long-press an agent's window, or long-press a card in the Agents chart, and pick Make a worker of…. The Whose worker? sheet lists the agent panes of every machine on the account (this machine first), leaving out the agent itself and the ones already under it. Pick one and it says "api-tests is now a worker of coordinator, with its conversation so far." If an agent could not be told, "Couldn't tell api-tests:" comes with it, and why. To let a worker go, long-press it and pick Release. VoiceOver and TalkBack offer the same actions by name.

The Whose worker? sheet: agent panes on this machine and on CI Mac, a machine that does not take workers dimmed
Make a worker of…
The toast after adopting: review is now a worker of planner, with why one agent could not be told
After adopting
The Whose worker? sheet on Android
Android

With a command. From the pane of the agent that is to direct (or naming it with --parent). Usually you ask the agent: "adopt %15 as your worker and give it the tests".

st worker adopt %15                 # a worker of this pane
st worker adopt %15 --parent %12    # a worker of %12
st worker adopt %15 --owns "web/**" # and what it may change
st worker release %15               # let it go

st tells both agents in one line each: the parent gets the worker's id and the commands to give it work (st worker send, read, wait); the worker learns whose worker it is and to report with st worker done when it finishes a task. From then on it is a worker like any other.

Two more things come with adopting (st 0.3.93 or later):

  • A role. Run from a terminal, st worker adopt asks why the agent is brought in, unless you give --role "<why>". The role shows under the worker's name; st worker about <id> "<role>" changes it, --clear takes it off.
  • A brief. When the adopted agent's turn ends, st asks it for up to five lines on what it has done so far, which it answers with st worker brief --summary. st worker read shows it, and st worker wait <id> --brief waits for it. --no-brief adopts without asking.
st worker send %15 "Add tests for the auth module. Done when go test ./internal/auth passes."
st worker wait %15
st worker read %15
  • An adopted worker has no task file: its work comes with st worker send. No need to explain again what it already knows.
  • When an agent is on a question or permission screen and its input is not showing, st does not type the note. The adoption still happens, and the result says who was not told and why: tell it yourself once the screen is gone, or with st worker send.
  • Both must be agents. Adopting itself, an agent already below it (a loop), or going deeper than the limits above (two levels in a run, four in the whole chain) is refused.
  • An agent with workers of its own comes in as a team; see the next section.
  • Let go, it is what it was before. In the app, hover the worker's card and click Release. st worker stop does not close an adopted worker: it was someone's session first.

A team as a worker

A team is not made; it happens. A coordinator is a seat, not a fixed role at the top, so an agent that directs workers of its own can be adopted like any other, and the moment it is, it and its workers become a team with it as the lead. Drag the head row of its card in the sidebar onto another agent's row, or run st worker adopt on it. The whole team comes along, nobody's conversation is lost, and to the new coordinator it is one worker. The new coordinator may itself be someone's worker already. This needs st 0.3.88 or later (on both machines when the lead is on another one).

  • The lead keeps its run. It joins the new coordinator's run as one worker, and its own workers still work for it; nothing changes for them. The lead's st worker list still shows its team.
  • Work goes to the lead only. The new coordinator gives the task to the lead with st worker send. The lead splits it among its team, waits for them, checks their results and reports once for the whole team with st worker done --summary. st tells both agents so when it adopts the team, and the guides say the same.
  • Waiting and reading. st worker wait with no ids waits for this pane's own workers, so it waits for the lead and not for the lead's workers. st worker read <lead> gives the lead's last answer and its report for the team.
  • On screen. st worker list shows the team under its lead, or (team N) on the lead's row when the team is not listed (--depth 1, another machine), and the lead has team of N after its name in the sidebar and the agent graph (desktop app 0.1.33 or later, the web terminal from st 0.3.88).
  • Depth. Inside the team, depth counts from the lead, so its workers can still start workers of their own. The whole chain, from the top coordinator down, is at most four levels. st refuses the adoption when the new coordinator plus one level would go past two levels in its own run, or when the new coordinator's chain plus one plus the team's own depth would go past four.
  • Releasing lets go of the lead alone. It is its team's coordinator again, with its team under it.

With st 0.3.87 or earlier an agent with workers could be adopted only while the whole stack stayed within two levels, and it then moved into the new coordinator's run, so its own st worker list lost sight of its workers.

Bringing in a session that already has the context

A session you have been talking to for a while knows the code and the decisions, so its work needs less explaining than a fresh worker's. To bring it in as it is:

  1. Find the session's id. In its pane, run echo $TMUX_PANE, or look it up in st ls or the desktop app's agent graph. It looks like %15.
  2. Ask the agent that should be its parent. For example: "adopt %15 as your worker and give it the auth tests, it already knows that module." The parent runs st worker adopt %15. You can run it yourself too, in the parent's pane or with --parent.
  3. Give it work. The parent sends with st worker send %15 "<the work>", waits with st worker wait %15, and reads with st worker read %15.
  4. Let it go when it is done. st worker release %15, or Release in the app. The session goes back to being your conversation.

Agents you can mix

Parents and workers can be Claude Code or Codex, mixed: a Codex worker under a Claude coordinator, or a Claude worker under a Codex one. A new worker is picked with st worker start --agent claude|codex|opencode|pi; adopting takes whichever is already running. An agent in another tmux session works too, and so does one on another machine of the account (st worker adopt ci-mac/%7, see Workers on other machines).

Claude Code background sessions

A conversation sent to the background runs under Claude's daemon, outside tmux, and its pane only shows it. From st 0.3.83 st takes that session's hooks to the pane whose title is the session's name, so its turns, questions and subagents show on that pane. With two panes of the same name, st does not guess.

After a tmux restart

Who works for whom is kept with the tmux panes, and a run's record names its coordinator and workers by pane id. When the tmux server goes down and st restores the sessions, every pane comes back with a new id, and the old ids may go to other panes. From st 0.3.88:

  • The relations come back. Within about ten seconds st finds every coordinator and worker again, on the pane that took its place, or by its conversation for an agent you resumed by hand in another window (claude --resume), and puts the relations back. The sidebar card, the agent graph, st worker list and st worker wait then work with the new ids.
  • What you let go stays let go. st does not put back a relation that was released or cleared on purpose, and never takes a pane that belongs to another run.
  • Workers on other machines are told their coordinator's new pane; see Workers on other machines.
  • A worker whose coordinator cannot be found shows (parent gone) in st worker list and ⚠ with Release in the sidebar. A coordinator still finds its run in st worker list.
  • By a key, not a pane number (st 0.3.97 or later). Each coordinator and worker is found again by a key kept on its pane, on both machines, so a worker on another machine is never told about a pane that only took its parent's old number. A worker whose key no pane has is lost; adopt it again where it runs.
  • A run with nothing left (its coordinator in no tmux server and no snapshot) is retired to ~/.sophonz/orch/archive. st orch unarchive <run> puts it back, and its agents are found again by their keys.

st also keeps an agent from stopping tmux by accident. st's Claude Code hook refuses these shell commands from an agent in a tmux pane, and tells the agent why and what to run instead:

  • tmux kill-server with no -S or -L, which can reach the default server, the one holding every session of the machine.
  • tmux kill-server for the server the agent itself runs in.
  • pkill or killall of tmux.

To stop a test server, name its socket: tmux -S <socket> kill-server. Text in quotes or a heredoc is not refused. Codex agents are not checked yet.

A run as one file

st project keeps an orchestration run as one .sophonz file and opens it again, on this machine or another (st 0.3.98 or later).

st project save [<run>] [-o <file>]     # this pane's run, or the one named
st project archive [<run>] [-o <file>]  # save it, then retire the run
st project show <file>                  # what is in it, and what open would do here
st project open <file> [--map <saved folder>=<folder here>]
  • What is in it: the run's record, its history, the handoff documents, the task files, each agent's last summary and brief, and which conversation each agent had. A run of seven agents and 55 handoff documents came to about 137 KB.
  • Conversations are not in it by default. --with-conversations adds the agents' transcripts, which hold the code they read and the commands they ran; the file then says so in its own notes.
  • Opening it makes each agent's window again in the same tree, by its key. An agent whose conversation is on this machine (or in the file) resumes it. One whose conversation is not here starts a new one and is given what st kept of its part: its role, its last summary and its handoff documents. st project open says which, for every agent. Workers on other machines are not opened here.
  • A folder that is not on this machine is asked for, or given with --map. Without either, nothing is changed and open exits with code 3.
  • Archiving saves the file and retires the run to ~/.sophonz/orch/archive. Opening the file again, or st orch unarchive, brings it back.

In the desktop app (0.1.43 or later) it is File → Open Project… (⌘O, Ctrl+Shift+O on Windows and Linux), Save Project… (⌘S, Ctrl+Shift+S) and Archive Project…. Closing a session that holds a run's coordinator asks first: Archive, Save as Project… or Cancel.