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 Shelbi session, where the agent runs: a small detached process that owns the agent's PTY and outlives the connection, so the agent survives a dropped SSH link or a closed terminal.
- 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 + Shelbi session + 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.
A project created with shelbi project add (or shelbi init -y) starts
with an empty pool, and the orchestrator asks how many workspaces you want on
its first boot. shelbi init without -y and the app's Add project form
instead start you with two workspaces on hub: dev and review (tagged
review). Add or remove workspaces later with shelbi workspace add <name> /
shelbi workspace rm <name>, or in Settings ▸ Workspaces. (rm refuses a
workspace with an active issue or unpushed work unless you pass --force.)
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, so a tag declared once on a machine applies to all of its
slots without repeating it per workspace.
machines:
- name: devbox
kind: ssh
host: devbox
work_dir: ~/Workspaces/myapp
tags: [gpu] # every slot on devbox inherits `gpu`
workspaces:
- { name: alpha, machine: devbox } # effective: {gpu}
- { name: bravo, machine: devbox, tags: [review] } # effective: {gpu, review}Because machine tags are inherited, put review on individual slots, not on
a machine: a machine-level review tag makes every slot on that machine a
review slot.
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 workspace is all it takes to pin that work to matching slots.
The routing query itself is generic. The review tag also carries built-in
meaning: review-tagged slots refuse direct dev dispatch, are hidden from the
sidebar's Workspaces section, are fed by the review auto-load queue, and
receive the workflow's review: recipe. See
Review workspaces.
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: alpha, machine: hub, slot: 0 }
- { name: bravo, machine: hub, slot: 1 }Alongside $SLOT, every transition command also gets $SHELBI_TASK,
$SHELBI_BRANCH, $SHELBI_WORKTREE, and $SHELBI_MACHINE.
For a workflow's review: recipe the slot value is the port, and there is
no index fallback: review slots need an explicit port-valued slot: (e.g.
slot: 3000). See
Review workspaces.
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
state badge, the name, and the loaded agent (or idle):
— Workspaces —
▾ hub
⏵ alpha Developer
? bravo Developer
· charlie idle
▾ devbox
⏵ delta Qa
· echo idleThe desktop draws the same rows with its own glyphs: a green ▶ for working, a
yellow ? for awaiting input, a dot for idle (the row reads Idle), plus
⚠ and ⏸. Busy rows are numbered ⌘1 to ⌘9 from the top, so a keystroke
opens that workspace.
Machine headers only appear when the pool spans more than one machine. Idle
slots carry no agent. 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 headerclick its machine row). 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 shows a (total, N active) count on its header
(e.g. ▸ hub (4, 2 active)hub (4, 2 active)) 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 agent is instructed to honor the hint: it considers only
workspaces on that machine, and if devbox is fully busy when the issue
becomes ready, it leaves the card in its ready-category status rather than
routing it to the wrong host. This is orchestrator prompt policy, not a hard
rule enforced by Shelbi's code. The hint is stored as prefers_machine;
clear it with shelbi issue edit --no-prefers-machine.
Workspace states
The hub polls each workspace's session every few seconds (see
workspace_poll_interval_secs, default 5) and writes the observed state to
~/.shelbi/projects/<project>/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. |
⏵ | serving | a review slot whose branch passed the review: ready probe. |
· | (no in-flight issue) | the slot is idle and ready to be assigned. |
… | (not read yet) | the workspace's status hasn't been read yet (e.g. right after a project switch). |
The badges above are the TUI's. The desktop app draws the same states with
its own glyphs: a green ▶ for working, a yellow ? for awaiting_input,
⚠ and ⏸ as above, a dot for idle, and … while loading; a serving review
shows as a cyan check in the review sections.
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 session is ended and re-created from scratch. The branch switch happens first, while the old session is still intact; the session is ended and restarted 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 workspace's agent session is ended.
- A new Shelbi session is started (on the remote machine, for remote workspaces) and the agent CLI is launched in it, loaded with the agent the issue's status calls for.
- The startup prompt (the agent's charter plus the issue context) is passed to the agent at launch. Shelbi then confirms the agent went busy, falling back to typing the prompt once the input box is ready (Claude only).
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>/.shelbi/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.
- Rebases the branch onto the base branch, pushes it, and moves the issue
forward: to an agent-owned gate status if the workflow declares one,
otherwise to the first
handoffstatus (reviewin the default workflow), firing that edge's actions (in the default workflow, opening the PR). - 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.
Completion is signalled only by the marker file; the agent doesn't call
shelbi to finish. (Agents also report state through Claude hooks and can
ask for clarification through the hub socket.) A remote workspace needs only
a compatible shelbi binary, git, and the agent CLI on its machine.
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" session. If the project declares review workspaces, the hub's review auto-loader 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 session runs on its machine, local or remote:
┌─ alpha ──────────┐ ┌─ bravo ──────────┐
hub workspaces → │ agent (session) │ │ agent (session) │ sessions on the hub
└──────────────────┘ └──────────────────┘
┌─ charlie ────────┐
remote → │ agent (session) │ one session per workspace,
workspaces └──────────────────┘ on the remote machineEvery workspace, hub or remote, runs its agent in its own Shelbi session: a
small detached process that owns the agent's PTY, so it survives a dropped SSH
connection or a closed terminal. Attach to any of them with
shelbi attach <workspace>. For a remote machine the hub
bridges every session over a single SSH channel (see
shelbi relay), and shelbi machine
makes sure a compatible shelbi binary is there to run them.
The hub's own SSH (dispatch, polling) never prompts, so a remote machine should take your key or agent. If it wants a password, opening one of its workspaces from the sidebar asks for it once, in a macOS dialog, and the hub then shares that connection until it closes. Cancel shows "Couldn't sign in to host" and stops; open the workspace again to retry.
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: add, remove, list, inspect, and stop workspaces.