Hand runs the supervisor in the background in the fleet's Luvus session, in full-auto mode:
claude --dangerously-skip-permissions;codex --dangerously-bypass-approvals-and-sandbox --disable hooks --cd DIR;opencode --standalone --auto --session ses_hand<26 hex>;agy --model ID --dangerously-skip-permissions;omp --auto-approve --no-title --model M [--thinking E] -- PROMPT, with--resume IDto resume.
copilot runs workers only: hand supervisor start and switch refuse it, and the board does not offer it for the supervisor. A worker starts as copilot --no-auto-update --model M [--reasoning-effort E] --allow-all-tools --allow-all-paths --session-id UUID -i BRIEF, with a new UUID per attempt kept in the attempt's stored command.
omp runs both roles. A worker starts as omp --auto-approve --no-title --model M [--thinking E] -- BRIEF; a supervisor adds the launch prompt in place of BRIEF, and a resume passes --resume ID with no prompt. Hand puts -- before the prompt, so a brief that starts with @ is text, not a file include, and one that starts with - is not a flag. omp has no --session-id, so Hand finds the supervisor's session by the launch cwd and launch marker under sessions/*/*.jsonl in the agent folder named in Install (PI_CODING_AGENT_DIR, else $PI_CONFIG_DIR/agent with a relative value under the home directory, else ~/.omp/agent). Give PI_CODING_AGENT_DIR as an absolute path and the same value in the shell that started the fleet's Luvus server, the shell that started hand watch and the Board: panes inherit the server's environment, while Hand reads with its own. A relative value is resolved against the folder omp starts in, the fleet home for a supervisor and the attempt's worktree for a worker. When discovery finds no session after the two-minute launch grace, hand supervisor show prints a note: naming the agent folder Hand reads and saying the Luvus server's environment may differ; the watcher says so once and resets when the session is found. Restart the Luvus server from the shell that has the variable, or unset it everywhere. Hand never passes --session-dir, --cwd or --restore to it: a custom session dir, a global PI_CODING_AGENT_SESSION_DIR or a named --profile keeps that discovery from finding the session. The watcher reads the served model from the latest assistant message, because a resume with a new --model appends no model_change, and keeps the requested effort because a resume with a new --thinking appends no thinking_level_change. Hand has no omp limit line yet: capture the screen before adding one. This was measured on Linux only; Windows and macOS have not run a real omp.
Hand passes the model and effort you give as flags (--model, --effort; -m and -c model_reasoning_effort= for Codex), except for opencode, whose TUI has no such flags: Hand creates its session on the model (provider/id) and the variant, given as the effort, with opencode api --standalone session.create before it creates the pane, and a failure of that call, or an answer that names another session than the one Hand chose, fails the start; a resume never calls it. Without a model opencode uses its own configuration, but a plain hand supervisor start after an opencode or Codex supervisor reuses the model and variant (the effort, for Codex) the watcher read back from it, so it pins the model that supervisor ran on and no longer follows a later change to opencode's or Codex's configuration. The watcher reads its model and variant back from the session, by the ses_hand session id Hand chose at launch and kept in the stored command (workers) or the session column (supervisors). It refuses a malformed value, but a model it cannot verify (a name Claude Code's own list lacks, a custom provider's model for Codex, a model agy models does not list, or any model while its harness has no model list yet) starts anyway, and the start prints warn: <harness> model <name> ... starting anyway. The board shows the same warning after Start. If the harness rejects the model, its pane says so and hand supervisor show names the harness and model. An effort outside Claude's five levels is passed on the same way, so a typo is Claude Code's to reject, not Hand's.
An opencode supervisor started by an older Hand keeps the session id Hand found for it, and is read and resumed by it. One with no id stored keeps running and receives messages, but its model is not read and hand supervisor resume refuses it: start a new one with hand supervisor start. An opencode worker launched before this change keeps running and its model is not read.
Workers run with the same flags, each in its own worktree. A prompt longer than 16 KiB is refused before anything starts: see Long prompts. Codex gets --cd DIR with the fleet home for the supervisor, new or resumed, and the worktree for a worker, so it does not depend on the folder it inherits. The supervisor's first message also names the fleet home. Safe use says what that allows and the one prompt Claude Code still shows.
agy asks once per folder whether to trust it. Every attempt is a new folder, and so is a new fleet home. Hand presses Enter on that screen only when it names the attempt's own worktree, or for the supervisor the fleet home. Otherwise it notes the attempt or the supervisor blocked. omp asked no trust screen in the measured setup (setupVersion: 2), so Hand presses nothing for it. Claude asks too, with Quick safety check: Is this a project you created or one you trust?. Its cursor starts on No, exit, so Hand sends Down once, re-reads the screen, and sends Enter only when the cursor is on Yes, I trust this folder. Codex asks too, with Trust this folder? (0.156 and later) or Do you trust the contents of this directory? (0.155 and earlier). Its cursor starts on Trust and continue; if it is on Quit, Hand sends Up first. Hand presses Enter only when the screen names the attempt's own worktree, or for the supervisor the fleet home, and when it names a repository root, only if that is the project's repository: the supervisor has no project, so a root there is never accepted. Paths are compared as written, except that a Windows drive-letter path may differ in case, in \ against /, in a \\?\ prefix, and in a space Codex dropped where it wrapped the line; a UNC path must match exactly. A trusted or projectless folder shows no screen, and Hand prints trust: not asked once the harness has shown something other than a trust screen for 3 seconds, because a slow harness draws its banner first. Hand keeps looking while the pane is still blank, up to 15 seconds, and sends its first key only when the screen has stopped changing, because Claude drops keys that arrive before it reads input. Accepting saves the trust for the repository in Codex's config.toml, which Safe use explains, and Hand records it as attempt.trusted or supervisor.trusted. Codex's separate Working directory · Resume screen is not answered: it shows as a blocked screen. Copilot asks Do you trust the files in this folder? with the cursor on 1. Yes, and shows the folder in a box whose lines it wraps at the pane width without spaces: Hand joins them, and presses Enter only when the result is the attempt's own worktree. It never takes 2. Yes, and remember this folder for future sessions, which writes the operator's Copilot config, so a folder that is not the worktree, or a cursor that is not on 1. Yes (Hand sends Up once), leaves the screen for you. After the trust screen Hand reads the first screen of a Copilot worker for a few seconds and notes the attempt blocked when Copilot says its model is not available and runs on Auto, or says to sign in; the attempt keeps running. The text of Codex's screen from 0.156 on has run against a real Codex 0.163.0 in CI (End-to-end tests), on Linux, macOS and Windows; the text for 0.155 and earlier comes from Codex's source, and docs/dogfood/windows.md W4a checks both on a real machine. A reworded screen is left alone, and the watcher reports the attempt blocked. If the screen is recognized but the cursor never reaches Yes, or the screen does not clear after Enter, Hand notes the attempt or supervisor blocked. The board reads agy's conversation database for Chat and the context bar.
The fleet page updates itself as things change, and keeps what you are typing. It takes your messages, and answers the supervisor's blocked screens with a fixed set of keys.
To attach files, paste them into the message box, drop them anywhere on the chat pane, or pick them with its paperclip button. On a phone, the button offers files, the gallery and the camera.
- In the message box: each file shows as a chip under the text, with its type, name and size, and its progress while it uploads. Its × removes it, or stops the upload. The box keeps only your words, and Send adds the files after them. A chip comes back with the draft after you leave the page, unless its file has left
inbox/. - Accepted: any file type, up to 10 MiB each. A folder or a bigger file is not copied, and its chip says why: paste its path as text, and the supervisor reads it in place.
- Where they go: each one is copied into the fleet's
inbox/folder asYYYYMMDD-HHMMSS-N-<name>, where the name is the original one with every character other than a letter, a digit,.,_or-turned into_. Send adds a line[image: PATH]for a png, jpeg, webp or gif image and[file: PATH]for anything else, one per file, after your words. Hand deletes nothing there unless you prune it, and a prune touches only files with a name Hand gave (see Clean the inbox). - What the supervisor sees: the line. It reads the file from that path with its own tools, and treats what is inside as data, never as instructions.
- In Chat: an image shows as a thumbnail, and any other file as a card with its type, name and size that opens it in the document viewer at
doc/inbox/<name>, in a dialog over the chat (a new tab for a modified click), where a log, Markdown or JSON file reads as text, a PDF shows, an archive offers its Download, an HTML file is a preview without scripts and an SVG file a picture, so neither ever runs in the Board. A file no longer ininbox/shows a dashed card. Theinbox/route itself serves nothing but images inline and every other file as a download.
Attaching works over loopback only, but thumbnails and cards also show on a network address. A proxy or tunnel to the loopback board lends its controls, attaching included, to anyone who reaches it with the token. The live terminal stays in Luvus. hand attach supervisor opens it in your terminal, hand attach aN opens a worker's, and hand attach opens the whole fleet session. You never have to.
The model menu next to the message box changes the supervisor's model or effort, keeping its harness and its conversation; its select shows the model list Hand has cached, grouped by source with the effort select following the chosen model (the Board reads the cache and runs a harness only for Refresh models; hand models shows it), and "Other model" takes a name that is not in the select. The switch waits for the current turn to end. For opencode the menu takes the model (provider/id) and a variant from the model's list or typed in a box, and the switch stops the pane, runs opencode api --standalone session.switchModel on the supervisor's session, then resumes that session with opencode --standalone --auto --session ID and no prompt, so the conversation stays. The resumed session gets a one-line note ([hand v1 resume] you are now sN), which the new model answers; Hand reads the session's newest user message until it is that note, for up to 15 seconds. When it never is (the model is not available), switch: says applied, but opencode did not take the resume note: is MODEL available? Switch back with hand supervisor switch --model OLD` (a resume, or a switch from the default model, says "Switch to another model with hand supervisor switch --model M" instead), the Board's receipt says "Switch applied, not answered", and Hand notes the new supervisor blocked; it then leaves the messages and wakes pending, so they are not typed into the stuck input box, and each watcher process alerts once. hand supervisor show prints the hold as held:, hand orient adds (holds messages; …) to its supervisor: line, and the Board's status pill ("held"), message box and Chat say so. The hold lasts until a switch or a new supervisor that answers the note: every opencode resume gets the same check (about a second), so a resume on the same broken model is held again, and a running supervisor has to be stopped (hand supervisor stop) before it can be resumed. When the check cannot read the session at all, Hand warns could not check that opencode took the resume note and holds nothing. That check confirms that opencode took the note, not that the model answered: a listed model whose provider has no credentials is applied, and its turns end in an error that the pane and Chat show. Hand warns about a variant that the model's variants in the model list lack (opencode accepts an unknown one without a word and resets it to default), and says nothing when the list has no variants for the model. A switch to another model keeps no variant unless you give one with --effort, and a switch with only --effort keeps the model; --effort default sends default and the supervisor shows no variant. Hand refuses a model that is not in opencode models, after refreshing that list once; when that refresh fails, the switch goes on and the resume note check decides. A failed switchModel, or a pane that cannot start after a successful one (Hand then puts the session back on the model it ran on, read from the session before the switch when the supervisor had none, and a failed read fails the switch; when the session names none, it stays on the new model and the error says so), leaves the supervisor stopped: hand supervisor resume retries it, and hand supervisor switch --cancel, or "Drop switch to MODEL" next to Resume on the Board, drops the switch, so that resume continues on the old model (or on the new one, when Hand could not put it back). Hand never switches a running opencode in place: a switch written to a live TUI is undone by its next prompt. The same controls exist as hand supervisor start|send|keys|interrupt|switch|stop|resume|show`.
hand init writes the folder's AGENTS.md and CLAUDE.md, which make the agent run hand orient at the start and hand orient --since CURSOR on each wake, and the secondhand skill, which describes the loop.
The supervisor keeps the judgement, and a worker produces the evidence. Its own desk holds the operator's intent and standing rules, the fleet's state, the gates — a clean review at the head, a green check, a merge the rules allow — the briefs, plans and reports, and the verdict on each report; code, tests, product docs (docs/, README.md, AGENTS.md), PRs, independent reviews and read-only investigations are attempts. A question the committed code can answer goes to a worker whose brief says it only reads, and the supervisor acts on the answer instead of reading file after file; it reads only what it needs, bounded, or uses a worker whose output it can evaluate. A worker starts blank, so delegating a judgement does not move it off the supervisor's desk: the supervisor still judges the result, and the hand-off costs a context copy and a turn, so no attempt is dispatched whose output it cannot evaluate.
You can still open an agent in the fleet folder by hand (cd ~/fleets/demo && claude). It then waits with hand wait --after CURSOR, and it stands down while a managed supervisor runs.
After a reboot
The supervisor shows as interrupted. Press Resume, or run hand supervisor resume, to continue the same session with its history. It spends no tokens until you write or a wake arrives. A session whose first Hand row predates hand fleet move is the exception: its history lives in the harness's own store under the old folder, so Resume starts a fresh session from hand orient instead, records supervisor.fresh, hand supervisor show prints fresh: with the move, and the earlier conversation is not carried over. New session starts fresh from hand orient. To resume automatically when hand watch starts, add "supervisor": {"autoresume": true} to routing.json.
When the context grows
A session's context grows with every turn, and each turn reads all of it again. For a claude or omp supervisor Hand starts a fresh session when the context passes supervisor.rollover_tokens in routing.json (300000 by default; 0 turns it off, and another harness is never rolled over):
"supervisor": {"rollover_tokens": 300000}The watcher reads the context size from the supervisor's transcript, the number the Board's gauge shows. Once it is over the limit and the supervisor is quiet (idle with its screen unchanged for 30 seconds, no message or wake waiting, no usage-limit line, no model switch pending and no hand update running), Hand sends one message that starts with [hand v1 rollover] and asks for a handoff note in memory/handoff.md: the tasks in flight and their next step, the decisions waiting, the timers that matter and anything Hand's state does not hold. When the file is written and the turn has ended, Hand stops the session and starts a new one on the same harness, model and effort. Its first message tells it to run the full hand orient and read the note, and it takes over the old session's wake cursor, so a wake not yet delivered still arrives. Hand records supervisor.rolled_over with the old and new session ids and the token count, and starts no second rollover within an hour.
The note is the only thing the fresh session knows beyond hand orient, so the supervisor's answer matters. Hand fails safe: it never leaves you without a supervisor and never asks twice.
- Hand checks that the harness binary exists and that this watcher still belongs to the current board generation before it asks. If either fails, or the request for the note still fails after three tries, or no note comes within 5 minutes, nothing is stopped: the supervisor keeps running, an alert says so once (
supervisor.rollover), and Hand does not ask that supervisor again until the watcher restarts.hand supervisor startbegins a fresh session by hand. - A message or wake that arrives during the exchange waits: while Hand holds the supervisor lock nothing is delivered to the old session, and what waits, the wake cursor included, reaches the fresh session.
- A
hand supervisor switchasked during the exchange ends the rollover without an alert and without stopping anything; the switch goes on as usual. One asked while Hand stops the old session starts the fresh session on the switched model and effort. - If Hand cannot replace the session while the old one is live (a stop that fails), nothing was stopped: one alert, no second ask.
- If the fresh session cannot start after the old one was stopped, Hand resumes the old session and does not ask again; if that fails too,
hand supervisor resumecontinues it. If Luvus gave no answer to the launch, the fresh session may still start: the alert says so, andhand supervisor resumeworks once Hand has failed the launch (2 minutes after it began).
Hand holds the supervisor lock for the whole exchange, up to 5 minutes plus the launch of the fresh session. A Board Stop, Send or Switch, and hand supervisor stop, send and switch, wait that long; hand supervisor interrupt does not wait, so Esc still works.
A limit below what a fresh session holds on its own (50000, say) rolls over every hour: the one-hour cooldown is the only guard. A negative value makes routing.json fail to load, like the other invalid keys.
When the supervisor idles
The supervisor acts only on a message. While a task is active, no attempt runs, no decision is open and no timer is armed that is due within 6 hours while the managed supervisor runs, no event will ever come for it, and its work is unwatched. A task with an armed waits timer (see Timers) is the exception: it is left out for as long as the timer is armed. The watcher then wakes the supervisor with supervisor.stalled once it has been idle for 20 minutes, again 40 minutes later, then every 60 minutes while nothing changes. The wake names the tasks and the idle time. Any event other than the stall wake and its delivery starts the count again, and the next wake comes no sooner than after (20 minutes by default) after the supervisor last worked. Hand never sends it to a supervisor that is stopped, blocked, at a usage limit or switching models. After the first one, the Board's Needs shows "Supervisor idle for 1h12m with 3 active tasks and no running attempt" until the work is watched again, and the alert goes out once, on the first due stall wake at or after 60 minutes of idle time. hand orient lists the tasks as unwatched. Change the delays in routing.json (each from 1m to 24h), or turn the wake off with "stall": "off":
"stall": {"after": "20m", "every": "60m"}Timers
The supervisor waits on something outside Hand, such as a PR review or CI, with a timer instead of a background script, which a switch or resume would lose. hand wake --after 30m --note "check PR 909" wakes it once 30 minutes from now, --at <UTC time, RFC 3339> at that time, and --every 30m --while t137 every 30 minutes until t137 is done or abandoned. The watcher delivers each one as the wake timer.fired w1: check PR 909. Timers live in hand.db, so they survive a watcher restart, a supervisor switch and a resume. Timers belong to the managed supervisor: hand wake is refused while none runs, and a supervisor opened by hand waits with hand wait --timeout instead. The watcher fires them only while the managed supervisor runs: one that came due while the watcher or the supervisor was down fires once when both are back, marked w1, 2h10m late, and a repeating one then carries on from there. While a timer due within 6 hours is armed and the managed supervisor runs, the work is not unwatched, so no stall wake comes; a timer due later does not count, so a far --at cannot silence the supervisor for days, and the stall wake names it (w2 is armed but due in 31d). A repeating timer counts while its next due time is within 6 hours. hand orient lists the armed timers and marks one beyond the horizon, and every one (no managed supervisor: not fired) while none runs. --after and --every take days too (3d). A timer is due at least 1 minute ahead, a fleet holds at most 20 armed timers, and a note is one line of at most 200 characters. hand wake list shows them and hand wake cancel wN drops one. Hand never asks GitHub or CI anything for a timer: it only wakes the supervisor, which does the checking.
A task that waits for an outside party for days, such as a PR from a contributor, takes a wait: hand wake --while t137 --waits --after 3d --note "outside PR". --waits needs --while, is one-shot (no --every), is due at most 30 days away, and a task has one at a time; the note is the reason. While it is armed and the managed supervisor runs, the task is not unwatched, so no stall wake names it, and it leaves the Board's STALLED, FAILED and INTERRUPTED items; another idle task stays unwatched, and a task that is not waiting keeps its red item. The timer fires once as timer.fired w1: outside PR (marked late like any timer) and ends, and the task is then plain active again: unwatched, with the stall wake counting from the fire. Finishing or abandoning the task, hand wake cancel wN and hand attempt start on the task end the wait early. hand orient marks the timer (waits t137) and the task's attempt cell (waits until 2026-11-03), hand task show t137 lists it, and the Board shows Waits until Nov 3: outside PR on the task's page and its row.
Concurrent workers
Each running attempt is a whole agent plus whatever it starts. On one machine, measured with five concurrent workers that each started a dev server and a browser: about 350 to 440 MB per Claude worker, about 0.5 GB per Vite dev server plus its runtime, and about 1.1 GB per Playwright Chromium. An omp pane measured 174 to 185 MB, and six panes plus their helper processes about 1.0 GB resident. Hand itself is small (about 240 MB for the Board and three watchers) and each Luvus server uses 4 to 7 MB. These are one machine's numbers, not guarantees; yours will differ.
Hand limits a task to one live attempt and sets no limit per fleet. hand attempt start prints a warn field when 5 or more attempts are already live in the fleet, and starts anyway. The lever is the brief: ask a worker to run one dev server and one browser at a time, and to close them when it is done.
Long prompts
A worker's prompt (the brief, and the instructions for reporting back that Hand adds) and the supervisor's first message reach the harness as the last argument of its command line. Two limits apply:
- Luvus takes at most 16 KiB (16384 bytes) in one argument, on every OS.
- On Windows the whole command line, quoted the way Windows reads it (a
"becomes\", and a backslash before a quote doubles), may be at most 32,766 characters. Ordinary text stays far below that; only a prompt that is mostly quotes comes near it.
Hand checks both before it creates a worktree or a pane, and refuses with exit code 2:
error: invalid: prompt is 17021 bytes; luvus accepts at most 16384 per argument; shorten it, or write the long text to a file and make the prompt say to read that file
error: invalid: starting codex.exe takes a 32779 character command line once quoted, and Windows allows 32766; shorten the prompt, or write the long text to a file and make the prompt say to read that fileTo give a worker more than that, write the whole brief to a file outside the worktree (the fleet's folder is a good place), and keep the file you pass to --prompt-file short and naming it: Read C:\Users\you\fleets\demo\briefs\t12.md in full and do what it says; it is the whole brief. The worker reads the file with its own tools. The limit counts the footer too, so a brief file of 15.5 KiB can already be over it.
Both limits were measured on a Windows runner, with the tested Luvus and a stand-in codex.exe that records the arguments it receives. Prompts of the sampled sizes from 1 to 16384 bytes arrive byte for byte, for a worker and for a supervisor, with several paragraphs, " and ', backslashes (also before a quote and at the end), %PATH%, ^ & | < > !, backticks, tabs, accented letters, emoji and CRLF or LF newlines. Luvus refuses 16385 bytes with command argument is invalid or exceeds the v1 limit. A command line of 32,766 characters starts, and one of 32,767 does not: Luvus answers create_failed: terminal failed to start, which is what the second message above replaces.
Hand never starts a .cmd or .bat file for this reason: cmd.exe cuts a prompt at its first newline, expands %VAR% in it, and stops with The command line is too long. and exit code 1 once the command line passes about 8,190 characters (see W4b).