Workers
A worker is a long-lived agent slot pinned to a machine and a worktree — the unit the orchestrator dispatches tasks to.
Workers
A worker is the unit Shelbi dispatches tasks to. It is a slot, not a process — declared once in the project YAML, alive for the lifetime of the project, with the agent process inside it cycled per task.
Each worker has three pieces:
- A machine — where it lives. The hub itself, or any host reachable
by
ssh. - A persistent worktree — its own git checkout at
<machine.work_dir>/.shelbi/wt/<worker-name>. The path is fixed; not configurable. - A runner — the agent CLI (
claude,codex, …) the worker will launch when it picks up a task.
A worker handles one task at a time. It does not spawn, it does not fan out, it does not multiplex. If the project declares five workers, you have five concurrent slots — no more, no less.
The pool model
The pool is declared up front in the project YAML and stays fixed:
workers:
- { name: alpha, machine: hub, runner: claude }
- { name: bravo, machine: hub, runner: claude }
- { name: charlie, machine: devbox, runner: claude }
- { name: delta, machine: devbox, runner: claude }This is deliberate. Workers are not allocated on demand — they are named slots the orchestrator routes work to. That gives you:
- Stable identities in the sidebar, the events log, and the kanban
card's
assigned_tofield. "bravo is on the palette task" means the same thing across sessions. - Pre-warmed worktrees — no
git worktree addon the hot path. A new task on bravo just switches branches in the worktree bravo already owns. - A real ceiling on concurrency. The number of declared workers is the parallelism cap; the orchestrator can't accidentally outrun your RAM by spawning more.
The wizard sizes the pool from total RAM (~10 GB per local worker, ~12
GB when spread across machines, clamped to [1, 16]). Add or remove
workers later by editing the YAML and running shelbi reload.
Worker states
The hub polls each worker's tmux pane title every few seconds (see
worker_poll_interval_secs, default 5) and writes the observed state to
~/.shelbi/workers/<name>/status.yaml. The sidebar reads from there.
| Badge | Persisted state | Meaning |
|---|---|---|
⏵ | working | agent is mid-turn — actively typing, calling tools, running shells. |
💬 | awaiting_input | agent finished a turn and is sitting at the prompt. |
⚠ | blocked | agent paused on a permission dialog or other interactive gate. |
✓ | awaiting_review | worker wrote the review-ready marker; its task moved to the review column. |
· | (no in-flight task) | the slot is idle and ready to be assigned. |
awaiting_input is the right state for "agent done with this turn,
waiting for the next prompt" — it is what fires when claude's Stop hook
runs at end of turn. The agent has not finished the task; it just
finished one round of work. The actual completion signal is the
review-ready marker (see below).
State changes are also appended to ~/.shelbi/events.log:
2026-06-22T14:22:11+00:00 worker=bravo none -> working
2026-06-22T14:24:03+00:00 worker=bravo working -> awaiting_input
That feed is what the orchestrator tails to know when to dispatch more work. See the events log.
Switching tasks clears context
When a worker picks up a new task its pane is killed and re-created from scratch:
- The pane (window for local workers, session for remote workers) is torn down.
- The worktree is switched to the task's branch — creating the branch
off
default_branchif it doesn't exist, refusing to switch if there are uncommitted changes. - A fresh
.claude/settings.jsonis deployed under the worktree. - A new pane is created and the agent CLI is launched in it.
- Once the agent's input box is ready (
shift+tab to cyclefooter detected), the initial prompt is typed.
This is intentional. The previous task's conversation history, scratchpad files, and any agent-local state are gone. Each task starts the agent with a clean context — no leakage between tasks on the same worker.
The worktree itself persists. Files committed on the previous task's branch are still there on disk; only the branch checkout changes. This keeps the on-machine cost of a task switch small (one branch checkout, not a whole clone).
See crates/shelbi-orchestrator/src/worker.rs (start_worker_on_task)
for the full sequence.
How a task completes
A worker reports task completion by writing its task id into a marker file in the worktree:
<worktree>/.claude/shelbi-review-ready
The hub poller cats this file on each tick (locally or over SSH), and
when it finds a non-empty value:
- Confirms the named task is in-progress and assigned to this worker.
- Moves the task to
review. - Clears the marker.
- Appends
task=<id> in_progress -> review reason=worker:review-markerto the events log.
The worker never runs shelbi itself. The marker file is the entire
on-worker protocol. This is what makes a remote worker possible with
nothing installed but tmux, git, and the agent CLI — see
crates/shelbi-tui/src/poller.rs (maybe_promote_to_review) for the
read-side, and crates/shelbi-orchestrator/src/worker.rs
(compose_prompt) for what the initial prompt tells the worker to do.
Local vs remote workers
A worker's pane lives in different places depending on its machine:
┌─ shelbi-myapp ─────────────────────┐
local workers → │ dashboard | alpha | bravo | … │ one tmux session
└────────────────────────────────────┘ on the hub
┌─ shelbi-w-charlie ─────────────────┐ one tmux session
remote workers → │ agent │ per worker, on
└────────────────────────────────────┘ the remote machine
Local workers share the project session (one window per worker). Remote
workers each get their own session on their machine so they survive
SSH drops — shelbi reattaches with ssh -t host tmux attach -t shelbi-w-<name> whenever you focus them. The session naming is
hard-coded; you can tmux ls on the remote to inspect.
See also
- Columns — what
in_progressandreviewmean, and where workers fit in the task lifecycle. - The events log — the shape of every worker transition line.
- The orchestrator — how it picks which worker to dispatch a task to.