# Shelbi — Full Documentation
> Do more with your agents — an open source, multi-machine orchestrator built on tmux. Dispatch tasks to a team of agents locally or over SSH.
> Generated from https://shelbi.dev/docs — see https://shelbi.dev/llms.txt for the index.
---
# shelbi init
Create a project from Shelbi's detected setup plan, interactively or without prompts for automation.
```text
shelbi init [OPTIONS]
```
`shelbi init` has two setup paths:
- Run it normally for the existing config-location and pick-up workflows.
- Add `-y` to detect the same plan as first-run `shelbi`, accept it without a
prompt, and write the standard global project layout.
The `-y` path performs the full prerequisite and project validation before it
writes anything. It uses the current directory by default, detects the Git
root, branch, origin, runner, and tmux, then creates the same project and
Welcome card as Enter on the interactive setup card.
The project is created with an **empty workspace pool**. `shelbi init` no longer
provisions workspaces; the orchestrator sets them up on first boot by asking how
many workspaces and which naming scheme you want, then creating each one with
[`shelbi workspace add`](/docs/cli/workspace). Run `shelbi` interactively for that
first-boot interview.
## Automation
From a repository with exactly one supported runner on `PATH`, this is the
copy-pasteable zero-prompt setup:
```bash
shelbi init -y
```
If both Claude Code and Codex are installed, runner selection is ambiguous.
Disambiguate it explicitly:
```bash
shelbi init -y --runner codex
```
For a different checkout or CI workspace:
```bash
shelbi init -y --root /workspace/myapp --runner claude
```
`--runner` selects the runner for the workspaces the orchestrator later creates
and is also the default orchestrator runner. Use `--orchestrator-runner` only
when those should differ. The selected runners must be installed on `PATH`.
`shelbi init -y` will stop rather than guess between Claude Code and Codex.
Pass `--runner claude` or `--runner codex` to make automation deterministic.
## Detected-plan options
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `-y, --yes` | flag | off | Accept the detected plan without prompts. This is a global flag and may appear before or after `init`. |
| `--project ` | string | project-root basename | Override the generated project name. |
| `--root ` | path | current directory with `-y` | Repository Shelbi will manage. |
| `--runner ` | `claude` \| `codex` | the only detected runner | Runner for the workspaces the orchestrator creates on first boot. Required when both supported runners are installed. |
| `--default-branch ` | string | detected branch, then `main` | Override the detected default branch. `--branch` is an alias. |
| `--github-url ` | string | detected origin | Override the detected origin URL. `--remote` is an alias; pass an empty value to omit it. |
| `--orchestrator-runner ` | `claude` \| `codex` | selected `--runner` | Use a different installed runner for the orchestrator. |
Detected-plan overrides require `-y`. The `-y` path cannot be combined with
`--mode` or `--pick-up`.
## Config-location and pick-up options
Without `-y`, `shelbi init` uses the explicit config-mode workflow:
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--mode ` | `in-repo` \| `global` | asked interactively | Choose committed in-repo config or per-user global config. Required for this path when stdin is not a TTY. |
| `--pick-up` | flag | off | Register an existing committed `/.shelbi/project.yaml` after cloning a teammate's project. |
Register an existing in-repo project like this:
```bash
git clone git@github.com:acme/myapp.git && cd myapp
shelbi init --pick-up
```
See [Config modes](/docs/concepts/config-modes) for the shared/local file split
and migration path.
## See also
- [Set up your first project](/docs/guides/getting-started/first-project) for
the visible preflight and one-confirmation experience.
- [`shelbi wizard`](/docs/cli/wizard) for the explicit interactive entry point.
- [`shelbi project`](/docs/cli/project) for adding or migrating projects.
[Source](https://shelbi.dev/docs/cli/init)
---
# Getting Started
Install Shelbi, confirm its detected setup plan once, run a task, then scale to a pool of workspaces and hand the loop to the orchestrator.
Shelbi is an agent orchestrator for the terminal: a board of tasks, a
pool of git workspaces, and an orchestrator that keeps the workspaces
loaded with work and shepherds each result back for review. This guide is
the shortest path from an empty machine to that loop running on one of
your own repositories.
You'll work through it roughly in order (each page assumes the one before
it), but the pages stand alone if you already have part of the setup.
Shelbi drives an existing agent runner (`claude`, `codex`, …) against a
git repository you already have. [Install](/docs/guides/getting-started/install)
lists the prerequisites; have a repo and an authenticated agent CLI ready
before you start.
## The arc
You'll start by **installing** the binary and **setting up a project**.
Shelbi checks the repo, runner, tmux, and machine, then shows one detected
plan. Press Enter once to write it and open the dashboard, or press `c` to
customize the detected values. Then you'll **run your first task** end to end,
watch a single card cross the board, and review the branch it produces.
From there the guide opens up: a **multi-workspace dispatch loop** fills
the whole pool and runs cards in parallel, **review workspaces** load a
finished branch onto a live dev server so you can click through the change,
**Zen Mode** hands both ends of that loop to the orchestrator, and
**authoring a custom workflow** shows how to reshape the board's routing to
your own conventions.
The last page steps back to the model underneath all of it:
[**Workflows**](/docs/guides/getting-started/workflows), the per-project
YAML that declares the statuses a task moves through and the git
side-effects that fire on each transition. Read it once here and the rest
of the docs (every concept page and every branching-model deep-dive)
clicks into place.
## Where to go next
- [Concepts](/docs/concepts/agents) — the reference pages behind the loop:
agents, workspaces, the orchestrator, the events log, and Zen Mode.
- [Understanding Workflows](/docs/guides/understanding-workflows) — deep-dives
that map trunk-based, git-flow, feature-branch, and forking onto a Shelbi
Workflow, once you know the [workflow basics](/docs/guides/getting-started/workflows).
[Source](https://shelbi.dev/docs/guides/getting-started)
---
# shelbi wizard
Run the same detected, one-confirmation project setup used by first-run Shelbi.
```text
shelbi wizard
```
`shelbi wizard` is the explicit interactive entry point for Shelbi's first
project setup. It inspects the current Git checkout and machine, prints the
preflight results, and presents one setup card:
```text
Enter launch c customize q quit
```
Press Enter to create the detected plan and launch the dashboard. Press `c` to
edit the prefilled project, repository, branch, remote, runner, workspace, and
orchestrator values. Press `q`, Esc, or Ctrl+C to leave without project state.
If both Claude Code and Codex are detected, the wizard first asks `Which
agent?`. If the current directory is not a Git repository, it first asks
permission to run `git init -b main`. Missing tmux or runner prerequisites stop
setup before the card and print an install-and-retry message.
The command is idempotent at the project-registration level. If any project is
already registered, it completes without creating another one. Use
`shelbi project add` to run the shared setup flow for an additional project.
## Examples
Run setup explicitly from the repository you want Shelbi to manage:
```bash
cd ~/code/myapp
shelbi wizard
```
For automation, use the non-interactive detected-plan path instead:
```bash
shelbi init -y --runner codex
```
## See also
- [Set up your first project](/docs/guides/getting-started/first-project) for
every happy-path and edge-path screen.
- [`shelbi init`](/docs/cli/init) for scriptable setup and config pick-up.
- [`shelbi project`](/docs/cli/project) for additional projects.
[Source](https://shelbi.dev/docs/cli/wizard)
---
# Agents
An agent is a role, a system prompt plus a skill set, not the task it's handed or the slot it runs in. Shelbi ships six (orchestrator, developer, review, and the qa, security, and adversarial reviewers); each is a directory you can edit, and you can author more.
An **agent** is a *role*: a system prompt plus a set of skills. It is the
"who" of a piece of work: the orchestrator that schedules, the developer
that writes the code, the reviewer that checks it. An agent is deliberately
*not* the task it's handed, and *not* the slot it runs in. Those are separate
things:
- The **task** is the work: a markdown card with a prompt and a branch.
- The **[workspace](/docs/concepts/workspaces)** is the capacity: a tmux
pane plus a git worktree on some machine.
- The **agent** is the role: the prompt and skills that decide *how* the
work gets done.
Splitting these apart is what lets the same workspace run a developer agent
on one task and a reviewer agent on the next, and what lets a
[workflow](/docs/guides/getting-started/workflows#owners-and-agents) say "this status is reviewed by
the security agent" as a line of YAML rather than a paragraph of prompt prose.
## The shipped agents
Every project ships with six agents. Three run the core loop, and three
are specialized reviewers:
| Agent | Role |
| --- | --- |
| `orchestrator` | The agent you talk to. Turns requests into cards, dispatches tasks to free workspaces, tails the events log, reports back. Runs in window 1, never in a workspace. See [Orchestrator](/docs/concepts/orchestrator). |
| `developer` | The default workspace agent. Implements a task on its branch, runs the project's checks, writes the review-ready marker. This is the agent an `agent`-owned status uses when no other is named. |
| `review` | The agent named by a review status, run on a [review workspace](/docs/concepts/review-workspaces). Its charter is to make the finished branch runnable for a human. The dev server itself is booted by the status's transition commands, and it hands over a working URL. It doesn't write code (bar an explicitly requested tweak). |
| `qa` | Exercises a finished change against its acceptance criteria and reports pass or fail with concrete repro steps. It verifies, it doesn't rewrite. |
| `security` | A defensive-only review of the diff: injection, broken authorization, leaked secrets, unsafe deserialization, path traversal, risky dependencies. Reports findings with severity and location. It does not write exploits. |
| `adversarial` | An automated skeptic. It tries to refute the change, defaulting to skeptical, and states what breaks and how to reproduce it, labeling confirmed versus suspected. |
`developer` runs most often; `orchestrator` and `review` bracket it. The
three reviewers ship materialized but unwired: they sit in the agents
directory ready to use, and none governs a column until you name it on a
status's [`agent:`](#assigning-an-agent-to-work) field. That keeps the
default board a plain `developer → review` loop, and makes adding a QA,
Security, or Adversarial gate a one-line YAML edit rather than a
`shelbi agent new` first.
Which role is loaded shows up in the sidebar next to each busy workspace. The
same slot runs `developer` on one task and a reviewer role on the next:
## Where agents live on disk
An agent is a directory under the project's Shelbi config:
```text
~/.shelbi/projects//agents/
├── _shared/
│ └── preamble.md # project-wide context, prepended to every agent
├── orchestrator/
│ └── instructions.md # the orchestrator's system prompt
├── developer/
│ ├── instructions.md # the developer agent's system prompt
│ └── skills/ # agent-scoped skills (optional)
├── review/
│ ├── instructions.md # the Review agent's system prompt
│ └── skills/ # ships a `load-run-detection` skill
├── qa/ # shipped reviewer preset
│ └── instructions.md
├── security/ # shipped reviewer preset
│ └── instructions.md
├── adversarial/ # shipped reviewer preset
│ └── instructions.md
└── perf/ # ← one you authored with `shelbi agent new`
└── instructions.md
```
Each agent has at minimum an `instructions.md`: its system prompt. It may
also carry a `skills/` directory of agent-scoped skills the runner loads when
that agent is active. The `_shared/preamble.md` file is special: its contents
are prepended to *every* agent's instructions, so project-wide context (the
repo layout, the house style, the test command) lives in one place instead of
being copy-pasted into four prompts.
When a task is dispatched, the agent named for that status (or `developer` by
default) has its rendered prompt (`_shared/preamble.md` followed by the
agent's own `instructions.md`) handed to the runner in the workspace. The
[orchestrator's own prompt](/docs/concepts/orchestrator#how-the-prompt-is-wired)
is rendered the same way; it's just the agent that happens to run in window 1.
## Customizing an agent
Agents are **project-local with shipped defaults**. The first time a project
loads, Shelbi materializes the six default agents into the directory above.
From then on they're yours to edit: change `developer/instructions.md` to
bake in your repo's conventions, drop a skill into a role's `skills/`, tighten
the shipped `security` prompt to your threat model.
The defaults are *seeded, not enforced*. On upgrade, Shelbi only writes an
agent file that doesn't already exist. It never clobbers a file you've
edited. New shipped agents (or new skills) show up; your customizations stay
put. If you want to reset an agent to the current default, delete its
`instructions.md` and reload.
Edit an agent by hand, or through the CLI:
```bash
shelbi agent list # the six shipped, plus any you've added
shelbi agent show developer # print instructions.md + skills list
shelbi agent edit developer # open instructions.md in $EDITOR
shelbi agent new perf # author a role of your own
```
See the [`shelbi agent`](/docs/cli/agent) reference for the full command set.
## Assigning an agent to work
You rarely assign an agent to a task directly. Instead, a
[workflow](/docs/guides/getting-started/workflows#owners-and-agents) status declares who owns it:
```yaml
# workflow file — reference-only; name/category live in statuses.yaml
statuses:
- { id: todo, owner: agent, agent: orchestrator }
- { id: in-progress, owner: agent, agent: developer }
- { id: review, owner: agent, agent: qa }
- { id: done, owner: user }
```
`owner: agent` makes a status the orchestrator's to act on; the optional
`agent:` field names *which* agent does it. Omit `agent:` and the
`developer` agent is used. The `review` status above is auto-reviewed by the
shipped `qa` agent before a human ever sees it. That's the whole "a
reviewer is a role you can drop on a status" idea, expressed declaratively.
Because `qa`, `security`, and `adversarial` already ship, opting one onto a
column is a **one-line edit**: name it on that status's `agent:` field. Point
a `handoff`- or `active`-category status at `agent: security` for a defensive
diff pass, or at `agent: adversarial` for an automated skeptic, and the
orchestrator loads that role into the workspace it dispatches. No
`shelbi agent new` first. See
[owners](/docs/guides/getting-started/workflows#owners-and-agents) for the
rules, and [Doing more with agents](/docs/guides/doing-more-with-agents) for a
worked wiring example.
## See also
- [Workspaces](/docs/concepts/workspaces) — the capacity an agent runs in,
and the agent/workspace split (role vs. slot).
- [Workflows](/docs/guides/getting-started/workflows#owners-and-agents) — the `owner` + `agent`
fields that route work to a named agent.
- [Orchestrator](/docs/concepts/orchestrator) — the agent in window 1, and
how its prompt is wired.
- [`shelbi agent`](/docs/cli/agent) — list, show, scaffold, and edit agents.
[Source](https://shelbi.dev/docs/concepts/agents)
---
# Install
Install Shelbi with Homebrew on macOS, APT on Ubuntu, or from source for development.
```text
Install and start Shelbi on my machine. On macOS, run `brew install jlong/shelbi/shelbi && shelbi`. On Ubuntu amd64, add the Shelbi APT keyring from https://apt.shelbi.dev/shelbi-archive-keyring.gpg, add the `https://apt.shelbi.dev stable main` source list with signed-by=/etc/apt/keyrings/shelbi-archive-keyring.gpg, then run `sudo apt update && sudo apt install shelbi`, followed by `shelbi`. Make sure tmux 3.2+ and one authenticated agent CLI, claude or codex, are available.
```
On macOS, install Shelbi and start its first-run setup in one command:
```bash
brew install jlong/shelbi/shelbi && shelbi
```
Shelbi checks your prerequisites and current Git checkout before it writes
anything. See [Set up your first project](/docs/guides/getting-started/first-project)
for the one-confirmation flow.
Shelbi also publishes a prebuilt Ubuntu package. The full package-manager
commands are:
```bash
brew install jlong/shelbi/shelbi
```
```bash
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://apt.shelbi.dev/shelbi-archive-keyring.gpg \
| sudo tee /etc/apt/keyrings/shelbi-archive-keyring.gpg >/dev/null
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/shelbi-archive-keyring.gpg] https://apt.shelbi.dev stable main" \
| sudo tee /etc/apt/sources.list.d/shelbi.list >/dev/null
sudo apt update
sudo apt install shelbi
```
The APT repository is signed, so `apt update` verifies the repository
metadata before `apt install` sees the package. The first Ubuntu package is
published for `amd64` in the `stable` suite.
The APT key fingerprint is published at:
```bash
curl -fsSL https://apt.shelbi.dev/shelbi-archive-keyring.fingerprint
```
The package installs the `shelbi` binary only. It does not install or enable
the daemon; run `shelbi daemon install` later if you want the hub daemon managed
by your user service supervisor.
If you're hacking on Shelbi or testing unreleased changes, use the
[source-build path](#install-from-source-for-development) instead.
## Prerequisites
You need three things before you start:
- **`tmux` 3.2 or later.** Shelbi runs every workspace inside a tmux pane
and drives it with `send-keys` / `capture-pane`; older releases miss
features the TUI relies on. Homebrew and APT install this dependency for
the hub, but remote workspace machines need it too.
- **An agent CLI.** At least one of `claude`
([Claude Code](https://docs.claude.com/en/docs/claude-code)) or `codex`,
installed and authenticated. Declare more than one later, and the
project YAML picks per workspace.
- **Git and SSH.** Shelbi creates git worktrees and can run workspaces on
remote machines over SSH. The Ubuntu package depends on `git` and
`openssh-client`; on macOS, install them with Xcode Command Line Tools or
your usual package manager if they are not already present.
Install `tmux` with your package manager:
```bash
brew install tmux
```
```bash
sudo apt install tmux
```
```bash
sudo dnf install tmux
```
Remote workspaces need the same `tmux` and agent CLI on the machine they
run on, plus an `ssh` host you can reach without a password prompt.
## Verify or start later
```bash
shelbi
```
With no project configured, this starts the guided setup. To check only the
installed version, run `shelbi --version`.
## Verify release artifacts manually
GitHub Releases are the source of truth for release artifacts, checksums,
and artifact attestations. Package-manager installs verify checksums or
signed repository metadata automatically, but you can verify downloads by
hand.
For an Ubuntu `.deb`, download the package and `checksums.txt` from the
same release tag:
```bash
version=0.1.0
curl -fsSLO "https://github.com/jlong/shelbi/releases/download/v${version}/checksums.txt"
curl -fsSLO "https://github.com/jlong/shelbi/releases/download/v${version}/shelbi_${version}_amd64.deb"
sha256sum -c checksums.txt --ignore-missing
```
For a macOS archive, verify the release tarball the Homebrew formula uses:
```bash
version=0.1.0
curl -fsSLO "https://github.com/jlong/shelbi/releases/download/v${version}/checksums.txt"
curl -fsSLO "https://github.com/jlong/shelbi/releases/download/v${version}/shelbi_Darwin_arm64.tar.gz"
grep "shelbi_Darwin_arm64.tar.gz" checksums.txt | shasum -a 256 --check -
```
Replace `shelbi_Darwin_arm64.tar.gz` with
`shelbi_Darwin_x86_64.tar.gz` on Intel Macs. To inspect GitHub artifact
attestations, use GitHub's attestation tooling against the same release
artifact.
## Install from source for development
The source install path is for contributors, local patches, and unreleased
builds. It requires the stable Rust toolchain from
[rustup](https://rustup.rs).
You can run the hosted source-build script:
```bash
curl -fsSL https://shelbi.dev/install.sh | sh
```
Or keep the checkout around to rebuild after pulling updates:
```bash
git clone https://github.com/jlong/shelbi.git
cd shelbi
./scripts/install.sh
```
The script runs `cargo build --release` and copies the binary to
`$HOME/bin/shelbi`. Override the destination with `SHELBI_INSTALL_PATH`:
```bash
SHELBI_INSTALL_PATH=/usr/local/bin/shelbi ./scripts/install.sh
```
Re-run it any time you pull updates. One step rebuilds and reinstalls.
The orchestrator and workspace panes re-shell into `shelbi` on every call,
so they pick up the new binary automatically; only the sidebar, Tasks, and
Review views need a manual `shelbi reload` to respawn against the new build.
### macOS: the codesign step
On macOS the script re-signs the copied binary ad-hoc:
```bash
codesign --remove-signature "$INSTALL_PATH"
codesign --sign - "$INSTALL_PATH"
```
`cargo build` embeds an ad-hoc signature; `cp` invalidates it, and the
next exec dies with `Killed: 9` and no useful error. Re-signing after the
copy restores it. `scripts/install.sh` does this for you, so you only hit
the failure if you copy the binary out of `target/release/` by hand.
Linux and Windows don't need it, and the script skips it there.
## Troubleshooting
**`command not found: shelbi`.** The default install path is
`$HOME/bin/shelbi` only when you install from source. Make sure that
directory is on your `PATH`:
```bash
export PATH="$HOME/bin:$PATH"
```
```zsh
export PATH="$HOME/bin:$PATH"
```
```fish
fish_add_path "$HOME/bin"
```
Add the line to `~/.zshrc`, `~/.bashrc`, or `~/.config/fish/config.fish` to
make it persistent. Or pass
`SHELBI_INSTALL_PATH=/usr/local/bin/shelbi` to the install script so the
binary lands somewhere already on your `PATH`.
If Homebrew or APT installed the package, open a new shell and run
`which shelbi` to confirm your package-manager binary directory is on
`PATH`.
**`Killed: 9` on macOS, no other output.** The codesign step didn't run.
This usually means you copied the binary out of `target/release/` by hand
instead of using `scripts/install.sh`. Re-run the script, or run the
codesign commands yourself against your install path.
**`cargo: command not found`.** Install Rust via
[rustup](https://rustup.rs) and restart your shell so `~/.cargo/bin` is on
your `PATH`. Cargo is only required for source builds.
**`tmux: command not found`** (or `tmux 1.x` warnings). Install or
upgrade tmux to 3.2+. The TUI assumes modern pane title and popup
support; older versions render incorrectly.
## Next
You have the binary. Now point it at a repo with
[Set up your first project](/docs/guides/getting-started/first-project).
[Source](https://shelbi.dev/docs/guides/getting-started/install)
---
# shelbi project
Manage projects: add a new one with the detected setup card, or migrate an existing global-mode project into committed in-repo mode.
```text
shelbi project [OPTIONS]
```
`shelbi project` groups the commands for managing which repos Shelbi knows
about. Use it to onboard an additional project after your first, or to move an
existing project from per-user global config into committed in-repo config.
Every subcommand accepts the global `--root ` (override the Shelbi root
directory, else `$SHELBI_ROOT`, else the install-time default) and
`-p, --project ` (defaults to `$SHELBI_PROJECT` or the registered
project whose `work_dir` contains the current directory) flags.
## Commands
| Command | Description |
| --- | --- |
| `add` | Run the same preflight and one-confirmation setup as first-run Shelbi, then launch the new project's dashboard. |
| `migrate-to-in-repo` | Migrate an existing global-mode project into in-repo mode. |
## add
```text
shelbi project add [OPTIONS]
```
Set up a new project interactively. Shelbi detects the current Git repository,
runner, tmux, and workspace recommendation, then shows the same setup card as
first-run onboarding. Press Enter to create the project and launch its TUI, or
press `c` to customize the prefilled values first.
## migrate-to-in-repo
```text
shelbi project migrate-to-in-repo [OPTIONS]
```
Migrate an existing global-mode project into
[in-repo mode](/docs/concepts/config-modes). It splits
`~/.shelbi/projects/.yaml` into a committed shared half at
`/.shelbi/project.yaml` and a per-machine
`~/.shelbi/projects//local.yaml`, and moves `workflows/`, `agents/`, and
the workspace-settings template from the state dir into the repo. State
(`state.json`, `tasks/`, `HANDOFF.md`, `.claude/`, `workspaces/`, `events.log`)
stays under `~/.shelbi/` in both modes.
The migration is idempotent: safe to re-run on an already-migrated project (a
no-op) or a half-migrated one (it completes the missing steps). It prints a
`.gitignore` snippet for the repo root and, outside `--dry-run`, offers to
auto-append it.
There is no `migrate-to-global` command. Reverting means `git revert` on the
migration commit (which restores `/.shelbi/` to its pre-migration state)
plus manually moving `local.yaml` back to
`~/.shelbi/projects/.yaml`. A merged migration commit is expensive to
undo. Run `--dry-run` first if you're unsure.
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--dry-run` | flag | off | Print the plan without touching disk — every write, move, and delete the real run would perform, in order, so a reviewer can vet it first. |
| `--yes` | flag | off | Skip the interactive prompt and auto-append the `.gitignore` snippet. Useful for scripts and headless runs with no TTY. Ignored under `--dry-run`. |
## Examples
Onboard a second project from inside its repo:
```bash
shelbi project add
```
Preview an in-repo migration without writing anything:
```bash
shelbi project migrate-to-in-repo --dry-run
```
Run the migration non-interactively, appending the `.gitignore` snippet:
```bash
shelbi project migrate-to-in-repo --yes
```
## See also
- [Config modes](/docs/concepts/config-modes) — the global vs in-repo model, the
full on-disk layout, and the `.gitignore` list `migrate-to-in-repo` prints.
- [`shelbi init`](/docs/cli/init) — first-time scaffolding, including the
`--pick-up` flow for cloning a teammate's in-repo config.
- [`shelbi wizard`](/docs/cli/wizard): the detected one-confirmation setup.
[Source](https://shelbi.dev/docs/cli/project)
---
# Workspaces
A workspace is capacity — a persistent slot pinned to a machine, made of one tmux pane and one git worktree. The orchestrator dispatches tasks to it; whichever agent the task calls for runs inside it.
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
`/.shelbi/wt/`. 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](/docs/concepts/agents) 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:
```yaml
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](/docs/concepts/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.
```yaml
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](/docs/configuration/workflow#statuses) declares the tags a
task needs while it sits there:
```yaml
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:
```yaml
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:
```text
— 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](/docs/concepts/agents) 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](/docs/concepts/review-workspaces)-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`:
```bash
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//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](#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`:
```text
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](/docs/concepts/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](/docs/concepts/agents) 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.
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:
```text
/.claude/shelbi-ready
```
The hub poller `cat`s 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= 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](/docs/concepts/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:
```text
┌─ 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-` whenever you focus them. The session naming is hard-coded;
you can `tmux ls` on the remote to inspect.
## See also
- [Project config](/docs/configuration/project#workspaces) — the
`workspaces:`, `machines:`, and `agent_runners:` field reference.
- [Review workspaces](/docs/concepts/review-workspaces) — the tag-routed slot
that loads and *serves* a finished branch for human review.
- [Agents](/docs/concepts/agents) — the role that runs *inside* a
workspace, and the role/slot split.
- [Workflows](/docs/guides/getting-started/workflows#lifecycle-who-moves-a-task-between-the-default-statuses)
— what the default workflow's statuses mean, and where workspaces fit in
the task lifecycle.
- [The events log](/docs/concepts/events-log) — the shape of every
workspace transition line.
- [Orchestrator](/docs/concepts/orchestrator) — how it picks which
workspace to dispatch a task to.
- [`shelbi workspace`](/docs/cli/workspace) — list and stop workspaces.
[Source](https://shelbi.dev/docs/concepts/workspaces)
---
# Set up your first project
Let Shelbi inspect your repo and machine, confirm one setup card, and land on a dashboard that is ready to use.
```text
Set up Shelbi for the Git repository in my current directory. Run `shelbi`, review the preflight and detected setup card with me, and press Enter only after I approve it. If I want to change a detected value, use `c` to Customize. Do not hand-write the generated config.
```
From the Git repository you want Shelbi to manage, install and start it:
```bash
brew install jlong/shelbi/shelbi && shelbi
```
On the happy path, Shelbi asks one question: launch the detected plan? It does
not write project state until you confirm the card.
## 1. Watch the preflight
First-run setup prints the Shelbi banner, then checks the environment in front
of you. A typical preflight looks like this:
```text
✓ git repo ~/code/myapp
✓ default branch main
✓ remote github.com:you/myapp.git
✓ agent codex 0.27.0 on PATH
✓ tmux 3.5a
✓ machine 10 cores, recommending 4 workspaces
```
Shelbi detects the repository root, default branch, origin, supported agent
runners, tmux, and a workspace count suited to the machine. The values appear
as each check completes.
When both Claude Code and Codex are on `PATH`, Shelbi cannot infer which one
you prefer. It asks exactly one runner question before the plan card:
```text
? Which agent?
claude ()
codex ()
```
Choose one and setup continues. This is the only extra question on an otherwise
detected Git repository.
## 2. Confirm the setup card
After preflight, Shelbi summarizes everything it will create:
```text
┌─ myapp ────────────────────────────────────────┐
│ │
│ repo ~/code/myapp (main) │
│ github github.com:you/myapp.git │
│ agent codex │
│ workspaces created on first boot │
│ workflows task (branch → PR → review) · subtask │
│ agents orchestrator · developer · review │
│ (+ qa, security, adversarial, opt-in) │
│ │
│ Everything above is editable later: Ctrl+Space → "Edit" │
└──────────────────────────────────────────────────────────┘
Enter launch c customize q quit
```
- Press **Enter** to create the project and open the dashboard.
- Press **`c`** to customize the detected values before creating anything.
- Press **`q`**, **Esc**, or **Ctrl+C** to quit without writing project state.
The displayed plan includes the local `hub`, the shipped `task` and `subtask`
workflows, and the default agent roles. Both built-in runner declarations remain
available in settings, even though the detected runner is the one the
orchestrator uses for the workspaces it creates. The **workspace pool starts
empty**: the orchestrator provisions it on first boot, asking how many
workspaces and which naming scheme you want and creating each with `shelbi
workspace add`.
### Customize instead
Pressing `c` opens the detailed path. Each detected value is prefilled, so
press Enter to keep it or edit it:
```text
Customize setup. Press Enter to keep each detected value.
? Project name:
? Path to the repo:
? Default branch:
? GitHub repo URL (optional):
? Agent runner (used by every workspace):
? Orchestrator runner:
```
This is where you can choose a different project name or root, change the
branch or remote, or use a different runner for the orchestrator. Workspaces
themselves are sized and named later, in the orchestrator's first-boot
interview.
## 3. Land on the dashboard
After Enter, Shelbi prints `✓ Project created.` and launches the TUI.
The new board contains one Backlog card named **Welcome to Shelbi** and the
sidebar briefly shows:
```text
Ctrl+P palette · type E to edit settings
```
The Welcome card is the first hands-on tour. Promote it from Backlog to Todo
and watch Shelbi dispatch it to an available workspace in the sidebar. Open the
command palette with `Ctrl+P`; type `E` to find project and agent settings.
The card is only a guide and is safe to delete.
To change settings later, use that `Ctrl+P`, then `E` path. You can also edit
the generated project files directly and run `shelbi reload`. See
[Project configuration](/docs/configuration/project) for the full schema and
[Config modes](/docs/concepts/config-modes) if you want to share config through
the repository.
## Edge cases before the card
### The directory is not a Git repository
Shelbi asks before initializing Git:
```text
? is not a Git repo. Initialize one here with git init -b main? (Y/n)
```
Accept to continue. Decline and Shelbi writes nothing, then prints:
```text
No files were written. Run git init -b main and try Shelbi again.
```
### A prerequisite is missing
Failed checks are shown with `✗`, and setup stops with a concrete next step.
For example:
```text
✗ tmux not found on PATH
tmux was not found on PATH. Run brew install tmux, then start Shelbi again.
```
If no supported runner is found, Shelbi points to Claude Code and Codex install
instructions and asks you to authenticate one before starting Shelbi again.
No partial project is left behind.
## Add another project
Run `shelbi project add` from another repository. It uses the same preflight,
card, Customize path, and dashboard launch as first-run setup.
## Next
Your project is configured and the Welcome card is ready. Continue with
[Run your first task](/docs/guides/getting-started/first-task).
[Source](https://shelbi.dev/docs/guides/getting-started/first-project)
---
# Review workspaces
Review routing is ordinary tag primitives: a status requires a tag, a workspace carries it, and the task loads onto a matching slot. On top of that, Shelbi ships a dedicated `review:` serve recipe that boots the branch, a review sidebar that lists tasks Ready and Queued for review, and a review interface for clicking through the running app and accepting or rejecting it.
Most real review of an app or a site means **running it**: booting the dev
server, hitting a URL, clicking through the change, not just reading a diff.
The *routing* that gets a task to a review slot is ordinary
[tag routing](/docs/concepts/workspaces#tags-capability-labels): a **status
requires tags**, a **workspace carries tags**, and the task loads onto a free
slot whose effective tags match. On top of that generic routing, Shelbi ships
three pieces of review-specific machinery: a workflow `review:` **serve recipe**
that boots the branch, a **review sidebar** listing tasks Ready and Queued for
review, and a **review interface** for running the change and accepting or
rejecting it.
This page covers the review tag, the serve recipe, and the review surfaces in
the TUI. The generic tag and slot mechanics they build on — effective tags,
superset routing, and `$SLOT` — live in
[Workspaces](/docs/concepts/workspaces#tags-capability-labels).
## The review tag
Review routing uses one tag by convention: `review`. A workflow's review
[status](/docs/configuration/workflow#statuses) requires it, and every slot
meant to run review work carries it:
```yaml
# workflows/default.yaml — the review status requires the tag
statuses:
- { id: review, owner: user, agent: review, tags: [review] }
```
```yaml
# project.yaml — tag the machine (or individual slots) so its slots carry it
machines:
- name: hub
kind: local
work_dir: ~/Workspaces/myapp
tags: [review] # every slot on hub inherits `review`
```
When a task enters the review status, the orchestrator routes it to a free
workspace whose effective tags are a superset of `{review}` — the same
[superset routing](/docs/concepts/workspaces#status-tag-routing) every tag uses.
Nothing branches on the literal word "review"; it is just the tag this workflow
happens to require. If no declared workspace carries it, the load fails loudly
rather than running review work on a general slot.
## Serving the branch: the `review:` block
Routing gets a task onto a review slot. **Serving** the branch — booting the
dev server so a human can click through it — is best expressed with the
workflow's dedicated [`review:`](/docs/configuration/workflow#review) block.
This is the recommended way to stand up a review server. Shelbi resolves the
recipe's `$SLOT` / `$PORT` placeholders against the review slot's port and
injects the resolved recipe into the [Review agent](/docs/concepts/agents)'s
dispatch prompt; the agent runs it verbatim, health-checks it, and hands back
a URL.
```yaml
# workflows/default.yaml
review:
workdir: site # relative to the worktree root
setup: npm install --no-audit --no-fund # one-shot; must exit 0 before serving
serve: npm run dev -- -p $SLOT # binds the dev server to the slot's port
ready: curl -sf http://localhost:$SLOT # readiness probe, polled until it exits 0
url: http://localhost:$SLOT # reviewable URL; gates the "Open Browser" action
```
The `review:` block carries exactly these fields — matching the
[`ReviewServe`](/docs/configuration/workflow#review) struct in source:
- **`workdir`** — subdirectory to run the recipe in, relative to the worktree
root. Omitted, it runs at the root.
- **`setup`** — a one-shot install/build command that must exit 0 before
serving. Omitted, setup is skipped.
- **`serve`** (required) — the command that starts the dev server, bound to
the slot's port.
- **`ready`** — a readiness probe polled until it exits 0. Omitted, there's
no HTTP probe.
- **`url`** — the reviewable URL handed to the human once the server is up. It
also gates the review interface's **Open Browser** action.
Because the recipe lives on the workflow, a monorepo's `app` / `site` / `docs`
workflows each serve their own subdirectory on the review slot's port without
colliding. When a workflow declares **no** `review:` block, the Review agent
does a **diff-only** review: it does not auto-detect a framework or boot a
default-port server.
### `$SLOT` (transition env) vs `$PORT` (review template)
Both spellings exist, and they are not the same mechanism:
- In a transition's [`run` / `ready`](#transition-commands-run-ready-teardown)
commands, **`$SLOT`** is a shell environment variable holding the
workspace's numeric slot. You derive a port from it yourself with shell
arithmetic (`$((3000 + $SLOT))`).
- In the `review:` block, **`$SLOT` and `$PORT` are interchangeable template
placeholders**, both resolved by Shelbi to the review slot's port *before*
the recipe reaches the agent. Both the `$X` and `${X}` spellings are
substituted (see `substitute_review_url` in source); a fixed URL with no
placeholder round-trips unchanged. There is no shell arithmetic here — the
agent receives the already-substituted string.
## Transition commands: run, ready, teardown
The `review:` block is the preferred way to serve a review branch, but a
status's **transitions** can also run arbitrary commands, and that's the right
tool for hub-side side-effects that aren't the review server itself. A
[transition](/docs/configuration/workflow#run-ready-and-teardown) can carry:
- **`run`** — shell commands executed in order, in the task's worktree, on the
assigned workspace's machine. They run after the edge's git `actions` and
share the same short-circuit contract.
- **`ready`** — a command polled until it exits 0 (bounded by `ready_timeout`,
default 90s), so a launched server can be confirmed up before the human is
handed the URL.
- **teardown** — expressed as the `run` of the *exit* transition; there is no
separate teardown hook.
Each command runs synchronously, so a long-lived server must background itself
(`… &`, `nohup`, a detached pane). The edge is entered the moment the launcher
returns; `ready` is what confirms the server actually answers.
Unlike the `review:` block, a transition `run:` server runs **hub-side and
declaratively** — Shelbi executes it, not the Review agent — so it can't
health-check, summarize a failure, or apply a human's tweak. Prefer `review:`
for the serve recipe; keep transition `run:` for the side-effects around it.
## A review column, from these pieces
Here is a complete review column: a tagged machine, a tagged status, a
`review:` serve recipe, and an accept edge that merges. Only the routing is
generic workflow config; the serve recipe and the review interface are the
review-specific machinery layered on top.
```yaml
# project.yaml — tag the machine (or individual slots)
machines:
- name: hub
kind: local
work_dir: ~/Workspaces/myapp
tags: [review]
workspaces:
- { name: alpha, machine: hub, runner: claude } # dev slot
- { name: bravo, machine: hub, runner: claude } # dev slot
- { name: review-0, machine: hub, runner: claude, tags: [review], slot: 3000 }
```
```yaml
# workflows/default.yaml — the review status requires the tag, and the
# review: block tells the Review agent how to boot the branch.
statuses:
- { id: in-progress, owner: agent, agent: developer }
- { id: review, owner: user, agent: review, tags: [review] }
- { id: done, owner: user }
transitions:
- from: review, to: done, actions: [merge, delete_branch]
review:
setup: npm install --no-audit --no-fund
serve: npm run dev -- -p $PORT
ready: curl -sf http://localhost:$PORT
url: http://localhost:$PORT
```
With `review-0` on slot `3000`, `$PORT` resolves to `3000` and the agent
serves the branch on `:3000`; a second review slot set to `3010` would serve
there. The port is deterministic and collision-free because it's derived from
the slot.
Routing does two things when a task enters the status: it loads the task onto
the matching workspace running the status's [`agent`](/docs/configuration/workflow#statuses),
**and** it hands that agent the resolved `review:` recipe. Shelbi ships a
built-in `review` agent whose charter is "make the change runnable for a human,
don't code" — name it here, or point `agent:` at any agent you've authored.
See [agents](/docs/concepts/agents).
## The review sidebar
Review-status tasks surface in the sidebar under two dedicated sections, split
by whether a task is loaded on a review slot yet:
```text
— Ready for Review —
✓ Polish dark-mode toggle hub:review-0
jlong/dark-mode-toggle-polish
— Queued for Review —
· Fix cookie domain bug
jlong/fix-cookie-domain-bug
```
- **Ready for Review** (`✓`, cyan) — the task's `assigned_to` names a
`review`-tagged workspace, so the branch is loaded on a review slot and
serving. Each entry is a two-line row: line 1 is the task title with a
right-aligned **location badge** (the `machine:workspace` it's loaded on,
e.g. `hub:review-0`); line 2 is the branch, dim.
- **Queued for Review** (`·`, dim) — every other Review-status task
(unassigned, or still pinned to the dev workspace that produced it), waiting
for a free review slot. No location badge, because nothing is serving yet.
A `review`-tagged slot **never appears under the `— Workspaces —` section**;
its capacity surfaces exclusively through these two review sections. Dev
workspaces list under `— Workspaces —` as usual.
Selecting a **Queued** row raises a "Load onto a review workspace?" confirm
popup and, on confirm, loads the branch onto a free `review`-tagged slot (never
the dev pane that built it). Selecting a **Ready** row opens its review
interface directly.
Machine groups under `— Workspaces —` can be collapsed (Space / Enter on a
machine header). The set of collapsed machine names is persisted 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. Machine names not present in the current
project are ignored, not dropped, so re-adding the machine restores its prior
state.
## The review interface
Opening a Ready task launches the **review interface** — a two-column tmux
layout: the **review panel** on the left and the swappable review content on
the right. The panel is the review window's own navigation, so the global nav
sidebar stays docked in the dashboard. The panel is where you drive the review:
- A square **back button** at the top (a back-arrow glyph) that switches focus
back to the dashboard window. It leaves the review interface loaded, so you
can return to it from the sidebar.
- A **header** with the review status (`Ready for review`) and the review
worktree's folder (`📂`) — click it to reveal the worktree in your OS file
manager.
- A **view switcher** that swaps the right-hand content pane:
- **🤓 Chat with Reviewer** (default) — talk to the Review agent.
- **🔀 View Diff** — open your system diff tool over the review branch's
changes (the same `merge-base(base, HEAD)..HEAD` range `shelbi diff`
shows) in the main pane. It launches git's configured diff tool
(`diff.tool`, or `diff.guitool` for a GUI tool); with none configured the
panel reports a short error rather than launching.
- **✍️ Edit in ``** — open the worktree in your configured editor
(see below).
- **🌐 Open Browser** — open the resolved review `url` in your system
browser. This entry renders **only** when the workflow declares a review
URL.
- An **Actions** group:
- **✅ Approve** — move the task one column forward along the normal accept
edge (`review → done` in the default workflow), firing that edge's
`actions` (e.g. `merge`, `delete_branch`), then tear the interface down.
- **❌ Reject** — open a type-the-reason dialog; on submit, the reason is
appended to the task body and the task is bounced back to the workflow's
ready status for another pass. An empty reason can't submit.
### The review editor
The **Edit in ``** view launches a **hub-wide** editor, configured once
in [`~/.shelbi/config.yaml`](/docs/configuration/global#configyaml) under the
`editor` key, so a reviewer's editor choice follows them across every project.
The value may be a bare command (`hx`) or a command with flags (`code --wait`).
Resolution order is:
1. `~/.shelbi/config.yaml`'s `editor`,
2. the `$EDITOR` environment variable,
3. `vim`.
The switch label is derived from the program's basename, first letter
upper-cased — `hx` shows as **Edit in Hx**, `code --wait` as **Edit in Code**.
## The load → serve → inspect flow
End to end, a finished task reaches a human like this:
1. **A dev workspace finishes.** It writes its review-ready marker; the hub
poller rebases the branch onto the base branch and moves the task into the
review status. The finishing workspace closes its own session and returns
to idle (see [Workspaces](/docs/concepts/workspaces#how-a-task-completes)).
2. **The orchestrator routes it.** The review status requires `tags: [review]`,
so the task loads onto a free `review`-tagged workspace (preferring the one
it ran on) and the Review agent boots the branch from the resolved `review:`
recipe. If every matching slot is busy, the task sits in **Queued for
Review** until one frees — nothing is preempted.
3. **A human inspects.** You open the task's review interface from the sidebar,
click through the running app at the served URL, and decide: **Approve**
(move to Done) or **Reject** (bounce back with a reason). Each move fires
that edge's `actions` and `run`.
The board's category vocabulary is unchanged: the task sits in the
`handoff`-category review status throughout.
## Review and Zen Mode
Routing a task to a review workspace is the *human* path — the whole point is
human eyes on the running app. Whether such a task is eligible for
[Zen Mode](/docs/concepts/zen-mode)'s auto-merge is governed by the ordinary
action-based rule: Zen's high-confidence bar fires on any transition whose
`actions` include `merge`. Keep `merge` off the *enter*-review edge and on the
*accept* edge (as above) so the orchestrator holds the task for you rather than
crossing the accept boundary on its own.
## See also
- [Workflow config](/docs/configuration/workflow) — the status `tags`, the
`review:` serve block, and transition `run` / `ready` / `ready_timeout`.
- [Project config](/docs/configuration/project) — machine and workspace
`tags`, and workspace `slot`.
- [Global config](/docs/configuration/global) — the hub-wide `config.yaml`
`editor` the review interface's "Edit in ``" view uses.
- [Workspaces](/docs/concepts/workspaces) — the pool model these slots extend.
- [Agents](/docs/concepts/agents) — the role that runs inside a workspace,
including the built-in `review` agent.
- [Set up review workspaces](/docs/guides/getting-started/review-workspaces) —
the step-by-step walkthrough.
[Source](https://shelbi.dev/docs/concepts/review-workspaces)
---
# Run your first task
Open the TUI, add a task, watch a workspace pick it up, and review the diff.
```text
Walk me through running my first Shelbi task. Open the TUI with `shelbi`, add a task to the backlog (through the orchestrator chat or `shelbi task add`), promote it to todo so the orchestrator dispatches it to a free workspace, then review the resulting branch and merge it. Explain the five columns — Backlog → Todo → In Progress → Review → Done — and the workspace badges as we go.
```
A task moves through five columns: **Backlog → Todo → In Progress →
Review → Done**. You drive the ends: what lands in the backlog and what
gets accepted out of review. The orchestrator drives the middle: the
moment a card hits todo, it picks a free workspace and starts it.
## Open the TUI
From inside a project, or from anywhere with the project on disk:
```bash
shelbi
```
With one project configured, `shelbi` launches its TUI directly. With
more than one, you get a fuzzy picker: type to filter, Enter to launch.
Inside a registered project's `work_dir`, that project always wins (Shelbi
resolves by reverse-lookup against `~/.shelbi/projects/*.yaml`).
You can also target a project by name:
```bash
shelbi -p myapp
```
The TUI is two panes. A borderless sidebar on the left lists the project
name, the nav (Chat, Tasks), your declared workspaces, and any tasks
waiting on review. The right pane is a real tmux pane: the orchestrator
agent by default, or one of the built-in views (Chat / Tasks / Machines
/ Review) when you switch.
`Ctrl+P` opens a fuzzy command palette as a tmux popup. Use it to
switch projects, jump to a workspace pane, or swap the right pane to
another view. `Enter` on a sidebar row activates it: focus a workspace,
open a review, switch to the Tasks board.
## Add a task
You have two ways in: through the orchestrator (the easy one) or
directly via the CLI.
### Through the orchestrator
The orchestrator is the conversational front door. Tell it what you
want in plain English:
```
you: fix the login bug on Safari — cookie domain mismatch
breaks the SSO redirect.
Orchestrator: ✓ added to backlog as `fix-login-bug-on-safari`.
branch: shelbi/fix-login-bug-on-safari
```
The orchestrator creates a markdown task card in the **backlog**
column. It won't auto-promote. The backlog is *your* triage queue.
### Via the CLI
Same end result, no chat:
```bash
shelbi task add "Fix login bug on Safari"
```
Flags you'll reach for occasionally:
```bash
shelbi task add "Fix login bug" \
--id fix-login \
--status todo \
--description "Cookie domain mismatch breaks the SSO redirect." \
--depends-on auth-refactor \
--prefers-machine devbox
```
- `--id` — override the auto-slug.
- `--status` — drop straight into todo (or any other column).
- `--description` — fill in the task body now instead of editing later.
- `--depends-on` — repeat for multiple dependencies. The orchestrator
skips a todo card until everything it depends on is in `done`.
- `--prefers-machine` — soft hint; the orchestrator routes to a free
workspace on that machine when one's available.
### Via the kanban view
Press `Ctrl+P` and pick "Tasks", or hit `Enter` on the **Tasks**
sidebar nav. You land on the 5-column Kanban board. Keys:
| Key | Action |
|---|---|
| `h` / `l` | Step between columns |
| `j` / `k` | Step between rows in the current column |
| `Enter` / `Space` | Open the highlighted card |
| `H` / `L` (shift) | Move the selected card to the previous / next column |
| `K` / `J` (shift) | Reorder within a column |
| `r` | Refresh |
| `Esc` / `q` | Close an open card |
Adding a task from the kanban view goes through the orchestrator, so it's
faster to just talk to it in chat.
## Promote to todo, watch the auto-dispatch
Promotion is the start signal. Move a backlog card into **todo**,
either by hitting `L` (shift-l) on the card in the kanban view or by
asking the orchestrator to:
```
you: promote fix-login-bug-on-safari.
Orchestrator: ✓ moved to todo. dispatched to delta.
workspace: delta
branch: shelbi/fix-login-bug-on-safari
```
The orchestrator reacts to the move on the event log
(`~/.shelbi/events.log`). It scans the workspace pool, picks the first
free workspace in YAML declaration order (honoring `prefers_machine` if
set), and runs:
```bash
shelbi task start fix-login-bug-on-safari --workspace delta
```
That moves the card to **In Progress**, creates the branch
`shelbi/fix-login-bug-on-safari` in the workspace's worktree, and feeds
the task prompt into the workspace's agent CLI. The sidebar badge next to
`delta` flips from `·` (idle) to `⏵` (working).
Jump in any time. `Enter` on the workspace's sidebar row drops you straight
into its pane to watch the run live.
| Badge | Workspace state |
|---|---|
| `·` | idle — no task assigned |
| `⏵` | working — agent actively running a turn |
| `💬` | awaiting input — finished a turn, sitting at the prompt |
| `⚠` | awaiting permission — showing a permission dialog |
| `✓` | review-ready — task moved to the review column |
## Review the workspace's branch
When the workspace finishes, it writes a review-ready marker into its
worktree. The hub poller picks it up on the next tick, moves the card
into **Review**, and the workspace's badge flips to `✓`. The sidebar's
*Ready for Review* list grows a row:
```
— Ready for Review —
✓ fix-login delta
```
Click that row (or `Enter` on it) to focus the workspace's window, where
the finished branch is checked out for you to interrogate the diff. Ask
the agent to walk you through the change, run tests, or explain a tricky
hunk. Nothing about the workspace's run is locked in yet; you can ask for
edits and have them committed onto the same branch.
To go a step further and *run* the change — boot its dev server and click
through the app — tag a slot for review and let the review status route the
branch onto it. See [set up review
workspaces](/docs/guides/getting-started/review-workspaces).
## Accept the task
Two flavors of accept.
**Merge into the default branch.** From the review pane or the chat,
just ask:
```
you: merge it.
```
Under the hood that's `shelbi merge fix-login-bug-on-safari`. The CLI
squash-merges the branch into `default_branch` and prints the resulting
commit hash. Add `--pr` to open a PR via the GitHub CLI instead of
merging directly.
**Move the card to Done.** The orchestrator never auto-completes
review. You confirm the merge landed and move the card yourself, with `L`
in the kanban view from **Review**, or:
```bash
shelbi task move fix-login-bug-on-safari --to done
```
The workspace is already free at that point (the review handoff flipped
its badge back) and the orchestrator dispatches it to the next ready
todo card on its own.
The orchestrator squash-merges on request but never marks a task Done for
you. Confirming the merge landed and moving the card out of Review is the
human checkpoint, until you hand that step off to
[Zen Mode](/docs/guides/getting-started/enable-zen-mode).
## What just happened
You added a task, promoted it, watched a workspace pick it up, reviewed
the result, and merged it. The same loop (backlog → todo → in progress
→ review → done) is the whole product surface. Everything else is
volume: more tasks at once, more workspaces, more machines.
## Next
The rest of getting-started walks the same loop at increasing levels
of autonomy:
- [Run a multi-workspace dispatch loop](/docs/guides/getting-started/multi-workspace):
fill the backlog with a stack of work and watch every workspace in
your pool run in parallel without you touching the dispatcher.
- [Enable Zen Mode](/docs/guides/getting-started/enable-zen-mode) — flip the
orchestrator from scheduler to lead. It auto-promotes eligible
backlog cards and auto-merges finished branches that clear a
project-defined confidence bar.
- [Author a custom Shelbi workflow](/docs/guides/getting-started/custom-workflow):
fork the orchestrator's prompt and encode routing rules, Zen
judgment categories, and reporting style for this project
specifically.
For deeper reference any time:
- The **Concepts** docs for the full mental model of workspaces,
columns, the events log, and the orchestrator.
- The **CLI reference** for every flag on every `shelbi` subcommand.
[Source](https://shelbi.dev/docs/guides/getting-started/first-task)
---
# Orchestrator
One agent in window 1 you talk to — it dispatches tasks to workspaces, tails the events log, and reports back. You are the priority-setter; it is the scheduler.
The orchestrator is the agent you talk to. It lives in window 1 of the
project's tmux session (`shelbi-:dashboard`), runs whatever
runner the project declares (typically `claude`), and treats the
`shelbi` CLI as its tool surface, the same CLI you use yourself.
It is a [**named agent**](/docs/concepts/agents) like any other
(`agents/orchestrator/`), with an editable `instructions.md` for its system
prompt. What makes it special is only *where* it runs: window 1, talking to
you, rather than in a [workspace](/docs/concepts/workspaces) picking up
tasks. The `developer` agent (and any reviewer roles you author) run inside
workspaces; the orchestrator runs the board.
The mental model is: **you are the priority-setter and reviewer. It is
the scheduler.** It does not edit code, it does not accept reviews, it
does not promote backlog items on its own. It turns natural-language
requests into Kanban cards, picks free workspaces, and reports progress
back.
The board it runs is the whole dashboard: the columns cards move through and
the workspaces it dispatches them to:
## You-are-the-scheduler
A useful contrast: a generic chatbot waits to be asked. The orchestrator
does not. As soon as a task lands in the `ready` category (`todo` in the
default workflow), that is the start signal. The orchestrator's job is
to find a free workspace, route the task to it, and tell you it did. When
a workspace hands off (its task moves into the `handoff` category,
`review` in the default workflow), the orchestrator's job is to give that
workspace the next ready task without waiting to be prompted.
This is what makes the loop feel continuous. You drop work into the
backlog, triage what's ready, and review what comes back. Everything in
between (assignment, branch setup, launch, completion detection,
re-dispatch) is the orchestrator's responsibility.
## Categories: the vocabulary the orchestrator reasons in
A [workflow](/docs/guides/getting-started/workflows) can rename statuses, split a
category across multiple statuses, or drop columns the project doesn't
need. To keep generic code (auto-dispatch, Zen Mode, the activity feed)
working unchanged across every workflow, the orchestrator reasons in a
fixed, closed set of **status categories** rather than literal status
names:
| Category | What it means | Default workflow status |
| ---------- | -------------------------------------------------------------------- | ----------------------- |
| `backlog` | Not yet ready for work (triage stage). | `backlog` |
| `ready` | Queued for whoever owns it next (typically a workspace). | `todo` |
| `active` | Owner is working on it now. | `in_progress` |
| `handoff` | One owner finished their part; another's input is required next. | `review` |
| `done` | Terminal; accepted by the user. | `done` |
The category set is fixed; status names are user-customizable per
workflow. A workflow that renames `Review` to `QA`, or splits `handoff`
into `Code Review` and `QA`, still triggers the same auto-dispatch and
auto-merge rules because the orchestrator matches on the category, not
the name. See the [Workflows](/docs/guides/getting-started/workflows#status-categories)
concept page for the full schema.
The `shelbi task move --to ` CLI accepts the literal status name
from the active workflow. `` is never a category. The category
is the semantic layer above; the orchestrator reads it from each task's
status definition (and, on the wire, from the `from_category=` /
`to_category=` tokens on every task event line; see [the events
log](/docs/concepts/events-log#task-transitions)).
## How the prompt is wired
The shape of the orchestrator's responsibility (the bootstrap flow,
the category-keyed reaction rules, the dispatch contract) is encoded in
the orchestrator agent's `instructions.md`. Like every
[agent](/docs/concepts/agents), its rendered prompt is its
`agents/_shared/preamble.md` (project-wide context) followed by its own
`agents/orchestrator/instructions.md`. The orchestrator is the configured
runner with that prompt staged in its launch directory.
When you boot a project (`shelbi orchestrate` or via the TUI launcher),
`ensure_dashboard()` does the following:
1. Resolves the orchestrator agent's prompt: `_shared/preamble.md`
prepended to `agents/orchestrator/instructions.md` (see
[customizing the prompt](#customizing-the-prompt-per-project) below).
2. Writes the composed prompt to
`.claude/agent-instructions.md` in the launch directory.
3. Launches the orchestrator runner in the right pane of the
`dashboard` window, with that path as its working directory.
For Claude, Shelbi adds Claude-specific launch wiring:
`--append-system-prompt "$(cat .claude/agent-instructions.md)"` and an
initial positional prompt that tells Claude to run the bootstrap sequence.
For Codex, Shelbi launches the configured `command` and `flags`, then adds
an initial positional prompt containing the project identity, worktree path,
rendered `.claude/agent-instructions.md` contents, reload handoff context,
and bootstrap request. Other non-Claude runners launch exactly as
configured, so they must support any prompt-loading flags you declare in
`agent_runners..flags`.
The split is: the *source* you edit is `instructions.md`;
`.claude/agent-instructions.md` is a rendered build artifact, not a file
you maintain by hand.
Orchestrator window setup lives in
`crates/shelbi-orchestrator/src/lib.rs`. `ensure_dashboard(project)`
resolves the agent prompt, renders it, and launches the runner in that
directory.
### Choosing Codex
Declare Codex in `agent_runners` and select it for the orchestrator:
```yaml
orchestrator:
runner: codex
agent_runners:
codex:
command: codex
flags: []
```
`orchestrator.runner` must name a declared runner; otherwise project
validation fails before the dashboard launches. If your Codex CLI needs a
specific approval mode, sandbox mode, or model flag for unattended work,
put those arguments in `flags`. Shelbi does not translate Claude's
`workspace_permissions_mode`, `--permission-mode`, `--continue`, or
`--append-system-prompt` behavior to Codex.
## Bootstrap flow on session start
The prompt instructs the orchestrator to do three things on the first
reply of a session (or right after `shelbi reload`), before answering
the user:
1. **Read the board.** `shelbi task list` for the column membership,
priorities, `assigned_to`, and any `prefers_machine` hints on each
card. Each status belongs to one of the five categories above; the
orchestrator maps column → category from the active workflow.
2. **Read the workspace pool.** `shelbi workspace list` to see which
workspaces are free (no `active`-category task assigned), which machine
each lives on, and which agent each is running.
3. **Start tailing.** Launch `shelbi events tail --follow` in the
background and watch it. Every emitted line is a trigger the
orchestrator must consider.
After that, the in-memory snapshot is kept up to date from the event
stream. The orchestrator only re-runs `workspace list` / `task list` if
the tail process dies and it has to rebuild.
The reaction rules pattern-match the **category** tokens on each task
event line, not the literal ` -> ` status pair. That's what
keeps these rules correct under a workflow that renames or splits
statuses:
- `task= ... to_category=ready reason=user:*` → find a free eligible
workspace, dispatch.
- `task= ... to_category=handoff reason=workspace:ready-marker` →
the finishing dev workspace has already closed its session, so it's
*already free*. Find it the next ready task. If the handoff status
requires [review tags](/docs/concepts/review-workspaces), also route the
finished branch onto a matching workspace — firing the status's enter
transition to boot a server (or leave it queued when they're all busy) so a
human can run the change.
- `worker= working -> awaiting_input` → same idea, the workspace
just freed up.
- `worker= pane_alive=false` → surface to the user; don't
auto-restart.
- `project= heartbeat` → no state change to react to, but the
watch is awake. A good time to re-check `active`-category tasks for
stuck workspaces or missed markers. See
[heartbeats](/docs/concepts/events-log#heartbeats) for the cadence
config and rationale; tune it via the `heartbeat` key in
`project.yaml`.
When dispatching, the orchestrator picks free eligible workspaces in the
order they're declared in the project YAML, honoring `prefers_machine`
hints. If a task names a machine and no workspace on that machine is free,
it stays in the `ready` category rather than getting routed to the wrong
host.
The full set of rules is in the default prompt, including when *not*
to dispatch (failed retries, deduplicated misclicks, mid-conversation
with the user). It's worth a read; it doubles as the spec for
what the orchestrator is supposed to do.
## Customizing the prompt per project
The orchestrator is a [project-local agent with shipped defaults](/docs/concepts/agents#customizing-an-agent),
so customizing it is the same as customizing any other agent. Edit a file:
- **Per-project prompt:** edit
`/agents/orchestrator/instructions.md`, where
`` is `~/.shelbi/projects//` in
[global mode](/docs/concepts/config-modes) or `/.shelbi/` in
in-repo mode. This is the orchestrator's system prompt; Shelbi seeds
it with the shipped default on first load and never clobbers your
edits on upgrade. On `shelbi reload`, Shelbi may append missing
runner-critical sections from the shipped default, such as
`Polling-only event drain`, so customized Codex orchestrators still
receive required polling contracts without losing local changes. Or
run `shelbi agent edit orchestrator`.
- **Project-wide context:** put anything that should apply to *every* agent
(repo layout, house style, the test command) in
`/agents/_shared/preamble.md`. It's prepended to
the orchestrator's prompt (and every other agent's), so you write it once
instead of pasting it into four `instructions.md` files.
- **Shipped default:** the bundled orchestrator prompt lives in the
`shelbi-orchestrator` crate. That's the right place for
shipped-with-Shelbi changes; edit it and rebuild.
The composed output (preamble + instructions) is rewritten to the launch
directory's `.claude/agent-instructions.md` on every `ensure_dashboard`
call, so your edits take effect the next time the dashboard is
bootstrapped (or `shelbi reload`d).
`shelbi reload` runs the agent self-heal before respawning the orchestrator
pane. If your customized `agents/orchestrator/instructions.md` is missing a
required shipped section, reload appends only that section and reports the
repair in its output; the rest of your prompt is preserved.
### Prompt artifact
The *source* you edit is `agents/orchestrator/instructions.md` plus
`agents/_shared/preamble.md`; the generated
`.claude/agent-instructions.md` is a rendered artifact. Don't hand-edit
it; your changes are overwritten on the next render.
A few things worth keeping when you fork the default prompt:
- The bootstrap flow. The orchestrator needs the initial `task list` /
`workspace list` snapshot and the tail process to do its job.
- The polling-only event drain. Codex-backed and other polling-only runners
must drain pending project events before each user-facing reply, using
`shelbi orchestrator events drain` (the durable cursor is persisted in the
project config dir and resumes automatically) or the documented
cursor-based `events.log` fallback if that CLI primitive is unavailable.
- The **category-based** reaction-rule matching. Patterns key off
`to_category=` (`ready`, `handoff`) rather than the literal status
pair, so the rules survive workflow customization. Hardcoding default-
workflow status names here is a footgun. A project that later renames
`Todo` to `Ready` or splits `Review` into `Code Review` + `QA` will
fall through the matches.
- The reaction rules tied to specific reason strings (`user:*`,
`workspace:ready-marker`, `orchestrator:auto-dispatch …`). These are
the contract the rest of Shelbi expects the orchestrator to honor.
- The "you are the scheduler" framing. Without it, the agent reverts to
chatbot mode and waits to be told what to do, which defeats the
point.
What you might *want* to change per project:
- Routing rules: "always send infra tasks to charlie", "never use the
hub for benchmarks".
- Merge policy: by default the orchestrator stops at the `handoff`
category (`review` in the default workflow); you can authorize specific
loops where it squash-merges and moves straight to `done` (see the
"Mark review done, merge, and push" section in the default prompt for
the existing recipe).
- Reporting style: the one-line activity summary is tunable.
## What the orchestrator does not do
- **It does not edit code.** The `developer` (and other workspace) agents
do that.
- **It does not move tasks into the `done` category.** That's your
accept signal.
- **It does not auto-promote `backlog` → `ready`.** Triage belongs to
the user.
- **It does not restart a workspace whose pane died.** It surfaces the
death and waits for direction.
- **It does not stop workspaces without asking.**
These are guardrails encoded in the prompt itself. They're worth
calling out because the temptation when watching a fast loop is to let
the orchestrator do *more*. Don't. The boundaries are what keep the
workflow legible.
## Going further: Zen Mode
When you're ready to let the orchestrator initiate instead of just
scheduling, [Zen Mode](/docs/concepts/zen-mode) is the next-level
autonomy switch. It flips the agent from scheduler to lead: it
auto-promotes `backlog`-category tasks into `ready` when it judges the
work in scope, and it lands finished branches past a project-defined
confidence bar without waiting on a human reviewer. The policy that
decides "in scope" lives in the orchestrator agent's `instructions.md`
described above. You tune it by editing prose, not code. Whether a given
status is automated and which agent handles it is separate, and lives in
the [workflow YAML](/docs/guides/getting-started/workflows#owners-and-agents).
## See also
- [Agents](/docs/concepts/agents) — the orchestrator is one; this is how
its prompt, skills, and customization work.
- [Workspaces](/docs/concepts/workspaces) — what the orchestrator
dispatches tasks to.
- [Review workspaces](/docs/concepts/review-workspaces) — the tag-routed slots
the orchestrator loads a finished branch onto on handoff, and the queue it
drains as they free.
- [Workflows](/docs/guides/getting-started/workflows) — the schema behind the board,
including the full category set and the events-log annotations the
orchestrator pattern-matches on.
- [The events log](/docs/concepts/events-log) — the orchestrator's
live feed.
- [Zen Mode](/docs/concepts/zen-mode) — flipping the orchestrator
from scheduler to lead.
[Source](https://shelbi.dev/docs/concepts/orchestrator)
---
# Run a multi-workspace dispatch loop
Fill the backlog, promote a stack of cards, and watch the orchestrator keep every workspace in your pool loaded with work in parallel.
One task at a time is the on-ramp. The reason Shelbi exists is the
other direction: half a dozen workspaces all running in their own
worktrees, the orchestrator keeping every free slot loaded with the
next ready card. This page walks the loop end-to-end.
You should already have a project with at least two workspaces declared.
If you only have one, edit `~/.shelbi/projects/.yaml`, bump the
pool, and run `shelbi reload` before you start.
## Confirm the pool
From the orchestrator pane (or any shell):
```bash
shelbi workspace list
```
```
NAME HOST RUNNER AGENT STATE
alpha hub claude - idle
bravo hub claude - idle
charlie hub claude - idle
delta devbox codex - idle
echo devbox claude - idle
foxtrot devbox codex - idle
```
Every row should show `idle` under `STATE` and `-` under `AGENT`.
The `RUNNER` column is the configured `workspaces[].runner` value, so it
is where you confirm whether each slot will launch Claude, Codex, or
another declared runner. Nothing is loaded yet. If a pane is dead, jump
to the workspace (`Enter` on its sidebar row): the session may have
stalled on a permission dialog, and resolving it puts the workspace back
in the rotation.
You want at least three free slots to see the loop do its job. Two cards
on a one-slot pool is a queue, not a loop.
## Queue up a stack of work
The fastest way to fill the backlog is to talk to the orchestrator in
batches:
```
you: add three tasks:
1. add CSV export to the reports page
2. fix the cookie-domain bug on Safari SSO
3. tighten the rate-limit error copy
Orchestrator: ✓ added 3 to backlog:
- add-csv-export-to-reports
- fix-cookie-domain-bug-on-safari-sso
- tighten-rate-limit-error-copy
```
The CLI does the same thing in a loop if you'd rather script it:
```bash
shelbi task add "Add CSV export to the reports page"
shelbi task add "Fix cookie-domain bug on Safari SSO"
shelbi task add "Tighten the rate-limit error copy"
```
Each lands in `backlog` as its own markdown card under
`~/.shelbi/projects//tasks/`. Nothing dispatches yet. The
backlog is still your triage queue.
### Route a task to a specific machine
When a task is RAM-heavy, latency-sensitive, or you just want to keep
it off the hub, hint where it should run with `--prefers-machine`:
```bash
shelbi task add "Re-encode the marketing video assets" \
--prefers-machine devbox
```
The orchestrator's dispatcher 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 `todo`.
Shelbi will not re-route a `--prefers-machine` task to the hub just because
the preferred machine is busy. It parks the card in `todo` and waits. Free
a workspace on that machine, or drop the preference, to get it moving.
### Block one task on another
If task B reads files task A is about to rewrite, declare the order
explicitly:
```bash
shelbi task add "Rename the auth_v1 module to auth" --id auth-rename
shelbi task add "Update the OpenAPI spec for the new auth module" \
--depends-on auth-rename
```
The dependent card carries a `🔒` badge in the Kanban view and the
orchestrator skips it during dispatch until every id in its
`depends_on` list reaches `done`. See
[dependent tasks](/docs/guides/getting-started/workflows#dependent-tasks-with-depends_on)
for the full state model.
## Promote everything to todo
Promotion is the start signal. Move all three cards into **todo** in
the order you want them picked up:
- **From the Kanban view** — `Ctrl+P` → Tasks, then `H`/`L` to walk
cards into the next column. `K`/`J` to reorder within `todo`. The
orchestrator dispatches from the top.
- **From the orchestrator** — just ask:
```
you: promote all three.
```
- **From the CLI** — one move per card:
```bash
shelbi task move add-csv-export-to-reports --to todo
shelbi task move fix-cookie-domain-bug-on-safari-sso --to todo
shelbi task move tighten-rate-limit-error-copy --to todo
```
The orchestrator is already tailing `~/.shelbi/events.log`. Each
promotion arrives as a `task= backlog -> todo reason=user:*` line
and triggers an immediate dispatch attempt.
## Watch the loop in action
Within a second or two, the sidebar lights up. Workspace badges flip
from `·` to `⏵` in YAML declaration order: `alpha`, then `bravo`,
then `charlie`, until either the `todo` column is empty or every
workspace is busy. The sidebar groups slots under the machine they're
pinned to, and each busy row shows the agent it's running:
The orchestrator's chat shows the same thing in prose:
```
Orchestrator: ✓ dispatched 3:
- add-csv-export → alpha
- fix-cookie-domain → bravo
- tighten-rate-limit → charlie
```
To see the raw stream, open a second pane and tail the log:
```bash
shelbi events tail --follow
```
You'll see the column moves and the workspace state changes interleaved,
exactly what the orchestrator is reacting to:
```
2026-06-22T14:11:02+00:00 task=add-csv-export-to-reports backlog -> todo reason=user:cli
2026-06-22T14:11:02+00:00 task=add-csv-export-to-reports todo -> in_progress reason=orchestrator:auto-dispatch_workspace=alpha
2026-06-22T14:11:03+00:00 workspace=alpha none -> working
2026-06-22T14:11:04+00:00 task=fix-cookie-domain-bug-on-safari-sso backlog -> todo reason=user:cli
2026-06-22T14:11:04+00:00 task=fix-cookie-domain-bug-on-safari-sso todo -> in_progress reason=orchestrator:auto-dispatch_workspace=bravo
…
```
Jump between workspace panes any time. `Enter` on `bravo` drops you
straight into its pane to watch the Safari fix happen live. Pop back
to the orchestrator with `Ctrl+P` → Chat when you're done.
## Review one branch while the others keep running
Workspaces finish at their own pace. When the first one writes its
review-ready marker, three things happen at once:
1. The hub poller moves the task from `in_progress` to `review` and
clears the marker.
2. The workspace's badge flips from `⏵` to `✓` and the **Ready for
Review** list in the sidebar grows a row.
3. The orchestrator, watching the event stream, sees the workspace free
up and immediately dispatches the next ready `todo` card to that
slot (if any).
That last step is the loop. You don't have to ask. The workspace that
just handed off a finished branch is already on its next task by the
time you've clicked into the review pane.
Open the review:
```
✓ add-csv-export alpha ← Enter
```
Shelbi checks the branch out into the project's working directory on
the machine that ran the task and spawns a fresh agent session pointed
at the diff. Ask it to walk you through the change, run tests, or
request edits. The workspace that produced the branch is busy on
something else, but the review pane is its own conversation in the
same worktree.
Accept when you're ready:
```
you: merge it.
```
Then move the card from `review` to `done` (`L` in the kanban view,
or `shelbi task move --to done`). The card is yours; the
orchestrator only takes the `review` → `done` step when you've
explicitly authorized it as part of [Zen
Mode](/docs/guides/getting-started/enable-zen-mode).
While you've been reviewing, the orchestrator has been working the
backlog. By the time you finish accepting one branch, two more may be
in review. The board state is the single source of truth: the
sidebar mirrors it, the events log records every move, and the
orchestrator keeps every free slot loaded.
## When the loop stalls
A few things stop the dispatcher from filling a free slot, in
intentional ways. They're worth recognizing so you don't mistake
intent for breakage:
- **The card is blocked.** Anything in `depends_on` is not yet `done`.
The card renders with `🔒` in the kanban view. Land the dependency
first.
- **The card prefers a machine and no workspace on it is free.** The
orchestrator parks it in `todo` rather than mis-routing. Free a
workspace on the target machine (kill another task, or remove the
preference).
- **The workspace's pane died.** The orchestrator surfaces it
(`workspace= pane_alive=false`) and waits for you. Re-launch with
`Enter` on the workspace
row, or `shelbi workspace stop ` to release the in-flight task
back to `todo`.
- **The mid-conversation guard.** If you're actively chatting with
the orchestrator about a specific task, it holds off on
back-to-back dispatches that would talk over you. The next event
resumes the loop.
Everything else (diff size, branch contents, file ownership) is the
orchestrator's job to figure out from the prompt. Move the card and
move on.
## Next
The orchestrator is now keeping every workspace fed without your
involvement past triage and review. Reviewing a diff is one thing.
The next step is *running* the change: [set up review
workspaces](/docs/guides/getting-started/review-workspaces) to load a
finished branch onto a live dev server and click through it before you
accept. After that, [enable Zen Mode](/docs/guides/getting-started/enable-zen-mode)
to let the orchestrator auto-promote eligible backlog cards and
auto-merge finished branches that clear a project-defined confidence
bar.
[Source](https://shelbi.dev/docs/guides/getting-started/multi-workspace)
---
# The events log
An append-only file at ~/.shelbi/events.log — every workspace state change and task status move, in one stream the orchestrator tails.
Every state change Shelbi observes is appended to one file:
```
~/.shelbi/events.log
```
(or `$SHELBI_HOME/events.log` if you've set it). It's the live wire
the orchestrator listens on to decide what to do next, and the audit
trail you grep when something looks wrong.
The log is **hub-global**, not per-project: lines for all projects
running against this hub interleave in the same file. Filtering by
project (or workspace, or task) is the orchestrator's job. At the
filesystem layer, it's one stream.
Defined in `crates/shelbi-state/src/worker_status.rs` (`events_log_path`,
`append_worker_event`, `append_task_event`).
## Line shape
The prefix after the timestamp tells you which kind of event the line
carries:
### Workspace transitions
```
workspace= ->
```
```
2026-06-22T14:22:11+00:00 workspace=alpha none -> working
2026-06-22T14:23:47+00:00 workspace=alpha working -> awaiting_input
2026-06-22T14:23:48+00:00 workspace=alpha awaiting_input -> working
```
- `` is `none` on the first observation of that workspace.
- States: `working`, `awaiting_input`, `blocked`. See
[workspace states](/docs/concepts/workspaces#workspace-states).
- The `workspace=` token names the [workspace](/docs/concepts/workspaces) whose state changed.
- Written by the hub-side poller every time it observes a state change
(not on every tick, only on actual transitions).
### Task transitions
```
task= -> reason=
```
```
2026-06-22T14:24:03+00:00 task=fix-login backlog -> todo reason=user:cli
2026-06-22T14:24:11+00:00 task=fix-login todo -> in_progress reason=orchestrator:auto-dispatch_workspace=alpha
2026-06-22T14:31:52+00:00 task=fix-login in_progress -> review reason=workspace:ready-marker
```
- `` and `` are the snake_case status names
from the task's workflow (default workflow: `backlog`, `todo`,
`in_progress`, `review`, `done`). See
[workflows](/docs/guides/getting-started/workflows).
- `reason=` is a single token (whitespace is folded to
underscores) describing who triggered the move.
### Heartbeats
```
project= heartbeat
```
```
2026-06-22T14:25:11+00:00 project=myapp heartbeat
```
- Emitted by the hub-side poller on the cadence set by the
[`heartbeat`](/docs/configuration/project#heartbeat) key in
`project.yaml`. Set to `"off"` to disable.
- Adaptive cadence: the poller holds at the standard `interval`
(default `3m`) while there's supervisable work in flight, then backs
off exponentially once the board is quiescent, doubling each idle tick
and capping at `max` (default `60m`). A fully idle hub relaxes
`3m → 6m → 12m → … → 60m`, so even a long-quiet board still sweeps for
a silently-stuck task about once an hour. Backing off on "no
supervisable work" rather than "no log line" is deliberate: a stuck
`in_progress` task also emits nothing, and the heartbeat sweep is
exactly what catches it.
- Resets on any real event: the poller compares `events.log`'s mtime
against a baseline it advances on each seed/emit/reset, so any genuine
line (a task move, a workspace transition, a dispatch) both skips
that tick's emission (the event already woke the orchestrator) and
snaps the cadence back to the standard `interval`. An active board
never sees padding lines.
- Crash-safe: a poller restart waits one full interval before its first
attempt, rather than firing the slot it missed.
- Paused while offline: each due tick TCP-probes `1.1.1.1:443` with a
one-second timeout, and skips emission if the probe fails. Heartbeats
resume on the first interval after connectivity is restored. The
motivation is that the orchestrator can't act on a heartbeat during a
network drop, so the line would only fill the feed with noise.
Heartbeats exist for the orchestrator's `events tail --follow` watch.
When nothing's happening on the board, the watch sits idle for as long
as it takes for *something* to happen, which can be hours, or never if
a marker-emitting code path silently regresses. The recurring line is
the orchestrator's fallback trigger to wake up and check active tasks.
The TUI activity feed filters heartbeats out by design. They'd
produce one "nothing happened" row every few minutes. Use
`shelbi events tail` if you want to see them.
The feed lives behind the **Activity** view in the dashboard: the same events
this log records, rendered for a human instead of grepped:
The orchestrator distinguishes the line kinds by the second token
(`task=`, `workspace=`, `project=`, `dispatch`, `mode=zen`,
`zen-dryrun`). New line shapes get added over time;
consumers that only recognize `task=` and `workspace=` should fall through
gracefully on the rest.
## Atomicity
Lines are appended via `O_APPEND` in a single `write_all` of the full
formatted line including the trailing newline. POSIX guarantees that
appends ≤ `PIPE_BUF` (4096 bytes) under `O_APPEND` are atomic relative
to other appenders, so concurrent writes from the CLI and the poller
interleave **whole lines** rather than tearing.
This matters because both the CLI (when you run `shelbi task move`) and
the poller (when it observes state changes) write to the same file
concurrently. The test
`concurrent_task_and_worker_appends_dont_tear` in
`crates/shelbi-state/src/worker_status.rs` locks this property in.
## Tailing the log
The CLI gives you a `tail -f`-shaped view:
```bash
shelbi events tail # last 20 lines, exit
shelbi events tail -n 100 # last 100 lines, exit
shelbi events tail --follow # last 20 lines, then stream
shelbi events tail --since 10m # everything in the last 10 minutes
shelbi events tail --since 2h --follow
```
`--since` accepts `s|m|h|d` (e.g. `30s`, `5m`, `2h`, `1d`); a bare
integer is seconds. When `--since` is set, `-n` is ignored and every
matching line is printed.
The follow loop polls the file every 250ms, holding back the final
fragment until its newline arrives so you never see a half-written event.
If the file is truncated or rotated underneath it (`len < offset`), it
restarts from the top rather than silently dropping the next writer's
content.
```text
2026-06-22T14:24:03+00:00 task=fix-login backlog -> todo reason=user:cli
2026-06-22T14:24:11+00:00 task=fix-login todo -> in_progress reason=orchestrator:auto-dispatch_workspace=alpha
2026-06-22T14:24:11+00:00 workspace=alpha awaiting_input -> working
2026-06-22T14:31:50+00:00 workspace=alpha working -> awaiting_input
2026-06-22T14:31:52+00:00 task=fix-login in_progress -> review reason=workspace:ready-marker
```
That five-line burst is what one task moving through the system looks
like end-to-end: user triages, orchestrator dispatches, workspace
starts working, workspace finishes its turn, marker fires, task lands
in review.
Implementation: `crates/shelbi-cli/src/commands/events.rs`.
## Reason strings
The `reason=` tag on task lines is a free-form short token. The system
doesn't enforce a vocabulary, but the orchestrator and CLI use a
consistent set:
| Reason | Source | Meaning |
|----------------------------------------------------|------------------------------------------|------------------------------------------------------------------------------------|
| `user:cli` | `shelbi task move` | You moved the card from the CLI with no explicit reason. |
| `user:cli:start` | `shelbi task start` | You launched a workspace on a task from the CLI. |
| `user:tui:…` | the Kanban TUI | You moved the card with `H`/`L` in the TUI. |
| `user:promote` | a `--reason` you passed | Free-form: anything starting with `user:` reads as "the human chose this." |
| `orchestrator:auto-dispatch workspace=` | the orchestrator's `shelbi task start` | The orchestrator picked a free workspace and dispatched per its routing rules. |
| `workspace:ready-marker` | the hub poller | The workspace wrote its ready marker; the poller promoted the task to review. |
These aren't enforced by code. They're a convention the orchestrator
parses to decide whether *it* triggered an event (and shouldn't react)
or *you* did (and it should respond). The fields the orchestrator pays
attention to today:
- A `user:*` reason on `backlog -> todo` means "newly triaged." Try to
dispatch.
- `workspace:ready-marker` on `in_progress -> review` means "the assigned
workspace just became free." Find it the next task.
- `orchestrator:auto-dispatch …` is the orchestrator's own action; it
doesn't react to it (otherwise it would loop).
Whitespace in your reason is replaced with `_` so the line stays
parseable on a single token. Newlines and tabs get the same treatment.
## Reading the log from code
If you're building something that consumes the log directly, the
contract is:
- Append-only. Never truncate; if you need to rotate, do it atomically
via rename.
- One line per event, RFC3339 timestamp first, ASCII fields separated
by single spaces.
- Two prefixes (`workspace=` or `task=`) keyed off the second
whitespace-separated token after the timestamp.
- Reason strings are tokens (no whitespace). If you need to embed
structured data, encode it inside the token (the convention
`key=value` works fine).
The full grammar lives in the two `append_*_event` functions in
`crates/shelbi-state/src/worker_status.rs`.
## See also
- [Workspaces](/docs/concepts/workspaces#workspace-states) — what each
workspace state actually means.
- [Workflows](/docs/guides/getting-started/workflows) — what each `from -> to`
transition means, including the per-workflow status schema and the
default workflow's lifecycle.
- [Orchestrator](/docs/concepts/orchestrator) — what the
orchestrator does with each line it reads.
[Source](https://shelbi.dev/docs/concepts/events-log)
---
# Set up review workspaces
Tag a workspace, require that tag on your review status, and give the workflow a `review:` serve recipe — the end-to-end path from a `review` tag in your YAML to clicking through a change on a live server and accepting or rejecting it from the review interface.
The [multi-workspace loop](/docs/guides/getting-started/multi-workspace) gets
finished branches back to you. But reading a diff isn't the same as *running*
the change. Most real review of an app or a site means booting the dev server,
opening a URL, and clicking through it.
The *routing* that gets a task to a review slot is assembled from generic
primitives: a **workspace carries a tag** and a **status requires that tag**.
On top of that, the workflow's **`review:` block** tells the Review agent how
to boot the branch, and Shelbi's **review interface** lets you run the change
and accept or reject it. This page wires them together end to end. For the model
underneath, see the reference:
[Review workspaces](/docs/concepts/review-workspaces).
This page assumes you already have a project with a workspace pool and can
promote a task through the board. The [first task](/docs/guides/getting-started/first-task)
and [multi-workspace](/docs/guides/getting-started/multi-workspace) pages get
you there. A review workspace is an ordinary slot with a tag, not a new kind of
thing.
## Set up and use the review flow
Add a `review` tag to a slot in your project YAML
(`~/.shelbi/projects/.yaml` in [global mode](/docs/concepts/config-modes),
`/.shelbi/project.yaml` in-repo). Tag one workspace directly, or tag the
machine so every slot on it inherits the tag:
```yaml
workspaces:
- { name: alpha, machine: hub, runner: claude }
- { name: bravo, machine: hub, runner: claude }
- { name: review-0, machine: hub, runner: claude, tags: [review], slot: 3000 }
```
`tags` accepts a bare string as shorthand (`tags: review`). The `slot: 3000`
sets the numeric slot the `review:` recipe below resolves `$PORT` to — so the
dev server binds `:3000`. Leave `slot:` out and it defaults to the slot's
zero-based index among the machine's workspaces, which is why an explicit,
port-shaped value is worth setting on a review slot.
Each review slot holds one running server bound to its port, so give a second
review slot a different `slot:` value (`3010`) to avoid a collision. One is
enough for a first pass.
In your [workflow file](/docs/configuration/workflow), require the `review` tag
on the review status so tasks route to the tagged slot, then add a
[`review:`](/docs/configuration/workflow#review) block telling the Review agent
how to boot the branch:
```yaml
statuses:
- { id: in-progress, owner: agent, agent: developer }
- { id: review, owner: user, agent: review, tags: [review] }
- { id: done, owner: user }
transitions:
- from: review, to: done, actions: [merge, delete_branch]
review:
setup: npm install --no-audit --no-fund
serve: npm run dev -- -p $PORT
ready: curl -sf http://localhost:$PORT
url: http://localhost:$PORT
```
Shelbi resolves `$PORT` (equivalently `$SLOT`) to the review slot's port —
`3000` here — and injects the recipe into the Review agent's prompt. The agent
runs `setup`, launches `serve`, polls `ready` until the server answers, and
hands back the `url`. Swap the `npm` lines for whatever your project needs —
`cargo run`, a `Makefile` target, a `Procfile web:` line. For a monorepo, add
`workdir: site` (or `app`, `docs`) so each workflow serves its own
subdirectory on the slot's port.
The `review → done` transition is the accept edge: it merges the branch and
deletes it. Omit the `review:` block entirely and the Review agent falls back
to a **diff-only** review, booting nothing.
```bash
shelbi reload
```
The tagged slot does **not** show up under **Workspaces** — a review slot's
capacity surfaces through the review sections instead, and only once a task is
routed to it.
Run a task the way you normally would: promote a card and let a dev workspace
pick it up, or dispatch one through the orchestrator. When the dev workspace
finishes it writes its review-ready marker; the hub poller rebases the branch
onto the base branch and moves the task into the **Review** status.
Because that status requires `tags: [review]`, the orchestrator routes the task
onto a free `review`-tagged slot (preferring the one it ran on) and the Review
agent boots the server from the `review:` recipe. If every matching slot is
busy the task waits under **Queued for Review** until one frees — nothing is
preempted.
The finishing dev workspace closes its own session and returns to `idle`. Its
work is now waiting to be *run*, not edited.
The task appears in the sidebar under **Ready for Review** (`✓`) with a
`machine:workspace` badge showing the slot it's loaded on. Select it (or press
`Enter` on its row) to open the **review interface**: a two-column layout with
the **review panel** on the left (its own navigation, with a back button at the
top that returns you to the dashboard) and the swappable review content on the
right.
From the review panel you can:
- **🤓 Chat with Reviewer** — talk to the Review agent (the default view).
- **🔀 View Diff** — open your system diff tool over the review branch's
changes in the main pane. This uses git's configured diff tool
(`diff.tool`, or `diff.guitool` for a GUI tool); if none is configured the
panel shows a short error instead of launching.
- **✍️ Edit in ``** — open the review worktree in your
[configured editor](/docs/configuration/global#configyaml).
- **🌐 Open Browser** — open the served `url` in your system browser. This
action only appears when the workflow declares a review URL.
The built-in `review` agent's charter is narrow: **load and serve, don't
code.** It won't modify the branch or move the card. Point `agent:` at an agent
you've authored to change what runs there.
You own the decision, and you make it from the review panel's **Actions**:
- **✅ Approve** — moves the card one column forward (`review → done`), firing
that edge's `actions`: per the workflow above, it merges the branch and
deletes it. The interface tears down and focus returns to the dashboard.
- **❌ Reject** — opens a type-the-reason dialog. On submit, the reason is
appended to the task body and the card bounces back to the ready status for
another pass. An empty reason can't submit.
Either decision frees the review slot for the next task routed for review, if
any.
## Next
Routing a task to a review workspace is the *human* path: the whole point is
human eyes on the running app. Keeping `merge` on the *accept* edge and off the
*enter*-review edge means the orchestrator holds a review task for you rather
than auto-merging it. When you're ready to hand the orchestrator the cases it
*can* judge on its own,
[enable Zen Mode](/docs/guides/getting-started/enable-zen-mode) to auto-promote
backlog and auto-merge finished branches that clear a confidence bar you define.
## See also
- [Review workspaces](/docs/concepts/review-workspaces) — the full model: tags,
status-tag routing, the `review:` serve recipe, the review sidebar, and the
review interface.
- [Workflow config](/docs/configuration/workflow) — status `tags`, the
`review:` block, and the transition command fields.
- [Project config](/docs/configuration/project) — machine and workspace `tags`,
and workspace `slot`.
- [Global config](/docs/configuration/global) — the hub-wide `editor` the
"Edit in ``" view launches.
[Source](https://shelbi.dev/docs/guides/getting-started/review-workspaces)
---
# Zen Mode
The orchestrator triages backlog and lands work through an exact-provenance high-confidence bar.
By default the orchestrator is the scheduler and you are the
priority-setter + reviewer. Zen Mode flips it: the orchestrator takes
the lead. It triages mechanically-eligible backlog into todo on its
own and lands finished work past a pre-defined confidence bar.
The mental model is **lead vs. scheduler**. Zen on, the agent makes
calls; you redirect when you disagree. Zen off (the default), the
agent waits to be told. Same prompt, same workspaces, same board. The
only thing that changes is who initiates.
## Turning it on
Three ways to flip the switch:
- **CLI**: `shelbi zen on`, `shelbi zen off`, `shelbi zen pause`.
- **TUI**: `Alt+Z` toggles between On and Off from any sidebar focus.
- **Status check**: `shelbi zen status` prints the current mode,
configured local checks, resolved danger paths, the last crash
timestamp (if any), and the count of in-flight Zen tasks.
When Zen is **on**, the sidebar shows a green **ZEN ON** pill anchored
to the bottom of the workspaces column. That pill is the at-a-glance
"the agent is in lead mode" signal. If it's not there, Zen is off or
paused.
### First-run hotkey probe
Some terminals (notably macOS Terminal.app, where Option/Alt types
accented characters) swallow `Alt+Z`. The first time you launch the
sidebar, Shelbi runs a tiny probe: it shows a centered overlay and
waits a few seconds for `Alt+Z`. If it arrives, the binding is saved
and you never see the prompt again. If it doesn't, a chooser pops up
with three fallback chords:
- `Ctrl+\`
- `Ctrl+G`
- `Ctrl+Shift+Z`
…plus a "skip" option that leaves the toggle unbound (use the CLI
instead). Your pick is persisted to `~/.shelbi/shelbi.yaml`.
## Turning it off
Three levels of "stop":
- **`shelbi zen off`** (or `Alt+Z` toggle): hard off. No new
auto-promotions, and the orchestrator stops initiating the merge
bar on any `merge`-action transition. Anything already mid-flight
in the conditions flow runs to completion on its current trajectory.
- **`shelbi zen pause`**: softer. No new auto-promotions, but the
merge bar keeps applying to in-flight tasks that reach a
`merge`-action transition. Use this when you want to triage
incoming work yourself without losing validation already in motion.
- **Per-task pull-back**: `shelbi task move --to backlog`
from a handoff status pulls a specific card out of Zen's hands
without changing the global mode.
### Crash recovery
If the orchestrator pane dies mid-session (kill signal, SIGHUP,
machine power loss), Zen Mode auto-disables on the next orchestrator
start. The wrapper script around the orchestrator runner writes a
heartbeat every 60s; if the previous run ended without a graceful
exit and Zen was on, the next start flips it off, writes a
`zen=off reason=crash-recovery` event line, and prints a stderr
warning so you see it immediately.
Re-enable manually with `shelbi zen on` once you've reviewed any
in-flight work. The orchestrator's bootstrap also scans recent event
lines for this pattern and calls it out in its first reply.
## The `zenmode.md` file
This is the power-user invitation. Both halves of Zen policy, the
auto-promote judgment categories and the
[merge conditions](#the-high-confidence-bar), live in one user-owned
file per project:
```text
~/.shelbi/projects//zenmode.md
```
(For an in-repo project it resolves to `/.shelbi/zenmode.md`
instead.)
`zenmode.md` is the **live source of truth for what Zen Mode means in
that project.** The orchestrator reads it in full at bootstrap and
re-reads it periodically, prompted by the heartbeat, to keep its
behavior aligned with your intent. The Rust side never inspects the
prose. The file exists purely for the orchestrator, so you can edit it
freely to tune Zen, and your changes take effect without a rebuild.
`shelbi reload` [preserves your edits](/docs/cli/reload) to
`zenmode.md`, leaving your prose intact.
### The first line is the heartbeat summary
The **first line of `zenmode.md` is the one-line summary the heartbeat
echoes back** to the orchestrator. While Zen is on, every few ticks a
`zen=on` [heartbeat](/docs/concepts/events-log#heartbeats) carries that
first line verbatim as a live reminder of what Zen means here, read
fresh each time so an edit shows up immediately. Roughly once an hour
the heartbeat instead injects a fuller "re-read `zenmode.md` now"
instruction, so the orchestrator refreshes the full policy before its
next Zen decision rather than acting on a faded memory.
Because that first line is what gets re-injected, keep it a single
accurate line: **if you change what Zen means below, change the first
line too.** It is the fastest lever on the orchestrator's live
behavior.
### The file shape
A `zenmode.md` is the summary line, then the policy sections the
orchestrator applies. This is the exact file `shelbi` writes for a new
project:
{/* Keep this block verbatim-in-sync with
crates/shelbi-state/src/default_zenmode.md.template */}
```markdown
Zen: auto-promote eligible backlog, review each handoff yourself (diff vs the task's acceptance criteria + scope), and run the exact-provenance merge flow without asking.
# Zen Mode policy
This file is the source of truth for what Zen Mode means in **this** project.
The orchestrator reads it (and re-reads it periodically, prompted by the
heartbeat) to keep its behavior aligned with your intent. The Rust side never
inspects the prose below, so edit it freely to tune Zen for your project.
The **first line of this file is the one-line summary** the heartbeat echoes
back to the orchestrator every few ticks. Keep it a single line and keep it
accurate: if you change what Zen means here, change that line too, because that
is what gets re-injected as a live reminder.
When Zen is on the orchestrator acts as lead rather than scheduler. It may:
1. Auto-merge work that passes the merge-conditions flow below.
2. Auto-promote `backlog`-category tasks into `ready` when they fit at least
one of the judgment categories below.
When Zen is `paused` it stops starting new auto-promotions but keeps applying
the merge-conditions flow to in-flight tasks that reach `handoff`. When Zen is
`off` the orchestrator is a plain scheduler: it dispatches what you promote and
leaves handoffs for you.
## Auto-promote judgment categories
The mechanical eligibility scan (`shelbi zen scan`) hands the orchestrator
`backlog`-category task ids that are safe from a state-machine standpoint: not
blocked, not opt-out, no file overlap with anything in flight. From that list,
**only auto-promote** a candidate if **at least one** of these is true:
1. **It's the kind of work the user generally trusts you with.** Look at the
`done`-category column. Does the user routinely accept tasks of this shape
without changes? (Examples: docs typo fixes, dependency bumps, content
sweeps that match a recently-stated convention.) If their done-history shows
pattern acceptance, this is in scope.
2. **It's part of fixing an issue the user recently raised.** Did the user
mention this bug, feature, or concern in conversation in the last few turns?
Tasks that respond to something the user explicitly asked for are in scope.
3. **It's part of a larger body of work the user explicitly kicked off.** If
the user filed a batch of related tasks (e.g. 13 vs-pages, a multi-step
refactor), the remaining items in that batch are in scope.
If a candidate fits none of these, **leave it in `backlog`** and surface it in
your next user-facing reply: *"I considered promoting `` but wasn't sure
if it fits your intent, want me to?"* Also emit
`reason=orchestrator:zen-decline reason-text=` on the task so the
activity feed shows what you considered.
For ones you do promote, run `shelbi task move --to
--reason "orchestrator:zen-promote category="` (`` is the
literal `ready`-category status name from the active workflow, `todo` in the
default) and then dispatch per the normal auto-dispatch contract.
## Merge conditions
**First, the review-workspace gate.** If the project declares review
workspaces, a task reaching `handoff` is destined for a **human** to inspect on
a running server, that's the entire point of the review workspace. So do
**not** run the merge-conditions flow on it, and do **not** merge it: leave it
in the review status for the human to load from the sidebar and resolve (there
is no `shelbi review` command for you to run). The human accepts by moving it
to `done` (which is when it merges per existing rules) or bounces it back to
`ready`. Zen never auto-merges a review-routed task out from under the human.
Only when the project has **no** review workspace does `handoff` mean "ready to
auto-merge", the case the rest of this section covers.
When a task enters `handoff` and Zen is on (and the project has no review
workspaces), run `shelbi zen probe ` to get a JSON report covering
local checks, merge conflicts, diff size, and danger-path matches. A passing
mechanical probe is necessary but not sufficient; your own review of the diff
(step 5) is what checks the branch against the task's intent. Apply these
conditions to the report:
1. **All `local_checks` must have `exit_code == 0`.** If any failed, leave in
`handoff` and emit `reason=zen:failed-checks` with the failing command +
output tail.
2. **`merge_conflict.conflicts` must be `false`.** Otherwise emit
`reason=zen:merge-conflict` with the files.
3. **`diff_size.files` <= 30 AND `diff_size.lines_added + lines_removed` <=
2000.** Otherwise emit `reason=zen:diff-too-large` with the stats.
4. **`danger_paths.matched` must be empty.** Otherwise emit
`reason=zen:danger-path` with the paths.
If all four pre-PR probe conditions pass, review the diff against the task
yourself before opening a PR; only if your review passes do the
pr-create/ci-watch/pr-merge steps run:
5. **Review the diff against the task.** A green mechanical probe proves the
branch builds and does not conflict; it does not prove the branch does what
the task asked. Read the task's acceptance criteria and the branch diff
(`git diff ...`) yourself and confirm:
- the diff satisfies every acceptance criterion the task states;
- the diff stays within the task's declared scope, with no unrelated files
and, in particular, no large deletions or reverts of code the task never
mentions (the signature of a branch cut from a stale base);
- the branch carries real new commits ahead of `` and its
content is not already present in the base under a different commit (a
branch whose diff is dominated by undoing recently-merged work, or whose
change is already merged under another SHA, must never be merged).
You are the reviewer here, not the builder: a workspace developer agent
wrote this code, so reviewing it yourself is not grading your own work.
Read the criteria and diff with fresh eyes and don't wave a branch through
just because you dispatched it. If it fails any check, bounce the task to
`ready` with concrete findings and emit `reason=zen:review-reject` with a
one-line summary. For a project that wants a genuinely independent,
de-biased reviewer instead of this self-review, wire a `qa`/`adversarial`
gate into the workflow (a first-class status) rather than reviewing here.
6. Freeze the probe's `repository`, `repository_id`, `base_branch`, `base_sha`,
`integration_sha`, and `head_sha`. Run `shelbi zen pr-create --match-repository --match-repository-id --match-base-branch --match-base-commit --match-integration-commit --match-head-commit `
using those exact values, and also pass `--match-published-head-commit `
from the probe's `published_head_sha` field. When the probe rebased the branch
onto a moved base, `head_sha` is a local-only commit while `published_head_sha`
is the pre-rebase tip the remote branch still points at; that flag lets
pr-create replace the tip it reviewed instead of refusing it as a concurrent
update. Capture the PR number it prints. Exit code 75 means the push landed
but GitHub's PR view had not caught up yet: re-run the exact same command (it
is idempotent). Exit code 1 is a real mismatch: leave the task in `handoff`
and re-probe.
7. Run `shelbi zen ci-watch --match-repository --match-repository-id --match-base-branch --match-base-commit --match-integration-commit --match-head-commit --timeout 15m`
using that same probe identity. Stdout is `green`,
`red::`, or `timeout`. The condition: must be `green`.
Any identity movement fails closed; never recompute or substitute a probe
field after CI begins.
8. If green, run `shelbi zen pr-merge --match-repository --match-repository-id --match-base-branch --match-base-commit --match-integration-commit --match-head-commit `
with that same identity. The command verifies the candidate has sole parent
`base_sha` and the exact tree of `head_sha`, then atomically advances only
`refs/heads/` from `base_sha` to `integration_sha` with Git's
compare-and-swap lease. If branch protection, rulesets, a merge queue, a
fork, permissions, or a non-squash strategy prevent that exact-ref path,
leave the task in `handoff` and follow the command's human-review action.
Only after a candidate SHA result, move the task into `done` and emit
`reason=orchestrator:zen-merge` with the merge SHA.
9. If red or timeout: leave in `handoff`; emit `reason=zen:failed-checks` (red)
or `reason=zen:ci-timeout`.
## Customization
Edit this file to tune Zen Mode per project. The prose is the source of truth
for both auto-promote and merge policy, and for the one-line summary at the
top.
- **Summary line** — rewrite the first line to match whatever policy you set
below. It is what the heartbeat re-injects, so it is the fastest lever on the
orchestrator's live behavior.
- **Auto-promote judgment categories** — add ("anything tagged `automation:`
is always in scope"), tighten ("only auto-promote if the user has accepted
>=3 tasks of this shape without changes"), or replace the list entirely.
- **Merge conditions** — raise/lower the diff-size thresholds, accept partial
CI red ("if only the integration-test job is red, treat as green"), add new
pre-merge probes ("before merging anything touching `migrations/`, also
require a passing `shelbi db dry-run`"), or change which conditions are
strict vs warnings.
```
The default ships with these categories and conditions filled in. Edit
the sections in place to reshape them.
### Pre-merge review is the orchestrator's own
Step 5 of the merge conditions is a **self-review**: the orchestrator reads
the task's acceptance criteria and the branch diff itself and confirms the
branch does what the task asked before opening a PR. It does not spawn a
separate evaluator subagent for this. The "don't let the builder grade its
own work" worry does not apply, because the orchestrator is not the builder:
a workspace [developer agent](/docs/concepts/agents) wrote the code, and the
orchestrator already owns the review column under Zen and holds the task's
full context. A green mechanical probe proves the branch builds and does not
conflict; the self-review is what proves it satisfies the task's intent and
stays in scope. On failure the orchestrator bounces the task back to `ready`
with findings (`reason=zen:review-reject`).
If you want a genuinely **independent, de-biased reviewer** rather than the
orchestrator reviewing its own dispatch, that is a first-class
[`qa`/`adversarial` agent](/docs/guides/doing-more-with-agents/adversarial-review-agent)
wired into the workflow as a **dedicated status**, not an ad-hoc subagent.
Give the status `owner: agent` and `agent: qa` (or `adversarial-review`), with
a pass edge forward and a plain bounce edge back to `in-progress`:
```yaml
# workflows/default.yaml
statuses:
- { id: in-progress, owner: agent, agent: developer }
- { id: qa-review, owner: agent, agent: qa } # the gate
- { id: review, owner: user, agent: orchestrator }
transitions:
- { from: in-progress, to: qa-review, actions: [push_branch, open_pr] }
- { from: qa-review, to: review, actions: [] } # pass: forward
- { from: qa-review, to: in-progress, actions: [] } # bounce: back
```
The full walkthrough (declaring the status in
[`statuses.yaml`](/docs/configuration/statuses), referencing it from the
[workflow](/docs/configuration/workflow), and why the bounce edge must be
listed) is in
[Add it to a workflow](/docs/guides/doing-more-with-agents/add-to-workflow).
### Relationship to the orchestrator prompt
The judgment categories and merge conditions live in `zenmode.md`, not in
the orchestrator's prompt. The `## Zen Mode` section of
`agents/orchestrator/instructions.md` tells the orchestrator to treat
`zenmode.md` as the source of truth and to re-read it on the heartbeat cues
above. So
**edit `zenmode.md` to tune Zen policy, not the prompt.** The
[orchestrator instructions](/docs/concepts/orchestrator#customizing-the-prompt-per-project)
remain yours to customize for how the orchestrator *reacts* to events,
but the Zen policy itself belongs in `zenmode.md`.
A starter override that tightens Zen's judgment, edited directly in
`zenmode.md`:
```markdown
Zen: auto-merge docs/content and automation-tagged tasks; hold everything else for me.
## Auto-promote judgment categories
Promote a candidate only if **at least one** is true:
1. **Tagged `automation:` in the title.** Always in scope: these are
bot-filed PR-bumps and lint sweeps we have years of acceptance
history on.
2. **Touches only `docs/**` or `site/content/**`.** Content edits are
reversible and never reach prod.
3. **The user has accepted ≥3 tasks of the same shape (same
verb-prefix, same target directory) without changes in the last
week.** Track this against the `done` column.
## Merge conditions
(Inherit from the default; see the bundled template.)
```
Note the first line changed to match the tighter policy. That new
summary is what the heartbeat now re-injects.
Other places to tune, all in the same file:
- **Raise/lower the diff thresholds**: change `files ≤ 30` to
whatever fits the project's typical PR size.
- **Accept partial CI red**: "if only the integration-test job is
red, treat as green" is a common project-specific carve-out.
- **Add pre-merge probes**: "before merging anything touching
`migrations/`, also require a passing `shelbi db dry-run`."
- **Replace categories entirely** with a project-specific
taxonomy.
The Rust side never parses any of this, so the prose is the source of
truth. That is the whole design: Zen's policy is one markdown file you
own. For the CLI that flips Zen on and off, see
[`shelbi zen`](/docs/cli/zen).
## What Zen does: two paths
Zen Mode does exactly two things the default scheduler doesn't:
1. **Auto-merge**: when a task is queued for a workflow transition
whose `actions:` include `merge`, Zen runs the high-confidence bar below.
If everything passes, it lands the reviewed head on the exact probed base
and moves the task to the transition's `to` status.
2. **Auto-promote**: when a workspace frees up, Zen scans the backlog
for tasks that are *mechanically eligible* (see
[`shelbi zen scan`](/docs/cli/zen#scan)) and then applies the
**judgment categories** from the orchestrator's prompt to decide
which ones to promote into todo.
The mechanical layer is in Rust. The judgment layer is in the
orchestrator's prompt, which means **you own it** and can tune it
per project. That's the most important point on this page.
## The high-confidence bar
The bar fires on **any [workflow](/docs/guides/getting-started/workflows#transitions)
transition whose `actions:` list includes `merge`**, not on the
`Review` column per se. The canonical default workflow happens to put
`merge` on the `Review → Done` edge, so in practice a task that hands off
to `review` trips the bar. But a
trunk-based workflow that skips `Review` entirely and merges straight
from `InProgress → Done` runs through the *same* probe. Gating is
action-based, not status-pair-based. See [What fires Zen Mode's
high-confidence bar](/docs/guides/getting-started/workflows#what-fires-zen-modes-high-confidence-bar).
When such a transition is queued, the orchestrator runs
`shelbi zen probe ` (which threads the task's workflow
through `probe_in_workflow` so per-workflow overrides take effect; see
below) to get a single JSON report covering every dimension below.
Then it applies the conditions in order. Any failure leaves the task
where it was, with a tagged reason event so the activity feed shows
what happened.
```text
merge-transition -> probe -> local checks -> merge conflict -> diff size
-> danger paths -> exact-provenance PR flow -> landing
```
The conditions, in order:
1. **All `local_checks` pass** (exit 0). These are the commands you
listed under `zen.checks.local` in the project YAML, or under the
workflow's own `zen.checks` block when one is set (see
[per-workflow overrides](#per-workflow-overrides)).
2. **No merge conflicts** with the transition's target branch (the
workflow's resolved `base_branch`, or a per-transition `target:` if
one is set).
3. **Diff size within bounds**: by default `files ≤ 30` and
`lines_added + lines_removed ≤ 2000`. Tunable in the prompt.
4. **No danger-path matches**: anything matching the resolved
danger-paths list (built-ins + detected shape + project
`zen.danger_paths.extend` + the active workflow's `zen.danger_paths`
override, if any) bails out.
5. **PR opens cleanly** via
`shelbi zen pr-create --match-repository --match-repository-id --match-base-branch --match-base-commit --match-integration-commit --match-head-commit `. All six values are copied directly from the probe report. The PR is returned
only after its remote identity matches the exact repository, resolved
workflow base name and commit, task branch, reviewed head, and prebuilt
integration commit. The PR head is the integration commit so CI runs on
the exact object eligible to land.
6. **CI is green**:
`shelbi zen ci-watch --match-repository --match-repository-id --match-base-branch --match-base-commit --match-integration-commit --match-head-commit --task ` polls
the PR number returned by that creation step, using the same complete probe
identity. One GraphQL response per poll binds the repository, base, PR head, latest commit,
required contexts, their results, and GitHub's merge state. A blocked state
prevents a not-yet-reported required context from being mistaken for no
check, while an optional failed check does not override passing required
checks. The
`--task` flag resolves the timeout against the task's workflow
(`zen.ci_timeout` override, then project default of `15m`); plain
`--timeout ` still overrides explicitly. Anything red or
a timeout bails. Each poll compares against the frozen probe identity
and grades only that atomic snapshot, so persistent movement and a brief
A-to-B-to-A change both bail instead of authorizing a merge. A snapshot
that would require pagination also fails closed.
7. **Landing** via
`shelbi zen pr-merge --match-repository --match-repository-id --match-base-branch --match-base-commit --match-integration-commit --match-head-commit `, passing the same probe identity again. The candidate has sole parent `base_sha` and exactly the tree of `head_sha`. Shelbi then updates only `refs/heads/` from `base_sha` to `integration_sha` with `--force-with-lease`. A same-head PR retarget cannot redirect that ref update. The lease is atomic at the remote Git ref, not across GitHub PR metadata. Protected branches, required-PR rules, forks, merge queues, and non-squash strategies fail closed with a human-review instruction.
The probe's `repository`, `repository_id`, `base_branch`, `base_sha`,
`integration_sha`, and `head_sha` form the contract for the whole sequence. They are not recomputed
between steps: PR creation publishes into that repository and base, CI grades
that identity continuously, and the landing boundary verifies it again. A
workflow, origin, base, or head change requires a fresh probe and a new flow.
Each step uses a single-purpose CLI primitive. The Rust side does
mechanical I/O; the orchestrator's prompt holds the policy. That
separation is what lets you raise/lower the bar by editing prose
instead of recompiling.
Example project-level config:
```yaml
# .yaml
zen:
checks:
local:
- 'cargo test --workspace'
- 'cargo clippy --workspace --all-targets -- -D warnings'
ci_timeout: 15m
danger_paths:
extend:
- 'site/public/install.sh'
- 'crates/shelbi-state/src/migrations/**'
```
## Per-workflow overrides
Each [workflow](/docs/guides/getting-started/workflows) may carry its own `zen:`
block that overrides any subset of the three project-level Zen knobs:
`checks`, `ci_timeout`, `danger_paths`. Anything you don't set falls
back to the project default, so a workflow can swap *just* its
checks without restating the rest.
```yaml
# ~/.shelbi/projects//workflows/research.yaml
name: research
description: Long-running investigations — no code-style checks, longer CI.
# References statuses by id from statuses.yaml (drafting is declared there
# with name: Drafting, category: active).
statuses:
- { id: drafting, owner: agent, agent: developer }
- { id: review, owner: user }
- { id: done, owner: user }
zen:
checks:
local:
- 'pytest -k research'
ci_timeout: 3600 # seconds — 1h, vs the project's 15m
danger_paths:
override:
- 'fixtures/**'
```
Why this exists: a `research:` workflow doesn't want the project's
`cargo clippy -D warnings` gate, and an integration-test-heavy
workflow may legitimately need an hour of CI. Pinning those to the
*workflow* rather than the project lets one project's `default`
workflow stay strict while another workflow in the same project runs
on relaxed rules.
Resolution rules:
- **`checks`**: when set on the workflow, the workflow's list
replaces the project's list outright. Per-task `checks_only` /
`checks_additional` still apply on top (see
[per-task overrides](#per-task-overrides)).
- **`ci_timeout`**: when set on the workflow, the workflow's value
wins. `shelbi zen ci-watch --task `
resolves through this
override; without `--task`, the project default is used.
- **`danger_paths`**: uses the same `extend:` vs `override:` shape as
the project block. `extend:` adds to the project's resolved list
(built-ins + detected shapes + project `extend`). `override:`
replaces the whole resolved list for tasks in this workflow.
An empty block (`zen: {}`) parses but overrides nothing, semantically
identical to omitting the block.
## The judgment layer
The high-confidence bar decides whether a finished branch is safe to
land. The **judgment layer** decides which backlog items the
orchestrator should promote on its own in the first place.
The Rust side (`shelbi zen scan`) gives the orchestrator a list of
backlog ids that are *mechanically* safe: not blocked on
dependencies, not explicitly opted out, no file overlap with anything
currently in flight. That list is intentionally generous. It doesn't
know what the user wants.
The orchestrator's prompt then applies three judgment categories
that ship as the default. A candidate gets promoted only if **at
least one** is true:
1. **It's the kind of work the user generally trusts you with.**
Pattern from the `done` column: does the user routinely accept
tasks of this shape without changes?
2. **It's part of fixing an issue the user recently raised.** The
user mentioned this bug or feature in the last few turns.
3. **It's part of a larger body of work the user explicitly kicked
off.** A multi-step refactor, a sweep across a dozen pages: the
remaining items in that batch are in scope.
If nothing fits, the candidate stays in backlog and the orchestrator
calls it out in its next reply ("I considered promoting `` but
wasn't sure. Want me to?"). It also emits
`reason=orchestrator:zen-decline reason-text=` so the
activity feed shows what was considered and why.
## Per-status automation is declarative
Zen's *judgment* (which backlog items are in scope, where the confidence
bar sits) is prompt prose, covered below. But the more basic question of
**which statuses the orchestrator is allowed to act on, and which agent it
uses**, is not prompt prose at all. It's the
[`owner` and `agent` fields](/docs/guides/getting-started/workflows#owners-and-agents) on
each workflow status.
Two examples:
- **"Auto-merge but don't auto-promote."** Make the merge-bearing handoff
status `owner: agent`, but leave `Backlog` as `owner: user`. The
orchestrator lands finished work while your triage queue stays yours. It's
two `owner:` values.
- **"Review with QA before a human signs off."** Give the `Review` status
`owner: agent, agent: qa`. The [`qa` agent](/docs/concepts/agents) runs
the review pass automatically; a downstream `owner: user` status holds
the final accept. Which agent reviews is data, not a prompt instruction.
Flipping Zen on/off still gates whether the orchestrator *exercises* this
autonomy at all. But once Zen is on, the workflow YAML (not the prompt)
decides per status whether the orchestrator acts and which agent it loads.
The prompt holds only the genuinely judgment-shaped policy: scope
and the confidence bar. See
[owners and agents](/docs/guides/getting-started/workflows#owners-and-agents) for the
field semantics and the four validation rules.
## Per-task overrides
Sometimes a single task needs different treatment than the project
default: kept on the manual-review path because it's sensitive, or
given an extra check the rest of the project doesn't need. Put a `zen:`
block in the task's frontmatter:
```yaml
---
id: refactor-payment-flow
title: Refactor payment processor adapter
column: backlog
priority: 0
zen:
enabled: false # opt this task out of Zen entirely
---
```
```yaml
---
id: docs-rewrite-tutorial
title: Rewrite the getting-started tutorial
column: backlog
priority: 0
zen:
checks_additional: # extend the project checks for this task
- 'cargo test --package shelbi-docs'
---
```
```yaml
---
id: ui-restyle-sidebar
title: Restyle the sidebar pill
column: backlog
priority: 0
zen:
checks_only: # replace project checks for this task
- 'cd site && npm run lint && npm run build'
---
```
Resolution rules:
- `enabled: false` keeps the task on the manual-review path even
with Zen on; `enabled: true` opts a task in even when Zen is off
(rare but supported).
- `checks_only` takes precedence over `checks_additional`. If both
are set, `checks_only` wins and `checks_additional` is ignored.
- `checks_additional` extends whichever list the workflow layer
resolved to: workflow `zen.checks.local` if set, else project
`zen.checks.local`.
- Absent both, the task inherits that resolved list verbatim. Full
precedence: per-task `checks_only` > per-task `checks_additional` >
per-workflow `zen.checks.local` > project `zen.checks.local`.
## Configuration reference
Zen Mode reads config from two places: the project YAML (project-wide
defaults) and per-workflow YAMLs (workflow-scoped overrides, covered
in [Per-workflow overrides](#per-workflow-overrides) above).
The full project-level schema:
```yaml
zen:
# Shell commands run from the worktree root before the merge bar.
# Each must exit 0. Default: empty.
checks:
local:
- 'cargo test --workspace'
- 'cargo clippy --workspace --all-targets'
# How long to wait for required GitHub checks. Accepts seconds as
# a bare integer, or a string like `30s`, `5m`, `2h`, `1d`.
# Default: 15m.
ci_timeout: 15m
# Glob patterns considered too sensitive to auto-merge. Choose
# one of `extend:` (keep built-ins + detected-shape paths and add
# yours) or `override:` (replace everything with your list).
# Default: an empty `extend:` — built-ins + detected shape only.
danger_paths:
extend:
- 'site/public/install.sh'
- 'crates/shelbi-state/src/migrations/**'
# or to replace everything Shelbi knows about:
# override:
# - 'config/**'
# - 'deploy/**'
```
The built-in danger paths Shelbi always includes (in `extend` mode):
```text
.github/workflows/** scripts/install.sh *.yaml *.yml
LICENSE package-lock.json Cargo.lock
```
On top of that, Shelbi detects the project shape from sentinel files
in your repo root and adds shape-specific paths:
| Shape | Detected from | Adds |
| --- | --- | --- |
| cargo workspace | `Cargo.toml` with `[workspace]` | `Cargo.toml`, `Cargo.lock`, `rust-toolchain.toml`, `.cargo/config.toml` |
| node / next.js | `package.json` | `package.json`, `package-lock.json`, `next.config.*`, `vercel.json`, `.npmrc` |
| github | `.github/` | `.github/CODEOWNERS`, `.github/dependabot.yml` |
| docker | `Dockerfile` or `compose.yaml` | `Dockerfile`, `compose.yaml` |
| shelbi | `shelbi.yaml` or `.shelbi/` | `.shelbi/**`, `shelbi.yaml` |
Run `shelbi zen status` to see the resolved list for your project
(it labels the detected shapes inline so you know which contributions
came from where). When any workflow declares its own `zen:` block, the
status output also lists each one and which dimensions it overrides,
with the resolved per-workflow values printed underneath.
## The activity feed
Zen-driven events get distinct visual treatment in the sidebar's
activity feed: a small **ZEN** avatar badge in the same column the
per-workspace avatars use, and a subtle dark-tinted row background so
machine-driven actions are visually separable from user actions in
the same stream.
The reason strings the feed recognizes (each renders with its own
specific phrasing):
| Reason | Meaning |
| --- | --- |
| `orchestrator:zen-promote category=` | Promoted a backlog task to todo under judgment category ``. |
| `orchestrator:zen-decline reason-text=` | Considered a candidate, decided not to promote. |
| `orchestrator:zen-merge sha=` | Auto-merged a reviewed task to `main`. |
| `zen:failed-checks cmd=<…> exit=<…>` | Local check failed; task stays in review. |
| `zen:diff-too-large files=<…> lines=<…>` | Diff exceeded the size threshold. |
| `zen:danger-path paths=<…>` | One or more danger-path globs matched. |
| `zen:merge-conflict files=<…>` | Branch wouldn't merge cleanly. |
| `zen:ci-timeout duration=<…>` | CI didn't settle within `ci_timeout`. |
These tags also surface in `~/.shelbi/events.log`. Grep there for a
full audit trail across all projects.
The orchestrator also wakes on the project's periodic `heartbeat` line
(see [the events log](/docs/concepts/events-log#heartbeats)). For Zen
Mode that's the recurring nudge to check in-flight reviews and stuck
auto-promotions when nothing else has fired. The merge bar can spend
minutes waiting on CI, and heartbeats keep the orchestrator from
sleeping through the settle. The cadence comes from the `heartbeat`
key in `project.yaml` (default `3m`).
## See also
- [Project config](/docs/configuration/project#zen) — the `zen:` block's
field reference (`checks.local`, `ci_timeout`, `danger_paths`).
- [`shelbi zen`](/docs/cli/zen) — CLI reference for every Zen
subcommand.
- [Workflows](/docs/guides/getting-started/workflows) — how a workflow's
`transitions:` declares which edges fire the merge bar, and where
the per-workflow `zen:` override block lives.
- [Orchestrator](/docs/concepts/orchestrator) — how the
underlying scheduler is wired and where the prompt template lives.
- [The events log](/docs/concepts/events-log) — the canonical
format of the `mode=zen` and `orchestrator:zen-*` lines Zen emits.
- [Workflows](/docs/guides/getting-started/workflows#lifecycle-who-moves-a-task-between-the-default-statuses)
— what `Review` and `Done` mean in the default flow Zen accelerates,
and how custom workflows declare their own `handoff` and `done`
statuses.
[Source](https://shelbi.dev/docs/concepts/zen-mode)
---
# Enable Zen Mode
Add local checks, flip the toggle, and let the orchestrator auto-promote work and run the exact-provenance merge flow.
By default the orchestrator is the scheduler and you own both ends: you
decide what gets promoted out of backlog, and you accept what comes back
from review. **Zen Mode changes that.** The orchestrator promotes backlog items
it judges safe to run and lands exact-provenance PRs that clear a
project-defined confidence bar.
This page is the practical on-ramp: configure the bar, flip the
switch, and watch one task land end to end. The
[Zen Mode concept page](/docs/concepts/zen-mode) has the full
reference for everything you can tune.
## Set the confidence bar
Before turning Zen on, tell Shelbi what "safe to merge" means for
this project. The minimum useful config is a list of local checks
the orchestrator runs against every Zen-eligible review before it
opens a PR.
Open the project's shared YAML (`~/.shelbi/projects/.yaml` in
[global mode](/docs/concepts/config-modes) or `/.shelbi/project.yaml`
in in-repo mode) and add a `zen` block:
```yaml
# ~/.shelbi/projects/.yaml (global mode)
name: myapp
repo: /Users/you/Workspaces/myapp
# … the rest of the wizard's output …
zen:
checks:
local:
- 'cargo test --workspace'
- 'cargo clippy --workspace --all-targets -- -D warnings'
ci_timeout: 15m
danger_paths:
extend:
- 'crates/shelbi-state/src/migrations/**'
```
Three things to know about that block:
- **`checks.local`** — shell commands run from the worktree root.
Each must exit `0` for the branch to clear the bar. Anything red
leaves the task in `review` for you, with a tagged reason in the
activity feed.
- **`ci_timeout`** — how long to wait for required GitHub checks to
settle after the PR opens. Anything still pending when the deadline
hits bails out. Default `15m`.
- **`danger_paths.extend`** — globs the orchestrator treats as too
sensitive to auto-merge. Shelbi already ships built-in danger
paths and detects more from your project shape (cargo workspace,
next.js app, `.github/`, etc.); `extend` adds yours on top.
`override` replaces everything if you want the explicit list.
```bash
shelbi reload
```
```bash
shelbi zen status
```
```
mode: off
checks.local:
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
ci_timeout: 15m
danger_paths (resolved):
built-in: .github/workflows/**, scripts/install.sh, *.yaml, *.yml, LICENSE, package-lock.json, Cargo.lock
cargo workspace: Cargo.toml, Cargo.lock, rust-toolchain.toml, .cargo/config.toml
github: .github/CODEOWNERS, .github/dependabot.yml
extend: crates/shelbi-state/src/migrations/**
in-flight zen tasks: 0
```
The resolved danger-path list labels every contribution so you can see
which globs came from where. If something you expected to be guarded
isn't on the list, add it under `extend`.
## Flip the switch
Three ways:
- **CLI** — `shelbi zen on`
- **TUI** — `Alt+Z` from any sidebar focus
- **First-run hotkey probe** — the very first time you launch the
sidebar, Shelbi runs a tiny probe to confirm `Alt+Z` survives your
terminal (some, like macOS Terminal.app, swallow `Alt`). If the
chord doesn't arrive, a chooser offers `Ctrl+\`, `Ctrl+G`, or
`Ctrl+Shift+Z` as fallbacks. Pick one and it's persisted to
`~/.shelbi/shelbi.yaml`.
You'll know it's on by the **ZEN ON** pill anchored to the bottom of
the workspaces column in the sidebar. That pill is the at-a-glance "the
agent is in lead mode" signal. If it's not there, Zen is off or
paused.
```
— hub —
· alpha
· bravo
· charlie
— Ready for Review —
(empty)
[ ZEN ON ]
```
## Watch one task land end to end
Drop a small, clearly-in-scope task into the backlog:
```
you: tighten the rate-limit error copy to mention the retry window.
```
With Zen off, the card would sit in `backlog` waiting for you to
promote it. With Zen on, the orchestrator scans backlog when a
workspace frees up, runs the **judgment categories** from its prompt
against each mechanically-eligible card, and promotes the ones at
least one category applies to. You'll see a Zen-tagged line in the
activity feed:
```
ZEN promoted tighten-rate-limit-error-copy
(category: routine task shape you accept without changes)
```
The dispatch and run that follow look exactly like the manual loop:
the card moves to `in_progress`, the workspace badge flips to `⏵`, and
the agent works the prompt.
What changes is what happens when the workspace writes its review-ready
marker. Instead of stopping at `review` for your inspection, the
orchestrator runs the bar:
```text
review-marker -> probe -> local checks -> merge conflict -> diff size
-> danger paths -> exact-provenance pr-create -> ci-watch
-> pr-merge
```
Each step is a single-purpose CLI primitive (see
[`shelbi zen`](/docs/cli/zen)). The probe's repository, base, reviewed head,
and prebuilt integration commit are passed unchanged through every command.
CI runs on that candidate. Landing advances only the reviewed base ref from
the reviewed base commit to the candidate with Git's compare-and-swap lease,
so a same-head PR retarget cannot redirect the update. The atomic boundary is
the remote Git ref, not GitHub PR metadata. Repositories that require protected
PR or merge-queue mutations stay in review for a human merge.
```
2026-06-22T14:31:08+00:00 task=tighten-rate-limit-error-copy review -> done reason=orchestrator:zen-merge sha=abc1234
```
The card lands in **Done** after GitHub reports the merge:
Fail any of them (a flaky test, a too-large diff, a touch on a
danger path, a red CI) and the card stops in `review` for you, with
a tagged reason line that tells you why:
| Reason | Meaning |
| --- | --- |
| `zen:failed-checks cmd=… exit=…` | A `checks.local` command exited non-zero. |
| `zen:diff-too-large files=… lines=…` | Diff exceeded the size threshold. |
| `zen:danger-path paths=…` | One or more danger-path globs matched. |
| `zen:merge-conflict files=…` | Branch wouldn't merge cleanly. |
| `zen:ci-timeout duration=…` | CI didn't settle within `ci_timeout`. |
The card behaves like any other manual review from there. You review
the branch, fix what needs fixing, and either merge by hand or push
new commits that clear the bar on the next attempt.
## The activity feed
Zen-driven moves get distinct visual treatment in the sidebar's
activity feed: a small **ZEN** badge in the avatar column, and a
dark-tinted row background, so machine-initiated actions are
visually separable from anything you did. The most common reason
strings:
| Reason | When it fires |
| --- | --- |
| `orchestrator:zen-promote category=` | The orchestrator promoted a backlog card under judgment category ``. |
| `orchestrator:zen-decline reason-text=` | A mechanically-eligible card was considered and explicitly left in backlog — the orchestrator surfaces it in its next reply. |
| `orchestrator:zen-merge sha=` | A reviewed task cleared the bar and landed on `main`. |
`shelbi events tail --follow` shows the same lines in raw form if you
want to watch from a shell.
## Pause and stop
Three flavors of "stop", in increasing severity:
- **`shelbi zen pause`** — softer. No *new* auto-promotions, but
Zen tasks already mid-flight keep applying the bar. Use this when
you want to triage incoming work yourself for a bit without
aborting in-flight merges.
- **`shelbi zen off`** (or `Alt+Z` toggle) — hard off. New
auto-promotions stop and the orchestrator stops initiating the
merge bar on review handoffs. Anything already in the bar runs to
completion on its current trajectory.
- **`shelbi task move --to backlog`** from `review` — pull one
specific card out of Zen's hands without changing the global mode.
### Crash recovery
If the orchestrator pane dies mid-session (kill signal, machine
power loss, SIGHUP), Zen auto-disables on the next orchestrator
start. The wrapper writes a heartbeat every 60s; an ungraceful exit
flips Zen off, writes a `zen=off reason=crash-recovery` event line,
and prints a stderr warning. The orchestrator's bootstrap also calls
this out in its first reply so you don't miss it. Re-enable with
`shelbi zen on` once you've reviewed any work that was in flight.
## Tune the bar over time
The values you set in `project.yaml` are the easy half. The
**judgment categories** that decide *which* backlog items the
orchestrator considers promoting live in its prompt, not the YAML.
That prompt is a file you own:
```text
~/.shelbi/projects//agents/orchestrator/instructions.md
```
Edit it directly, or run `shelbi agent edit orchestrator`. You can
tune the auto-promote logic for your project (what counts as
"routine," when to refuse, how to phrase a decline) by editing
prose, not code. The next page,
[author a custom workflow](/docs/guides/getting-started/custom-workflow),
walks through doing it end-to-end.
In the meantime, the deep reference for everything Zen touches:
- [Zen Mode](/docs/concepts/zen-mode) — the full mental model, the
high-confidence bar, judgment categories, per-task overrides,
configuration reference.
- [`shelbi zen`](/docs/cli/zen) - every subcommand the orchestrator
sequences during exact-provenance auto-merge, callable by hand for one-off
probes.
- [The events log](/docs/concepts/events-log) — the canonical format
of every `mode=zen` and `orchestrator:zen-*` line Zen emits.
## Next
You've handed the agent autonomous triage and merge flow, and given it a
confidence bar that matches this project. The last step is
making the workflow itself yours: [author a custom Shelbi
workflow](/docs/guides/getting-started/custom-workflow) to change routing
rules, tune Zen's judgment, and adjust how the orchestrator reports
back to you.
[Source](https://shelbi.dev/docs/guides/getting-started/enable-zen-mode)
---
# Global vs Repo Config
Two places Shelbi will look for a project's config — under ~/.shelbi/ or committed at /.shelbi/ — and how to move between them.
Shelbi splits everything it writes to disk into two categories:
- **Config** covers the declarative decisions about the project: its name, the
default branch, workflows, agent prompts, runner settings. These are
the same for every teammate.
- **State** covers the per-user, per-machine, mutable data: task cards, the
events log, workspace status files, the orchestrator's dashboard
worktree. Two teammates on the same project have different state.
**Config can live in two places**: under your `$HOME` (the default,
*global mode*), or committed to the repo itself (*in-repo mode*).
State always lives under `~/.shelbi/`, regardless of mode.
First-run `shelbi` and `shelbi init -y` create global-mode config. You can move
a project to in-repo mode later with a one-shot command. The interactive
`shelbi init` path also offers an explicit `--mode` choice.
Solo repos and quick experiments belong in **global mode**. It's the
default and needs zero repo-side changes. Reach for **in-repo mode**
when you want teammates to clone your workflows, agent prompts, and
runner settings without each of them having to redo the setup wizard.
## The two modes at a glance
| Piece | Global mode | In-repo mode |
| --- | --- | --- |
| Shared config (name, default_branch, orchestrator, agent_runners, zen, heartbeat, git, workflows/, agents/, workspace-settings.json.template) | `~/.shelbi/projects/.yaml` and `~/.shelbi/projects//` | `/.shelbi/project.yaml` and `/.shelbi/` |
| User-local config (repo path, machines, workspaces, editor) | Same YAML: `~/.shelbi/projects/.yaml` | `~/.shelbi/projects//local.yaml` |
| State (state.json, tasks/, HANDOFF.md, .claude/, workspaces/, events.log) | `~/.shelbi/projects//` (+ `~/.shelbi/events.log`) | `~/.shelbi/projects//` (+ `~/.shelbi/events.log`) |
| Discovery | Reverse-lookup: cwd matched against every registered project's local `work_dir` | Walk up from cwd for `/.shelbi/project.yaml`, then require a matching `local.yaml` |
| Shared with clones? | No: every teammate reruns `shelbi init` | Yes: everything under `/.shelbi/` (minus the gitignored state pieces) travels with `git clone` |
The full field-by-field bucket lists are in
[`shelbi_core::model`](https://github.com/jlong/shelbi/blob/main/crates/shelbi-core/src/model.rs)
under `SHARED_PROJECT_FIELDS` and `LOCAL_PROJECT_FIELDS`; a shared YAML
that carries a user-local field (or vice versa) errors on load rather
than silently letting one side win.
## Global mode
The default. First-run `shelbi`, the interactive wizard, and `shelbi init -y` write
`~/.shelbi/projects/.yaml` with everything (shared and
user-local) in one flat file. Nothing is added to your repo. The
project only exists on your machine.
Resolution is by reverse-lookup: Shelbi scans `~/.shelbi/projects/*.yaml`
once, collects each project's `machines[].work_dir` (local machines only;
an SSH host's work_dir is a path on another box), and matches cwd (or
an ancestor) against them. The deepest match wins, so nested checkouts
resolve to the sub-project. `shelbi -p ` short-circuits the walk
entirely.
Global mode is the right choice for:
- Solo projects nobody else clones.
- Scratch experiments where committing anything to the repo would be noise.
- Repos you don't own the write-access story for.
If you want to try in-repo mode later, [migrate](#migrating-a-project-to-in-repo-mode)
with a single command.
## In-repo mode
The project's shared config is committed at `/.shelbi/project.yaml`
and everyone who clones the repo gets the same workflows, agent prompts,
and runner settings. Each teammate still has their own machines and
workspace pool. Those live in a per-user `local.yaml` that never gets
committed.
### On-disk layout
```
/
.shelbi/
project.yaml # ← shared: name, default_branch, orchestrator,
# agent_runners, zen, heartbeat, git, github_url,
# workspace_permissions_mode, config_mode: in-repo
workflows/
default.yaml
statuses.yaml
agents/
orchestrator/
developer/
_shared/ # optional shared preamble
workspace-settings.json.template
.gitignore # ← carries the state entries; see below
```
```
~/.shelbi/
projects/
/
local.yaml # ← user-local: repo, machines, workspaces,
# editor
state.json # ← state — never committed
tasks/
HANDOFF.md
.claude/
workspaces/
events.log # ← state — cross-project
```
The shared/local split is enforced at parse time: a `machines:` block
inside `/.shelbi/project.yaml`, or a `zen:` block inside
`local.yaml`, raises an error identifying the misplaced field and
naming the file it belongs in.
### Discovery walk-up
From anywhere inside the repo (the root, a nested directory, wherever
you happen to `cd`), Shelbi walks up looking for the first ancestor
containing `.shelbi/project.yaml`. This mirrors how `git` finds its
`.git` directory: no env var, no marker file to place by hand.
Once the walk-up finds a shared config, Shelbi reads the `name:` field
out of it and checks that a matching
`~/.shelbi/projects//local.yaml` exists on your machine. If it
doesn't, the resolver returns `ProjectNotPickedUp` and points at
`shelbi init --pick-up`, the flow for a fresh clone that hasn't been
registered locally yet ([see below](#picking-up-a-teammates-project)).
Walk-up wins over reverse-lookup: if a repo happens to sit inside another
registered project's `work_dir`, the walk-up match takes precedence.
### The `.gitignore` list
State that lives at `/.shelbi/` (bind-mounted or symlinked in
from `~/.shelbi/`) must never land in a commit. `shelbi project
migrate-to-in-repo` prints (and optionally appends) this snippet at
`/.gitignore`:
```
.shelbi/state.json
.shelbi/tasks/
.shelbi/HANDOFF.md
.shelbi/.claude/
.shelbi/workspaces/
.shelbi/events.log
.shelbi/local.yaml
```
Every line names one state footprint. Using in-repo mode without running
the migration? Add the snippet by hand. The point is that
`git grep -F 'shelbi' .gitignore` at the repo root surfaces the full
list, so you never have to memorize the layout.
The `local.yaml` line is defensive. Today `local.yaml` lives under
`~/.shelbi/` and needs no ignoring. But if you ever symlink it into the
repo (say, so an editor picks it up), the ignore line prevents an
accidental commit.
### Local-alias collisions
The `name:` in `/.shelbi/project.yaml` is a shared contract with
every future clone. It stays stable. But two teammates can each clone
into `~/work/shelbi/` and end up with a local registry collision on the
name. `shelbi init --pick-up` handles this by auto-suffixing the local
alias:
- First clone: registered as `shelbi`.
- Second clone on the same machine: registered as `shelbi-2` (`-3`, `-4`,
…). The committed name is unchanged; the alias is only how *you* refer
to that clone locally.
You reach the suffixed clone with `shelbi -p shelbi-2`. Rename the
alias later with `shelbi project rename` if you want something friendlier.
## Migrating a project to in-repo mode
`shelbi project migrate-to-in-repo` is a one-way command that splits an
existing global-mode project into the in-repo layout:
1. Splits `~/.shelbi/projects/.yaml` into a committed
`/.shelbi/project.yaml` (shared half, with
`config_mode: in-repo`) and a per-machine
`~/.shelbi/projects//local.yaml`.
2. Moves `workflows/`, `agents/`, and `workspace-settings.json.template`
from `~/.shelbi/projects//` into `/.shelbi/`. The mover
prefers `fs::rename`; on cross-filesystem failure it copies then
removes.
3. Prints the `.gitignore` snippet above and offers to auto-append it
(interactively), or applies it non-interactively with `--yes`.
4. Deletes the original `~/.shelbi/projects/.yaml`, whose contents
now live in the two files above.
State (`state.json`, `tasks/`, `HANDOFF.md`, `.claude/`, `workspaces/`,
`events.log`) stays put under `~/.shelbi/`.
### Try it dry first
```bash
shelbi project migrate-to-in-repo --project myapp --dry-run
```
Prints the ordered plan (every write, move, delete) without touching
disk. It's diff-oriented, so a reviewer can vet the migration before it runs.
### Apply it
```bash
shelbi project migrate-to-in-repo --project myapp
```
Interactive: prompts for the `.gitignore` append. Pass `--yes` to skip
the prompt (useful in scripts). `--project` is optional: a bare
`shelbi project migrate-to-in-repo` resolves the project from `$SHELBI_PROJECT`
or the current directory the same way any other subcommand does.
The migration is **idempotent**: rerunning on an already-migrated
project is a no-op, and rerunning on a half-migrated one completes the
outstanding steps. Safe to retry.
### What to commit
After a successful migration, commit:
```
.shelbi/project.yaml
.shelbi/workflows/
.shelbi/agents/
.shelbi/workspace-settings.json.template
.gitignore # updated with the state snippet
```
Everything else is either state (already ignored) or lives outside the
repo entirely.
### It's one-way
There is no `migrate-to-global` command. Reverting is:
1. `git revert` the migration commit, which restores `/.shelbi/`
to its pre-migration state.
2. Move `~/.shelbi/projects//local.yaml` back to
`~/.shelbi/projects/.yaml` (the global-mode YAML shape).
A merged migration commit is expensive to undo. `--dry-run` first.
## Picking up a teammate's project
When a teammate has committed `/.shelbi/` and you clone the repo,
you need a local registry entry (the `local.yaml` with your machines
and workspace pool) before the walk-up will resolve. `shelbi init
--pick-up` walks you through it:
```
$ git clone git@github.com:acme/shelbi.git
$ cd shelbi
$ ls .shelbi/
agents/ project.yaml workflows/ workspace-settings.json.template
$ shelbi init --pick-up
✓ scaffolded /Users/you/.shelbi
✓ registered project: /Users/you/.shelbi/projects/shelbi.yaml
✓ wrote workspace settings template: /Users/you/.shelbi/projects/shelbi/workspace-settings.json.template
✓ created agent workspace: agents/orchestrator/
✓ created agent workspace: agents/developer/
✓ wrote project statuses: /Users/you/.shelbi/projects/shelbi/workflows/statuses.yaml
✓ picked up `shelbi` from /Users/you/work/shelbi/.shelbi/project.yaml.
next:
1. add machines/workspaces to ~/.shelbi/projects/shelbi.yaml if needed
2. spawn your first agent: shelbi spawn TASK --on hub --runner claude "…"
```
`--pick-up` walks up from cwd to find the committed
`/.shelbi/project.yaml`, reads the canonical name, and registers a
matching entry in your local `~/.shelbi/projects/`. From here, `shelbi`
inside the repo just works.
If the canonical name is already taken locally (you already have another
clone, or another project named the same), the alias is auto-suffixed:
```
$ shelbi init --pick-up
✓ scaffolded /Users/you/.shelbi
note: local alias `shelbi` was already taken — using `shelbi-2` on this machine instead (the committed name is unchanged)
✓ registered project: /Users/you/.shelbi/projects/shelbi-2.yaml
…
✓ picked up `shelbi` from /Users/you/work/shelbi/.shelbi/project.yaml as local alias `shelbi-2`.
Tip: `shelbi project rename` can retitle the local alias to something friendlier.
next:
1. add machines/workspaces to ~/.shelbi/projects/shelbi-2.yaml if needed
2. pass `-p shelbi-2` on the command line to target this project
(the committed name `shelbi` was already taken locally — the alias only affects your machine)
```
### Safety net: bare `shelbi init` on an unregistered clone
If you forget the `--pick-up` and run a plain `shelbi init` in a cloned
repo that already carries `/.shelbi/project.yaml`, Shelbi refuses
to scaffold over the top:
```
$ shelbi init
Error: found /Users/you/work/shelbi/.shelbi/project.yaml (committed) but no local registry entry for `shelbi` — this repo looks like a teammate's shelbi project. Run `shelbi init --pick-up` to register it locally.
```
## `shelbi reload` semantics
`shelbi reload` respawns the Shelbi-owned panes (sidebar, tasks, review,
machines) and self-heals default agent workspaces and the workspace
settings template. As part of that, it re-reads the project's YAML, so
edits to `/.shelbi/project.yaml` or `local.yaml` (in-repo mode) or
`~/.shelbi/projects/.yaml` (global mode) take effect on the next
reload.
The one important nuance: **a running workspace keeps its current prompt
until it hands off.** A reload never swaps the prompt mid-task; changes
reach a workspace on its *next* dispatch, once it picks up a fresh card.
This is deliberate. A mid-turn prompt swap would surprise the agent and
make debugging much harder.
If you need a running workspace to pick up new instructions immediately,
kill its task, reload, then re-dispatch.
## See also
- [Project config](/docs/configuration/project) — the field-by-field
reference for the YAML this page routes to disk, including the exact
shared vs. user-local field split.
- [Set up your first project](/docs/guides/getting-started/first-project) —
the detected, one-confirmation global-mode setup.
- [`shelbi reload`](/docs/cli/reload) — respawn the TUI panes after
editing the YAML or installing a new binary.
- `shelbi init --help` and `shelbi project migrate-to-in-repo --help` —
the full flag reference for the two commands this page covers.
[Source](https://shelbi.dev/docs/concepts/config-modes)
---
# Author a custom Shelbi workflow
Edit the orchestrator agent's instructions to encode routing rules, project-specific Zen judgment, and reporting style — the workflow is two markdown files you own.
The orchestrator is your runner (`claude`, `codex`, …) booted with a
system prompt that explains the board, the workspaces, and the loop. That
prompt is the **workflow**: the contract for who initiates what, how
dispatch is decided, and how the agent reports back. Shelbi ships a
sensible default and seeds it into every project as an editable file.
This page customizes the orchestrator's prompt, encodes three concrete
tweaks, and applies them. The underlying machinery lives in
[the orchestrator concept page](/docs/concepts/orchestrator#customizing-the-prompt-per-project);
this is the practical version.
## Where the prompt lives
The orchestrator is a [project-local agent with shipped defaults](/docs/concepts/agents).
Its prompt is composed from two files, both under the project's `agents/`
directory (`~/.shelbi/projects//agents/` in the default
[global mode](/docs/concepts/config-modes), or `/.shelbi/agents/`
in in-repo mode):
- **`agents/orchestrator/instructions.md`** — the orchestrator-specific
system prompt. Seeded with the shipped default on first load; your
edits stay put on upgrade.
- **`agents/_shared/preamble.md`** — project-wide context prepended to
every agent (developer, qa, orchestrator…). Put the repo layout,
house style, and test command here so you write it once instead of
pasting it into four prompts.
On every `shelbi reload` (and at session start) Shelbi composes
`preamble.md + instructions.md` and writes the rendered output to
`~/.shelbi/projects//CLAUDE.md`,
which is state, so it lives under `~/.shelbi/` in both modes. The
orchestrator launches in that directory and auto-loads the rendered
prompt. No flags, no env vars, no MCP.
`CLAUDE.md` is a rendered artifact. Put project-wide bits in
`_shared/preamble.md` and orchestrator-specific bits in
`agents/orchestrator/instructions.md`. Every `shelbi reload` overwrites
`CLAUDE.md`, so hand-edits are silently lost.
## Start from the default
Don't rewrite from a blank file. The default prompt encodes
load-bearing contracts (bootstrap, reaction rules, reason strings)
that the rest of Shelbi expects the orchestrator to honor. Open the
seeded default and edit in place:
```bash
shelbi agent edit orchestrator
```
That opens `agents/orchestrator/instructions.md` in `$EDITOR` (and
materializes the shipped default first if you haven't touched it
yet). The file is plain markdown. Read it top to bottom before changing
anything. The comments explain why each section exists.
## What to keep when you fork
A handful of pieces are the contract between the orchestrator and the
rest of the system. Edit them carefully, but don't delete:
- **The bootstrap flow.** The first reply of every session runs
`shelbi task list`, `shelbi workspace list`, starts
`shelbi events tail --follow`, and reads `shelbi zen status`.
Drop any of these and the orchestrator boots without state.
- **The reaction rules tied to specific reason strings.** The
orchestrator dispatches on `user:*` promotions, rolls workspaces on
`workspace:ready-marker`, and wakes on `heartbeat` lines. The rest
of Shelbi emits those strings. Your prompt has to recognize them.
- **The "you are the scheduler" framing.** Without it, the agent
reverts to chatbot mode and waits to be told what to do. The
whole loop falls apart.
- **The Zen-Mode section** (if you intend to use Zen). Replace the
judgment categories and the merge conditions if they don't fit
your project, but keep the section structure. The wrapper around
`shelbi zen probe` reads the same primitives regardless of the
prose around them.
Everything else is fair game.
## Three example tweaks
A workflow override pays for itself the first time the agent stops
asking a question you've answered the same way a dozen times. Three
patterns worth stealing.
### 1. Pin certain task shapes to specific workspaces
The default scheduler picks the first free workspace in YAML
declaration order, honoring `prefers_machine` hints. If your project
has natural routing (infra tasks always to a beefy box, UI tasks
always somewhere with the design system pre-cached), bake it into
the workflow:
```markdown
## Routing rules
In addition to the project YAML's `prefers_machine` hints, apply
these project-specific rules when picking a workspace for a `todo`
card:
- **Infra tasks** — titles containing `infra:`, `terraform`,
`helm`, `kubectl`, or paths under `infra/`. Always route to
`devbox` (the beefier remote). If no `devbox` workspace is free,
leave the card in `todo` rather than routing to the hub.
- **Docs and content** — titles containing `docs:` or paths under
`site/content/**`. Route to the first free hub workspace; these
are short, IO-light tasks that don't need the remote.
- **Migration touch-ups** — anything matching `db/migrate/**` or
`crates/shelbi-state/src/migrations/**` always goes to
`charlie` (so the same agent context accumulates across the
series). If charlie is busy, hold in `todo`.
These rules take precedence over default declaration-order
selection. The YAML's `prefers_machine` hint still overrides
everything if set explicitly on a card.
```
The orchestrator now applies your routing on top of the default
free-workspace scan. No code change.
### 2. Tune Zen's judgment categories for this project's reality
The bundled default's judgment categories are deliberately
conservative: promote if you've routinely accepted this shape, or
the user just raised the issue, or it's part of a larger body of
work they kicked off. That's a sensible starting bar, but every
project has its own "always safe" pattern. Encode it:
```markdown
## Zen Mode
### Auto-promote judgment categories
Promote a backlog candidate to `todo` only if **at least one** is
true:
1. **Tagged `automation:` in the title.** Always in scope —
these are bot-filed PR-bumps and lint sweeps we have years of
acceptance history on.
2. **Touches only `docs/**` or `site/content/**`.** Content
edits are reversible and never reach prod. If the task body
names a file outside those globs, fall through.
3. **The user has accepted ≥3 tasks of the same shape (same
verb-prefix, same target directory) without changes in the
last week.** Track this against the `done` column via
`shelbi task list --column done`.
If none of the above, leave the card in `backlog`, emit
`reason=orchestrator:zen-decline reason-text=`, and surface
it in the next reply ("I considered promoting `` but wasn't
sure — want me to?").
### Merge conditions
(Inherit from the default — see the bundled template.)
```
The Rust side (`shelbi zen scan`) never inspects this prose. It
just emits the mechanically-eligible candidates. Your override
decides which of those actually get promoted.
### 3. Change the reporting style
The default prompt errs toward terse one-line status updates ("✓
dispatched to delta. branch: shelbi/fix-login"). If you'd rather
get a richer rundown (or a quieter one), say so:
```markdown
## Reporting style
When dispatching, reply with:
- The card id and chosen workspace (always).
- The full first line of the task body (so the user sees what
you're about to make the workspace work on).
- An estimated complexity tag (`xs` / `s` / `m` / `l`) based on
the body and any `depends_on`. Don't overthink it — a one-token
guess is better than nothing.
When reporting a finished review:
- The card id, the workspace, and the branch.
- A two-bullet summary of what the diff changed (read it via
`git diff main.. --stat`).
- Whether a relevant `done`-column precedent exists for this
shape of change.
Background sweeps (heartbeat-triggered re-checks) report nothing
unless they take action. Silence is fine when the board is quiet.
```
This is the easiest place to start customizing. The behavior the
orchestrator gives you is downstream of the prose you write here.
Ask for the report you actually want.
## Apply the edits
```bash
shelbi agent edit orchestrator
```
Opens `agents/orchestrator/instructions.md` in `$EDITOR`.
```bash
shelbi reload
```
Composes the prompt against your edits and respawns the panes.
```bash
shelbi agent show orchestrator
```
Prints `instructions.md` followed by the agent's `skills/`.
The composed prompt (preamble + instructions) is written to
`~/.shelbi/projects//CLAUDE.md` on every `shelbi reload`. Read
that file to see exactly what the runner loads.
`shelbi reload` respawns the sidebar, Tasks, and Review panes against the
new config without restarting workspaces or losing chat history. It does
recycle the orchestrator pane, so its conversation context resets: the
cost of changing the system prompt mid-session. Heads up before the next
turn.
## Iterate from prose, not code
The whole point of putting the workflow in markdown is that you can
keep tightening it. Two practical habits:
- **Treat surprises as prompt bugs.** When the orchestrator does
something you didn't expect (promotes a card you'd have held,
reports in a style you find noisy, picks the wrong workspace), open
`agents/orchestrator/instructions.md` and add the rule that would
have produced the outcome you wanted. Reload. Try again.
- **Version-control the prompt.** Check `agents/orchestrator/` and
`agents/_shared/` into the repo (or a dotfiles repo) and symlink
them into place. These prompts are the most opinionated artifacts
in your Shelbi setup, and the ones most worth preserving across
machine moves and reinstalls.
What you should **not** do is try to encode policy in code. The
mechanical layer (the Rust CLI) is intentionally generous.
`shelbi zen scan` will hand the orchestrator everything that's
mechanically safe, and your prompt picks which of those to actually
run. That separation is what lets you raise or lower the bar by
editing prose.
## See also
- [Workflow config](/docs/configuration/workflow) — the YAML field
reference for the status pipeline the orchestrator runs work through.
- [Orchestrator](/docs/concepts/orchestrator) — how the prompt
template is loaded, the bootstrap flow it triggers, and the
reaction rules tied to specific event reason strings.
- [Zen Mode](/docs/concepts/zen-mode) — the high-confidence bar
your custom judgment categories sit on top of.
- [The events log](/docs/concepts/events-log) — the full reason
string vocabulary the orchestrator emits and your override may
want to recognize or extend.
- [`shelbi zen`](/docs/cli/zen) — the single-purpose primitives the
orchestrator sequences during auto-merge, available to call by
hand any time.
[Source](https://shelbi.dev/docs/guides/getting-started/custom-workflow)
---
# Workflows
A per-project YAML that declares the statuses a task moves through, who owns each one, and what side-effects fire on each transition. The default workflow is the canonical Backlog → Todo → InProgress → Review → Done board; custom workflows are a YAML edit.
A **workflow** is a YAML file that declares the statuses a task moves
through, who owns each one, and what git side-effects fire on each
transition. `shelbi init` ships two: **`task`**, the review-gated default
(`Backlog → Todo → In Progress → Review → Done`, plus `Canceled`), and
**`subtask`**, a lighter flow for a piece of a parent task that opens no PR
and has no review (see [Shipped workflows](#shipped-workflows-task-and-subtask)
below). Drop another YAML next to them and you have a third pipeline:
a docs-only flow, a research pipeline that never opens PRs, a
feature-stacking flow whose tasks branch off a long-lived feature
branch.
Status *names* are user-facing labels: what shows up on the Kanban
card. Generic code (orchestrator auto-dispatch, Zen Mode's confidence
bar, the events log, the activity feed) keeps working unchanged
because every status maps to a small, closed set of **categories**:
`backlog`, `ready`, `active`, `handoff`, `done`. Categories are the
vocabulary the rest of the system reasons in.
The default workflow renders as the canonical five-column board, one column
per category, each colored by its category:
Schema and parser live in `crates/shelbi-core/src/workflow.rs`.
Workflow files live at
`~/.shelbi/projects//workflows/.yaml`.
## Choosing the default workflow
A task can pin a workflow explicitly:
```yaml
workflow: feature-task
```
When `workflow:` is absent, Shelbi checks the project config for
`default_workflow:`:
```yaml
# ~/.shelbi/projects/myapp.yaml or /.shelbi/project.yaml
default_workflow: task
```
That makes tasks without `workflow:` behave as if they declared
`workflow: task`. Fresh projects get `default_workflow: task` written by the
scaffold, so the review-gated `task` flow is the default out of the box. If
the project field is omitted, Shelbi falls back to the built-in `default`.
A configured default must name an existing `workflows/.yaml`; if it does
not, project loading fails with a workflow error instead of silently falling
back.
## Schema
A workflow is split across **two files**. Status *identity* (the stable
`id`, the display `name`, and the `category`) is declared once in
`workflows/statuses.yaml`, the project-wide status catalog. Each workflow
file then **references** statuses by `id` and adds only what's
workflow-specific: the `owner`, and an optional `agent:`.
```yaml
# ~/.shelbi/projects//workflows/statuses.yaml
# The status catalog: identity lives here, once, shared by every workflow.
statuses:
- { id: backlog, name: Backlog, category: backlog }
- { id: todo, name: Todo, category: ready }
- { id: in-progress, name: In Progress, category: active }
- { id: review, name: Review, category: handoff }
- { id: done, name: Done, category: done }
```
```yaml
# ~/.shelbi/projects//workflows/default.yaml
name: default
description: The standard one-track flow shipped with every project.
# Reference statuses by id. name/category come from statuses.yaml and are
# never repeated here — a workflow file adds only owner (+ optional agent).
statuses:
- { id: backlog, owner: user, agent: orchestrator }
- { id: todo, owner: agent, agent: orchestrator }
- { id: in-progress, owner: agent, agent: developer }
- { id: review, owner: user, agent: orchestrator }
- { id: done, owner: user }
# Optional. Which status a new task lands in. Defaults to the first
# entry in `statuses:` above.
# initial_status: backlog
# Optional. Per-workflow override of the project-level `git:` block in
# project.yaml. Most workflows omit it and inherit base_branch/merge_strategy
# unchanged.
# git:
# base_branch: main
# merge_strategy: squash # squash | merge
# Optional. Edge-by-edge action declarations. Each entry says what side
# effects to run when a task crosses that edge. Omitted edges run no
# actions — pure status moves. Transitions are any-to-any: this block
# does NOT restrict which moves are legal.
# transitions:
# - from: in-progress
# to: review
# actions: [push_branch, open_pr]
# - from: review
# to: done
# actions: [merge, delete_branch]
```
Why two files? A status's identity is project-wide, but its automation
differs per workflow. The same `review` status can be owned by a human in
one workflow and handled by a `qa` agent in another. Declaring `name` and
`category` once in `statuses.yaml` keeps those facts from drifting between
workflow files; the loader **rejects** a workflow that repeats `name:` or
`category:` inline, or that references an `id` the catalog doesn't declare.
The required fields are `name` and `statuses` (at least one). Everything
else is optional. A workflow without `transitions:` is a pure
bookkeeping workflow: no git side-effects fire on any move.
Validation runs at load time and catches status ids not declared in
`statuses.yaml`, duplicate status ids, `initial_status` pointing at
nothing, transition edges referencing undeclared statuses, and the
[`owner`/`agent` rules](#validation-rules) covered below. The error
message names the workflow and the bad field.
## Status categories
Every status, default or custom, declares one of five categories:
| Category | Semantic | Example status names |
| ---------- | ------------------------------------------------------------------- | ----------------------------------------------------- |
| `backlog` | Not yet ready for work — triage stage. | `Backlog`, `Inbox`, `Triage` |
| `ready` | Ready for whoever owns it to pick up. | `Todo`, `Ready`, `Next Up` |
| `active` | Owner is working on it now. | `InProgress`, `Building`, `Designing`, `Drafting` |
| `handoff` | One owner has finished their part; another's input is required. | `Review`, `QA`, `Awaiting Sign-off` |
| `done` | Terminal — accepted, shipped. | `Done`, `Shipped`, `Released` |
The category set is **closed at five**. A workflow can't introduce a
sixth. Generic code (Zen Mode, activity feed, orchestrator routing)
keys off the category, and an unknown one would have nowhere to fit. A
workflow *can* repeat a category, though: a long pipeline might have
three `active` statuses (Design, Build, QA) with different owners.
Categories let the rest of the system reason without caring about
names:
- The orchestrator dispatches `ready`-category statuses whose `owner`
is `agent`. It doesn't care whether you called the column `Todo` or
`Ready` or `Next Up`.
- The hub poller's review-marker promotion lands a task in the next
`handoff` status: the same code path for `Review`, `QA`,
`Awaiting Sign-off`.
- The activity feed renders an `active → handoff` move as
"handed off" regardless of the actual status names involved.
The category for each status appears on every events log line so
consumers can match on the semantic without looking up the workflow.
See [events log line shape](/docs/concepts/events-log#task-transitions).
## Owners and agents
Each status declares two things about who handles it, one required, one
optional:
```yaml
# workflow file — reference-only; name/category come from statuses.yaml
statuses:
- { id: todo, owner: agent, agent: orchestrator }
- { id: in-progress, owner: agent, agent: developer }
- { id: review, owner: agent, agent: qa }
- { id: signoff, owner: user }
- { id: done, owner: user }
```
**`owner: user | agent`** declares *who* the next mover is:
- **`agent`** — the orchestrator is the next mover. Tasks sitting in an
`agent`-owned `ready` status are eligible for auto-dispatch onto a free
[workspace](/docs/concepts/workspaces).
- **`user`** — the orchestrator does not act. The task waits for you to
move it, surfaced in the activity feed so you know your attention is
requested.
There's no third "either": a status is either work the orchestrator will
pick up or work that waits for a human. If you want to grab an agent-owned
task by hand, reassign it through the normal CLI/TUI; that's an action, not
a schema field.
**`agent: `** names *which* [agent](/docs/concepts/agents) role does
the work, when `owner: agent`. Omit it and Shelbi derives a default from the
category (`developer` for `active`, `orchestrator` for `ready`). Name
one (a shipped reviewer like `qa`, `security`, or `adversarial`, or any
agent you've authored) and the orchestrator loads *that* role into the
workspace it dispatches to. The
`Review` status above is auto-handled by the `qa` agent before any human is
involved; that's how "this status is reviewed by QA" becomes a line of YAML
instead of a paragraph of orchestrator prompt.
### Validation rules
The loader enforces these rules, failing the load with a message that
names the workflow and status:
1. **Every `id` must be declared in `statuses.yaml`.** A workflow
references statuses by id; an id the catalog doesn't know about (a typo,
a status you forgot to add) fails the load, and the error lists the
available ids. Repeating an inline `name:` or `category:` in a workflow
file is rejected too. Identity lives in the catalog, so the two files
can't drift.
2. **`owner` is `user` or `agent`.** No other value parses, and a missing
`owner` is an error too. There's no implicit default.
3. **An `agent`-owned status must name its `agent:`.** For the `ready` and
`active` categories a bare `owner: agent` is accepted and defaults to
`orchestrator` / `developer` (with a deprecation warning); any other
category with `owner: agent` and no `agent:` fails the load.
4. **A `user`-owned status may still name an `agent:`.** That declares
"under [Zen Mode](/docs/concepts/zen-mode) this agent may act without
me". It's how the shipped default lets the orchestrator handle
`Backlog` and `Review` under automation. Terminal statuses (`Done`,
`Canceled`) simply omit it: no automation path at all.
### Zen as declarative data
Because the `owner`/`agent` pair lives in the workflow YAML, *per-status
automation is data, not prompt prose*. Whether the orchestrator acts at a
given edge is the `owner` field; which agent it uses is the `agent` field.
That makes each of these choices a YAML edit:
- **"Auto-merge but don't auto-promote."** Keep `Backlog` as
`owner: user` (you still triage), but make the merge-bearing handoff
status `owner: agent`. The orchestrator lands finished work without
touching your triage queue. No prompt surgery.
- **"Review with QA, sign off by hand."** Split the handoff into a
`Review` status (`owner: agent, agent: qa`) and a `Signoff` status
(`owner: user`), as in the example above. The QA pass is automated; the
final accept stays human.
The [judgment policy](/docs/concepts/zen-mode#the-judgment-layer) (*which*
backlog items are in scope to promote, and the confidence bar a merge must
clear) still lives in the orchestrator's prompt, because it's a judgment
call. But the structural "is this status automated, and by whom" question is
now answered declaratively in the workflow. See
[Zen Mode](/docs/concepts/zen-mode#per-status-automation-is-declarative).
## Transitions
The `transitions:` block declares **side-effects**, not legal moves.
Cards can be dragged between any two declared statuses; what `transitions:`
controls is what *happens* on the way. An undeclared edge is a pure
status change with no git activity.
Each transition lists actions in execution order. Failures
short-circuit the rest: if `merge` fails, `delete_branch` does not
run.
| Action | Effect |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `push_branch` | Push the task's branch to `origin`. No-op if already up to date. |
| `open_pr` | Open a PR. Base is `transition.target` if set, else the workflow's resolved `base_branch`, else the parent task's branch when `depends_on` is set. No-op if a PR is already open. |
| `merge` | Merge the task's branch with the effective `merge_strategy`. Via `gh pr merge` when a PR is open; via local `git merge` on the hub otherwise. v1 strategies: `squash`, `merge`. |
| `close_pr` | Close any open PR without merging. |
| `delete_branch` | Delete the local + remote branch. Skipped if a workspace still has the branch checked out. That worktree replaces the branch on its next dispatch. |
| `restack` | Rebase the task's branch onto its parent task's current branch. Fires automatically on dependent tasks when a parent's `merge` completes (see [`depends_on`](#dependent-tasks-with-depends_on)). |
All actions are **hub-side** and idempotent. The orchestrator (or the
CLI) on the hub machine runs them; workspaces never invoke actions
directly.
The canonical default workflow's transitions, written out:
```yaml
transitions:
- from: in-progress
to: review
actions: [push_branch, open_pr]
- from: review
to: done
actions: [merge, delete_branch]
```
### Per-transition `target:`
Each transition may declare a `target:` field that overrides where
`merge` and `open_pr` land for *that edge only*:
```yaml
transitions:
- from: in-progress
to: review
target: develop # merge feature work into develop here
actions: [push_branch, merge]
- from: review
to: done
# no target → uses the workflow's base_branch (e.g., main)
actions: [open_pr, merge]
```
`target:` does **not** affect where new task branches are cut from.
That's always the resolved `base_branch`. It only controls where
merges land for this particular hop.
### What fires Zen Mode's high-confidence bar
[Zen Mode](/docs/concepts/zen-mode#the-high-confidence-bar) gates *any*
transition whose actions include `merge`, regardless of which
statuses sit on either side. A trunk-based workflow that skips
`Review` and goes straight `Active → Done` with `actions: [merge,
delete_branch]` trips the same probe as the canonical `Review → Done`.
That's the whole point of expressing side-effects as actions: the
gate is action-based, not status-pair-based.
## The `branch:` task field
Every task carries an optional `branch:` field in its frontmatter: the
name of the git branch the task operates on. Its presence at creation
controls whether Shelbi cuts a fresh branch or uses an existing one.
```yaml
---
id: build-login-form
title: Build the login form
workflow: default
# branch: ← omitted → orchestrator generates jlong/build-login-form on dispatch
---
```
- **Omitted at creation.** When the task moves into its first
`active`-category status (typically `InProgress`), the orchestrator
cuts a fresh branch off the workflow's resolved `base_branch`, names it
by rendering the workflow's (or project's) `git.branch` template (the
shipped `task` workflow uses `{{github_user}}/{{id}}`, so
`build-login-form` becomes `jlong/build-login-form`), and writes the name
back into the task's frontmatter. When no `branch` template is configured,
Shelbi falls back to your GitHub username. The user never types a branch
name. This is the common case.
- **Set at creation.** The orchestrator uses that branch as-is. No new
branch is cut. This is how **release tasks** work: the user already
knows the branch (typically `feature/`) and pre-fills it.
```yaml
---
id: ship-auth-rewrite
title: Ship the auth rewrite to main
workflow: feature-release
branch: feature/auth-rewrite # pre-existing — use this branch as-is
---
```
Once populated, `branch:` is the source of truth for every subsequent
action: `push_branch`, `open_pr`, `merge`, and `delete_branch` all
operate on it. Workflows don't carry a separate "what branch to ship"
field; the task does.
## Parameterization
A `git:` block can use `{{var}}` placeholders that resolve from the
task's frontmatter at load time. This is what makes one
`feature-task.yaml` reusable across any number of concurrent features:
```yaml
# workflows/feature-task.yaml
name: feature-task
required_params: [feature] # every task must set feature:
git:
base_branch: feature/{{feature}} # resolved per task
merge_strategy: squash
statuses:
- { id: todo, owner: agent, agent: orchestrator }
- { id: in-progress, owner: agent, agent: developer }
- { id: review, owner: user }
- { id: done, owner: user }
transitions:
- from: in-progress
to: review
actions: [push_branch] # push so reviewers can see; no PR
- from: review
to: done
actions: [merge, delete_branch] # merge into feature/{{feature}}
```
Two tasks, two features, one workflow file:
```markdown
---
id: build-login-form
title: Build the login form
workflow: feature-task
feature: auth-rewrite # base_branch resolves to feature/auth-rewrite
---
```
```markdown
---
id: wire-dashboard-shell
title: Wire the dashboard shell
workflow: feature-task
feature: dashboard-v2 # base_branch resolves to feature/dashboard-v2
---
```
Substitution rules:
- Plain string replacement. No conditionals, no expressions, no
defaults.
- Placeholders are allowed in `git:` field values only. They are not
resolved inside status names, category names, or the `transitions:`
shape.
- A missing key produces one clear error per workflow listing every
unresolved placeholder, so a single edit to the task's frontmatter
fixes them all: *"workflow `feature-task` requires task params:
`feature`"*.
- Every `{{var}}` in `base_branch` must be declared in the workflow's
[`required_params`](/docs/configuration/workflow#required-params).
Shelbi rejects the workflow at load time otherwise, so a base branch
that references a field no task carries is caught before dispatch, not
after.
## Dependent tasks with `depends_on`
Tasks declare a `depends_on:` list in their frontmatter to express a
parent/child relationship. This is how Graphite-style stacked PRs are
modeled: task B builds on top of task A's branch, which builds on
`main`.
```markdown
---
id: build-login-form
title: Build the login form
workflow: default
---
```
```markdown
---
id: build-dashboard
title: Build the dashboard (uses login form)
workflow: default
depends_on: [build-login-form] # branch cut off build-login-form's branch
---
```
Three behaviors fire when `depends_on:` is set:
1. **Blocked while waiting.** A task is blocked from auto-dispatch
while any id in `depends_on` is not yet in a `done`-category
status. The Kanban renders blocked cards with a `🔒` badge; they
sit in the `ready`-category column until their dependencies clear.
This is the same gate the default workflow's `Todo` status applies
to dependent tasks. See the
[lifecycle walkthrough](#lifecycle-who-moves-a-task-between-the-default-statuses)
above.
2. **Branch cut from the parent.** When the dependent task transitions
into its first `active`-category status, the orchestrator cuts its
branch off the **parent task's `branch:`** (not the workflow's
`base_branch`). The child branch is still generated as
`/`.
3. **PR base is the parent.** When the dependent task's `open_pr`
action runs, the PR's base is the parent task's branch, not the
workflow's `base_branch`.
A fourth behavior fires *on the parent*:
4. **Auto-restack on parent merge.** When the parent's `merge` action
completes, the orchestrator iterates every task with
`depends_on:` containing the parent's id and runs the `restack`
action on each. The child branch is rebased onto the parent's
target (typically `main`), and any open PR's base is updated to
match. A child with multiple parents waits until every parent is
in a `done`-category status, then restacks once onto the converged
target. Chains restack transitively.
Cycles in `depends_on:` are rejected at task-load time with a clear
error naming the offending task and the cycle path. A task whose
parent ended up in a non-`done` terminal state (cancelled, won't fix)
is surfaced in the activity feed and won't proceed past the
`ready`-category column until the user resolves it.
## Per-task workflow assignment
Tasks declare which workflow they belong to in frontmatter:
```yaml
---
id: design-the-thing
title: Design the thing
workflow: design-review # optional; otherwise project default, then "default"
column: Designing # uses a status name from the assigned workflow
---
```
If `workflow:` is absent, the task uses `default_workflow:` from project
config. If the project does not declare one, Shelbi falls back to `default`.
If an explicit task `workflow:` references a missing definition, Shelbi
falls back to the built-in `default`; a missing configured project default
fails project loading because that would otherwise reroute every implicit
task silently.
A task is bound to its workflow at creation. **Cross-workflow moves are
not allowed.** To switch a task into a different workflow, create a
fresh task in the target workflow (and archive the original if you
want).
## Shipped workflows: task and subtask
Every new project ships two workflow files, and `default_workflow: task`
is written into the project config so `task` is the default.
### task
`workflows/task.yaml` is the review-gated default: a task branches off
`main` as `/` (the shipped `branch:
'{{github_user}}/{{id}}'` template), moves through `Backlog → Todo → In Progress →
Review → Done` (with a `Canceled` archive status), and squash-merges to
`main` on accept. It opens exactly one PR, on `In Progress → Review`,
which is both the review surface and, once accepted, what lands on `main`.
```yaml
# workflows/task.yaml — references statuses.yaml (see Schema above)
name: task
description: "Branch work with a review gate: the default track shipped with every project."
git:
base_branch: main
branch: '{{github_user}}/{{id}}'
merge_strategy: squash
statuses:
- { id: backlog, owner: user, agent: orchestrator }
- { id: todo, owner: agent, agent: orchestrator }
- { id: in-progress, owner: agent, agent: developer }
- { id: review, owner: user, agent: review, tags: [review] }
- { id: done, owner: user }
- { id: canceled, owner: user }
transitions:
- { from: in-progress, to: review, actions: [push_branch, open_pr] }
- { from: review, to: done, actions: [merge, delete_branch] }
- { from: in-progress, to: canceled, actions: [close_pr, delete_branch] }
- { from: review, to: canceled, actions: [close_pr, delete_branch] }
```
The `Review` status is where the review workspace comes in. It's
`owner: user` (you still own the accept), but it names `agent: review` and
carries `tags: [review]`. When a task lands here, the orchestrator dispatches
it to a **`review`-tagged workspace** and loads the [Reviewer
agent](/docs/concepts/agents), which stands up the running app so you can
click through the branch before you merge. Declare a `review`-tagged workspace
slot on the hub for exactly this (add `tags: [review]` to a workspace in the
project YAML). See
[Review workspaces](/docs/concepts/review-workspaces) for the tag-matching
rules and how the dev server is booted.
To put a shipped reviewer on the board, name it on a status's `agent:`
field. The three reviewer presets (`qa`, `security`, `adversarial`) ship
materialized but unwired, so wiring one in is a one-line edit: point a status
at `agent: qa` (or `security`, or `adversarial`). [Doing more with
agents](/docs/guides/doing-more-with-agents) walks a full example, including
adding a dedicated review column and its transitions.
### subtask
`workflows/subtask.yaml` is for a piece of a parent `task`, done on its own
branch with **no PR and no review**. A subtask names its parent task's id in
its `task:` frontmatter, and the templated `base_branch: task/{{task}}`
resolves to the parent's branch, so the subtask branches from and
squash-merges directly into that branch, never `main`. Its work surfaces only
on the parent task's single PR. It's handoff-less by design: it declares no
`handoff` status, so the hub poller auto-advances a finished worker straight
to `Done`, firing the merge into the parent branch.
```yaml
# workflows/subtask.yaml
name: subtask
description: A piece of a parent task, squash-merged into the parent's branch with no PR or review.
required_params: [task] # every subtask must name its parent task
git:
base_branch: task/{{task}} # resolves to the parent task's branch
branch: 'subtask/{{id}}'
merge_strategy: squash
statuses:
- { id: backlog, owner: user, agent: orchestrator }
- { id: todo, owner: agent, agent: orchestrator }
- { id: in-progress, owner: agent, agent: developer }
- { id: done, owner: user }
- { id: canceled, owner: user }
transitions:
- { from: in-progress, to: done, actions: [merge, delete_branch] }
- { from: in-progress, to: canceled, actions: [delete_branch] }
```
Shipping these as editable files makes the default flow a thing you can
read and change rather than implicit behavior. You can rename `Todo` to
`Ready` in `statuses.yaml` without breaking anything, since the rest of the
system reasons in categories.
### The built-in `default` fallback
The `default` workflow name is a **built-in fallback**. If a project has no
`workflows/` directory at all, or a task names a `workflow:` that can't be
found, Shelbi falls back to the built-in `default`. A task with no
`workflow:` field belongs to the project default workflow (`task` on a
freshly scaffolded project), or `default` when the project configures none.
### Lifecycle: who moves a task between the default statuses
Every status declares an `owner`, and the owner of the *next* status
is who picks up the move. That makes the lifecycle of a task in the
default workflow legible at a glance:
```
┌──────────┐ ┌──────┐ ┌─────────────┐ ┌────────┐ ┌──────┐
│ Backlog │ → │ Todo │ → │ InProgress │ → │ Review │ → │ Done │
└──────────┘ └──────┘ └─────────────┘ └────────┘ └──────┘
you you orchestrator poller you
(triage) (promote) (auto-dispatch) (marker) (accept)
```
- **`Backlog → Todo` — you.** The orchestrator drops anything it
creates from a natural-language request into `Backlog`. You decide
what's worth doing now, and in what order. Order within `Todo` is
your priority list; the orchestrator dispatches from the top.
- **`Todo → InProgress` — orchestrator.** As soon as a card sits in
`Todo` (or a workspace frees up), the orchestrator routes it to a
free workspace. This is the auto-dispatch loop. A task is **blocked**
and skipped while any id in [`depends_on:`](#dependent-tasks-with-depends_on)
is not yet in a `done`-category status. The Kanban shows blocked
cards with a `🔒` badge.
- **`InProgress → Review` — hub poller.** Promoted automatically the
moment the workspace writes its review-ready marker (see
[next subsection](#review-marker-promotion)). No `shelbi` command
on the workspace, no orchestrator participation.
- **`Review → Done` — you.** Accepting a diff is the moment the human
signs off on the work. The orchestrator never crosses this edge on
its own (except under specific multi-task loops the user has
authorized, typically via [Zen Mode's auto-merge](/docs/concepts/zen-mode#the-high-confidence-bar)).
The pattern generalizes: **you own the intent boundaries (triage and
accept), the system handles the mechanical middle**. A custom
workflow that swaps `Review` for `QA` + `Awaiting Sign-off` doesn't
change the principle: anywhere `owner: user` sits, the orchestrator
waits for you.
### Review-marker promotion
The `active → handoff` transition (`InProgress → Review` in the
default workflow) doesn't need any active participant. The workspace
doesn't run a `shelbi` command. The orchestrator doesn't watch the
workspace's pane. Instead:
1. The workspace, when done, writes its task id into
`/.claude/shelbi-ready`. This instruction is part
of the initial prompt every task ships with (see `compose_prompt`
in `crates/shelbi-orchestrator/src/workspace.rs`).
2. The hub poller `cat`s that file on every tick (locally for hub
workspaces, via SSH for remote ones).
3. If the file is non-empty and names a task that's in an
`active`-category status for this workspace, the poller moves the
task into the next `handoff` status, clears the marker, and
appends `task= -> reason=workspace:ready-marker` to
the events log.
The orchestrator hears about it through its `shelbi events tail
--follow` feed and dispatches the next ready task to the now-free
workspace.
That this works without the workspace holding any Shelbi-specific state
is the point. The marker is the *entire* on-workspace protocol. See the
[workspaces concept page](/docs/concepts/workspaces#how-a-task-completes)
for the rationale and the [reason strings reference](/docs/concepts/events-log#reason-strings)
for the full set of tokens.
## A worked example: stacked feature branches
The full picture: two workflows working together to ship a multi-task
feature.
`workflows/feature-task.yaml`:
```yaml
name: feature-task
description: Stack tasks on a long-lived feature branch.
required_params: [feature] # every task must set feature:
git:
base_branch: feature/{{feature}} # supplied per task
statuses:
- { id: todo, owner: agent, agent: orchestrator }
- { id: in-progress, owner: agent, agent: developer }
- { id: review, owner: user }
- { id: done, owner: user }
transitions:
- from: in-progress
to: review
actions: [push_branch] # push so reviewers see it; no PR
- from: review
to: done
actions: [merge, delete_branch] # squash into feature/{{feature}}
```
`workflows/feature-release.yaml`:
```yaml
name: feature-release
description: Ship a long-lived feature branch into main.
# Inherits project default base_branch: main, merge_strategy: squash —
# no git: block needed.
statuses:
- { id: todo, owner: agent, agent: orchestrator }
- { id: in-progress, owner: agent, agent: developer }
- { id: review, owner: user }
- { id: done, owner: user }
transitions:
- from: in-progress
to: review
actions: [push_branch, open_pr] # PR the existing feature branch
- from: review
to: done
actions: [merge, delete_branch] # squash into main, delete branch
```
Now tasks in `feature-task` carry `feature: auth-rewrite` in their
frontmatter, so `feature-task`'s `base_branch` resolves to
`feature/auth-rewrite`, and the orchestrator cuts each task branch off
it at dispatch. A second feature's tasks carry `feature: dashboard-v2`.
Same workflow file, two concurrent stacks.
When auth-rewrite is ready to ship, create a task in `feature-release`
with `branch: feature/auth-rewrite` pre-filled. Because `branch:` is
set, the orchestrator skips the "cut a new branch" step and operates
directly on the feature branch. One PR-and-merge cycle lands the
whole feature into `main`.
## CLI reference
- `shelbi workflow list` — list every workflow in the project.
- `shelbi workflow show ` — print the resolved YAML.
- `shelbi workflow new ` — scaffold a starter file.
- `shelbi workflow edit ` — open in `$EDITOR`.
- `shelbi task add "Title" [--workflow ]` — create a card,
optionally in a non-default workflow.
- `shelbi task move --to ` — validates that ``
is a member of the task's workflow; errors with a list of valid
options on mismatch.
- `shelbi task list [--workflow ]` — filter by workflow.
## See also
- [Workflow config](/docs/configuration/workflow) and
[Statuses](/docs/configuration/statuses): the field-by-field YAML
reference for the files this page explains conceptually.
- [Workspaces](/docs/concepts/workspaces) — what's on the receiving end
of an `active`-category dispatch, and the review-marker mechanism
workspaces use to hand off.
- [Agents](/docs/concepts/agents) — the role the `agent:` field names.
- [Orchestrator](/docs/concepts/orchestrator) — how
`ready`-category statuses get dispatched.
- [The events log](/docs/concepts/events-log#task-transitions) — line
shape for task transitions, including the workflow + category
annotations.
- [Zen Mode](/docs/concepts/zen-mode) — what the action-based
confidence bar gates, and how per-workflow `zen:` overrides work.
[Source](https://shelbi.dev/docs/guides/getting-started/workflows)
---
# shelbi task
Manage the project's Kanban task board — add, list, move, assign, start, and resume tasks from the CLI.
```text
shelbi task [OPTIONS]
```
`shelbi task` is the CLI face of the Kanban board. Each card is a file under
`~/.shelbi/projects//tasks/`; each column transition appends a line to
`~/.shelbi/events.log`. The TUI's tasks pane and this command are two views on
the same files. The orchestrator drives the board through it, and it's where
scripting, automation, and one-off fixes live.
A board mid-flight, with one task promoted to `TO DO`, one dispatched to a
workspace, and one already `DONE`:
Every subcommand accepts the global `-p / --project ` flag: it targets
the named project, otherwise `$SHELBI_PROJECT` or the registered project whose
`work_dir` contains the current directory. It's omitted from the per-subcommand
tables below.
## add
```text
shelbi task add [OPTIONS]
```
Create a new task. Lands in `backlog` by default, your inbox for triage.
Pass `--status todo` to skip triage when you're already sure.
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `` | string | — | Human-readable title (positional, required). |
| `--id ` | string | slug of title | Override the auto-generated id. |
| `--status ` | string | `backlog` | Initial status. |
| `-d, --description ` | string | empty | Task body. Use `shelbi task edit` later if omitted. |
| `--depends-on ` | string (repeatable) | — | Block this task on another. Repeat for multiple deps. |
| `--prefers-machine ` | string | — | Hint for the orchestrator to route this task to a workspace on a specific machine. |
| `--workflow ` | string | project default | Workflow this task runs under. Names a file in `workflows/.yaml`. Omit to inherit the project's default workflow. |
| `--branch ` | string | generated at dispatch | Pre-fill the task's `branch:` frontmatter field. Omit to let the orchestrator generate `/` from workflow config, project config, or your GitHub username; supply a value to point the task at an existing branch (the *release task* pattern). |
## list
```text
shelbi task list [OPTIONS]
```
Print every task grouped by column, in priority order within each column. Use
`--status ` to scope to one status, or `--ready` to see only unblocked
`todo` items in dispatch order. That's the orchestrator's view of "what
should I assign next." `--workflow ` narrows either view to a single
workflow.
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--status ` | string | — | Restrict to a single status. |
| `--ready` | flag | off | Show only unblocked `todo` items, in priority order. Mutually exclusive with `--status`. |
| `--workflow ` | string | — | Restrict to tasks resolved to the named workflow. Tasks with no explicit `workflow:` field inherit the project's `default_workflow`, or `default` when unset. Composes with `--status` and `--ready`. |
## show
```text
shelbi task show
```
Print a task's frontmatter and body, plus the resolved status of each
`depends_on` entry. The same view the orchestrator reads before deciding
whether a task is ready to dispatch.
## depends
```text
shelbi task depends [OPTIONS]
```
Edit a task's dependency list without opening the file.
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--add ` | string (repeatable) | — | Dependency id to add. |
| `--remove ` | string (repeatable) | — | Dependency id to remove. |
## move
```text
shelbi task move --to [OPTIONS]
```
Move a task between statuses. Promoting to `todo` is the orchestrator's start
signal. It picks a free workspace and runs `task start` itself.
The destination is validated against the task's workflow. A status the workflow
doesn't declare (or a custom status with no backing column) errors with the
list of reachable statuses, and the task stays put. Matching is case- and
punctuation-insensitive (`InProgress` matches `in_progress`); a task pinned to a
workflow with no YAML on disk falls back to the built-in `default`, so
projects keep moving while the workflows directory is empty.
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--to ` | string | — | Destination status (required). Must be a status declared by the task's workflow. |
| `--reason ` | string | `user:cli` | Reason tag recorded in `~/.shelbi/events.log`. The orchestrator parses this to tell auto-dispatches apart from user actions. |
## assign
```text
shelbi task assign --to
```
Set the task's `assigned_to` field without launching the workspace. Use
this when you want to pre-allocate before promoting to `todo`. The
workspace must be declared in the project YAML.
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--to ` | string | — | Workspace name (required). |
## unassign
```text
shelbi task unassign
```
Clear a task's `assigned_to` field. The task stays in its current column.
## start
```text
shelbi task start [OPTIONS]
```
Launch the assigned workspace on this task. Three things happen in one shot:
1. The workspace's worktree is checked out to the task's branch (generated
from workflow config, project config, or your GitHub username when unset).
2. Any existing pane for that workspace is killed. Context is wiped clean.
3. The runner relaunches with the task prompt and the column transitions
into `in_progress`.
Pass `--workspace` to assign and launch in one call. This is the command
the orchestrator runs when a task hits `todo`.
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--workspace ` | string | task's `assigned_to` | Assign and launch in one call. |
| `--branch ` | string | generated | Override the default branch name. |
| `--reason ` | string | `user:cli:start` | Reason tag recorded in `~/.shelbi/events.log` when the column transitions to `in_progress`. |
## resume
```text
shelbi task resume [OPTIONS]
```
Relaunch the assigned workspace on the task it is **already** working, without
throwing away progress. Use it when a worker stalls or its session dies
(a killed tmux session, a wedged pane, an agent that stopped mid-task) and you
want it going again on the same task with its work intact.
`resume` is the recovery counterpart to [`start`](#start). `start` wipes
the agent's context (kills the pane and re-checks-out a clean branch): right
for a fresh dispatch, wrong for recovery. `resume` instead preserves the
in-flight state:
1. The workspace's worktree is left **as-is**: its branch, commits, and
uncommitted changes stay exactly where the worker left them. The branch is
never reset or re-checked-out. (If the worktree was torn down entirely, it's
recreated on the task's existing branch.)
2. The runner pane is recreated or reclaimed: a killed session is stood back
up, and a stale or wedged one (the duplicate-session case) is torn down
before the fresh pane comes up.
3. For a **claude** runner the pane relaunches with `--continue`, so the worker
reloads its prior conversation and picks up mid-thought with full context.
Other runners fall back to re-injecting the task prompt. The agent continues
by reading its own prior work in the worktree. Either way the prompt is
auto-submitted.
If the card has drifted out of `in_progress` (a killed worker whose task
someone moved back), `resume` restores it.
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--workspace ` | string | task's `assigned_to` | Workspace whose worktree holds the in-flight work. |
| `--reason ` | string | `user:cli:resume` | Reason tag recorded in `~/.shelbi/events.log` if the resume has to move the card back into `in_progress`. |
## prio
```text
shelbi task prio [OPTIONS]
```
Re-order a task within its column. The orchestrator picks the top-most ready
task when dispatching, so `prio` is how you steer "what's next" without
opening the TUI.
| Flag | Type | Default | Description |
| --- | --- | --- | --- |
| `--up` | flag | — | Move up one slot. |
| `--down` | flag | — | Move down one slot. |
| `--top` | flag | — | Move to the top of the column. |
| `--bottom` | flag | — | Move to the bottom of the column. |
| `--set ` | integer | — | Move to a specific 0-based slot. |
## edit
```text
shelbi task edit [OPTIONS]
```
With no flags, opens the task's markdown file in `$EDITOR` for free-form
edits. Any field flag switches to non-interactive mode, the path for the
orchestrator, CI, or a script to revise a task after creation. A
non-interactive edit bumps `updated_at`, validates the touched fields
before writing, and emits an `edited` event so the change is visible to
the board and the activity feed. Fields that already have a dedicated
command (`move`, `prio`, `assign`, `depends`) are out of scope; use those.
| Flag | Type | Description |
| --- | --- | --- |
| `--title