Workspaces
View as markdownA workspace is the unit Shelbi dispatches issues 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 issue.
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 issue 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 issue that fits its machine. - An agent is a system prompt + skill set: the
developer,qa, orsecurityrole. It's loaded into a workspace when an issue is dispatched.
That split is what lets the same slot run a developer agent on one issue 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 issue'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 }
- { name: bravo, machine: hub }
- { name: charlie, machine: devbox }
- { name: delta, machine: devbox }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 issue" means the same thing across sessions. - Pre-warmed worktrees: no
git worktree addon the hot path. A new issue 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 an issue 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 } # effective: {review}
- { name: bravo, machine: hub, 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 an issue needs while it sits there:
statuses:
- { id: review, owner: user, agent: review, tags: [review] }When an issue 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, tags: [review], slot: 0 }
- { name: review-1, machine: hub, 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 issue 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 issue. 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
An issue can hint where it wants to run with prefers_machine:
shelbi issue 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 issue 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 issue'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. |
⏸ | paused | the workspace stalled on a usage limit and is waiting to resume. |
· | (no in-flight issue) | the slot is idle and ready to be assigned. |
A workspace that finishes an issue doesn't get its own "done" badge. The moment it writes the review-ready marker and the poller promotes the issue, 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 issue; 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 project=myapp workspace=bravo none -> working
2026-06-22T14:24:03+00:00 project=myapp workspace=bravo working -> awaiting_inputThat feed is what the orchestrator tails to know when to dispatch more work. See the events log.
Switching issues clears context
When a workspace picks up a new issue its pane is killed and re-created from scratch. The branch switch happens first, while the old pane is still intact; the pane is torn down and rebuilt only after the worktree and settings are in place:
- The worktree is switched to the issue's branch, creating the branch off
the project's
base_branch()(thegit.base_branchsetting when present, otherwisedefault_branch, and a workflow can override it further) if it doesn't exist, and refusing to switch if there are uncommitted changes. .claude/settings.local.jsonis re-wired under the worktree — an additive, self-healing merge, not a clobber (Shelbi no longer overwrites the committedsettings.jsonon every dispatch).- The pane (window for hub workspaces, session for remote ones) is torn down.
- A new pane is created and the agent CLI is launched in it, loaded with the agent the issue'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 issue's conversation history, scratchpad files, and any agent-local state are gone. Each issue starts the agent with a clean context, with no leakage between issues on the same workspace.
Uncommitted work blocks the switch
Step 1 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 issue's branch are still there on disk; only the branch checkout changes. This keeps the on-machine cost of an issue switch small (one branch checkout, not a whole clone).
How a task completes
A workspace reports issue completion by writing its issue 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 issue is in-progress and assigned to this workspace.
- Moves the issue to the next
handoffstatus (reviewin the default workflow). - Clears the marker.
- Appends the issue transition to the events log, e.g.
project=<name> task=<id> workflow=<name> in_progress -> review reason=workspace:ready-marker from_category=active to_category=handoff.
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 issue is safely promoted, the finishing dev workspace closes its own session and frees its slot for the next issue. 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 issue lifecycle.
- The events log — the shape of every workspace transition line.
- Orchestrator — how it picks which workspace to dispatch an issue to.
shelbi workspace— list and stop workspaces.