Workspaces
View as markdownA workspace is the unit Shelbi dispatches tasks to. It is capacity, not a role and not a process: a slot declared once in the project YAML, alive for the lifetime of the project, with an agent process cycled inside it per task.
Each workspace is two pinned resources on one machine:
- One tmux pane, where the agent runs. A window for hub workspaces, a whole session for remote ones (so they survive SSH drops).
- One git worktree, its own checkout at
<machine.work_dir>/.shelbi/wt/<workspace-name>. The path is fixed; not configurable.
A workspace handles one task at a time. It does not spawn, it does not fan out, it does not multiplex. If the project declares five workspaces, you have five concurrent slots, no more, no less.
The pool lives in the sidebar, grouped by machine. Busy slots show the agent they're running; idle ones wait for work:
Workspace vs. agent: capacity vs. role
The single most important distinction on this page: a workspace is where work runs; an agent is who does it.
- A workspace is a machine + pane + worktree. It's interchangeable
capacity.
bravois just as good asalphafor any task that fits its machine. - An agent is a system prompt + skill set: the
developer,qa, orsecurityrole. It's loaded into a workspace when a task is dispatched.
That split is what lets the same slot run a developer agent on one task and
a qa agent on the next, and it's why a workflow can say "review this status
with the security agent" without caring which workspace is free to do it.
The workspace supplies the compute; the task's status supplies the agent.
The pool model
The pool is declared up front in the project YAML and stays fixed:
workspaces:
- { 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. Workspaces 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 workspaces 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 workspace, ~12 GB
when spread across machines, clamped to [1, 16]). Add or remove workspaces
later by editing the YAML and running shelbi reload.
A slot can also carry tags — capability labels a workflow status can
require, so a task routes to a matching workspace. That routing is generic:
review workspaces are just the canonical
use of it (a slot tagged review that a review status routes to), but the same
mechanism pins any capability to any pool.
Tags: capability labels
Every machine and every workspace can carry a free-form tags list. A
workspace's effective tags are its own tags unioned with its
machine's tags — a tag declared once on a machine applies to all of its
slots without repeating it per workspace.
machines:
- name: hub
kind: local
work_dir: ~/Workspaces/myapp
tags: [review] # every slot on hub inherits `review`
workspaces:
- { name: alpha, machine: hub, runner: claude } # effective: {review}
- { name: bravo, machine: hub, runner: claude, tags: [gpu] } # effective: {review, gpu}Both fields accept a scalar tag: alias and a bare string as shorthand for a
one-element list (tags: review ≡ tags: [review]). Both are elided from the
on-disk form when empty, so existing project YAMLs round-trip unchanged.
Status-tag routing
A workflow status declares the tags a task needs while it sits there:
statuses:
- { id: review, owner: user, agent: review, tags: [review] }When a task enters that status, the orchestrator routes it to a free
workspace whose effective tags are a superset of the status's required set
(set-AND). Empty required tags — the default — match any free workspace, so
any idle slot qualifies. tags: [review] on the status plus tags: [review]
on a machine is all it takes to pin that work to matching slots. Nothing in the
routing branches on a literal tag name — it's the same superset query every tag
uses.
If no declared workspace matches the required tags, the load fails loudly rather than silently running the work on a general slot.
Slots and $SLOT
Each workspace has a numeric slot. Set it explicitly with slot:, or let
it default to the workspace's zero-based index among its machine's slots. The
slot is exported to transition commands as $SLOT, which is how two
workspaces on one machine avoid colliding on a port:
workspaces:
- { name: review-0, machine: hub, runner: claude, tags: [review], slot: 0 }
- { name: review-1, machine: hub, runner: claude, tags: [review], slot: 1 }Alongside $SLOT, every transition command also gets $SHELBI_TASK,
$SHELBI_BRANCH, $SHELBI_WORKTREE, and $SHELBI_MACHINE.
The sidebar: grouped by machine
Workspaces render in the sidebar grouped under the machine they're pinned to, so the layout mirrors where your compute actually lives. Each row shows the workspace's state badge, its name, the agent currently loaded (if any), and the task it's on:
— hub —
⏵ alpha developer add-csv-export-to-reports
💬 bravo developer fix-cookie-domain-bug
· charlie
— devbox —
⏵ delta qa review:auth-rewrite
· echo
· foxtrotIdle slots (·) carry no agent or task. They're capacity waiting to be
filled. A busy slot shows which agent role it's
running, which is how you tell at a glance that delta is doing a QA pass,
not writing code.
Each machine group can be collapsed (Space / Enter on its header). The set of
collapsed machine names persists to ~/.shelbi/state.json under
sidebar.collapsed_machines (the SidebarPrefs struct), so the choice
survives a sidebar respawn and follows you across projects that share a machine
name. A collapsed machine keeps its (total, active) count on the header so
capacity stays visible at a glance.
Review-tagged slots are the exception: they never render here. Their capacity surfaces under the sidebar's Ready for Review and Queued for Review sections instead.
Machine-aware routing
A task can hint where it wants to run with prefers_machine:
shelbi task add "Re-encode the marketing video assets" \
--prefers-machine devboxThe orchestrator honors the hint when at least one workspace on that machine
is free. If devbox is fully busy when the task becomes ready, the card
stays in its ready-category status rather than getting routed to the wrong
host. Shelbi never silently re-routes RAM-heavy or latency-sensitive work to
the hub. The hint rides along in the task's frontmatter as prefers_machine
and is surfaced on the kanban card.
Workspace states
The hub polls each workspace's tmux pane title every few seconds (see
workspace_poll_interval_secs, default 5) and writes the observed state to
~/.shelbi/workspaces/<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. |
· | (no in-flight task) | the slot is idle and ready to be assigned. |
A workspace that finishes a task doesn't get its own "done" badge. The moment it writes the review-ready marker and the poller promotes the task, the workspace closes its session and returns to idle (see How a task completes). Completion shows up in the sidebar's Ready for Review section, never as a lingering check on the workspace row.
awaiting_input is the right state for "agent done with this turn, waiting
for the next prompt." It's 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_inputThat feed is what the orchestrator tails to know when to dispatch more work. See the events log.
Switching tasks clears context
When a workspace picks up a new task its pane is killed and re-created from scratch:
- The pane (window for hub workspaces, session for remote ones) is torn down.
- The worktree is switched to the task's branch, creating the branch off
default_branchif it doesn't exist and 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, loaded with the agent the task's status calls for.
- 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, with no leakage between tasks on the same workspace.
Uncommitted work blocks the switch
Step 2 refuses to switch branches if the worktree has uncommitted changes. An agent that leaves work uncommitted will stall its own next dispatch. The completion protocol is a commit plus the review-ready marker, never a dirty tree.
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).
How a task completes
A workspace reports task completion by writing its task id into a marker file in the worktree:
<worktree>/.claude/shelbi-readyThe 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 workspace.
- Moves the task to the next
handoffstatus (reviewin the default workflow). - Clears the marker.
- Appends
task=<id> in_progress -> review reason=workspace:ready-markerto the events log.
The workspace never runs shelbi itself. The marker file is the entire
on-workspace protocol. This is what makes a remote workspace possible with
nothing installed but tmux, git, and the agent CLI.
Once the task is safely promoted, the finishing dev workspace closes its own session and frees its slot for the next task. No lingering "done" pane. If the project declares review workspaces, the orchestrator then loads the promoted branch onto one so a human can run the change, not just read the diff.
Local vs. remote workspaces
A workspace's pane lives in different places depending on its machine:
┌─ shelbi-myapp ─────────────────────┐
hub workspaces → │ dashboard | alpha | bravo | … │ one tmux session
└────────────────────────────────────┘ on the hub
┌─ shelbi-w-charlie ─────────────────┐ one tmux session
remote → │ agent │ per workspace, on
workspaces └────────────────────────────────────┘ the remote machineHub workspaces share the project session (one window each). Remote
workspaces 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
- Project config — the
workspaces:,machines:, andagent_runners:field reference. - Review workspaces — the tag-routed slot that loads and serves a finished branch for human review.
- Agents — the role that runs inside a workspace, and the role/slot split.
- Workflows — what the default workflow's statuses mean, and where workspaces fit in the task lifecycle.
- The events log — the shape of every workspace transition line.
- Orchestrator — how it picks which workspace to dispatch a task to.
shelbi workspace— list and stop workspaces.