A 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:

jlong@hub — shelbi
Tasks · my-project 34 total Workflow: All ▾ Workspace: All ▾ BACKLOG (10) TO DO (9) IN PROGRESS (4) REVIEW (5) DONE (6) Rework onboarding UX Add API ratelimit Deploy staging env Cold-start cache Migrate to PG 16 app app app ⎇ shelbi/deploy… app ⎇ shelbi/cold-s… app Audit OSS licenses Fix mobile nav Wire up OAuth flow CSV import fix Ship dark mode app app app ⎇ shelbi/wire-u… app ⎇ shelbi/csv-im… app Draft Q3 roadmap Wire webhook retries Backfill order index Nightly report Retry dead-letters app app app ⎇ shelbi/backfi… app ⎇ shelbi/nightl… app Migrate CI to arm64 Split OTel spans Trim vendor bundle Validate webhook Redis cache /profile app app app ⎇ shelbi/trim-v…payloads app app ⎇ shelbi/valida… Sunset legacy v1 API Sync i18n strings Add audit logging app app Harden token refresh app app ⎇ shelbi/harden… Add SSO for admins Paginate search API Fix flaky CI tests app app app Prune stale flags Cache user sessions app app Dedupe error reports Add health probes app app Archive S3 buckets Debounce autosave app app Refresh brand assets app
h/l col j/k row open n new f filter r refresh

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. bravo is just as good as alpha for any task that fits its machine.
  • An agent is a system prompt + skill set: the developer, qa, or security role. 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_to field. "bravo is on the palette task" means the same thing across sessions.
  • Pre-warmed worktrees: no git worktree add on 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: reviewtags: [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
 · foxtrot

Idle 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 devbox

The 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.

BadgePersisted stateMeaning
workingagent is mid-turn: actively typing, calling tools, running shells.
💬awaiting_inputagent finished a turn and is sitting at the prompt.
blockedagent 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_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 workspace picks up a new task its pane is killed and re-created from scratch:

  1. The pane (window for hub workspaces, session for remote ones) is torn down.
  2. The worktree is switched to the task's branch, creating the branch off default_branch if it doesn't exist and refusing to switch if there are uncommitted changes.
  3. A fresh .claude/settings.json is deployed under the worktree.
  4. A new pane is created and the agent CLI is launched in it, loaded with the agent the task's status calls for.
  5. Once the agent's input box is ready (shift+tab to cycle footer 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-ready

The hub poller cats this file on each tick (locally or over SSH), and when it finds a non-empty value:

  1. Confirms the named task is in-progress and assigned to this workspace.
  2. Moves the task to the next handoff status (review in the default workflow).
  3. Clears the marker.
  4. Appends task=<id> in_progress -> review reason=workspace:ready-marker to 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 machine

Hub 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:, and agent_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.