Repository navigation
Restarting a machines seats
seat-stage builds one multiplexer session for a machine, with a window per seat, and starts each
seat joined to the fleet. It is for the restart after a reboot or an update: the seats you had
come back, resumed, in the places they were.
Starting multiplexer and Claude Code sessions is your act. --dry-run only prints, and check
changes nothing.
-
composer install(PHP 8.4 or later), and install the multiplexer: tmux on macOS and Linux, psmux on Windows. - Copy
examples/seats.tomlinto your private configuration repository asseats.tomland edit it. Remember it once withseat-stage config set <path>(or pointFLEET_KIT_CONFIGor--configat it). The same file holds seat-pace's[[account]]tables. See Getting started. -
bin/seat-stage check <machine>reports every reason staging would fail. -
bin/seat-stage stage <machine> --dry-run(seat-stageif installed with Composer) prints each seat's exact command and first prompt. -
bin/seat-stage stage <machine>builds the session. Attach withtmux attach -t <session>(psmux attachon Windows).
On Windows, bin/seat-stage.ps1 is a thin wrapper.
After a reboot, with the path remembered, from any directory:
seat-stage check <machine> --pull # --pull: git pull --ff-only the seats file's repository first
seat-stage stage <machine> --dry-run
seat-stage stage <machine>
The seats file is --config, else FLEET_KIT_CONFIG, else the path seat-stage config set remembered, else
seats.toml in the current directory; seat-stage config show says which. Without --pull, check and
stage fetch and print a note: when the seats file's repository is behind its upstream, and change nothing.
| Table | Keys |
|---|---|
| top level |
channel: the tagged entry passed to the channel flag, server:<name> or plugin:<name>@<marketplace> (default server:robot-council) |
[[machine]] |
name, multiplexer (tmux or psmux), session, start, start_gap, confirm_channels, remote_control
|
[[seat]] |
machine, slot, repository, checkout, role, and optionally work_location, capacity, conversation, remote_control, after, prompt, start
|
Per machine, slot, conversation and the window name (the repository name and the
slot, with anything but letters, digits, dash and underscore turned into a dash) must each be unique,
and so must work_location per machine and repository (two seats on one machine may share a
work_location when their repository differs); the file is refused otherwise. session is letters, digits, dash and underscore. A conversation is an
id of letters, digits, dot, dash and underscore. remote_control and checkout may not start with a
dash, a checkout may not hold #(, and a seat prompt may not hold a double quote, which Windows
PowerShell 5.1 would split an argument on. In PowerShell the curly single quotes (U+2018 to U+201B) are
doubled like '.
Whose seat it is. An [[account]] may carry github = "<username>". Every seat on a machine that the
account lists in machine then gets one more sentence in its first prompt, after the join or resume text and
before the seat's own prompt: Your operator's GitHub username is `<username>`. A seat told to assign
tickets and pull requests to its operator no longer has to ask in chat. Leaving github unset leaves the
prompt exactly as it was. The value is 1 to 39 letters, digits and single hyphens, not starting or ending
with a hyphen, or the file is refused. Codex seats get the same sentence. A machine listed under more than one
[[account]] is refused (naming the machine and the accounts) when any of them sets github: seat-pace
reads that list as a fallback order, which does not say whose seat it is, and nothing guesses. List the machine
under one account. If no account lists a seat's machine, the seat gets no sentence.
work_location is at most 32 characters, the width of core's column (WorkIdentity::MAX_LOCATION in robot-council/core): a
longer one is recorded cut short at join and every later session_move to the full name is refused, so the file is
refused instead, naming the seat. The default, the slot, is held to the same limit. It defaults to the slot, is unique per machine and repository, and is always passed explicitly,
because a join without it from a primary checkout records primary. remote_control defaults to
<repo>/<machine>/<slot>. harness is claude (the default) or codex; anything else is refused. What each
harness can do (its program, launch and resume arguments, flags, where check looks) is a table the code reads,
lib/stage/harnesses.toml, not prose: a new harness is a new table there.
A seat with harness = "codex" is started with codex instead of claude, with the same first prompt:
codex "<first prompt>", or codex resume <conversation> "<first prompt>" when the seat has a conversation.
Both forms were run live on macOS arm64 with codex-cli 0.163.0 (fleet-kit#26 and robot-council/cli#375):
Codex takes the prompt at launch, and codex resume <id> "<prompt>" reopens the conversation and runs the prompt
as a new turn. What a Codex seat can do after either depends on its bridge arguments, below.
-
Not woken by a channel notice. Codex has none (
openai/codex#15299), so a staged Codex seat sits at its prompt until something prompts it.codex queue --thread <id> --message <text>woke an idle session from outside, with no keypress (fleet-kit#6, macOS, codex-cli 0.163.0), butseat-stagedoes not run it.--dangerously-load-development-channels,confirm_channelsand--remote-controldo not apply, and none is passed. -
Enroll the machine for Codex first. A machine enrolled only for Claude Code is refused at
join. Wire the bridge into~/.codex/config.tomlas[mcp_servers.robot-council]withROBOT_COUNCIL_HARNESS = "codex"(the cli wiki's "Wiring the bridge into a harness"), then runrobot-council enroll --harness=codex --machine-label=<label>and approve it in the browser. In the live runenrollcould not reach the service from inside Codex's sandbox on the first try; a retry did. -
The operator still answers Codex's own prompts. The run showed Codex's folder-trust prompt and a per-tool
approval for
join;seat-stageanswers neither. -
A Codex seat's bridge must auto-join. Codex does not re-read a server's tool list, so a session that calls
joinitself keeps onlyjoinand cannot claim, narrate orleave. Andcodex resumestarts a new bridge process that has not joined, so a resumed seat offersjoinalone again, whatever the earlier session did (measured oncli#375; robot-council/cli#373). So the bridge entry in the seat's Codex configuration must carry--auto-joinwith the seat's--role,--repositoryand--work-location, the same values as its seats-file entry:args = ["mcp", "--auto-join", "--role=build", "--repository=<owner/name>", "--work-location=<work_location>"]. With that, the fleet's tools are there from the first turn, on a fresh start and on a resume. The args can come from~/.codex/config.toml, a per-checkout config, or-c 'mcp_servers.robot-council.args=[...]'at launch. Prefer the config file: a-coverride runs the session embedded, without Codex's shared background server, andcodex queuegoes through that server (queueing to an embedded session was not tested). -
seat-stagedoes not write those args, and its prompt still saysjoin. It starts the window with the prompt it builds for every seat. For a Codex seat whose bridge auto-joins, a fresh seat's "calljoin" has nothing to call (a resumed seat's heartbeat-or-join text should fit, sincepresence_heartbeatis offered, but that was not run). Say what the seat should do in its ownprompt, as below. -
Name the fleet tools in the seat's
prompt. Codex keeps MCP tools out of the model's context until it searches for them, so a model asked to list its robot-council tools said it had none, yet found and calledleavewhen told the name. Put the tools the seat needs in itsprompt, for instance "claim tasks withtask_claim, report withevents_narrate, read the feed withevents_read". Each tool call also needs Codex's own approval (Allow the robot-council MCP server to run tool "<name>"?) unless the operator chooses "Always allow". -
seat-stage checkrequirescodexonPATHonly when a machine has a Codex seat, readscodex mcp list --jsonfor therobot-councilserver and itsROBOT_COUNCIL_HARNESS = "codex", and looks for the conversation asrollout-<timestamp>-<id>.jsonlunder$CODEX_HOME/sessions(default~/.codex/sessions). What it cannot read (the list output, the sessions directory) it reports as not checked, never as passing. -
start = "when-free"is refused for a Codex seat: the holder files it reads are Claude Code's, so it cannot tell whether a live process holds a Codex conversation. Useenterornow, per seat if need be. -
seat-stage collectdoes not read Codex conversation ids and says so for each Codex seat; recordconversationby hand.
Built from the seat's own fields.
- A seat with no
conversationis asked to calljoinwithrole,repository,work_locationand, where declared,capacity. - A seat with a
conversationis resumed, and its fleet session may have ended: exiting a seat to restage ends it (session.gone). It is told to callpresence_heartbeatif the fleet's tools are offered (it is still joined;session_moveif its recorded work location differs), and to calljoinwithrole,repository,work_locationand, where declared,capacityif onlyjoinis offered. A session that has ended holds nothing, so the join releases nothing. - No prompt tells a seat to
leave, which releases the tasks it holds. - The seat's own
promptis appended.
claude "<first prompt>" [--resume <id>] [--remote-control <name>] --dangerously-load-development-channels <channel>
The prompt goes before the channel flag, and only tagged entries follow it. Measured on Windows
with Claude Code 2.1.304 (fleet-kit#7): a bare prompt after the flag is read as another channel entry
and the session refuses to start (entries must be tagged). Before the flag, the prompt is delivered,
with or without --resume. A prompt can follow the flag only after a --. seat-stage uses the
first form.
On POSIX the window runs the command under env -i with only HOME, PATH, USER, LOGNAME,
SHELL, TERM and LANG. Windows has no env -i, so the window removes the CLAUDE_CODE_*,
CLAUDECODE and CLAUDE_PID variables a parent Claude Code session leaks into a child instead.
start |
Behaviour |
|---|---|
enter |
each window waits for Enter before starting (staging ahead of a restart) |
now |
each window starts at once (after a reboot) |
when-free |
each window starts once no live process holds its conversation, in after order, start_gap apart |
A seat may set its own start (enter, now or when-free) to override its machine's; without one it inherits
the machine's, so a seats file with no seat start stages as before. This keeps seats that are only needed
sometimes (one per repository, say) waiting for a keypress while the always-on seats start on their own:
set the machine to when-free and mark the occasional seats start = "enter". --dry-run prints each seat's
effective mode. An enter seat on a when-free machine is created waiting, is skipped by the start loop and
does not hold back the seats after it; confirm_channels answers the warning only for seats that started on
their own, and the operator answers it for a seat they start. A seat's own start = "when-free" needs the
session files like a machine's does. Starting a waiting seat from outside its window is not supported.
when-free starts seats strictly in order: a seat whose conversation is still held holds back the seats
after it. It waits at most an hour on one conversation, then stops with an error that names the seat; the
seats before it have started, and it and the rest are left at idle shells. In enter mode the holders are
not looked at again when Enter is pressed. confirm_channels runs after every window has been started.
Two screens can still wait for a person: the folder trust screen for a directory Claude Code has
not seen, and the channels warning ("Loading development channels … 1. I am using this for local
development"). With confirm_channels = true (tmux and psmux, and not in enter mode) seat-stage reads
the pane, matches the channels text (whitespace ignored, so a wrapped line still matches), chooses the local-development option, and reads the pane
again to see it cleared. It never answers the folder-trust screen: it prints what it saw and leaves it
for you. The channels warning did not appear on a repeat run in a directory where it had already been
answered, so a missing prompt is reported, not treated as a failure.
when-free and check use the answer measured in fleet-kit#3. A conversation is held when a
~/.claude/sessions/<pid>.json has its sessionId, its pid is alive, and its procStart equals
that process's start time (a FILETIME against the process's CreationDate on Windows, a UTC
ps -o lstart= string on macOS), within two seconds. That finds a conversation however it was
started, with or without --resume.
Where the files are not read at all, nothing is claimed: when-free refuses, and check says it can't
tell. This is the case on Linux until it is measured. Where they are read but one process's start time
can't be (the command fails, or prints something that is not a time, or a file's procStart is not
one), the answer is "cannot tell", never "free": check fails and staging refuses. A pid that is simply
not running is free. CLAUDE_CONFIG_DIR is honoured in place of ~/.claude.
Reports, without editing anything: the multiplexer and claude on PATH; the multiplexer session
not already existing (staging refuses to touch one); each checkout existing; each conversation
having a transcript under ~/.claude/projects; no live process already holding a conversation
(except in when-free, which waits); the channel's MCP server listed by claude mcp list in each
checkout, with its --project and --harness, where it sets them, agreeing with the seats file;
and when-free only where holders can be detected. Work
locations being unique per machine is checked when the file is read.
Reads conversation ids from live processes into the seats file. A live conversation belongs to the
seat whose checkout is its working directory. The file is edited line by line, so comments (including
one after conversation = "x") and layout stay. It records nothing for a seat with no live conversation
or with two candidates, never records one id for two seats, and skips an id the seats file would refuse.
--dry-run prints what it would record and writes nothing. It writes the file before it prints "recorded".
Windows 11 (10.0.26200), psmux 3.3.8 (66cf613, 2026-08-18), Claude Code 2.1.304, fleet-kit#4. psmux is
driven with the same new-session, new-window, send-keys and capture-pane arguments as tmux, with
two differences, both handled: it does not prefix-match a session name, and it rejects the =window
form (can't find window: =w1, printed with exit 0), so it is given plain session:window targets.
-
capture-pane -p -t session:windowreads a window, exit 0, in UTF-8. Thirty reads of an unchanging window were byte-identical. A window created a moment ago reads as empty, not as an error. - It read the real folder-trust screen and the real channels warning of Claude Code, from a throwaway
session in an empty folder that never received a prompt.
send-keys Enterchose the highlighted "I am using this for local development", and the next read showed the Claude Code screen. - A pane narrower than a line wraps it, even in the middle of a word, and
capture-pane -Jdoes not join it again. Attaching a client also re-wraps the text.seat-stagetherefore matches the screens with whitespace ignored. - A window whose command exits closes, and reading it then fails with exit 1;
seat-stagereports that as "could not read the pane". - The command text
seat-stagebuilds for Windows (the environment scrub, thenclaude '...', with doubled single quotes) runs under psmux as written. -
send-keys -lwithEntertyped into an idle window ran the command, which is howwhen-freestarts a seat.
So confirm_channels is supported under psmux. Its shell was PowerShell throughout.
-
tmux target syntax. tmux is given
=namefor a session and=windowfor a window. That is tmux's own exact-match form, but it was not run here. -
macOS and Linux. The
sessions/<pid>.jsonfile and itsprocStartform were measured on Windows only. The macOS comparison follows the same rule; Linux is not read. -
claude mcp list. The<name>: <command and arguments> - <status>line format was seen on one Windows machine, where the entry sets neither--projectnor--harness. -
The order of flags other than the prompt-first form, such as
--resumetogether with--remote-control, beyond what was run in fleet-kit#7. -
Live staging.
seat-stageitself is tested against recorded and faked multiplexer output, and has not been run against a live multiplexer. The psmux behaviour above was measured by hand with the same arguments.