# 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 | | --- | --- | --- | --- | | `<TITLE>` | string | — | Human-readable title (positional, required). | | `--id <ID>` | string | slug of title | Override the auto-generated id. | | `--status <STATUS>` | string | `backlog` | Initial status. | | `-d, --description <DESCRIPTION>` | string | empty | Task body. Use `shelbi task edit` later if omitted. | | `--depends-on <ID>` | string (repeatable) | — | Block this task on another. Repeat for multiple deps. | | `--prefers-machine <NAME>` | string | — | Hint for the orchestrator to route this task to a workspace on a specific machine. | | `--workflow <NAME>` | string | project default | Workflow this task runs under. Names a file in `workflows/<NAME>.yaml`. Omit to inherit the project's default workflow. | | `--branch <BRANCH>` | string | generated at dispatch | Pre-fill the task's `branch:` frontmatter field. Omit to let the orchestrator generate `<prefix>/<task-id>` 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 <NAME>` 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 <NAME>` narrows either view to a single workflow. | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--status <STATUS>` | string | — | Restrict to a single status. | | `--ready` | flag | off | Show only unblocked `todo` items, in priority order. Mutually exclusive with `--status`. | | `--workflow <NAME>` | 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 <ID> ``` 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] <ID> ``` Edit a task's dependency list without opening the file. | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--add <DEP>` | string (repeatable) | — | Dependency id to add. | | `--remove <DEP>` | string (repeatable) | — | Dependency id to remove. | ## move ```text shelbi task move --to <STATUS> [OPTIONS] <ID> ``` 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 <STATUS>` | string | — | Destination status (required). Must be a status declared by the task's workflow. | | `--reason <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 <WORKSPACE> <ID> ``` 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 <WORKSPACE>` | string | — | Workspace name (required). | ## unassign ```text shelbi task unassign <ID> ``` Clear a task's `assigned_to` field. The task stays in its current column. ## start ```text shelbi task start [OPTIONS] <ID> ``` 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 <WORKSPACE>` | string | task's `assigned_to` | Assign and launch in one call. | | `--branch <BRANCH>` | string | generated | Override the default branch name. | | `--reason <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] <ID> ``` 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 <WORKSPACE>` | string | task's `assigned_to` | Workspace whose worktree holds the in-flight work. | | `--reason <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] <ID> ``` 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 <N>` | integer | — | Move to a specific 0-based slot. | ## edit ```text shelbi task edit <ID> [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 <TITLE>` | string | Change the display title. The task's `id` stays stable (never re-slugged). | | `--body <TEXT>` | string | Replace the body with this text. | | `--body-file <PATH>` | path | Replace the body with a file's contents. | | (stdin) | pipe | Pipe the body: `shelbi task edit <id> <<EOF … EOF`. | | `--append` | flag | Append the body source to the existing body instead of replacing it. | | `--workflow <NAME>` | string | Set the workflow. Must name an existing `workflows/<NAME>.yaml`. | | `--branch <BRANCH>` | string | Set the `branch:` override. | | `--prefers-machine <NAME>` | string | Set the machine-affinity hint. | | `--no-prefers-machine` | flag | Clear the machine-affinity hint. | | `--sub <OLD> <NEW>` | 2× string | Literal in-place body substitution. Repeatable. | | `--sub-regex <PATTERN> <REPLACEMENT>` | 2× string | Regex body substitution; `REPLACEMENT` may reference capture groups (`$1`). Repeatable. | | `--allow-no-match` | flag | Permit a substitution that matches zero occurrences (default: error, writing nothing). | | `--reason <REASON>` | string | Annotation recorded in the emitted `edited` event. Defaults to `user:cli`. | The body sources (`--body`, `--body-file`, stdin) are mutually exclusive with each other and with the substitution flags. Substitutions replace **all** occurrences of each match and report a per-substitution count; multiple `--sub`/`--sub-regex` apply in command-line order, each operating on the previous one's output. Editing a task whose column is an active status (e.g. `in_progress`) prints a warning: the running worker won't see the change until the task is re-dispatched with `shelbi task start`. ```bash # Fix a stale path and swap a value in place, before dispatch. shelbi task edit fix-login-flow \ --sub "src/old_auth.rs" "src/auth.rs" \ --sub-regex "timeout=([0-9]+)" "timeout=30" # Append a late acceptance criterion without rewriting the body. shelbi task edit fix-login-flow --append <<'EOF' - [ ] Session cookies are cleared on logout. EOF ``` ## rm ```text shelbi task rm <ID> ``` Delete a task file. <Callout type="warning" title="Irreversible"> `shelbi task rm` deletes the task's markdown file outright. There's no trash and no undo. To take a card off the board without destroying its history, move it back to `backlog` instead. </Callout> ## Examples Triage a new request into the backlog with a dependency on existing work: ```bash shelbi task add "Wire up changelog page" \ --depends-on docs-write-getting-started-section ``` Promote a triaged task to `todo`, and the orchestrator picks it up: ```bash shelbi task move docs-write-cli-reference-pages --to todo ``` Reorder ready work so a specific task is dispatched first: ```bash shelbi task prio fix-sidebar-clamp --top ``` Manually dispatch an explicit workspace (overrides the orchestrator's routing): ```bash shelbi task start docs-write-changelog-page --workspace delta ``` Recover a stalled worker without losing its in-flight work, relaunching the pane and resuming the conversation on the same task: ```bash shelbi task resume docs-write-changelog-page ``` ## See also - [Workflows](/docs/guides/getting-started/workflows) — the schema behind `--workflow`, what `--to` is validated against on `task move`, and the default workflow's lifecycle (who moves a task between which statuses). - [`shelbi workflow`](/docs/cli/workflow) — list, show, scaffold, and edit the workflow YAMLs tasks run under. - [Workspaces](/docs/concepts/workspaces) — the pool model that `assign` / `start` route into. - [Orchestrator](/docs/concepts/orchestrator) — how `--reason` tags get parsed by the scheduler. [Source](https://shelbi.dev/docs/cli/task) --- # shelbi agent Manage the project's agents — the roles (system prompt + skills) a workspace runs. List them, print one's instructions, scaffold a new one, or open one in your editor. ```text shelbi agent <SUBCOMMAND> [OPTIONS] ``` `shelbi agent` manages the [agents](/docs/concepts/agents) a project ships with: the roles (a system prompt plus an optional skill set) a [workspace](/docs/concepts/workspaces) loads when it picks up a task. Every project starts with six (`orchestrator`, `developer`, `review`, `qa`, `security`, `adversarial`), and these subcommands surface them and let you author more. Agents live under the project's `agents/` directory: `~/.shelbi/projects/<project>/agents/<name>/` in the default [global mode](/docs/concepts/config-modes) or `<repo>/.shelbi/agents/<name>/` in in-repo mode. Each has an `instructions.md` (its prompt) and an optional `skills/` directory. The shared `agents/_shared/preamble.md` is prepended to every agent's prompt at launch time. The defaults are seeded on first load and never clobbered on upgrade. See [customizing an agent](/docs/concepts/agents#customizing-an-agent). Every subcommand accepts the global `-p / --project <PROJECT>` flag, omitted from the per-subcommand tables below. ## list ```text shelbi agent list ``` Print a table of every agent in the project: its name, the workflow statuses that reference it via the [`agent:` field](/docs/guides/getting-started/workflows#owners-and-agents), the count of skill files under its `skills/` dir, and whether its `instructions.md` has been customized away from the bundled default. ```text AGENT STATUSES SKILLS CUSTOMIZED orchestrator - 0 no developer InProgress, Todo 0 no review Review 1 no qa - 0 no security - 0 no adversarial - 0 no ``` The `CUSTOMIZED` column reads: - `no` — shipped default whose `instructions.md` matches the bundled body byte-for-byte. - `yes` — shipped default that's been edited (or whose file is missing on disk and would re-materialize on the next `shelbi reload`). - `-` — an agent you authored; there's no bundled body to compare against. ## show ```text shelbi agent show <NAME> ``` Print the agent's `instructions.md` to stdout, followed by a `Skills:` section listing each `skills/*.md` file with the `description` from its frontmatter. Errors if the agent doesn't exist. This shows you what *one* agent's file holds in isolation. It does **not** prepend `agents/_shared/preamble.md`. That composition happens at launch time, not at `show` time. To see the shared preamble, `cat ~/.shelbi/projects/<name>/agents/_shared/preamble.md`. | Argument | Type | Default | Description | | --- | --- | --- | --- | | `<NAME>` | string | — | Agent name (positional, required). | ## new ```text shelbi agent new <NAME> ``` Scaffold a new agent directory under `agents/<NAME>/` with a starter `instructions.md` and an empty `skills/`. The starter prompt is a minimal role template, not a copy of `developer`. It's there to be replaced. Errors if an agent with that name already exists. Names must be non-empty, must not start with `.` or `_`, and may only contain `a-z`, `0-9`, `-`, `_`. The name doubles as the identifier referenced from workflow `agent:` fields, so the validator is stricter than POSIX. | Argument | Type | Default | Description | | --- | --- | --- | --- | | `<NAME>` | string | — | Agent name (positional, required). | ## edit ```text shelbi agent edit <NAME> ``` Open the agent's `instructions.md` in `$EDITOR` (falling back to `$VISUAL`, then `vim`). Errors if the agent doesn't exist. Run `shelbi agent new <NAME>` first, or `shelbi reload` to materialize the shipped defaults if this is the first time the project's been opened. Edits take effect the next time the affected agent is launched: for the orchestrator, on the next `shelbi reload`; for a workspace agent, on its next task dispatch. ## Examples See the project's agents and where each is used: ```bash shelbi agent list ``` Read the QA agent's instructions: ```bash shelbi agent show qa ``` Bake your repo's conventions into the default developer agent: ```bash shelbi agent edit developer ``` Scaffold a focused reviewer: ```bash shelbi agent new perf-review shelbi agent edit perf-review ``` Then wire it into a workflow status. Reference the status by `id` (its `name` and `category` live in `statuses.yaml`) and name the agent: ```yaml statuses: - { id: perf, owner: agent, agent: perf-review } ``` ## See also - [Agents](/docs/concepts/agents) — what an agent is, the six shipped roles, on-disk layout, and the customization story. - [Workflows](/docs/guides/getting-started/workflows#owners-and-agents) — the `owner` + `agent` fields that route a status to one of these agents. - [`shelbi workspace`](/docs/cli/workspace) — the slots agents run in, and the `AGENT` column that shows which is loaded where. [Source](https://shelbi.dev/docs/cli/agent) --- # shelbi workspace Inspect and control the project's declared workspace pool — list slots with their host, runner, and loaded agent, change slot runners, and stop stuck panes. ```text shelbi workspace <SUBCOMMAND> [OPTIONS] ``` `shelbi workspace` manages the project's declared workspace pool. A freshly [`init`](/docs/cli/init)'d project starts with an empty pool; the orchestrator provisions it on first boot (asking how many workspaces and which naming scheme), adding each slot with `shelbi workspace add <name>`. Beyond that you use these commands to observe (what's idle or working, which runner and agent are loaded where), grow or shrink the pool (`add` / `rm`), change existing slot runners, and intervene (stop a pane to release a stuck task). A [workspace](/docs/concepts/workspaces) is capacity: a machine, a tmux pane, and a worktree; the [agent](/docs/concepts/agents) inside it is chosen per task by the workflow, which is why `list` reports both. The sidebar shows the same pool, grouped by machine, with a badge marking each state (`⏵` working, `·` idle): Every subcommand accepts the global `-p / --project <PROJECT>` flag, omitted from the per-subcommand tables. ## list ```text shelbi workspace list ``` Print every declared workspace as a row. The columns: | Column | What it shows | | ------- | ----------------------------------------------------------------------------- | | `NAME` | The workspace's stable name (`alpha`, `bravo`, …). | | `HOST` | The machine it's pinned to — `hub` or a declared remote. | | `RUNNER` | The workspace runner name from `workspaces[].runner` (`claude`, `codex`, `opus`, …). | | `AGENT` | The [agent](/docs/concepts/agents) role the in-flight task loaded (`developer`, `qa`, …), or `-` when idle. | | `STATE` | `idle`, or `in_progress: <task-id>` when a task is running here. | ```text NAME HOST RUNNER AGENT STATE alpha hub opus developer in_progress: t-009 bravo hub opus - idle charlie hub opus - idle delta devbox sonnet qa in_progress: t-017 echo devbox sonnet - idle ``` This is the first command the orchestrator runs at session start to snapshot the pool, and the same snapshot you want when answering "who's free, and what are they running?" An `AGENT` of `-` with `idle` state is a free slot. `STATE` here is board-derived. For the poller's live read (`working`, `awaiting_input`, `blocked`), use [`status`](#status). ## set-runner ```text shelbi workspace set-runner <RUNNER> [WORKSPACE ...] shelbi workspace set-runner <RUNNER> --all ``` Change the runner assigned to existing workspace slots. `<RUNNER>` must be a declared key under [`agent_runners`](/docs/configuration/project#agent-runners). This edits `workspaces[].runner`; it does not change `orchestrator.runner`. Use this after setup when you want existing worker slots to launch Codex instead of Claude: ```bash shelbi workspace set-runner codex --all ``` Or migrate selected slots only: ```bash shelbi workspace set-runner codex alpha bravo ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `<RUNNER>` | string | — | Runner name declared in `agent_runners`. | | `[WORKSPACE ...]` | string list | — | Workspace names to update. | | `--all` | flag | off | Update every declared workspace. Cannot be combined with workspace names. | ## stop ```text shelbi workspace stop [OPTIONS] <NAME> ``` Stop the workspace's tmux pane. By default the in-flight task is released back to its `ready` status (`todo` in the default workflow) and its `assigned_to` field is cleared. The board never shows an orphaned `in_progress` card pointing at a dead pane. Pass `--keep-task` when you're about to restart on the same task and don't want the card to move. For a remote workspace, `stop` also kills the remote tmux session so no detached agent process lingers on the other machine. | Flag | Type | Default | Description | | --- | --- | --- | --- | | `<NAME>` | string | — | Workspace name (positional, required). | | `--keep-task` | flag | off | Leave the in-flight task in `in_progress` with `assigned_to` pointing at this workspace. Use when you're about to restart on the same task and don't want the card to move. | ## status ```text shelbi workspace status [NAME] ``` Print observed workspace state from the hub-side poller. Reads `~/.shelbi/workspaces/<name>/status.yaml` files; no tmux probing. Cheaper than `list` and safe to call repeatedly. With a `NAME`, prints a single row plus the raw status.yaml contents. | Flag | Type | Default | Description | | --- | --- | --- | --- | | `[NAME]` | string | — | Workspace to inspect (positional, optional). Omit to show every declared workspace. | <Callout type="note" title="Serving a review branch is a transition, not a subcommand"> Booting a dev server for review is not a workspace subcommand. A review status boots and tears down its server through [transition `run`/`ready` commands](/docs/configuration/workflow#run-ready-and-teardown); see [review workspaces](/docs/concepts/review-workspaces) for the full model. </Callout> ## Examples Snapshot the pool to find a free workspace: ```bash shelbi workspace list ``` Stop a stuck workspace and let the board release its task: ```bash shelbi workspace stop bravo ``` Switch all existing worker slots to Codex: ```bash shelbi workspace set-runner codex --all ``` Restart a workspace on the same task without losing the card: ```bash shelbi workspace stop charlie --keep-task shelbi task start docs-write-cli-reference-pages ``` Inspect a single workspace's raw status without spawning a tmux probe: ```bash shelbi workspace status alpha ``` ## See also - [Workspaces](/docs/concepts/workspaces) — the slot model, the machine-grouped sidebar, and the lifecycle states `status` reports. - [Review workspaces](/docs/concepts/review-workspaces) — tag-routed review slots and the transition commands that boot and tear down their servers. - [Agents](/docs/concepts/agents) — the role shown in the `AGENT` column. - [`shelbi agent`](/docs/cli/agent) — manage the agents a workspace can run. - [Orchestrator](/docs/concepts/orchestrator) — how the scheduler picks a free workspace for the next ready task. [Source](https://shelbi.dev/docs/cli/workspace) --- # shelbi workflow Manage the per-project workflow YAML files — list, show, scaffold, and edit the status schemas tasks run under. ```text shelbi workflow <SUBCOMMAND> [OPTIONS] ``` `shelbi workflow` manages the YAML files that declare the statuses a task moves through, who owns each one, and which git side-effects fire on each transition. Every project has a built-in `default` workflow: the canonical five-status flow (`Backlog → Todo → InProgress → Review → Done`). These subcommands surface it alongside any custom workflows the project authors. The default is virtual: it lives in code, not on disk, until `workflow new` or `workflow edit` writes a file. Workflow files live at `<config-root>/workflows/<name>.yaml`. `<config-root>` is `~/.shelbi/projects/<project>/` in the default [global mode](/docs/concepts/config-modes), `<repo>/.shelbi/` in in-repo mode. A file's basename is its name; the in-file `name:` field must match. Most projects never touch this command. Reach for it when you want a second pipeline: a docs-only flow, a research track that never opens PRs, a feature-stacking flow off a long-lived branch. Every subcommand accepts the global `-p / --project <PROJECT>` flag, omitted from the per-subcommand tables below. ## list ```text shelbi workflow list ``` Print every workflow under the project's `workflows/` directory (`~/.shelbi/projects/<project>/workflows/` in global mode, `<repo>/.shelbi/workflows/` in in-repo mode), one per line, with a count of statuses and the YAML description. A `·` marker in the first column means the workflow is the built-in fallback. No file has been written yet. A blank marker means a file exists on disk. When the workflows directory is empty, the only entry is the built-in `default` workflow. ## show ```text shelbi workflow show <NAME> ``` Print the workflow's per-status table: `STATUS`, `OWNER`, and `AGENT` for each status, in the canonical `statuses.yaml` order (statuses the workflow doesn't declare are dropped). `show default` works even before any file is written, rendering the built-in default from code; any other missing name errors with the resolved file path. ## new ```text shelbi workflow new [OPTIONS] <NAME> ``` Scaffold a new workflow YAML pre-populated with the canonical five-status default. The file's `name:` field is set to `<NAME>` so it matches the basename. For non-`default` names the placeholder description is dropped. The default's "standard one-track flow…" copy would misrepresent a freshly scaffolded workflow whose author hasn't written a real description yet. Errors if a workflow with that name already exists. Names must be non-empty, must not start with `.`, and may only contain `a-z`, `0-9`, `-`, `_`. The name doubles as a YAML identifier referenced from task frontmatter, so the validator is stricter than POSIX. | Flag | Type | Default | Description | | --- | --- | --- | --- | | `<NAME>` | string | — | Workflow name (positional, required). | | `--edit` | flag | off | Open the new file in `$EDITOR` after creating it. | ## edit ```text shelbi workflow edit <NAME> ``` Open a workflow YAML in `$EDITOR` (defaults to `vi`). If the file doesn't exist and the name is `default`, the built-in default is materialized to disk first so you have something concrete to tweak. Any other missing name errors with a hint to run `shelbi workflow new <NAME>` first. ## Examples See every declared workflow plus the built-in default fallback: ```bash shelbi workflow list ``` Inspect the canonical default before customizing it: ```bash shelbi workflow show default ``` Scaffold a docs-only workflow and open it in `$EDITOR`: ```bash shelbi workflow new docs --edit ``` Materialize the built-in default to disk and edit it: ```bash shelbi workflow edit default ``` ## See also - [Workflows](/docs/guides/getting-started/workflows) — the schema, the category model, and how transitions wire into the orchestrator. - [`shelbi task`](/docs/cli/task) — the `--workflow` flag on `task add` and the workflow-aware `--to` validation on `task move`. [Source](https://shelbi.dev/docs/cli/workflow) --- # shelbi events Inspect the hub-global workspace-state transition log — the same feed the orchestrator reacts to. ```text shelbi events <SUBCOMMAND> [OPTIONS] ``` `shelbi events` exposes `~/.shelbi/events.log`, the append-only log of every column transition and workspace state change across the hub. The orchestrator follows it as its trigger stream. To see what the orchestrator is seeing, run `events tail --follow` in another pane. The most common line shapes are: ```text <ts> task=<id> <from> -> <to> reason=<short> <ts> worker=<name> <prev> -> <new> <ts> project=<name> heartbeat ``` `heartbeat` is the periodic wake-up the hub poller writes when the board is otherwise quiet. Cadence comes from the `heartbeat` key in `project.yaml` (default `3m`). See [the events log](/docs/concepts/events-log#heartbeats) for the full shape catalog. Every subcommand accepts the global `-p / --project <PROJECT>` flag. ## tail ```text shelbi events tail [OPTIONS] ``` Print recent transitions, and optionally stream new ones as they're appended. Useful for live debugging, for orchestrator bootstrap (`--follow` in the background and watch with `Monitor`), and for post-hoc forensics (`--since 1h` to scope to a window). | Flag | Type | Default | Description | | --- | --- | --- | --- | | `-n, --lines <LINES>` | integer | `20` | Number of trailing lines to print before following (or before exiting if `--follow` is not set). | | `--since <SINCE>` | duration (e.g. `10m`, `2h`, `1d`) | — | Only show events newer than this. When set, `-n` is ignored and *all* matching lines print. | | `-f, --follow` | flag | off | Stream new transitions as they're appended. Exit on Ctrl-C. | | `--format <FORMAT>` | `raw`, `envelope` | `raw` | Print historical log lines, or a normalized JSON envelope with `kind`, `project`, `timestamp`, and the original `line`. Push-capable harness callbacks use this same envelope. | Shelbi only transports events. Whether a harness wakes the orchestrator through Claude-style `Monitor` output, a callback socket, or the pre-turn drain path, the orchestrator remains responsible for deciding what action to take. ## Examples See the last twenty transitions: ```bash shelbi events tail ``` Follow the log live, the same thing the orchestrator does on bootstrap: ```bash shelbi events tail --follow ``` Print everything from the last two hours, no live tail: ```bash shelbi events tail --since 2h ``` Combine a longer history with a live follow: ```bash shelbi events tail --lines 100 --follow ``` ## See also - [Events log](/docs/concepts/events-log) — the full schema of both line kinds and which reason tags the orchestrator recognizes. - [Orchestrator](/docs/concepts/orchestrator) — how the scheduler turns events into dispatch decisions. [Source](https://shelbi.dev/docs/cli/events) --- # shelbi merge Merge a workspace's branch into the project's default branch, locally or via a GitHub PR. ```text shelbi merge [OPTIONS] <ID> ``` `shelbi merge` bridges a task in `review` and a clean default branch. By default it squash-merges the task's branch into the project's default branch (typically `main`) using the workspace's commit message. Pass `--pr` to push the branch and open a GitHub pull request instead, for teams that require a code-review roundtrip before anything lands. The command does not move the card. After a successful local merge, the orchestrator's "merged → done" sweep (or you, manually) moves the task to `done`. After a `--pr` merge, the card stays in `review` until the PR itself lands. ## Arguments | Argument | Type | Default | Description | | --- | --- | --- | --- | | `<ID>` | string | — | Task id whose branch to merge (required). | ## Flags | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--pr` | flag | off | Push the branch and open a GitHub PR instead of a local squash-merge. | | `-p, --project <PROJECT>` | string | env / cwd lookup | Project to operate on. | ## Examples Squash-merge a reviewed task locally: ```bash shelbi merge docs-write-cli-reference-pages ``` Open a GitHub PR for the branch instead of merging locally: ```bash shelbi merge docs-write-cli-reference-pages --pr ``` ## See also - [Workflows](/docs/guides/getting-started/workflows#lifecycle-who-moves-a-task-between-the-default-statuses) — where `Review` and `Done` sit in the default workflow's lifecycle and what each transition signals. - [Review workspaces](/docs/concepts/review-workspaces) — the upstream step that puts the branch in front of a reviewer before merge. [Source](https://shelbi.dev/docs/cli/merge) --- # shelbi reload Respawn the Shelbi-owned tmux panes in place so a freshly installed binary takes effect. ```text shelbi reload [OPTIONS] [TARGET] [NAME] ``` `shelbi reload` is the "I just rebuilt the binary, pick up the new code" command. It respawns the Shelbi-owned panes in place (the sidebar, the hidden tasks / review / machines panes, and the orchestrator pane) so a new binary takes effect and edits to the orchestrator's instructions or the shared preamble land without tearing down your tmux session. The orchestrator's context survives the respawn. Before the old pane is replaced, it's asked to write `agents/orchestrator/handoff.md` covering its in-flight state; the new instance ingests that file (then deletes it) and resumes mid-thought rather than cold. A missing or timed-out handoff degrades to a cold start, never fatal. The workspace panes are left alone: they re-shell into `shelbi` on every call and pick up the new binary on their next invocation, so there's nothing to restart there. Use it any time `scripts/install.sh` (or your own build) has produced a new binary and you want the long-lived TUI surfaces to start using it. ## Targets Pass a target to reload just one part in place without bouncing the whole hub (and, for `chat`, without losing the orchestrator's context). A targeted reload respawns only the named pane and leaves every other pane and its state untouched. Most targets skip the whole-hub self-heal below. `chat` first self-heals the agent automation that it is about to redeploy. | Target | Reloads | | --- | --- | | `chat` | The orchestrator chat pane. Carries the handoff forward exactly like the whole-hub reload, so the reloaded orchestrator keeps its mid-thought context instead of starting cold. | | `tasks` | The tasks / kanban pane. | | `activity` | The activity / events-feed pane. | | `sidebar` | The workspace-roster sidebar pane. | | `workspace <name>` | A single worker's pane. Local workers respawn with `--resume` so the agent keeps its conversation and its task wiring (`TASK_ID`, hub socket) is preserved. A missing or unknown name is a clear error. | | omitted, or `all` | The whole-hub reload described above (the default). | ```bash # Get the tasks pane back without touching the orchestrator's context. shelbi reload tasks # Respawn just the orchestrator, carrying its handoff forward. shelbi reload chat # Bounce one stuck worker pane. shelbi reload workspace alpha ``` An unknown target, or a bare `workspace` with no name, errors with the valid set so you can self-correct. `shelbi reload` also sweeps any stray `.shelbi/project` marker files. Project resolution reverse-looks-up against the registered project YAMLs, so these markers are redundant. Reload also self-heals the shipped agent workspaces before respawning the orchestrator pane. User-customized `agents/*/instructions.md` files are preserved. In `agents/orchestrator/instructions.md` and `zenmode.md`, Shelbi upgrades only exact stock `pr-create`, `ci-watch`, and `pr-merge` command snippets to carry the probe's repository, base, and head identity, while leaving surrounding custom prose intact. Both unpinned and head-only stock forms are upgraded. It also appends any missing runner-critical orchestrator sections, such as `Polling-only event drain`, instead of overwriting the whole file. Both a whole-hub reload and `shelbi reload chat` apply these automation safeguards before the new orchestrator starts. ## Flags | Flag | Type | Default | Description | | --- | --- | --- | --- | | `-p, --project <PROJECT>` | string | env / cwd lookup | Project to operate on. | ## Examples Rebuild, install, and reload the TUI in one shot: ```bash ./scripts/install.sh && shelbi reload ``` Reload a specific project's panes from outside that project's directory: ```bash shelbi reload --project shelbi ``` ## See also - [Install](/docs/guides/getting-started/install) — where `scripts/install.sh` is documented; `reload` is its TUI-side counterpart. [Source](https://shelbi.dev/docs/cli/reload) --- # shelbi zen Toggle Zen Mode and run the exact-provenance merge primitives the orchestrator sequences. ```text shelbi zen <SUBCOMMAND> [OPTIONS] ``` `shelbi zen` is the CLI face of [Zen Mode](/docs/concepts/zen-mode). The toggle subcommands (`on`, `off`, `pause`, `status`) flip the project's mode in `state.json` and write a `mode=zen <prev> -> <new> reason=user:cli` line to `~/.shelbi/events.log`. The orchestrator reads those events to know when to switch behavior. The remaining subcommands are single-purpose primitives. The orchestrator sequences them per the merge-conditions policy in the project's [`zenmode.md`](/docs/concepts/zen-mode#the-zenmodemd-file) file; each one does exactly one thing and prints a single line on stdout. The same primitives are available to you at the shell when you want to probe a branch or open a PR by hand without flipping the global mode. Every subcommand accepts the global `-p / --project <PROJECT>` flag, omitted from the per-subcommand tables. ## on ```text shelbi zen on ``` Turn Zen Mode on. The orchestrator may auto-promote and auto-merge finished work through the exact-provenance PR flow. Idempotent. Mirrors `Alt+Z` in the TUI. ## off ```text shelbi zen off ``` Turn Zen Mode off. Every promotion goes through manual review. In-flight workspaces keep going; nothing already running is cancelled. ## pause ```text shelbi zen pause ``` Pause Zen Mode: no *new* auto-promotions, but tasks already on the Zen track may still complete their merge flow. Use this to triage incoming work yourself while preserving work already in motion. ## status ```text shelbi zen status ``` Print the current mode, the project's configured local check commands, the resolved danger-paths list (with detected project shapes labeled inline), the last crash timestamp (if any), and the count of in-flight tasks on the Zen track. Cheap and safe to call repeatedly. ## scan ```text shelbi zen scan ``` Print backlog task ids that are *mechanically* eligible for Zen auto-promotion, one per line, in priority order. "Mechanical" means not blocked on dependencies, not opted out via task frontmatter, and no file overlap with anything currently in flight. The orchestrator's prompt applies the judgment categories on top of this list. See [the judgment layer](/docs/concepts/zen-mode#the-judgment-layer). ## dry-run ```text shelbi zen dry-run [OPTIONS] ``` Preview what Zen Mode would do without changing task, board, PR, or branch state. On every tick the backlog scan and merge-conditions bar are evaluated, and each "would have …" decision is logged to stdout, the dedicated dry-run log (`~/.shelbi/logs/zen-dryrun.log`), and the activity feed. No PRs, merges, or board moves happen. Use it before flipping Zen on for real to confirm the policy matches your intent. | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--for <DURATION>` | duration | run until Ctrl-C | Stop after this long. Accepts `30s`, `5m`, `2h`, `1d`, or a bare integer of seconds. | | `--interval <DURATION>` | duration | `5s` | Override the per-tick interval. Same duration grammar as `--for`. | ## probe ```text shelbi zen probe <ID> ``` Run every readiness probe for the task and print the full report as pretty JSON. Covers the four pre-PR conditions the orchestrator checks before opening a PR: local checks, merge-conflict probe, diff size, and danger-path matches. The task must be assigned to a workspace so the probe can locate the repository that holds its named branch. Shelbi checks out that exact branch commit in an isolated temporary worktree, so a newer task reusing the assigned workspace is not modified or tested by mistake. A successful rebase advances the durable task branch. The report freezes `repository`, `repository_id`, `base_branch`, `base_sha`, `integration_sha`, and `head_sha`. Together those fields identify the exact repository and workflow base used for every check and fact, plus the reviewed task commit and exact squash candidate. Pass the same six values to every later PR command. | Argument | Type | Default | Description | | --- | --- | --- | --- | | `<ID>` | string | — | Task id to probe (required). | ## pr-create ```text shelbi zen pr-create <ID> --match-repository <REPOSITORY> --match-repository-id <REPOSITORY_ID> --match-base-branch <BRANCH> --match-base-commit <BASE_SHA> --match-integration-commit <INTEGRATION_SHA> --match-head-commit <HEAD_SHA> ``` Push the task's named branch and open a PR. Idempotent. If an open PR for the branch already exists, Shelbi updates the branch and returns its number only after the PR head matches the exact task branch commit that was pushed. Otherwise, it opens a new PR. Prints the PR number on stdout. Reuse also requires the exact task branch, resolved workflow base branch and commit, and `origin` repository identity. A same-named PR aimed at another base or coming from another repository is rejected. All six `--match-*` flags are required. Copy them from the immediately preceding `probe` report without re-resolving configuration or `origin`. Shelbi rejects the operation if any part of that identity moved after the probe, so mutable workflow, remote, or branch state cannot silently replace what was reviewed. | Argument or flag | Type | Default | Description | | --- | --- | --- | --- | | `<ID>` | string | — | Task id whose branch to push and PR (required). | | `--match-repository <REPOSITORY>` | string | required | Require the `origin` repository selector to equal probe `repository`. | | `--match-repository-id <REPOSITORY_ID>` | string | required | Require the immutable GitHub repository id to equal probe `repository_id`. | | `--match-base-branch <BRANCH>` | string | required | Require the PR target to equal probe `base_branch`. | | `--match-base-commit <SHA>` | string | required | Require the resolved target commit to equal probe `base_sha`. | | `--match-integration-commit <SHA>` | string | required | Require the published PR head to equal probe `integration_sha`. | | `--match-head-commit <SHA>` | string | required | Require the durable task branch to match probe `head_sha`. | ## ci-watch ```text shelbi zen ci-watch <PR_NUMBER> --match-repository <REPOSITORY> --match-repository-id <REPOSITORY_ID> --match-base-branch <BRANCH> --match-base-commit <BASE_SHA> --match-integration-commit <INTEGRATION_SHA> --match-head-commit <HEAD_SHA> [OPTIONS] ``` Watch the PR's checks until they settle or the timeout fires. Prints one of: - `green` — every watched check passed; exit 0. - `red:<check>:<summary>` — at least one check failed; exit 1. - `timeout` — checks still pending when the deadline hit; exit 2. Which checks it watches is auto-selected from the target branch: with branch-protection required checks configured, it watches only those; on an unprotected branch (or one with no required set) it falls back to every check reported on the PR and requires GitHub's merge state to be clean. When required rows exist, a blocked merge state keeps waiting for a required context that has not reported, while an optional failed check does not override passing required checks. All six `--match-*` values must be the unchanged probe identity passed to `pr-create`. Each poll verifies that original repository, base name and commit, and head commit. Shelbi reads the PR identity and status rollup together, including which contexts are required, and grades that one atomic snapshot. There is no separate check-result read that a brief A-to-B-to-A head change could confuse. Incomplete, paginated, or moved-head results fail closed and the whole pinned flow must restart from a fresh probe. | Flag | Type | Default | Description | | --- | --- | --- | --- | | `<PR_NUMBER>` | integer | — | The PR number returned by `pr-create` (required). | | `--match-repository <REPOSITORY>` | string | required | Require probe `repository` on every snapshot. | | `--match-repository-id <REPOSITORY_ID>` | string | required | Require probe `repository_id` on every snapshot. | | `--match-base-branch <BRANCH>` | string | required | Require probe `base_branch` on every snapshot. | | `--match-base-commit <SHA>` | string | required | Require probe `base_sha` on every snapshot. | | `--match-integration-commit <SHA>` | string | required | Require the live PR head to stay at probe `integration_sha`. | | `--match-head-commit <SHA>` | string | required | Bind the candidate back to probe `head_sha`. | | `--timeout <DURATION>` | duration | project's `zen.ci_timeout` (default `15m`) | Override the wait. Accepts `30s`, `5m`, `2h`, `1d`, or a bare integer of seconds. | | `--task <TASK_ID>` | string | — | Resolve the default timeout against the task's workflow's `zen.ci_timeout` (falling back to the project value). Without it, the project default is used directly. | ## pr-merge ```text shelbi zen pr-merge <PR_NUMBER> --match-repository <REPOSITORY> --match-repository-id <REPOSITORY_ID> --match-base-branch <BRANCH> --match-base-commit <BASE_SHA> --match-integration-commit <INTEGRATION_SHA> --match-head-commit <HEAD_SHA> ``` Land the prebuilt squash candidate after `ci-watch` reports `green`. Shelbi verifies that `integration_sha` has sole parent `base_sha` and exactly the tree of `head_sha`, then advances only `refs/heads/<base_branch>` from `base_sha` to `integration_sha` with Git's compare-and-swap lease. A concurrent same-head PR retarget cannot redirect that update. This atomicity applies to the remote ref update, not to GitHub PR metadata. Repositories with branch protection, active rulesets, required-PR rules, fork heads, merge queues, or a non-squash strategy cannot use this boundary. Zen leaves the base unchanged and tells a human to review and merge through the repository's required GitHub workflow. A landed candidate prints its SHA. | Argument or flag | Type | Default | Description | | --- | --- | --- | --- | | `<PR_NUMBER>` | integer | — | The PR number to merge (required). | | `--match-repository <REPOSITORY>` | string | required | Require probe `repository`. | | `--match-repository-id <REPOSITORY_ID>` | string | required | Require probe `repository_id`. | | `--match-base-branch <BRANCH>` | string | required | Require probe `base_branch`. | | `--match-base-commit <SHA>` | string | required | Require probe `base_sha`. | | `--match-integration-commit <SHA>` | string | required | Require probe `integration_sha`. | | `--match-head-commit <SHA>` | string | required | Require probe `head_sha`. | ## Examples Flip Zen on for the current project: ```bash shelbi zen on ``` See the resolved danger-path list and configured local checks: ```bash shelbi zen status ``` Inspect what Zen would consider promoting right now (without promoting anything): ```bash shelbi zen scan ``` Watch the policy in motion for ten minutes without committing to it: ```bash shelbi zen dry-run --for 10m ``` Run the full pre-merge probe by hand on a finished branch and pipe it through `jq`: ```bash shelbi zen probe docs-write-cli-reference-pages | jq . ``` Drive the full merge flow manually, one primitive at a time, exactly the sequence the orchestrator runs internally: ```bash REPORT=$(shelbi zen probe docs-write-cli-reference-pages) REPOSITORY=$(printf '%s\n' "$REPORT" | jq -r .repository) REPOSITORY_ID=$(printf '%s\n' "$REPORT" | jq -r .repository_id) BASE_BRANCH=$(printf '%s\n' "$REPORT" | jq -r .base_branch) BASE_SHA=$(printf '%s\n' "$REPORT" | jq -r .base_sha) INTEGRATION_SHA=$(printf '%s\n' "$REPORT" | jq -r .integration_sha) HEAD_SHA=$(printf '%s\n' "$REPORT" | jq -r .head_sha) PIN=(--match-repository "$REPOSITORY" --match-repository-id "$REPOSITORY_ID" --match-base-branch "$BASE_BRANCH" --match-base-commit "$BASE_SHA" --match-integration-commit "$INTEGRATION_SHA" --match-head-commit "$HEAD_SHA") PR=$(shelbi zen pr-create docs-write-cli-reference-pages "${PIN[@]}") shelbi zen ci-watch "$PR" "${PIN[@]}" --timeout 10m shelbi zen pr-merge "$PR" "${PIN[@]}" ``` Pause Zen during a release window without aborting in-flight merges: ```bash shelbi zen pause ``` ## See also - [Zen Mode](/docs/concepts/zen-mode) — the full mental model, the high-confidence bar, and how to tune the judgment categories per project. - [Orchestrator](/docs/concepts/orchestrator) — how the prompt template that drives Zen's policy is loaded. - [The events log](/docs/concepts/events-log) — the `mode=zen` and `orchestrator:zen-*` lines the toggles and primitives emit. - [`shelbi merge`](/docs/cli/merge) — the user-driven counterpart that lands a single reviewed branch without the Zen bar. [Source](https://shelbi.dev/docs/cli/zen) --- # shelbi open Focus a workspace's tmux pane, creating it (with the agent running) if it doesn't exist yet. ```text shelbi open [OPTIONS] <NAME> ``` `shelbi open` focuses a [workspace](/docs/concepts/workspaces)'s tmux pane, creating it (with the agent already running inside) if it doesn't exist yet. It's the single entry point behind both the sidebar's click-to-focus and the dispatch path: the "does this pane exist?" check lives here, so callers never have to branch on it themselves. For a **local** workspace, an empty pane is created by re-entering this same command under `--as-pane`, the wrapper that owns the agent subprocess and emits a `pane_alive=false` event when it exits. For a **remote** workspace, the pane is a proxy window that `ssh -t … tmux attach`es into the workspace's own remote tmux session, since the lifecycle wrapper isn't deployed to remote machines. `open` focuses (or creates) the pane inside the Shelbi tmux session. It's how you jump to a workspace from the sidebar. To connect your own terminal directly to a workspace's pane instead, use [`shelbi attach`](/docs/cli/attach). ## Arguments | Argument | Type | Default | Description | | --- | --- | --- | --- | | `<NAME>` | string | — | Name of the workspace to open (required). | ## Flags | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--root <PATH>` | string | install-time default / `$SHELBI_ROOT` / `~/.shelbi` | Override the Shelbi root directory. The flag wins over both the env var and the compile-time default. | | `-p, --project <PROJECT>` | string | env / cwd lookup | Project to operate on. | ## Examples Focus (or create) the `alpha` workspace's pane: ```bash shelbi open alpha ``` ## See also - [Workspaces](/docs/concepts/workspaces) — what a workspace is, its pane lifecycle, and the local vs. remote distinction. - [`shelbi attach`](/docs/cli/attach) — connect your terminal directly to a workspace's pane instead of focusing it inside the Shelbi session. [Source](https://shelbi.dev/docs/cli/open) --- # shelbi attach Attach your terminal to a workspace's tmux pane. ```text shelbi attach [OPTIONS] <ID> ``` `shelbi attach` connects your terminal to a [workspace](/docs/concepts/workspaces)'s tmux pane, dropping you straight into the agent's session so you can watch or drive it interactively. Where [`shelbi open`](/docs/cli/open) focuses (and, if needed, creates) the pane *inside* the Shelbi tmux session, `attach` wires *your* terminal to the workspace's pane directly. ## Arguments | Argument | Type | Default | Description | | --- | --- | --- | --- | | `<ID>` | string | — | Workspace to attach to (required). | ## Flags | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--root <PATH>` | string | install-time default / `$SHELBI_ROOT` / `~/.shelbi` | Override the Shelbi root directory. The flag wins over both the env var and the compile-time default. | | `-p, --project <PROJECT>` | string | env / cwd lookup | Project to operate on. | ## Examples Attach your terminal to the `alpha` workspace's pane: ```bash shelbi attach alpha ``` ## See also - [Workspaces](/docs/concepts/workspaces) — what a workspace is and how its pane is created and owned. - [`shelbi open`](/docs/cli/open) — focus (or create) a workspace's pane inside the Shelbi tmux session rather than attaching your own terminal to it. [Source](https://shelbi.dev/docs/cli/attach) --- # Project config Field-by-field reference for a project's YAML — name, repo, machines, orchestrator, agent runners, workspaces, and Zen Mode. The project YAML is the root of a Shelbi project: it declares the repo, the machines work runs on, the runners agents boot, the workspace pool, and the Zen Mode policy. This page is the authoritative field reference. For the *why* behind each block, follow the concept links. ## Where it lives The file's location depends on the project's [config mode](/docs/concepts/config-modes): | Mode | Shared fields | User-local fields | | --- | --- | --- | | Global (default) | `~/.shelbi/projects/<id>.yaml` | same file | | In-repo | `<repo>/.shelbi/project.yaml` (committed) | `~/.shelbi/projects/<id>/local.yaml` | ## The project id comes from the filename A project's **id** is the config file's basename: `~/.shelbi/projects/shelbi.yaml` has the id `shelbi`. The id is the machine-facing key: it names the state folder (`~/.shelbi/projects/<id>/`), the settings file, the tmux session, the event-log `project=<id>` field, and is what `--project` / `$SHELBI_PROJECT` match against. It must be a valid slug: lowercase ASCII letters, digits, `-`, and `_`, starting with a letter or digit. A file whose stem breaks that rule (`Shelbi.yaml`, `my project.yaml`) fails to load with an actionable error; the fix is to rename the file, not to edit a field. The `name:` key *inside* the file is a free-form human **label** (see [top-level fields](#top-level-fields)); it never sets the id. In global mode every field lives in one flat file. In in-repo mode the **shared** fields (`name`, `default_branch`, `default_workflow`, `orchestrator`, `agent_runners`, `zen`, …) are committed and the **user-local** fields (`repo`, `machines`, `workspaces`, `editor`) live in a per-machine `local.yaml`. Putting a field on the wrong side errors at load time. The canonical bucket lists are `SHARED_PROJECT_FIELDS` and `LOCAL_PROJECT_FIELDS` in [`shelbi_core::model`](https://github.com/jlong/shelbi/blob/main/crates/shelbi-core/src/model.rs). ## Minimal example ```yaml # ~/.shelbi/projects/myapp.yaml (id `myapp`, taken from the filename). # `name:` is an optional free-form label; omit it and the id `myapp` shows. name: My App repo: git@github.com:me/myapp.git default_branch: main # Optional: tasks without `workflow:` use workflows/app.yaml instead of # workflows/default.yaml. default_workflow: app machines: - name: hub kind: local work_dir: ~/Workspaces/myapp orchestrator: runner: claude agent_runners: claude: command: claude flags: [] codex: command: codex flags: [] workspaces: - { name: alice, machine: hub, runner: claude } ``` ## Top-level fields | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `name` | string | no | the id | Free-form human display **label** (uppercase, spaces, anything) shown in the sidebar and command palette. It is *not* the id; the id comes from the [filename](#the-project-id-comes-from-the-filename). Omit it and the id is displayed. | | `display_name` | string | no | — | **Deprecated** alias for `name`, accepted for one release with a load-time warning and removed in the next. Move its value to `name:`. When both are set, `display_name` wins as the label. | | `repo` | string | yes | — | Clone URL or path to the repository. User-local. | | `default_branch` | string | no | `main` | Branch new work is based on and the fallback merge target when `git.base_branch` is unset. | | `default_workflow` | string | no | `default` | Workflow used by tasks that omit `workflow:` frontmatter. Names `workflows/<name>.yaml`; explicit task `workflow:` still wins. | | `machines` | list of [Machine](#machines) | yes | — | The hosts workspaces run on. User-local. | | `orchestrator` | [Orchestrator](#orchestrator) | yes | — | Which runner boots the orchestrator. | | `agent_runners` | map of name → [AgentRunner](#agent-runners) | yes | — | The command lines agents and the orchestrator are launched with. | | `workspaces` | list of [Workspace](#workspaces) | no | `[]` | Fixed pool of workspace slots. User-local. | | `zen` | [Zen](#zen) | no | all defaults | Zen Mode check list, CI timeout, and danger paths. | | `editor` | string | no | `$EDITOR` | Editor invoked by `shelbi … edit` commands. User-local. | | `github_url` | string | no | — | Informational GitHub URL recorded by the setup wizard. | | `config_mode` | `global` \| `in-repo` | no | `global` | Which on-disk layout the project uses. Elided from the wire form when `global`. See [config modes](/docs/concepts/config-modes). | | `workspace_poll_interval_secs` | integer | no | `5` | How often the hub poller samples each workspace pane for state changes. | | `workspace_permissions_mode` | string | no | `auto` | Permissions posture rendered into the workspace settings template (`auto` → claude's `acceptEdits`). | | `heartbeat` | [Heartbeat](#heartbeat) | no | `3m` / `60m` | Adaptive hub heartbeat written to `events.log` so the orchestrator's watch fires on a quiet board. Holds at `interval` while work is in flight, backs off toward `max` when quiescent. | | `git` | [Git](#git) | no | all defaults | Base branch, generated task branch prefix, and merge strategy for `shelbi merge` and Zen's auto-merge. | ## machines Each entry is a host where workspaces can run. The worktree for a workspace lives at `<work_dir>/.shelbi/wt/<workspace-name>`. | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `name` | string | yes | — | Machine identifier, referenced by `workspaces[].machine`. | | `kind` | `local` \| `ssh` | yes | — | Whether commands run on this box or over SSH. | | `work_dir` | path | yes | — | Base directory the repo checkout and worktrees live under. `~` is expanded. | | `host` | string | no | `name` | SSH hostname. Required when `kind: ssh`; falls back to `name` if omitted. Ignored for `local`. | | `tags` | list of string | no | `[]` | Capability tags every workspace on this machine inherits (e.g. `gpu`, `review`). A tag declared here applies to all of the machine's slots without repeating it per workspace. Accepts a scalar `tag:` alias and a bare string as shorthand. Elided when empty. | ```yaml machines: - name: hub kind: local work_dir: ~/Workspaces/myapp tags: [review] # every slot on hub inherits the review tag - name: m2 kind: ssh host: m2.local work_dir: ~/work/myapp ``` ## orchestrator The orchestrator is a single agent that watches the board and dispatches work. See [the orchestrator concept](/docs/concepts/orchestrator). | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `runner` | string | yes | — | Name of an entry in [`agent_runners`](#agent-runners) used to boot the orchestrator. | ```yaml orchestrator: runner: claude ``` To run the orchestrator with Codex, point `orchestrator.runner` at a declared Codex runner: ```yaml orchestrator: runner: codex agent_runners: codex: command: codex flags: [] ``` Shelbi validates that `orchestrator.runner` names an entry in `agent_runners`. Claude gets Shelbi's Claude-specific launch wiring (`--append-system-prompt` and the first-message bootstrap prompt). Codex is launched as the configured `command` plus `flags`, then receives an initial startup prompt containing the rendered orchestrator instructions and bootstrap request. Other runners launch exactly as configured. Put any required Codex mode flags in `agent_runners.codex.flags`. Worker hooks are deployed into each worktree under `.shelbi/hooks/`, and Claude's `.claude/settings.json` references `.shelbi/hooks/claude.*` so the pane pushes orchestrator messages to the agent as they arrive. Codex has no hook channel Shelbi can wire without overwriting user-owned `.codex/` configuration, so a Codex worker instead pulls its messages: its startup prompt carries a polling contract that tails `.shelbi/messages/<task-id>.log`. The chosen channel (hooks or polling) is recorded per launch in the events log. `orchestrator.runner` only controls the dashboard/orchestrator pane. It does not change worker slots. Worker slots use their own [`workspaces[].runner`](#workspaces) value, so a project can run a Codex orchestrator while still dispatching tasks to Claude workers, or mix Claude and Codex workers in the same pool. ## agent_runners A map from runner name to the command line agents are launched with. Referenced by `orchestrator.runner` and `workspaces[].runner`. | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `command` | string | yes | — | Executable to invoke (e.g. `claude`, `codex`). | | `flags` | list of string | no | `[]` | Extra flags appended to every invocation. | | `dialog_signatures` | list of object | no | built-in per-runner set | Blocking-dialog text signatures the poller uses to detect a frozen pane. When empty, defaults keyed on `command` apply. | ```yaml agent_runners: claude: command: claude flags: [] codex: command: codex flags: [] ``` ## workspaces The fixed pool of workspace slots. Each owns a stable worktree on its machine and picks up tasks from the board, switching branches (with cleared context) between assignments. See [workspaces](/docs/concepts/workspaces). | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `name` | string | yes | — | Workspace identifier. Names the worktree at `<machine.work_dir>/.shelbi/wt/<name>`. | | `machine` | string | yes | — | Name of a [machine](#machines) this slot runs on. | | `runner` | string | yes | — | Name of an [agent runner](#agent-runners) this slot boots. | | `tags` | list of string | no | `[]` | Capability tags for this slot. A workspace's *effective* tags are these unioned with its machine's [`tags`](#machines). Tag-based routing picks a free workspace whose effective tags are a superset of a status's required [`tags`](/docs/configuration/workflow#statuses). Accepts a scalar `tag:` alias and a bare string as shorthand for a one-element list. Elided when empty. | | `slot` | integer | no | declaration-order index | Numeric slot exported to transition [`run`](/docs/configuration/workflow#transitions) commands as `$SLOT` (e.g. to derive a per-slot port). When unset, falls back to the slot's zero-based index among its machine's workspaces. Elided when unset. | ```yaml workspaces: - { name: alice, machine: hub, runner: claude } - { name: bob, machine: m2, runner: codex } - { name: rev, machine: hub, runner: claude, tags: [review], slot: 0 } ``` To switch existing worker slots after setup, use: ```bash shelbi workspace set-runner codex --all ``` That command edits the same `workspaces[].runner` fields shown above and validates that `codex` is declared in `agent_runners`. You can also update the YAML directly by changing each desired workspace row: ```yaml workspaces: - { name: alice, machine: hub, runner: codex } - { name: bob, machine: m2, runner: codex } ``` Leave `orchestrator.runner` unchanged unless you also want to change the dashboard/orchestrator pane. Run `shelbi workspace list` afterward; its `RUNNER` column shows the runner each workspace will launch. Route a slot to review work by giving it the `review` tag. See [review workspaces](/docs/concepts/review-workspaces) for the tag-routing model. ## zen Zen Mode configuration: which local checks gate a promotion, how long to wait on CI, and which paths always require human review. See [Zen Mode](/docs/concepts/zen-mode). Per-workflow overrides live in the [workflow file](/docs/configuration/workflow#zen); per-task overrides live on the task frontmatter. | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `checks.local` | list of string | no | `[]` | Shell commands run in the worktree root before handoff to CI. Each entry is one command line. | | `ci_timeout` | duration (seconds) | no | `900` (15m) | How long Zen waits for CI to report before timing out the promotion. | | `danger_paths` | see below | no | extend built-ins with `[]` | Glob patterns too sensitive to auto-promote. | `danger_paths` accepts three forms. A bare sequence is shorthand for `extend`, which keeps the built-in danger list and adds yours; `override` replaces the built-ins entirely: ```yaml zen: checks: local: - cargo test --workspace - npm --prefix site test ci_timeout: 1200 danger_paths: extend: [".env", "infra/**"] # or: override: [...], or a bare list ``` ## heartbeat The recurring line the hub poller writes into `events.log` so the orchestrator's `events tail --follow` watch wakes up to sweep active tasks even when no real transition has fired. See [the events log](/docs/concepts/events-log#heartbeats) for how the orchestrator consumes it. The cadence is **adaptive**: it holds at `interval` whenever there's supervisable work in flight (any active, ready, or in-review task, even one emitting no events, which is exactly when the sweep earns its keep), and once the board is quiescent it backs off exponentially, doubling each idle tick up to `max`. Any real event snaps the cadence straight back to `interval`. So a fully idle hub relaxes `3m → 6m → 12m → … → 60m`, then resets to `3m` the moment something happens. `heartbeat` accepts three shapes: | Form | Meaning | | --- | --- | | `heartbeat: 3m` | Bare duration — sets `interval` and keeps the default `max` (`60m`). | | `heartbeat: off` | Disables heartbeats entirely. | | `heartbeat: { interval: 3m, max: 60m }` | Map — sets both bounds explicitly. | The map form's fields: | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `interval` | duration | no | `3m` | Standard cadence, used whenever supervisable work is in flight. | | `max` | duration | no | `60m` | Back-off cap the interval doubles toward while the board is quiescent. A `max` ≤ `interval` pins the cadence at `interval` (no back-off). | Durations take an explicit unit: `45s`, `3m`, `1h`. A bare integer is rejected (there's no implicit unit). ```yaml heartbeat: interval: 3m # standard cadence while work is in flight max: 60m # back-off cap once the board is quiescent ``` ## git Where workspace branches are based, how generated task branches are named, and how they integrate back. See [the merge command](/docs/cli/merge). | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `base_branch` | string | no | `default_branch` | Branch new work is based on and merged into. Falls back to the project's `default_branch`. | | `branch` | string | no | none | Full branch-name template for generated task branches when a task omits `branch:`. Rendered with `{{var}}` substitution, including `{{github_user}}` (authenticated GitHub username) and `{{id}}` (task id), plus the task's frontmatter params. So `branch: '{{github_user}}/{{id}}'` yields e.g. `jlong/fix-login`. | | `merge_strategy` | `squash` \| `merge` \| `rebase` | no | `squash` | How `shelbi merge` integrates a workspace branch. Zen exact-ref auto-integration currently supports `squash`; other strategies require human PR merge. | ```yaml git: base_branch: develop branch: '{{github_user}}/{{id}}' merge_strategy: rebase ``` After substitution, `branch` must use the same safe characters Shelbi allows for generated refs: ASCII letters, numbers, `-`, `_`, and `/`. It must not end with `/`. Resolution runs in order: an explicit `branch:` in the task's frontmatter wins; otherwise Shelbi renders `git.branch`; when neither is set it names the branch after your authenticated GitHub username, falling back to `user`. The shipped `task` workflow sets `branch: '{{github_user}}/{{id}}'`. ## See also - [Config modes](/docs/concepts/config-modes) — global vs. in-repo layout and the shared/user-local field split. - [Workspaces](/docs/concepts/workspaces) and [the orchestrator](/docs/concepts/orchestrator) — the concepts behind `workspaces:` and `orchestrator:`. - [Workflow config](/docs/configuration/workflow) — the per-project status pipeline and its per-workflow `zen` / `git` overrides. [Source](https://shelbi.dev/docs/configuration/project) --- # Understanding Workflows Deep-dives that map a well-known git branching model — trunk-based, git-flow, feature-branch, or forking — onto a Shelbi Workflow. Assumes you already know what a workflow is; each guide gives the statuses, transitions, and orchestrator tweaks for one model. These are reference deep-dives for readers who already know the workflow basics. If a Shelbi Workflow (the per-project YAML that declares the statuses a task moves through and the git side-effects that fire on each transition) is new to you, start with **[Workflows](/docs/guides/getting-started/workflows)** for the fundamentals, then come back here. Shelbi doesn't impose a branching model. Each guide below takes one well-known git branching model and shows the Shelbi Workflow that implements it: the statuses it references, the transitions that push, PR, and merge branches in the right place, and any adjustment the [orchestrator prompt](/docs/guides/getting-started/custom-workflow) needs to route and merge the way that model expects. ## The models Read **[Feature-branch](/docs/guides/understanding-workflows/feature-branch)** first. It's the model Shelbi ships by default, written out in full, and the other three guides are described as deltas from it. | Model | Shape | Guide | | ----- | ----- | ----- | | **Trunk-based** | Short-lived branches, merge to `main` fast. Pairs naturally with Zen auto-merge. | [Trunk-based →](/docs/guides/understanding-workflows/trunk-based) | | **Git-flow** | Long-lived `develop` integration branch with `feature`/`release`/`hotfix` branches. | [Git-flow →](/docs/guides/understanding-workflows/git-flow) | | **Feature-branch** (GitHub-flow) | Branch per task off `main`, PR, merge. The default Shelbi shape. | [Feature-branch →](/docs/guides/understanding-workflows/feature-branch) | | **Forking** | Contributions arrive from forks; a review gate stands between them and `main`. | [Forking →](/docs/guides/understanding-workflows/forking) | <Callout type="note" title="Statuses are defined once, referenced by id"> Every guide shows two YAML shapes. Status *identity* (`id`, `name`, and `category`) is declared once in `workflows/statuses.yaml`, the project-wide status catalog. Each workflow file **references** statuses by `id` and adds only its `owner` and optional `agent:`; it never repeats `name:` or `category:` (the loader rejects a workflow that does). See [Workflows → Schema](/docs/guides/getting-started/workflows#schema) for the full rule. </Callout> ## See also - [Workflows](/docs/guides/getting-started/workflows) — the schema every guide is built on: statuses, categories, transitions, `target:`, parameterization. - [Author a custom Shelbi workflow](/docs/guides/getting-started/custom-workflow) — how to edit the orchestrator prompt each model's routing note asks for. - [Zen Mode](/docs/concepts/zen-mode) — the action-based confidence bar that gates any transition whose actions include `merge`. [Source](https://shelbi.dev/docs/guides/understanding-workflows) --- # shelbi status Print the orchestrator's bootstrap snapshot — a concise human summary by default, or the full LLM-consumable payload with --full. ```text shelbi status [OPTIONS] [COMMAND] ``` `shelbi status` prints the orchestrator's bootstrap snapshot for a project. Bare `shelbi status` emits a concise, human-readable summary of where the board stands right now. Pass `--full` to emit the full sectioned payload the [orchestrator](/docs/concepts/orchestrator) consumes on bootstrap: board + workspaces + zen + handoff-presence. Both the plain summary and `--full` are idempotent and safe to re-run. `--handoff` prints the contents of `HANDOFF.md` from the project's local `work_dir` and then **deletes the file**. It's a no-op when no `HANDOFF.md` is present. Because it's destructive, it's kept separate from `--full` so that bootstrap snapshots stay safe to re-run. The flags **compose**: `shelbi status --full --handoff` emits the full payload *and* drains the handoff note in one call, the exact shape the orchestrator reads when it wakes up and picks up where a previous session left off. ## Flags | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--full` | flag | off | Emit the full sectioned bootstrap payload (board, workspaces, zen, handoff-presence). Idempotent — safe to re-run. | | `--handoff` | flag | off | Print `HANDOFF.md` from the project's local `work_dir`, then delete it. No-op when absent. Destructive; kept separate from `--full` so snapshots stay re-runnable. | | `-p, --project <PROJECT>` | string | env / cwd lookup | Project to operate on. Defaults to `$SHELBI_PROJECT`, or the registered project whose `work_dir` contains the current directory. | ## list ```text shelbi status list ``` The `list` subcommand prints the canonical status catalogue (order, id, name, and category) read from the project's `workflows/statuses.yaml`. The order here is the left-to-right column order every view in the project uses. ## Examples Get a quick human read on the board: ```bash shelbi status ``` Emit the full bootstrap payload for the orchestrator to consume: ```bash shelbi status --full ``` Bootstrap-drain: emit the full snapshot and pick up any pending handoff note in a single call: ```bash shelbi status --full --handoff ``` Print the project's status catalogue (columns, in order): ```bash shelbi status list ``` ## See also - [Orchestrator](/docs/concepts/orchestrator) — what consumes the `--full` payload on bootstrap and how the handoff note keeps sessions continuous. - [`shelbi events`](/docs/cli/events) — the transition log the orchestrator follows after it has read the bootstrap snapshot. [Source](https://shelbi.dev/docs/cli/status) --- # Workflow Field-by-field reference for a workflow YAML — statuses (reference-only), initial_status, transitions and their actions, and per-workflow git/zen overrides. A workflow file declares the pipeline a task moves through: which statuses it references, which side-effects fire on which edges, and any per-workflow git or Zen overrides. For the model behind these fields (owners, categories, the any-to-any transition policy), see [the workflows concept](/docs/guides/getting-started/workflows). ## Where it lives ``` <config-root>/workflows/<name>.yaml ``` `<config-root>` is `~/.shelbi/projects/<name>/` in the default [global mode](/docs/concepts/config-modes) and `<repo>/.shelbi/` in in-repo mode. The file's basename is its id; the in-file `name:` field must match. Every project has a built-in virtual `default` workflow that lives in code until `shelbi workflow new` or `edit` writes a file. See [`shelbi workflow`](/docs/cli/workflow). ## Example ```yaml # ~/.shelbi/projects/myapp/workflows/default.yaml name: default description: standard one-track flow # Reference-only statuses: name + category come from statuses.yaml. 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 } transitions: - { from: in-progress, to: review, actions: [push_branch, open_pr] } - { from: review, to: done, actions: [merge, delete_branch] } ``` ## Top-level fields | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `name` | string | yes | — | Workflow id. Must match the filename (`<name>.yaml`); referenced from task frontmatter. | | `statuses` | list of [Status](#statuses) | yes | — | Ordered statuses this workflow uses. At least one required. Order is the column order in the TUI. | | `description` | string | no | — | Free-form description surfaced in `shelbi workflow list` and the picker. | | `initial_status` | string | no | first status | Stable `id` a new task lands in. Must reference a status declared here. | | `transitions` | list of [Transition](#transitions) | no | — | Side-effect declarations for specific edges. Omitting it means all moves are pure status changes. | | `required_params` | list of string | no | — | Task frontmatter fields every task on this workflow must carry. Every `{{var}}` in [`git.base_branch`](#git) must appear here, or the workflow is rejected at load. See [Required params](#required-params). | | `git` | [Git](#git) | no | inherit project | Per-workflow override of the project's `git:` block. | | `zen` | [Zen](#zen) | no | inherit project | Per-workflow override of the project's Zen Mode config. | ## statuses <Callout type="tip" title="Reference-only form"> A workflow status entry carries **only** `id`, `owner`, and optional `agent`. The display `name` and `category` are declared once in [`statuses.yaml`](/docs/configuration/statuses). Repeating an inline `name:` or `category:` here fails the load. This keeps identity from drifting across workflows. </Callout> | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `id` | string | yes | — | Stable id, declared in [`statuses.yaml`](/docs/configuration/statuses). Conventionally lowercase kebab-case. | | `owner` | `user` \| `agent` | yes | — | Who moves the task next when automation is off. `agent` makes it eligible for auto-dispatch; `user` waits. | | `agent` | string | no | derived from category | Which agent runs this status under automation. Defaults to `developer` for `active` and `orchestrator` for `ready` when omitted. `None` means no automation path. | | `tags` | list of string | no | `[]` | Required workspace tags. The task routes to a free workspace whose [effective tags](/docs/configuration/project#workspaces) are a **superset** of this set (set-AND). Empty means any free workspace — the default. Accepts a scalar `tag:` alias and a bare string as shorthand. Elided when empty. | <Callout type="note" title="Tags route work to the right slot"> A status's `tags` are how a workflow says "this step needs a workspace with these capabilities." Pair them with a workspace/machine that carries the same [`tags`](/docs/configuration/project#workspaces) so the orchestrator loads the task there. `tags: [review]` on a handoff status is the primitive behind [review workspaces](/docs/concepts/review-workspaces). </Callout> ## transitions Each entry declares the hub-side side-effects that fire when a task crosses one edge. Transitions do **not** restrict which moves are legal. Moves are any-to-any; unlisted edges are pure status changes. See [transitions](/docs/guides/getting-started/workflows#transitions). | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `from` | string | yes | — | Status `id` the task moves out of. Must be a declared status. | | `to` | string | yes | — | Status `id` the task moves into. Must be a declared status. | | `actions` | list of [action](#actions) | no | `[]` | Ordered git side-effects to run on this edge. Failures short-circuit the rest. | | `target` | string | no | resolved `base_branch` | Overrides where `merge` / `open_pr` land for this edge only. Supports `{{var}}` placeholders resolved from task params. | | `run` | list of string | no | `[]` | Ordered [shell commands](#run-ready-and-teardown) to run on this edge, in the task's worktree. Compose with — and run *after* — this edge's `actions`. | | `ready` | string | no | — | Shell command polled until it exits 0 (or `ready_timeout` elapses) after `run`. Confirms a launched server is up. | | `ready_timeout` | duration (seconds) | no | `90` | How long to poll `ready` before failing the edge. Ignored when `ready` is unset. | ### actions The six hub-side action primitives an edge's `actions` list may contain: | Action | Effect | | --- | --- | | `push_branch` | Push the task's branch to origin. | | `open_pr` | Open a PR for the task's branch. | | `merge` | Merge the task's branch into its target. Trips [Zen's high-confidence bar](/docs/concepts/zen-mode#the-high-confidence-bar). | | `close_pr` | Close any open PR without merging. | | `delete_branch` | Delete the local and remote branch. | | `restack` | Rebase the task's branch onto its parent's current branch. | ```yaml transitions: - from: in-progress to: review target: develop # this edge merges into develop, not base_branch actions: [push_branch, merge] ``` ### run, ready, and teardown Beyond the six git actions, an edge can run arbitrary shell commands. This is what lets a status *do* something on entry — install dependencies, boot a dev server, warm a cache — and undo it on exit. - **`run`** is a list of commands executed in order, in the task's worktree, on the assigned workspace's machine (host-routed over SSH for remote machines). They run **after** this edge's git `actions`, sharing the same short-circuit contract: the first non-zero exit aborts the edge. - **`ready`** is a single command polled until it exits 0, after `run` finishes. Use it to block until a server the `run` step launched actually answers. `ready_timeout` (seconds, default `90`) caps the wait; a timeout fails the edge. - **Teardown** is not a separate field: express it as the `run` of the *exit* transition (the edge that moves the task out of the status). Each command runs synchronously, so a long-lived server must background itself — the edge is considered entered the moment the launcher returns, and `ready` is what confirms the server is up. Every command has these variables exported into its environment: | Variable | Value | | --- | --- | | `$SLOT` | The workspace's numeric [`slot`](/docs/configuration/project#workspaces) — e.g. derive a per-slot port with `$((3000 + $SLOT))`. | | `$SHELBI_TASK` | The task id. | | `$SHELBI_BRANCH` | The task's git branch. | | `$SHELBI_WORKTREE` | Absolute path to the worktree the commands run in. | | `$SHELBI_MACHINE` | The machine the workspace runs on. | ```yaml transitions: # Entering review: boot a dev server on a per-slot port, wait for it. - from: in-progress to: review run: - npm install - npm run dev -- --port $((3000 + $SLOT)) & ready: curl -fsS http://localhost:$((3000 + $SLOT)) > /dev/null ready_timeout: 120 # Leaving review: tear the server down. - from: review to: done actions: [merge, delete_branch] run: - pkill -f "port $((3000 + $SLOT))" || true ``` <Callout type="note" title="run needs an assigned workspace"> A transition that declares `run`/`ready` must fire on a task that's assigned to a workspace — that's what tells Shelbi *where* to run the commands. Route the status to a workspace with the status's [`tags`](#statuses); an unassigned task on such an edge is an error, not a silent hub-side run. </Callout> ## review A workflow-scoped recipe for how the [Review agent](/docs/concepts/review-workspaces) boots this branch so a human can run it. This is the recommended way to stand up a review server: Shelbi resolves the recipe's `$SLOT` / `$PORT` against the review workspace's slot and injects it into the Review agent's prompt, and the agent runs it verbatim, health-checking it and handing back a URL. 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. | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `workdir` | string | no | worktree root | Directory to run the recipe in, relative to the worktree root. | | `setup` | string | no | — | One-shot install/build command; must exit 0 before serving. | | `serve` | string | yes | — | Command that starts the dev server, bound to the slot's port via `$SLOT`. | | `ready` | string | no | — | Readiness probe polled until it exits 0. | | `url` | string | no | — | Reviewable URL handed to the human; also gates the review interface's "Open Browser" action. | Every field may reference the review slot's port as `$SLOT` or `$PORT` (both `$X` and `${X}` spellings); Shelbi substitutes the resolved port before the recipe reaches the agent. ```yaml review: workdir: site setup: npm install --no-audit --no-fund serve: npm run dev -- -p $SLOT ready: curl -sf http://localhost:$SLOT url: http://localhost:$SLOT ``` <Callout type="note" title="review: vs. a transition run: serve block"> The [`run` / `ready`](#run-ready-and-teardown) block on a review-entering transition also boots a server, but it runs **hub-side** and declaratively. Prefer `review:` for a serve recipe: the Review agent executes it, so it can health-check, summarize failures, and apply a human's tweak. Keep transition `run:` for hub-side side-effects that aren't the review server itself. </Callout> ## required_params The task frontmatter fields every task on this workflow must carry. This is the contract that makes a templated [`git.base_branch`](#git) safe: because `base_branch` resolves only against a task's own frontmatter params (unlike `branch`, it gets no `{{id}}` or `{{github_user}}` context), a task that omits the field produces an unresolved base branch at dispatch. Shelbi validates the invariant when the workflow loads: every `{{var}}` in `git.base_branch` must appear in `required_params`, or the load fails naming the variable and the workflow. The shipped `subtask` workflow declares it: ```yaml name: subtask required_params: [task] # every subtask must set `task:` git: base_branch: task/{{task}} # the parent task's branch ``` Leave it out for workflows whose `base_branch` is a literal like `main`. ## git Per-workflow override of the project's [`git:`](/docs/configuration/project#git) block. When omitted, the workflow inherits `base_branch`, `branch`, and `merge_strategy` from the project. Field values may contain `{{var}}` placeholders resolved against task params at load time. | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `base_branch` | string | no | project's `base_branch` | Branch this workflow's tasks are based on and merged into. | | `branch` | string | no | project's `branch` | Full branch-name template for generated task branches in this workflow, rendered with `{{var}}` substitution when a task omits `branch:`. The shipped `task` workflow uses `branch: '{{github_user}}/{{id}}'`. | | `merge_strategy` | `squash` \| `merge` \| `rebase` | no | project's `merge_strategy` | How this workflow's branches integrate back. | ```yaml git: base_branch: develop branch: 'app/{{id}}' merge_strategy: squash ``` Explicit task `branch:` frontmatter still wins over a workflow `branch` template. If no task, workflow, or project `branch` is configured, Shelbi names the generated branch after your authenticated GitHub username, falling back to `user` when it cannot determine one. ## zen Per-workflow override of the project's [`zen:`](/docs/configuration/project#zen) block. Each subfield is independently optional: override just `checks`, just `ci_timeout`, just `danger_paths`, or any combination. Unset subfields fall back to the project. The canonical use is a `research:` workflow opting out of code-style checks without affecting `default`. | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `checks.local` | list of string | no | project's `zen.checks.local` | Replaces the project's local check list for this workflow's tasks. | | `ci_timeout` | duration (seconds) | no | project's `zen.ci_timeout` | Overrides how long Zen waits on CI. | | `danger_paths` | `extend` / `override` / bare list | no | project's `zen.danger_paths` | Overrides the danger-path globs. Same shape as the project field. | ```yaml # workflows/research.yaml — no code checks, no auto-merge risk name: research statuses: - { id: todo, owner: agent, agent: developer } - { id: done, owner: user } zen: checks: local: [] # skip the project's cargo/npm checks entirely ``` ## See also - [Workflows](/docs/guides/getting-started/workflows) — owners, categories, the transition model, and worked examples. - [Statuses](/docs/configuration/statuses) — the `statuses.yaml` catalog that supplies each status's `name` and `category`. - [`shelbi workflow`](/docs/cli/workflow) — list, show, scaffold, and edit workflow files. [Source](https://shelbi.dev/docs/configuration/workflow) --- # Trunk-based Short-lived branches that merge to main fast, with no separate review column. The merge is action-based, so it pairs naturally with Zen Mode auto-merge. **Trunk-based development** keeps a single always-releasable trunk (`main`) and merges small, short-lived branches into it as fast as they go green. There's no long-lived integration branch and no drawn-out review column. Work goes from *in progress* to *merged* the moment checks pass. In Shelbi this is the [feature-branch model](/docs/guides/understanding-workflows/feature-branch) with the `Review` handoff collapsed: the `merge` action moves onto the `InProgress → Done` edge. Because [Zen Mode's confidence bar is action-based](/docs/guides/getting-started/workflows#what-fires-zen-modes-high-confidence-bar) (it gates *any* transition whose actions include `merge`, regardless of which statuses sit on either side), that collapsed edge trips exactly the same high-confidence probe the review-gated model does. Trunk-based is where Zen [auto-merge](/docs/concepts/zen-mode#the-high-confidence-bar) earns its keep: the whole point is to land green work without a manual accept step. ## The board Four columns, no `Review`. A task branch is cut off `main` at **In Progress** and squash-merged straight back into `main` at **Done**, the moment checks pass and Zen clears the bar. ## The workflow Trunk-based reuses the shipped [status catalog](/docs/guides/getting-started/workflows#schema) (`workflows/statuses.yaml`). It just leaves the `review` status out of the flow. Identity (`name`, `category`) stays in the catalog; the workflow references statuses by `id` and adds only `owner` and `agent:`: ```yaml # workflows/trunk.yaml — reference-only (identity lives in statuses.yaml) name: trunk description: Trunk-based — short-lived branches, merge to main on green. git: base_branch: main merge_strategy: squash # one squashed commit per task on trunk 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 } initial_status: backlog transitions: - from: in-progress to: done actions: [push_branch, open_pr, merge, delete_branch] - from: in-progress to: canceled actions: [close_pr, delete_branch] zen: checks: local: - cargo build --workspace - cargo test --workspace ``` The single `in-progress → done` transition does the whole cycle: push the short branch, open a PR into `main`, squash-merge it, and delete the branch. There is no `handoff`-category status because there's no separate review stage. The [`zen.checks`](/docs/concepts/zen-mode) that run before the merge bar *are* the gate. ## Orchestrator adjustments Trunk-based only pays off with auto-merge on, so the orchestrator prompt's [Zen judgment](/docs/guides/getting-started/custom-workflow#2-tune-zens-judgment-categories-for-this-projects-reality) does the accepting a human would otherwise do at a `Review` column. Two tweaks are worth encoding: ```markdown ## Zen Mode — trunk-based ### Merge conditions A task on the `InProgress → Done` edge may auto-merge when **all** hold: 1. The workspace has written its review marker (work is complete). 2. `shelbi zen probe` reports every local check green. 3. The diff touches no path in the danger set (migrations, auth, CI). Otherwise, hold the card in `InProgress` and surface it: "`<task>` is green but touches `<danger-path>` — merge to main?" Trunk stays releasable because nothing lands without a green probe. ``` Keep branches short. If a card sits in `InProgress` long enough to drift from `main`, prefer restacking or re-cutting it over a big catch-up merge. That's the discipline trunk-based trades the review column for. ## See also - [Feature-branch](/docs/guides/understanding-workflows/feature-branch) — the review-gated version of the same branch-per-task shape. - [Zen Mode](/docs/concepts/zen-mode) — the confidence bar and the `zen.checks` that stand in for a review column here. - [Workflows: what fires the high-confidence bar](/docs/guides/getting-started/workflows#what-fires-zen-modes-high-confidence-bar): why a collapsed `InProgress → Done` merge still trips the probe. [Source](https://shelbi.dev/docs/guides/understanding-workflows/trunk-based) --- # shelbi send Inject a follow-up message as keystrokes into a running workspace's tmux pane. ```text shelbi send [OPTIONS] <ID> <MESSAGE> ``` `shelbi send` injects a follow-up message into a running [workspace](/docs/concepts/workspaces). The text is typed into the workspace's live tmux pane as keystrokes, exactly as if you'd typed it there yourself. Use it to nudge, correct, or add a note to an agent that's already working, without interrupting or restarting the pane. Shelbi delivers the text first, sends Enter as a separate key event, and then waits for evidence that the pane accepted the submission. If the text remains in the input box, Shelbi retries Enter once. A busy Claude pane may keep the accepted text visible as queued input until its current turn ends. Every result is recorded in `~/.shelbi/events.log` as `status=submitted`, `status=queued`, or `status=stuck`; a stuck result also makes the command fail instead of silently waiting for a human keypress. Codex and custom runners still receive the split text/Enter sequence, but Shelbi does not apply Claude's UI parser to them. Their result is explicit `status=unverified`, which preserves delivery without claiming runner-specific proof. `NAME` is resolved against the project YAML's `workspaces:` block. Because `send` writes keystrokes into a pane, the message content is **ephemeral**. Shelbi records and verifies the delivery result, but the worker does not semantically acknowledge or persist the message body. When you need a message that survives, is logged, and is acked by the workspace, reach for [`shelbi message`](/docs/cli/message) instead. See the comparison there. ## Arguments | Argument | Type | Default | Description | | --- | --- | --- | --- | | `<ID>` | string | — | Workspace name, resolved against `workspaces:` (required). | | `<MESSAGE>` | string | — | The message to type into the pane (required). | ## Flags | Flag | Type | Default | Description | | --- | --- | --- | --- | | `-p, --project <PROJECT>` | string | env / cwd lookup | Project to operate on. Defaults to `$SHELBI_PROJECT`, or the registered project whose `work_dir` contains the current directory. | ## Examples Nudge the `alpha` workspace with a follow-up instruction: ```bash shelbi send alpha "also update the changelog before you commit" ``` ## See also - [`shelbi message`](/docs/cli/message) — the durable, acked counterpart: appends a JSON record to a task's message log instead of typing into a pane. - [Workspaces](/docs/concepts/workspaces) — what a workspace is and how the `workspaces:` block names them. [Source](https://shelbi.dev/docs/cli/send) --- # Statuses Field-by-field reference for statuses.yaml — the project-wide status catalog that gives every status its id, display name, and category. `statuses.yaml` is the single source of truth for **status identity** in a project. Every [workflow](/docs/configuration/workflow) references these entries by `id` and inherits their `name` and `category`; workflows may pick a subset but cannot reorder or rename them. Declaration order here is the canonical left-to-right column order in the TUI's all-view. ## Where it lives ``` <config-root>/workflows/statuses.yaml ``` `<config-root>` is `~/.shelbi/projects/<name>/` in the default [global mode](/docs/concepts/config-modes) and `<repo>/.shelbi/` in in-repo mode, the same directory as the workflow files. ## Example ```yaml # ~/.shelbi/projects/myapp/workflows/statuses.yaml 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 } ``` ## Top-level fields | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `statuses` | list of [Status](#status) | yes | — | Ordered catalog of every status in the project. Order is significant — it's the column order in the TUI all-view. | ## status | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `id` | string | yes | — | Stable identifier referenced from every workflow's `statuses:` list. Must be unique and non-empty. Conventionally lowercase kebab-case. | | `name` | string | yes | — | User-facing display label (e.g. `Backlog`, `In Progress`). Must be non-empty. | | `category` | [category](#categories) | yes | — | Closed semantic category. Generic code keys off this, so a renamed status keeps its behavior. | ## categories The `category` field is a closed vocabulary of exactly six values: generic code (the orchestrator, Zen Mode, event-log reactions) keys off the category, not the display name, so renaming a status leaves its semantics intact. See [status categories](/docs/guides/getting-started/workflows#status-categories). | Category | Meaning | | --- | --- | | `backlog` | Not yet ready for work — triage stage. | | `ready` | Ready to be picked up by whoever owns it. | | `active` | Owner is working on it now. | | `handoff` | One owner finished their part; another's input is required next. | | `done` | Terminal — accepted, shipped. | | `archived` | Terminal — closed without shipping (cancelled, won't fix, duplicate). | <Callout type="warning" title="At least one terminal category"> Loading fails if no status has category `done` or `archived`. A board with no terminal state is degenerate, since a task could never leave it. A category set is otherwise unconstrained: repeating a category is allowed (a long pipeline might have several `active` statuses), though a missing `handoff` or a duplicated single-instance category raises a non-fatal warning. </Callout> ## See also - [Workflows](/docs/guides/getting-started/workflows) — how workflows reference these statuses and the category model in depth. - [Workflow config](/docs/configuration/workflow) — the reference-only status form that pairs with this catalog. [Source](https://shelbi.dev/docs/configuration/statuses) --- # Git-flow A long-lived develop integration branch with feature, release, and hotfix branches. Model it as two Shelbi Workflows — features land on develop, releases ship develop to main. **Git-flow** keeps two long-lived branches, `main` (released) and `develop` (integration), and cuts three kinds of short branches off them: `feature/*` off `develop`, `release/*` off `develop` toward `main`, and `hotfix/*` off `main`. It trades the simplicity of trunk-based for an explicit staging branch where work accumulates before a release. Git-flow doesn't fit one board, because a feature and a release are different lifecycles. Model it as **two Shelbi Workflows**: a `feature` workflow whose tasks branch off and merge back into `develop`, and a `release` workflow whose task PRs `develop` into `main`. Hotfixes reuse the [feature-branch](/docs/guides/understanding-workflows/feature-branch) default with `main` as their base. ## The board Every card on the `feature` workflow cuts its branch off `develop` and squash-merges back into `develop`. The **On Develop** column is "landed on the integration branch," not "released." Shipping to `main` is a separate `release` task. ## The statuses Status identity lives once in the project's [status catalog](/docs/guides/getting-started/workflows#schema), `workflows/statuses.yaml`. Git-flow adds one status the shipped defaults don't have: `on-develop`, the done-category landing for feature work that's merged into `develop` but not yet released. It also adds `staging` and `released` for the release workflow below. Declare them (`id` + `name` + `category`) in the catalog first, then reference them by `id`: ```yaml # workflows/statuses.yaml — git-flow statuses alongside the defaults. statuses: # shipped defaults - { 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: canceled, name: Canceled, category: archived } # git-flow additions - { id: on-develop, name: On Develop, category: done } - { id: staging, name: Staging, category: active } - { id: released, name: Released, category: done } ``` ## The feature workflow ```yaml # workflows/feature.yaml — reference-only; identity lives in statuses.yaml name: feature description: Git-flow feature — branch off develop, merge back into develop. git: base_branch: develop # feature/* branches cut off develop 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: orchestrator } - { id: on-develop, owner: user } - { id: canceled, owner: user } initial_status: backlog transitions: - from: in-progress to: review actions: [push_branch, open_pr] # PR base is develop (the workflow base_branch) - from: review to: on-develop actions: [merge, delete_branch] # squash into develop - from: in-progress to: canceled actions: [close_pr, delete_branch] ``` Setting `git.base_branch: develop` is the whole trick: branches are cut from `develop` and the `open_pr` action targets `develop` because no transition overrides it with a [`target:`](/docs/guides/getting-started/workflows#per-transition-target). ## The release workflow A release task PRs the accumulated `develop` into `main`. Pre-fill its `branch:` so the orchestrator operates on `develop` [directly instead of cutting a new branch](/docs/guides/getting-started/workflows#the-branch-task-field): ```yaml # workflows/release.yaml — reference-only; identity lives in statuses.yaml name: release description: Git-flow release — PR develop into main and tag. git: base_branch: main merge_strategy: merge # a true merge preserves develop's history statuses: - { id: staging, owner: agent, agent: developer } - { id: review, owner: user, agent: orchestrator } - { id: released, owner: user } initial_status: staging transitions: - from: staging to: review actions: [push_branch, open_pr] # open PR: base main, head develop - from: review to: released actions: [merge] # merge develop into main; keep develop ``` ```markdown --- id: release-2025-07 title: Release 2025.07 workflow: release branch: develop # operate on develop as-is; don't cut a branch --- ``` Note `merge_strategy: merge` (not `squash`) and the absence of `delete_branch`: `develop` is long-lived, so the release preserves its history and never deletes it. ## Orchestrator adjustments Two routing rules keep the three lifecycles from colliding. Encode them in the orchestrator prompt (see [Author a custom workflow](/docs/guides/getting-started/custom-workflow#1-pin-certain-task-shapes-to-specific-workspaces)): ```markdown ## Routing rules — git-flow - **Features** default to the `feature` workflow (base `develop`). Ordinary work goes here; do not route feature work at `main`. - **Hotfixes** — titles prefixed `hotfix:` — use the `default` workflow (base `main`) so the fix branches off the released line. After a hotfix merges to `main`, open a follow-up task to merge `main` back into `develop` so the branches don't diverge. - **Releases** are created by the user on the `release` workflow with `branch: develop` pre-filled. Never auto-promote a release — cutting one is a human decision about what's ready to ship. ``` The develop-back-merge after a hotfix is the one bit of bookkeeping git-flow needs that a single workflow can't express. Surface it as a follow-up task rather than trying to encode it as a transition. ## See also - [Workflows: parameterization](/docs/guides/getting-started/workflows#parameterization) — `{{var}}` in `base_branch` if you run several concurrent integration branches instead of one `develop`. - [Workflows: the `branch:` task field](/docs/guides/getting-started/workflows#the-branch-task-field) — how a release task operates on `develop` without cutting a new branch. - [Feature-branch](/docs/guides/understanding-workflows/feature-branch) — the simpler model hotfixes fall back to. [Source](https://shelbi.dev/docs/guides/understanding-workflows/git-flow) --- # shelbi message Append a durable JSON record to a task's file-based message log that its workspace tails, then query or wait for the worker's delivery ack. ```text shelbi message [OPTIONS] <ID> <KIND> <BODY> shelbi message status <MSG-ID> ``` `shelbi message` pushes a message to a task's assigned [workspace](/docs/concepts/workspaces) through a **file-based message log** at `<worktree>/.shelbi/messages/<task-id>.log`. Each call appends one durable JSON record. The workspace tails that log and folds the message into what it's doing. `shelbi message` does not type text or Enter into the tmux pane, so the pane's verified-submit mechanism intentionally does not apply. Runner hooks consume the file record. Keeping this path file-based avoids duplicate delivery and lets the message survive a pane restart. ### Queued is not delivered The push being durable is not the same as the worker having read it. A worker only drains and acknowledges its messages at the end of a turn, which for a busy worker can be well after you send. So a plain `shelbi message` reports the message as **queued**, not delivered, and does not print a success check. To learn the real outcome, either block on it or query it: - `--wait[=SECS]` blocks until the worker confirms delivery (default 120s), and **exits non-zero** if the window elapses with no confirmation. - `shelbi message status <msg-id>` reports the current state (`delivered`, `queued`, or `unconfirmed`) from the durable events stream, exiting 0 only when the worker has acked. A message to a task that is already `done`, or to a workspace with no live reader, is reported as undeliverable rather than silently queued. `<KIND>` classifies the message so the workspace knows how to treat it: | Kind | Meaning | | --- | --- | | `reply` | Response to a workspace's `request-clarification`. Pair with `--in-response-to <question-id>`. | | `directive` | Course correction — "stop what you're doing, the spec changed." | | `context` | Additional background info the workspace should fold in. | ## send vs message `send` and `message` are easy to confuse. They both get words to a running workspace, but through different channels: | | `shelbi send` | `shelbi message` | | --- | --- | --- | | Channel | Keystrokes into the tmux pane | JSON record in the message log file | | Durability | Message text is ephemeral; delivery verdict is recorded in `events.log` | Durable record in `<worktree>/.shelbi/messages/<task-id>.log` | | Delivery proof | Pane submission is verified; no worker semantic ack | Worker acks the `msg_id` at its next turn; query with `status` or block with `--wait` | | Typed / classified | Free text | `reply` / `directive` / `context` | | Reach for it when | A quick, live nudge to a pane | A message that must survive, be logged, and be confirmed | Rule of thumb: use [`shelbi send`](/docs/cli/send) for an off-the-cuff nudge, and `shelbi message` when the workspace genuinely needs to receive, record, and act on the message. ## Arguments | Argument | Type | Default | Description | | --- | --- | --- | --- | | `<ID>` | string | — | Task id whose assigned workspace receives the message (required). | | `<KIND>` | `reply` \| `directive` \| `context` | — | Message kind (required). | | `<BODY>` | string | — | Message body (required). | ## Flags | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--in-response-to <QUESTION-ID>` | string | — | Question id this message replies to (sets `in_response_to`). Typically paired with `kind = reply`. | | `--wait [<SECS>]` | int | 120 when bare | Block until the worker confirms delivery, polling the events stream. Exits non-zero if the window elapses without an `ack=worker`. | | `-p, --project <PROJECT>` | string | env / cwd lookup | Project to operate on. Defaults to `$SHELBI_PROJECT`, or the registered project whose `work_dir` contains the current directory. | ## Subcommands ### `shelbi message status <MSG-ID>` Report a pushed message's delivery state from `events.log`, keyed on the `msg-id` that `shelbi message` printed. Prints `delivered`, `queued`, or `unconfirmed` and exits 0 only when the worker has acked, so a script can gate on it. ## Examples Send a course correction to the workspace working `add-auth`: ```bash shelbi message add-auth directive "hold off — the auth spec changed, wait for the updated task" ``` Reply to a workspace's clarification question: ```bash shelbi message add-auth reply "yes, use the existing session table" --in-response-to q-004 ``` Fold in extra background without redirecting the work: ```bash shelbi message add-auth context "the staging DB is seeded with the fixtures you'll need" ``` Send a directive and block until the worker confirms it read it (fail the script if it does not within 90 seconds): ```bash shelbi message add-auth directive "the auth spec changed, wait for the update" --wait 90 ``` Check delivery of an earlier push without blocking: ```bash shelbi message status m-1785764991921-86377 ``` ## See also - [`shelbi send`](/docs/cli/send) — the ephemeral counterpart: types a message straight into the workspace's pane instead of the durable message log. - [Workspaces](/docs/concepts/workspaces) — what a workspace is and how a task gets assigned to one. [Source](https://shelbi.dev/docs/cli/message) --- # Global config Field-by-field reference for the hub-wide files — ~/.shelbi/config.yaml (UI preferences) and ~/.shelbi/keys.yaml (keybindings). Two files under `~/.shelbi/` hold hub-wide settings that apply across every project on your machine, independent of any single project's [config mode](/docs/concepts/config-modes): `config.yaml` (UI preferences) and `keys.yaml` (keybindings). Both are optional: absent or partial files fall back to built-in defaults, never an error. <Callout type="note" title="Home resolution"> `~/.shelbi/` is the default base. It can be relocated via `--root`, then `$SHELBI_ROOT`, then `$SHELBI_HOME`, before falling back to `$HOME/.shelbi`. Every path below joins a filename onto that base. </Callout> ## `config.yaml` Per-user UI preferences. Distinct from `shelbi.yaml` (the per-project `last_launched` index) so a future `shelbi config reset` can wipe UI tweaks without touching hub bookkeeping. A missing file, or one that omits a block, resolves to defaults. **Path:** `~/.shelbi/config.yaml` | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `keymap.zen_toggle` | [chord](#zen_toggle-chords) | no | `alt-z` | Chord that toggles Zen Mode on and off. The canonical place to set it is `keys.yaml` (`defaults.global.zen_toggle`). | | `editor` | string | no | `$EDITOR`, else `vim` | Editor the [review interface](/docs/concepts/review-workspaces#the-review-editor)'s "Edit in `<editor>`" view launches in the review worktree. Hub-wide, so it follows you across projects. May be a bare command (`hx`) or one with flags (`code --wait`). Resolution order: this key, then `$EDITOR`, then `vim`. | ```yaml # ~/.shelbi/config.yaml keymap: zen_toggle: ctrl-g editor: code --wait ``` The `editor` display label in the switcher is the program's basename with its first letter upper-cased — `hx` shows as **Edit in Hx**, `code --wait` as **Edit in Code**. ### `zen_toggle` chords The accepted values for the Zen toggle chord: | Value | Key | | --- | --- | | `alt-z` | Alt+Z (default) | | `ctrl-backslash` | Ctrl+\ | | `ctrl-g` | Ctrl+G | | `ctrl-shift-z` | Ctrl+Shift+Z | | `none` | disabled — no chord toggles Zen Mode | ## `keys.yaml` Overrides for the TUI keybindings. Built-in chords apply out of the box; this file layers on top of them, either globally (`defaults`) or per project (`projects.<name>`). Parse errors never fail the load. A mistyped entry is skipped and that action keeps its built-in chord. **Path:** `~/.shelbi/keys.yaml` ### Shape ```yaml # ~/.shelbi/keys.yaml defaults: # applies to every project <mode>: <action>: <chord | [chords] | null> projects: # per-project overrides, keyed by project name <project-name>: <mode>: <action>: <chord | [chords] | null> ``` Bindings resolve in three layers, each overriding the last: built-in defaults → `defaults.<mode>.<action>` → `projects.<name>.<mode>.<action>`. | Block | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `defaults` | map of mode → action → chord(s) | no | — | Overrides applied to every project. | | `projects` | map of project name → (mode → action → chord(s)) | no | — | Overrides scoped to one project; win over `defaults`. | ### Leaf values The value under each action is untyped so one bad entry only affects itself: | Form | Meaning | | --- | --- | | scalar string (`alt-z`) | Bind the action to a single chord. | | list (`[k, up]`) | Bind the action to several chords. | | `[]` (empty list) | Deliberately unbind the action. | | `null` | No override — fall through to the layer below. | ### Modes The valid top-level keys under `defaults` and each `projects.<name>`: `global`, `sidebar`, `kanban`, `popover`, `review`, `activity`, `palette`. `global` bindings apply everywhere; the rest scope to their pane. ### Actions The action names accepted under each mode, with their built-in chords: | Mode | Action | Built-in | | --- | --- | --- | | `global` | `quit` | `ctrl-c` | | `global` | `zen_toggle` | `alt-z` | | `global` | `open_palette` | `ctrl-p` | | `sidebar` | `nav_up` / `nav_down` | `k`,`up` / `j`,`down` | | `sidebar` | `activate` | `enter`,`space` | | `sidebar` | `refresh` | `r` | | `kanban` | `nav_left` / `nav_right` | `h`,`left` / `l`,`right` | | `kanban` | `nav_up` / `nav_down` | `k`,`up` / `j`,`down` | | `kanban` | `move_card_left` / `move_card_right` | `H` / `L` | | `kanban` | `reorder_up` / `reorder_down` | `K`,`shift-up` / `J`,`shift-down` | | `kanban` | `open_popover` | `enter`,`space` | | `kanban` | `cycle_workflow_filter` | `tab` | | `popover` | `close` | `esc`,`enter`,`space`,`q` | | `popover` | `scroll_up` / `scroll_down` | `k`,`up` / `j`,`down` | | `popover` | `page_up` / `page_down` | `page-up`,`u` / `page-down`,`d` | | `review` | `nav_up` / `nav_down` | `k`,`up` / `j`,`down` | | `review` | `scroll_body_up` / `scroll_body_down` | `K` / `J` | | `review` | `activate` | `enter`,`space` | | `activity` | `scroll_up` / `scroll_down` | `k`,`up` / `j`,`down` | | `activity` | `reset_filter` | `a` | | `activity` | `toggle_zen_filter` | `z` | | `activity` | `toggle_workspaces_filter` | `w` | | `palette` | `close` | `esc`,`ctrl-c`,`ctrl-p` | | `palette` | `activate` | `enter` | | `palette` | `nav_up` / `nav_down` | `up` / `down` | Most modes also carry `refresh` (`r`) and scroll-navigation actions (`page_up`/`page_down`, `scroll_home`); the table lists the ones you're most likely to rebind. ```yaml # rebind the palette opener globally, and vim-swap kanban nav in one project defaults: global: open_palette: ctrl-k projects: myapp: kanban: move_card_left: [H, ctrl-h] ``` ## Sibling files Two more files live under `~/.shelbi/` but are managed by Shelbi rather than hand-edited: - **`shelbi.yaml`** — hub-wide bookkeeping: a per-project `last_launched` index used by the project picker. Written when you launch a project. - **`events.log`** — the cross-project [events log](/docs/concepts/events-log). Append-only state, not config. ## See also - [Config modes](/docs/concepts/config-modes) — per-project config, which these hub-wide files sit above. - [Project config](/docs/configuration/project) — the per-project YAML. [Source](https://shelbi.dev/docs/configuration/global) --- # Feature-branch (GitHub-flow) Branch per task off main, open a PR, merge back. This is the default Shelbi shape — the workflow every project ships with, written out in full. **Feature-branch** (a.k.a. GitHub-flow) is the simplest durable model: `main` is always deployable, every task cuts a short branch off it, opens a pull request, and merges back. There are no long-lived integration branches. The PR *is* the integration point. This is the model Shelbi's [shipped `task` workflow](/docs/guides/getting-started/workflows#shipped-workflows-task-and-subtask) already implements, so this guide doubles as a full annotation of the shipped default. Read it first; the other guides are described as deltas from this one. ## The board Each card's branch is cut off `main` when it enters **In Progress**, is pushed and PR'd on the way to **Review**, and squash-merged back into `main` on the way to **Done**. ## The workflow Status identity (the stable `id`, the display `name`, and the `category`) lives once in `workflows/statuses.yaml`, the project-wide [status catalog](/docs/guides/getting-started/workflows#schema). Every workflow file then references those statuses by `id` and adds only what's workflow-specific (`owner`, and an optional `agent:`). The shipped catalog is the canonical six: ```yaml # workflows/statuses.yaml — the status catalog, 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 } - { id: canceled, name: Canceled, category: archived } ``` The default workflow references them by `id`. No `name:` or `category:` is repeated here (the loader rejects a workflow file that does): ```yaml # workflows/default.yaml name: default description: Feature-branch flow — branch per task off main, PR, merge. # Inherits the project's base_branch: main and merge_strategy from # project.yaml. A git: block here would only be needed to override them. 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 } - { id: canceled, owner: user } initial_status: backlog transitions: - from: in-progress to: review actions: [push_branch, open_pr] # push the task branch, open a PR into main - from: review to: done actions: [merge, delete_branch] # squash-merge the PR, delete the branch - from: in-progress to: canceled actions: [close_pr, delete_branch] - from: review to: canceled actions: [close_pr, delete_branch] ``` Nothing here overrides the project git defaults, so branches cut off `main` and merges land back on `main`. The `open_pr` action opens the PR with `main` as its base because no transition declares a [`target:`](/docs/guides/getting-started/workflows#per-transition-target). The `user`-owned `review` status still names `agent: orchestrator`. That's what lets [Zen Mode](/docs/concepts/zen-mode) land the merge without you. ## Orchestrator adjustments None. This is the shape the shipped orchestrator prompt already assumes: triage into `Backlog`, dispatch `Todo`, hand off to `Review` on the [review marker](/docs/guides/getting-started/workflows#review-marker-promotion), and leave the `Review → Done` accept to you. If you turn on [Zen Mode](/docs/concepts/zen-mode), the merge on `Review → Done` is what the confidence bar gates. ## See also - [Workflows](/docs/guides/getting-started/workflows) — the full schema and the lifecycle walkthrough this board follows. - [Trunk-based](/docs/guides/understanding-workflows/trunk-based) — the same model with the `Review` gate collapsed and auto-merge turned on. - [Author a custom Shelbi workflow](/docs/guides/getting-started/custom-workflow) — start here when a routing or reporting tweak is worth encoding. [Source](https://shelbi.dev/docs/guides/understanding-workflows/feature-branch) --- # shelbi config Inspect and validate Shelbi configuration: take a versioned inventory of every owned surface, lint live or staged candidates, and manage keybindings. ```text shelbi config <COMMAND> [OPTIONS] ``` `shelbi config` inspects and validates the configuration Shelbi owns. Two commands cover every configuration surface: `inventory` discovers the canonical files and materializes an isolated candidate snapshot you can edit safely, and `lint` validates those surfaces through the same parsers Shelbi uses at runtime. Three older commands (`list-actions`, `dump-keybindings`, `check`) stay focused on the TUI keybindings in `~/.shelbi/keys.yaml`. These commands are what the built-in configuration workflow runs. When you ask a built-in orchestrator to change Shelbi configuration, it drives `inventory` and `lint` behind the reserved [`update-shelbi-configuration`](/docs/maintainers/system-configuration) skill so edits are staged, validated, previewed, and confirmed before any live file is touched. Every subcommand accepts the global `--root <PATH>` (override the Shelbi root directory, else `$SHELBI_ROOT`, else the install-time default) and `-p, --project <PROJECT>` (defaults to `$SHELBI_PROJECT` or the registered project whose `work_dir` contains the current directory) flags. ## Commands | Command | Description | | --- | --- | | `inventory` | Discover every Shelbi-owned configuration file and materialize an isolated candidate snapshot for safe editing. | | `lint` | Validate the selected configuration surfaces (live or staged) through Shelbi's production parsers. | | `list-actions` | Print every action with its mode, name, description, and the chord(s) bound to it after the `keys.yaml` merge. | | `dump-keybindings` | Dump the full default keymap as YAML (a starting point for `~/.shelbi/keys.yaml`). | | `check` | Validate `~/.shelbi/keys.yaml` and print any errors or warnings. | ## inventory ```text shelbi config inventory [--project <PROJECT> | --all] [--format json] ``` Discover the canonical global and per-project configuration files, copy the ones that exist into a fresh candidate directory, and print a JSON manifest. Scope it with `--project <PROJECT>` for one project or `--all` for every locally registered project (the two are mutually exclusive); with neither, it covers the global surfaces plus the project resolved from your working directory. The candidate snapshot lives in a temporary `staged_dir`. Edit files there, not the canonical originals, so nothing goes live until you apply it deliberately. The manifest is a stable contract: | Field | Meaning | | --- | --- | | `schema_version` | Inventory format version. A newer staged snapshot is rejected by an older `shelbi` (see [Compatibility](#compatibility)). | | `shelbi_version` | The `shelbi` build that produced the snapshot. | | `staged_dir` | Absolute path to the candidate directory. | | `entries[]` | One record per configuration surface. | Each entry carries a `logical_id` (a stable name like `global.keybindings` or `project.demo.workflow.task`), its `scope` (`global` or `project:<name>`), the `canonical_path` it was read from, the relative `candidate_path` inside `staged_dir`, its `format` (`yaml`, `json`, or `markdown`), whether it `exists`, and whether it is `lifecycle_owned`. A `lifecycle_owned` surface is one Shelbi regenerates through its own lifecycle commands (for example `shelbi reload`), so changing it usually means running that command, not only writing the file. ## lint ```text shelbi config lint [--project <PROJECT> | --all] [--staged <DIR>] [--format human|json] ``` Validate configuration surfaces without mutating them. By default `lint` reads live files; pass `--staged <DIR>` to validate a candidate directory produced by `inventory` instead. A staged snapshot is self-contained, so with `--staged` the `--all` flag means every project the snapshot represents, regardless of what is registered on the machine running the lint. ### Lint scope `lint` covers every configuration family Shelbi owns, each through the parser that Shelbi itself uses, so a clean lint means the file will load at runtime: - global preferences (`config.yaml`), the hub config (`shelbi.yaml`), and keybindings (`keys.yaml`); - per-project registration (flat `demo.yaml`, or the split `project.yaml` + `local.yaml` for [in-repo config](/docs/concepts/config-modes)), statuses, workflows, and the workspace-settings template; - per-project Markdown (Zen Mode policy, agent instructions, the shared preamble) and per-agent `settings.json` and skills. Diagnostics come back with a stable `code` (for example `CONFIG_UNKNOWN_FIELD`, `KEYBINDINGS_COLLISION`, `WORKFLOW_STATUS_REFERENCE_INVALID`, `TEMPLATE_UNKNOWN_PLACEHOLDER`, `ZENMODE_SUMMARY_MISSING`), a `severity` (`warning` or `error`), a source `location`, and often a `remediation` hint. ### Exit code `lint` **exits 1 when the report is not clean**: warnings and errors both count. Configuration is either valid or it is not, so a warning is not something to apply and move past; it slots the command cleanly into a pre-flight check. ## list-actions ```text shelbi config list-actions ``` Print every action with its mode, name, description, and the chord(s) bound to it **after** the `keys.yaml` merge, so it reflects your overrides, not just the built-in defaults. This is the reference for what's bindable and what each action currently does. ## dump-keybindings ```text shelbi config dump-keybindings ``` Dump the full default keymap as YAML. Drop the output into `~/.shelbi/keys.yaml` as a starting point for customization, then edit the chords you want to change. ## check ```text shelbi config check ``` Validate `~/.shelbi/keys.yaml` and print any errors or warnings. It **exits 1** on errors, so it slots cleanly into a pre-flight script; warnings still exit 0. (`config lint` validates `keys.yaml` alongside every other surface; `check` is the keybindings-only view.) ## Compatibility The inventory manifest is versioned by `schema_version`. A staged snapshot is only valid for the `shelbi` build that produced it: linting a snapshot whose version a newer or older binary does not recognize is refused rather than misread. Take a fresh `inventory` with the `shelbi` you are running instead of reusing an old `staged_dir`. Unknown fields are reported (`CONFIG_UNKNOWN_FIELD`) rather than silently ignored, so a config written for a different Shelbi version surfaces as a diagnostic you can act on. ## Examples Take an inventory of one project and read the candidate directory it staged: ```bash shelbi config inventory --project demo --format json ``` Lint every registered project's live configuration before a release: ```bash shelbi config lint --all ``` Validate a staged candidate directory you have been editing: ```bash shelbi config lint --project demo --staged /tmp/shelbi-config-1234 --format json ``` See what every action is bound to after your overrides are applied: ```bash shelbi config list-actions ``` ## See also - [System configuration](/docs/maintainers/system-configuration): how the reserved configuration skill and system plugin drive these commands, plus ownership, fallback, and compatibility behavior. - [Global config](/docs/configuration/global#keysyaml): the `keys.yaml` schema, modes, actions, chord syntax, and the built-in defaults. - [Config modes](/docs/concepts/config-modes): flat versus in-repo project layouts, which shape the registration surfaces `inventory` reports. [Source](https://shelbi.dev/docs/cli/config) --- # Forking Contributions arrive as pull requests from forks, and a review gate stands between them and main. Model the maintainer's side — triage, an automated review pass, and a human sign-off before merge. In the **forking workflow** contributors don't push to the canonical repository at all. They fork it, work on their own copy, and open a pull request from the fork. The maintainer's job is a *review gate*: triage what comes in, run it through checks, and merge only what clears sign-off. It's the model most open-source projects run. Shelbi models the **maintainer's side**. Because the branch lives on someone else's fork, Shelbi doesn't cut branches or push here. Each task carries the fork's PR branch in [`branch:`](/docs/guides/getting-started/workflows#the-branch-task-field), and the workflow is mostly about the gate: a `handoff` sign-off status that's `owner: user` so nothing reaches `main` without a human (or a [Zen](/docs/concepts/zen-mode)-authorized reviewer agent) accepting it. ## The board A fork PR lands in **Incoming**, the orchestrator triages it into scope, the `reviewer` agent runs it through checks in **In Review**, and it waits in **Sign-off** for a maintainer to accept before it's **Merged**. ## The statuses The forking flow relabels the whole board, so declare its statuses (`id` + `name` + `category`) in the project's [status catalog](/docs/guides/getting-started/workflows#schema) first (`workflows/statuses.yaml`), then reference them by `id` from the workflow: ```yaml # workflows/statuses.yaml — the forking project's status catalog. statuses: - { id: incoming, name: Incoming, category: backlog } - { id: triage, name: Triage, category: ready } - { id: in-review, name: In Review, category: active } - { id: sign-off, name: Sign-off, category: handoff } - { id: merged, name: Merged, category: done } - { id: declined, name: Declined, category: archived } ``` ## The workflow ```yaml # workflows/contribution.yaml — reference-only; identity lives in statuses.yaml name: contribution description: Forking flow — review gate for pull requests from forks. git: base_branch: main merge_strategy: squash # squash external contributions into one commit statuses: - { id: incoming, owner: user } - { id: triage, owner: agent, agent: orchestrator } - { id: in-review, owner: agent, agent: reviewer } - { id: sign-off, owner: user } - { id: merged, owner: user } - { id: declined, owner: user } initial_status: incoming transitions: - from: in-review to: sign-off actions: [push_branch] # push the fork branch to a review ref; no PR to open - from: sign-off to: merged actions: [merge] # squash-merge the existing fork PR into main - from: in-review to: declined actions: [close_pr] # close the PR without merging; the fork keeps its branch ``` Two side-effects that the [feature-branch](/docs/guides/understanding-workflows/feature-branch) model uses are deliberately *absent* here: - **No `open_pr`.** The contributor already opened the PR from their fork. The task just tracks it. Pre-fill `branch:` with the PR's head ref so `merge` and `close_pr` act on the right PR. - **No `delete_branch`.** Shelbi can't delete a branch it doesn't own. The fork keeps its branch whether the PR merges or is declined. The `in-review` status names an [`agent: reviewer`](/docs/concepts/agents) role, an agent you author to run the untrusted contribution through the project's checks and summarize risk before a human ever looks. The `sign-off` status stays `owner: user`: the gate is the whole point. ## Orchestrator adjustments The review gate needs the orchestrator to treat incoming forks as untrusted and never to auto-accept. Encode a triage rule and a hard stop at sign-off: ```markdown ## Routing rules — forking - **Incoming fork PRs** are untrusted. Never dispatch a contribution to a workspace that holds credentials or can reach production. Route `Triage` cards only to sandboxed hub workspaces. - In `Triage`, decline anything out of scope (wrong direction, duplicate, no linked issue) into `Declined` with a one-line reason. Promote the rest to `InReview` for the `reviewer` agent. ## Zen Mode — forking The `SignOff → Merged` edge is `owner: user` and must stay a human decision. Do **not** auto-merge contributions even when checks are green: report "`<PR>` passed review and is ready to sign off" and wait. A green check is necessary but not sufficient for merging someone else's code. ``` That last rule is the difference between forking and [trunk-based](/docs/guides/understanding-workflows/trunk-based): trunk-based leans *into* auto-merge for your own green work; forking deliberately keeps a human at the gate for code that arrived from outside. ## See also - [Agents](/docs/concepts/agents) — how to author the `reviewer` role the `InReview` status dispatches to. - [Workflows: the `branch:` task field](/docs/guides/getting-started/workflows#the-branch-task-field) — pointing a task at an existing fork PR branch instead of cutting one. - [Trunk-based](/docs/guides/understanding-workflows/trunk-based) — the opposite end of the auto-merge spectrum, for work you trust. [Source](https://shelbi.dev/docs/guides/understanding-workflows/forking) --- # shelbi daemon Run the hub-side daemon that ingests worker messages and appends them to the events log — or manage its platform supervisor. ```text shelbi daemon [OPTIONS] [COMMAND] ``` `shelbi daemon` runs the hub-side daemon that listens on `~/.shelbi/hub.sock` (overridable via `$SHELBI_HUB_SOCK`) for worker messages and appends `event`-verb payloads to [`~/.shelbi/events.log`](/docs/concepts/events-log). Bare `shelbi daemon` (no subcommand) is the foreground entry point that launchd/systemd call into; the `install` / `uninstall` / `status` / `restart` subcommands manage that platform supervisor on your behalf. ## Subcommands | Subcommand | Description | | --- | --- | | `run` | *(default — also the form launchd/systemd invoke)* Bind the hub socket and accept worker messages in the foreground until killed. | | `install` | Install the platform supervisor unit (launchd plist on macOS, systemd user service on Linux) so the daemon auto-starts at login and is restarted on crash. Idempotent — re-running just refreshes the unit file and reloads it. | | `uninstall` | Stop the daemon and remove the platform supervisor unit. | | `status` | Print a short human-readable status by wrapping `launchctl print` or `systemctl --user status`. | | `restart` | Stop the daemon so the supervisor relaunches it — picks up a freshly installed binary without losing the auto-restart guarantee. | Running `shelbi daemon` with no subcommand is equivalent to `shelbi daemon run`: it binds the hub socket and stays in the foreground, ingesting worker messages until killed. That's the form the platform supervisor invokes. You rarely run it by hand except to debug ingestion. ## Flags Every subcommand accepts the same options: | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--root <PATH>` | string | install-time default / `$SHELBI_ROOT` / `~/.shelbi` | Override the Shelbi root directory. The flag wins over both the env var and the compile-time default. | | `-p, --project <PROJECT>` | string | env / cwd lookup | Project to operate on. | ## Reverse-forward health and tuning Every shelbi-routed `ssh` invocation carries a reverse forward so remote workers can write to the hub's events log over the multiplexed channel. The TUI poller rechecks that forward on a slow cadence and repairs it if it has gone stale. Two failure modes are transient (direct SSH stays healthy) but can be noisy in the events log, so the recheck handles them explicitly: - A flaky `master_open` is retried with exponential backoff before it is reported. A blip that self-heals is logged once as `detail=master_open_recovered attempts=N status=established`; only an outage that survives the whole retry budget logs `detail=master_open_failed attempts=N status=failed`. - Loopback-port exhaustion on a TCP-fallback host is surfaced distinctly as `detail=loopback_port_exhausted band=<lo>-<hi>`, so it reads apart from a master-open blip. Before allocating, the recheck reclaims the port its own prior master was holding, then sweeps the configured band. These environment variables tune the behavior (all optional; sensible defaults apply): | Variable | Default | Description | | --- | --- | --- | | `SHELBI_FORWARD_RETRY_ATTEMPTS` | `3` | Master-open attempts before a transient failure is reported. Clamped to `1..=10`. | | `SHELBI_FORWARD_RETRY_BACKOFF_MS` | `250` | Base backoff between attempts; doubles each retry. Clamped to `<= 5000`. | | `SHELBI_TCP_FORWARD_PORT_BASE` | `47100` | First loopback port for a TCP-fallback forward. | | `SHELBI_TCP_FORWARD_PORT_SPAN` | `64` | Width of the loopback band swept on a bind collision. Widen this if you see `loopback_port_exhausted`. | ## Examples Install the supervisor unit so the daemon auto-starts at login and restarts on crash: ```bash shelbi daemon install ``` Check whether the daemon is running: ```bash shelbi daemon status ``` Pick up a freshly installed binary without losing the auto-restart guarantee: ```bash shelbi daemon restart ``` Remove the supervisor unit and stop the daemon: ```bash shelbi daemon uninstall ``` ## See also - [Events log](/docs/concepts/events-log) — the `~/.shelbi/events.log` append target the daemon writes worker `event` payloads to. [Source](https://shelbi.dev/docs/cli/daemon) --- # AI prompts Curated, copyable prompts for handing Shelbi's docs to a coding agent — plus how to wire them into Claude Code, GitHub Copilot, or your agent of choice. Shelbi's docs are built to be read by agents as well as people. Every page is available as clean markdown (append `.md` to any docs URL). The index lives at [`/llms.txt`](/llms.txt), and the entire corpus is one file at [`/llms-full.txt`](/llms-full.txt). This page collects prompts that put those artifacts to work. ## The prompts Copy one, fill in the bracketed placeholders, and paste it into your agent. Each prompt points the agent at the docs so it works from the source of truth rather than guessing. > **Learn Shelbi from its docs** — Get your agent up to speed before you ask it anything else. ```text Read the Shelbi documentation at https://shelbi.dev/llms-full.txt. Shelbi is an open-source, terminal-native agent orchestrator: a Kanban board that dispatches coding tasks to a pool of workspaces running coding agents, locally or over SSH. Once you have read it, give me a three-paragraph summary of how Shelbi works — workspaces, the orchestrator, workflows, and Zen Mode — then wait for my next question. ``` > **Set up my first Shelbi project** — Install Shelbi and configure this repository, with the docs as the source of truth. ```text I want to set up Shelbi on this repository. Use the official docs: the index is at https://shelbi.dev/llms.txt, and any page is available as clean markdown by appending .md to its URL (for example https://shelbi.dev/docs/guides/getting-started/install.md). Read the Getting Started section, install Shelbi, run the detected preflight, and help me review its one-question setup card. Use the Customize path only if I want to change a detected value. Do not hand-write config that Shelbi can generate. ``` > **Design a custom workflow for my team** — Map your existing git branching model onto a Shelbi workflow. ```text Read https://shelbi.dev/docs/guides/getting-started/workflows.md and the workflow guides linked from https://shelbi.dev/llms.txt. My team uses [describe your branching model — trunk-based, git-flow, feature-branch, or forking]. Design a Shelbi workflow that matches it: the statuses a task moves through, who owns each one, and the side-effects on each transition. Explain your choices, then produce the workflow YAML and the orchestrator instruction tweaks I need. ``` > **Enable Zen Mode safely** — Add the local checks Zen Mode needs, then turn it on. ```text Read https://shelbi.dev/docs/guides/getting-started/enable-zen-mode.md and https://shelbi.dev/docs/concepts/zen-mode.md. Zen Mode lets the Shelbi orchestrator auto-promote backlog and auto-merge finished branches that clear a confidence bar. Look at this repository's test and lint setup, recommend the local checks I should wire in before trusting auto-merge, help me add them, then tell me exactly what to flip to enable Zen Mode. ``` > **Debug my Shelbi setup** — Hand your agent the docs and your config so it can diagnose the problem. ```text Something in my Shelbi setup is not working: [describe what you see]. Read the relevant docs — start from the index at https://shelbi.dev/llms.txt and fetch the markdown for any page by appending .md to its URL. Then inspect the .shelbi/ config and project YAML in this repo, compare them against the docs, and tell me what is misconfigured and how to fix it. ``` ## Using them with your agent The prompts above work in any agent that can fetch a URL. These notes wire the docs in a little more permanently for the two most common setups. ### Claude Code (CLAUDE.md) Claude Code reads a `CLAUDE.md` file at the root of your project at the start of every session. Add a pointer to Shelbi's docs so it always knows where to look: ```markdown ## Shelbi This project is orchestrated with Shelbi. When you need to know how Shelbi works, read https://shelbi.dev/llms.txt for the index, and append .md to any docs URL for its clean markdown source (e.g. https://shelbi.dev/docs/guides/getting-started/workflows.md). ``` With that in place you can paste any prompt above, or just ask Claude Code to follow the pointer: "check the Shelbi docs, then help me add a review column." ### GitHub Copilot (#file) Copilot Chat pulls a local file into context with `#file`. Save the full corpus into your repo once: ```bash curl -fsSL https://shelbi.dev/llms-full.txt -o docs/shelbi-llms-full.txt ``` Then reference it in Copilot Chat and add your question: ```text #file:docs/shelbi-llms-full.txt Using the Shelbi docs above, help me set up my first project. ``` Re-run the `curl` whenever you want to refresh the snapshot. ### Your agent of choice Nothing here is Claude- or Copilot-specific. Point any agent at [`/llms.txt`](/llms.txt) for the index or [`/llms-full.txt`](/llms-full.txt) for the whole corpus, and remember that every docs page has a `.md` twin. The same URL with `.md` appended returns clean markdown. Paste a prompt above and go. [Source](https://shelbi.dev/docs/ai-prompts) --- # Doing More with Agents An agent is just a role, a system prompt plus a skill set, so nothing stops you authoring your own and slotting it into a workflow's statuses. This guide builds an Adversarial Review agent, an automated skeptic that tries to break a change before a human sees it, and wires it into a workflow. Shelbi already ships that reviewer as a preset; the guide is the mechanism behind it. Shelbi ships six agents: the `orchestrator`, `developer`, and `review` roles that run the core loop, plus three reviewer presets (`qa`, `security`, and `adversarial`). None of them is privileged machinery. An **[agent](/docs/concepts/agents) is just a role**: a system prompt plus an optional skill set. Nothing stops you from authoring your own and dropping it onto a workflow status, exactly the way the built-ins are wired. This guide builds one worth having and shows where it goes. <Callout type="note" title="The Adversarial Review agent already ships"> The worked example below builds an adversarial reviewer from scratch, because doing it once teaches the whole author-and-wire mechanism. If you only want the reviewer, you already have it: the shipped `adversarial` preset (alongside `qa` and `security`) is the same role, materialized and ready. Skip to [Add it to a workflow](/docs/guides/doing-more-with-agents/add-to-workflow) and name `agent: adversarial` on a status to wire the shipped one in one line. </Callout> ## The worked example: an Adversarial Review agent The core loop is optimistic. The `developer` implements a task and marks it review-ready; the `review` agent serves the finished branch to a human. Nothing in that path is *trying to prove the change is wrong* (that's the job of the shipped `adversarial`, `qa`, and `security` reviewers, which is why they exist). An **Adversarial Review** agent is that missing skeptic. Its job is not to approve a change. It's to try to *break* it: find the bug, the injection, the unhandled error, the edge case the tests quietly skip. It reads the diff on the task's branch, writes up what it finds with severity and `file:line` references, and only signs off when "no issues found" has actually been earned. Slotted between "in progress" and human review, it becomes an automated adversarial pass that every task walks through before a person spends attention on it. The developer's optimism gets a counterweight, and the human reviewer arrives to findings already on the table instead of a blank diff. This is the same "a reviewer is a role you can drop on a status" idea the [Agents](/docs/concepts/agents) concept describes: a task is the work, a [workspace](/docs/concepts/workspaces) is the capacity, and the agent is the role that decides *how* the work gets done. Here we author a new role and give it a status to own. ## Other agents you could build The Adversarial Review agent is a **gate**: it reviews work and can send it back. That's one of two shapes a custom agent tends to take, and the distinction is worth keeping in mind as you design your own. - **Gate agents** review a task and can bounce it back to an earlier status when they find problems. They rely on [send-back](/docs/guides/doing-more-with-agents/adversarial-review-agent#how-send-back-works), which the reviewer here uses to return work to the developer. - **Producer agents** do work and hand off forward when they're done, the way the `developer` marks a branch review-ready. A few worth having (the first two ship as presets already, the rest are yours to author): - **Security reviewer** *(gate)* — audits a branch's diff for injection, broken authorization, leaked secrets, and risky dependency bumps. Ships as the `security` preset; wire it onto a status to use it. - **QA** *(producer + gate)* — exercises a change against its acceptance criteria and reports pass or fail with repro steps. Ships as the `qa` preset. - **Docs writer** *(producer)* — keeps docs, READMEs, and the changelog in sync with a code change, then hands off forward. - **Performance reviewer** *(gate)* — flags regressions, N+1 queries, and hot-path allocations a diff introduces. Each slots onto a workflow status the same way the Adversarial Review agent does. A gate agent owns an `active` status between the work and the human and needs a bounce edge declared in the workflow's `transitions`. A producer agent owns a status where its output is the deliverable, then hands off forward. ## Where to go next The guide is two parts. Author the agent, then wire it in: 1. **[Create the agent →](/docs/guides/doing-more-with-agents/adversarial-review-agent)** — scaffold `adversarial-review` with `shelbi agent new`, understand the `instructions.md` / preamble / skills model, and drop in a concrete, usable skeptic prompt. 2. **[Add it to a workflow →](/docs/guides/doing-more-with-agents/add-to-workflow)** — declare an `adversarial-review` status, point it at the new agent, and rewire the transitions so a branch flows through the automated review on its way to a human. ## See also - [Agents](/docs/concepts/agents) — the role/task/workspace model, the six shipped agents, and how a custom one slots in. - [Understanding Workflows](/docs/guides/understanding-workflows) — the branching models a workflow can implement, if you want to place this review gate inside a larger shape. [Source](https://shelbi.dev/docs/guides/doing-more-with-agents) --- # Create the Adversarial Review agent Scaffold an adversarial-review agent with shelbi agent new, understand the instructions.md / preamble / skills model, and drop in a concrete role prompt that tries to break a change instead of approving it. An [agent](/docs/concepts/agents) is a directory under your project's Shelbi config with, at minimum, an `instructions.md`: its system prompt. That's the whole contract. Authoring the Adversarial Review agent means scaffolding that directory and writing a prompt that makes the agent a genuine skeptic. ## Scaffold the agent <Steps> <Step title="Create the directory"> Scaffold a new agent with the CLI: ```bash shelbi agent new adversarial-review ``` This creates `~/.shelbi/projects/<project>/agents/adversarial-review/` with a starter `instructions.md` and an empty `skills/`. The starter prompt is a minimal role template meant to be replaced. See [`shelbi agent new`](/docs/cli/agent#new). The name doubles as the identifier you'll reference from a workflow's `agent:` field, so it has to be lowercase kebab-case (`a-z`, `0-9`, `-`, `_`). </Step> <Step title="Understand what you're editing"> `instructions.md` **is** the agent's system prompt. There's no wrapper or config layer. Two other things share the launch-time prompt with it: - **`agents/_shared/preamble.md`** is prepended to *every* agent in the project. Your repo layout, house style, and the test command already live there, so the rendered prompt the runner sees is the preamble followed by this agent's `instructions.md`. - **`skills/`** is an optional directory of agent-scoped skills the runner loads when this agent is active. Reach for it if the reviewer needs a repeatable procedure (say, a security checklist) rather than just a role. <Callout type="tip" title="Let the preamble carry shared context"> Don't restate the repo structure, coding conventions, or how to run the tests in this agent's prompt. `_shared/preamble.md` is prepended to every agent for exactly that reason. Keep `instructions.md` focused on the *role*: what an adversarial reviewer is for and how it should behave. Duplicated context is just one more place to drift out of sync. </Callout> </Step> <Step title="Write the role prompt"> Open it in your editor: ```bash shelbi agent edit adversarial-review ``` Replace the starter body with a prompt that makes the agent adversarial by default. The sample below is tight enough to paste and get a genuinely useful skeptic: ```markdown # Adversarial Review You are an adversarial code reviewer. A developer agent has finished a task and believes its branch is ready. Your job is **not** to confirm that belief — it is to try to prove the change is wrong before a human spends attention on it. Assume there is a bug until you have looked hard enough to say otherwise. ## What to review Review the diff on the current task's branch against the base branch — only what this task changed, plus the code that change touches. You are looking for defects the developer and the tests missed, not for style nits the preamble's checks already cover. ## How to review Work through every changed hunk and actively try to falsify it: - **Correctness** — Does it do what the task asked? Trace the non-obvious paths by hand. Off-by-one, wrong operator, inverted condition, a branch that silently does nothing. - **Security** — Untrusted input reaching a query, a shell, a path, or a template. Missing authz checks. Secrets in logs or errors. Unsafe defaults. - **Error handling** — What happens when the call fails, the input is empty, the list is huge, the value is null, two requests race? - **Edge cases** — Boundaries, empty and maximal inputs, concurrency, encoding, time zones — whatever this code plausibly meets in production. - **Test coverage** — Do the tests actually exercise the new behavior, or do they assert around it? Find a real input the change gets wrong that no test would catch, and treat that as a finding. ## How to report Write your findings as a structured review on the branch — a markdown `## Adversarial Review` section (a review comment on the PR, or a note committed with the branch, per this project's convention). For each finding: - **Severity** — `blocker`, `major`, or `minor`. - **Location** — `path/to/file.rs:120` (or a range). - **The problem** — what breaks, and the concrete input or sequence that triggers it. A finding with a repro beats a vague worry. - **Suggested fix** — one line, when the fix is obvious. Order findings by severity, blockers first. ## Signing off or bouncing Every review ends one of two ways. Decide from the highest-severity finding. **Clean pass, hand off forward.** If nothing rises to a `blocker`, sign off. "No issues found" is a conclusion you have to earn, not a default: only sign off clean when you have walked every changed hunk and have a specific reason each is sound. Say briefly *why* the change holds up (what you checked) so the human reviewer can trust the pass, then hand off forward the normal way, by writing the review-ready marker. **Blocking findings, bounce it back.** If any finding is a `blocker`, the change is not ready for a human. Write up your findings, then send the task back to the developer by writing the transition marker: printf '%s\n%s\n' "$TASK_ID" bounce \ > .claude/shelbi-transition.tmp && mv .claude/shelbi-transition.tmp .claude/shelbi-transition The first line is your task id (`$TASK_ID` in your environment); the second is `bounce`, which sends the task back to the active status the developer works. Do not sign off and bounce in the same run. Bouncing is the signal that blockers exist. ``` Tune it to your stack: swap the `.rs:120` example for your language, add a line about a framework you use, point the write-up at wherever your team keeps review notes. The shape is what matters: a reviewer that falsifies first and earns its sign-off. </Step> </Steps> ## How send-back works A gate agent bounces a task by writing one file: the **transition marker** at `<worktree>/.claude/shelbi-transition`. It's the same kind of plain-file signal as the review-ready marker a finishing agent writes. Workspaces have no `shelbi` binary, so the agent just writes the file and the hub poller acts on it. The format is two lines of UTF-8: ```text <task-id> <target-status> ``` - **Line 1** is the agent's own task id. It has to match the task the workspace is currently assigned, or the poller treats the marker as stale and clears it without moving anything. - **Line 2** is either a status `id` the workflow declares (the primitive, e.g. `in-progress`) or the verb `bounce` (equivalently `reject`), which Shelbi resolves to the workflow's active status. That verb is why the sample prompt can write `bounce` without hard-coding a status name. Write it atomically, temp file then `mv`, so the poller never reads a half-written marker: ```sh printf '%s\n%s\n' "$TASK_ID" bounce \ > .claude/shelbi-transition.tmp && mv .claude/shelbi-transition.tmp .claude/shelbi-transition ``` It lives under `.claude/` because that directory is Shelbi's gitignored deploy footprint, so the marker never dirties the worktree or trips a clean-branch check between tasks. <Callout type="note" title="The workflow has the final say"> Writing the marker is a *request*, not a guaranteed move. The poller applies it only if the workflow permits that edge. A backward bounce needs the `adversarial-review → in-progress` edge declared in the workflow's `transitions`, or the poller rejects the marker and the task stays put. Wiring that edge is the next part. </Callout> ## Confirm it landed Check the agent is registered: ```bash shelbi agent list ``` `adversarial-review` shows up in the table with no statuses referencing it yet. That's the next part. `shelbi agent show adversarial-review` prints the `instructions.md` you just wrote (without the shared preamble, which is composed in at launch time). ## Next You have a skeptic on disk with nothing to review. Wire it into a workflow so tasks actually flow through it: **[Add it to a workflow →](/docs/guides/doing-more-with-agents/add-to-workflow)** ## See also - [Agents](/docs/concepts/agents) — the role/task/workspace model and the on-disk agent layout. - [`shelbi agent`](/docs/cli/agent) — the full command set: `list`, `show`, `new`, `edit`. [Source](https://shelbi.dev/docs/guides/doing-more-with-agents/adversarial-review-agent) --- # Add it to a workflow Declare an adversarial-review status in statuses.yaml, reference it from a workflow with owner + agent, and rewire the transitions so a branch flows through the automated skeptic on its way to human review. The [agent exists](/docs/guides/doing-more-with-agents/adversarial-review-agent); now give it a status to own. Wiring a new agent into a workflow is two files: the status *identity* is declared once in `statuses.yaml`, and the workflow file *references* it by `id`, adds an `owner`, and points it at the agent. Keep that split correct: repeating `name:` or `category:` in the workflow file fails the load. ## 1. Declare the status identity Add the status to the project-wide catalog in [`workflows/statuses.yaml`](/docs/configuration/statuses), placed where you want its column to sit, between `in-progress` and `review`: ```yaml # workflows/statuses.yaml statuses: - { id: backlog, name: Backlog, category: backlog } - { id: todo, name: Todo, category: ready } - { id: in-progress, name: In Progress, category: active } - { id: adversarial-review, name: Adversarial Review, category: active } - { id: review, name: Review, category: handoff } - { id: done, name: Done, category: done } ``` `category` comes from the [closed vocabulary](/docs/configuration/statuses#categories): `backlog`, `ready`, `active`, `handoff`, `done`, `archived`. Because an agent is *actively working* the review while a task sits here, it's `active`, not `handoff`: `handoff` means "one owner finished, another's input is required next," which is what the human `review` status already is. Declaration order here is the left-to-right column order in the TUI. ## 2. Reference it from the workflow In the [workflow file](/docs/configuration/workflow), reference the status by `id` between `in-progress` and `review`. Make it agent-owned and point it at the agent you authored: ```yaml # workflows/default.yaml statuses: - { id: backlog, owner: user, agent: orchestrator } - { id: todo, owner: agent, agent: orchestrator } - { id: in-progress, owner: agent, agent: developer } - { id: adversarial-review, owner: agent, agent: adversarial-review } - { id: review, owner: user, agent: orchestrator } - { id: done, owner: user } ``` `owner: agent` makes the status the orchestrator's to act on, so tasks that land here are [auto-dispatched](/docs/concepts/orchestrator); the `agent: adversarial-review` field names *which* agent runs it. No `name:` or `category:` appears here. Those live in `statuses.yaml`, and repeating them fails the load. ## 3. Rewire the transitions [Transitions](/docs/configuration/workflow#transitions) declare the hub-side side-effects that fire on an edge. For the moves a human makes on the board, an unlisted edge is just a pure status change. For an **agent-initiated** move (the reviewer's bounce), the block does double duty as an allowlist: once a `transitions:` block exists, an agent may only take edges it declares, so the bounce edge has to be listed or the poller rejects it. Route the developer's branch into adversarial review, then on to human review, with a bounce-back for when the skeptic finds problems, and keep `merge` on the single final edge into `done`: ```yaml transitions: - { from: in-progress, to: adversarial-review, actions: [push_branch, open_pr] } - { from: adversarial-review, to: review, actions: [] } - { from: adversarial-review, to: in-progress, actions: [] } - { from: review, to: done, actions: [merge, delete_branch] } ``` What each edge does: - **`in-progress → adversarial-review`** runs `push_branch` and `open_pr`, so the branch is on the remote with a PR open before the reviewer starts. The reviewer works against a real diff, and its findings have somewhere to land. - **`adversarial-review → review`** has no actions: the branch is already pushed and PR'd, so promoting a clean pass to human review is a pure status change. - **`adversarial-review → in-progress`** is the bounce-back. When the skeptic files blockers and writes the [transition marker](/docs/guides/doing-more-with-agents/adversarial-review-agent#how-send-back-works), the task drops to the developer with no git side-effects: same branch, same PR, another pass. This edge is what makes that bounce legal; drop it and the poller refuses the reviewer's send-back. - **`review → done`** is the only edge carrying `merge` (and `delete_branch`). That's deliberate. The [action primitives](/docs/configuration/workflow#actions) in play are `push_branch` (push the task branch to origin), `open_pr` (open its PR), `merge` (merge the branch into its target), and `delete_branch` (delete the local and remote branch). <Callout type="note" title="Keep merge on the final edge"> `merge` trips [Zen Mode's high-confidence bar](/docs/concepts/zen-mode). Keeping it on the single `review → done` edge, and nowhere earlier, means the automated reviewer never lands code. It surfaces findings; the human still owns the accept. </Callout> ## What you see now The board grows an **Adversarial Review** column between In Progress and Review. Tasks the developer marks ready flow into it, get auto-dispatched to the `adversarial-review` agent, and either bounce back with blockers or advance to human review with the skeptic's findings already waiting on the PR. ## See also - [Workflow config](/docs/configuration/workflow) — the field-by-field reference for `statuses`, `transitions`, and their actions. - [Statuses](/docs/configuration/statuses) — the `statuses.yaml` catalog and the closed category vocabulary. - [Orchestrator](/docs/concepts/orchestrator) — how agent-owned statuses get auto-dispatched and handed off. [Source](https://shelbi.dev/docs/guides/doing-more-with-agents/add-to-workflow) --- # System Configuration How Shelbi's reserved configuration skill, system plugin, and config inventory/lint interfaces fit together: ownership, fallback, lint scope, and compatibility. This is the maintainer's map of the system-owned configuration-update path: the reserved skill that safely edits Shelbi configuration, the plugin that carries it into a built-in orchestrator, and the `shelbi config` interfaces both rely on. It complements the user-facing [`shelbi config`](/docs/cli/config) reference; read that first for the command surface. ## The pieces - **The skill** `update-shelbi-configuration` (`plugins/update-shelbi-configuration/skills/`) is prose the orchestrator agent follows to change Shelbi configuration safely: inventory, stage, lint, one combined preview, explicit confirmation, race detection, atomic apply, and a live lint. It never hardcodes paths or schemas (those move between versions). It drives the installed `shelbi config` CLI, which is authoritative for the running version's layout and validation. - **The system plugin** `plugins/update-shelbi-configuration` ships one bundle with two runner manifests (`.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`) that both reserve the name `update-shelbi-configuration` and point at the shared `./skills/` directory. - **The CLI** `shelbi config inventory` and `shelbi config lint` (`crates/shelbi-cli/src/commands/config_surfaces.rs`) are the machinery the skill orchestrates: versioned discovery and read-only validation of every owned surface. ## Ownership The name `update-shelbi-configuration` is reserved. It is installed **last**, after user and project skills are mirrored into the worktree, so a customized orchestrator prompt cannot shadow or disable the operational safety workflow. A project skill that tries to claim the same name is suppressed with a warning rather than silently winning. The plugin is staged into a **Shelbi-owned, session-scoped** path (`.claude/shelbi-system-plugins/update-shelbi-configuration`), never a runner's global registry. Each built-in runner then picks its own transport for that one bundle: - **Claude** loads the isolated bundle with a session-scoped `--plugin-dir`, leaving the user's global plugin registry untouched. - **Codex** discovers the bundle's `skills/` directory through the app-server's process-scoped `skills/extraRoots/set` request. Older app-servers that lack it fall back to receiving the same `SKILL.md` bytes as developer instructions on the new or resumed owned thread. Generic and custom runners are intentionally outside the system plugin, and non-orchestrator agents never receive it. Injecting the bundle changes no global runner registry and no runner-owned Claude or Codex configuration. ## Fallback Release packages carry an editable copy of the plugin next to the binary; the binary also embeds the exact same bundle. Resolution (`crates/shelbi-orchestrator/src/system_plugin.rs`) prefers a parseable installed copy so packaging defects stay visible, and falls back to the embedded copy so orchestrator startup keeps working when the packaged asset is absent or broken: - **Missing or unreadable** installed asset → embedded copy, with a warning. - **Malformed** installed asset (bad manifest JSON, mismatched skill directories, missing frontmatter) → embedded copy, with a warning. - **Valid but modified** installed asset → the installed copy is loaded (a maintainer may legitimately patch the prose), with a warning that it differs from the compiled checksum. - **Valid and matching** installed asset → loaded with no warning. The checksum is an FNV-1a integrity fingerprint, not a security primitive: a valid local patch must remain usable, so a mismatch warns but never blocks. The installed-path lookup handles the standalone archive layout (plugin beside the binary), prefix installs (`share/shelbi/plugins` preferred over a stale adjacent `bin/plugins`), Homebrew's versioned pkgshare, and the cargo development layout. Release, Homebrew, and APT packaging checks assert all three packaged files ship, so dot-directories cannot be silently dropped from an archive. ## Lint scope `shelbi config lint` validates every configuration family Shelbi owns through the same parser Shelbi uses at runtime, so a clean lint means the file loads. The families: - **Global:** preferences (`config.yaml`), hub config (`shelbi.yaml`), keybindings (`keys.yaml`). - **Per-project:** registration (flat `demo.yaml`, or split `project.yaml` + `local.yaml`), statuses, workflows, and the workspace-settings template. - **Per-agent:** instructions and the shared preamble (Markdown), `settings.json` (JSON), and skills (Markdown). Both live and staged linting run the identical checks; `--staged` points them at an inventory candidate directory instead of live files. Diagnostics carry a stable `code`, a `severity`, a source `location`, and often a `remediation` hint. **Both warnings and errors make the report unclean and exit non-zero**: configuration is valid or it is not, so the workflow never applies a config that lints with warnings. Lint is deliberately read-only. The write half of the workflow (atomic apply to canonical paths, running lifecycle commands for `lifecycle_owned` surfaces) is the skill's responsibility, gated behind explicit confirmation and re-checked for concurrent source changes immediately before the first live write. ## Compatibility The inventory manifest is versioned (`schema_version`). A staged snapshot is only valid for the `shelbi` build that produced it: a snapshot whose version the running binary does not recognize is refused, not misread. The skill always takes a fresh inventory from the installed CLI rather than reusing an old `staged_dir`, and reads paths, candidate layout, and `lifecycle_owned` facts from that inventory rather than from any version-specific prose. Unknown configuration fields are reported (`CONFIG_UNKNOWN_FIELD`) rather than ignored, so a file written for a different Shelbi version surfaces as an actionable diagnostic instead of loading with silent surprises. ## Verifying the workflow The end-to-end behavior is covered by `crates/shelbi-cli/tests/system_config_workflow_e2e.rs`, which drives the shipped binary through the full inventory → stage → lint → preview → confirm → race-check → apply → live-lint sequence and asserts the guardrails: no live write before confirmation, a raced source blocking the confirmed apply, a recovery that changes the confirmed diff or command list re-entering confirmation, and a valid/invalid scenario for every configuration family across both the flat and in-repo layouts and both orchestrator runners. Surface-level discovery and validation are covered by `crates/shelbi-cli/tests/config_inventory_lint.rs`, and plugin resolution and fallback by the unit tests in `system_plugin.rs`. ## See also - [`shelbi config`](/docs/cli/config): the command reference for `inventory`, `lint`, and the keybinding subcommands. - [Config modes](/docs/concepts/config-modes): flat versus in-repo layouts, which shape the registration surfaces inventory reports. - [Agents](/docs/concepts/agents): how orchestrator and role agents are materialized into a worktree, where the system skill is installed last. [Source](https://shelbi.dev/docs/maintainers/system-configuration) --- # Release Runbook Maintainer checklist for tagging, GoReleaser dry runs, package verification, rollback, and signing-key recovery. This is the operational checklist for Shelbi maintainers publishing a release. Run every command from a clean checkout of `https://github.com/jlong/shelbi` unless a step says otherwise. ## Current Release Scope - **Primary repository:** `jlong/shelbi` - **Release binary:** `shelbi` - **Release branch:** `main` - **Release tags:** `vMAJOR.MINOR.PATCH`, for example `v0.1.0` - **Homebrew tap:** unresolved; plan recommends `shelbi/homebrew-shelbi` if a Shelbi org exists, otherwise `jlong/homebrew-shelbi` - **APT repository:** unresolved; plan recommends a dedicated Pages or object storage repository - **APT repository domain:** unresolved; plan recommends `https://apt.shelbi.dev` - **Current release owner:** unresolved - **Signing-key owner:** unresolved Do not ship a release until every unresolved owner, repository, and domain is confirmed in the maintainer channel. ## Required Access The release operator needs these local tools: ```bash cargo --version git --version gh --version goreleaser --version jq --version dpkg-deb --version ar --version tar --version gpg --version apt-ftparchive --version brew --version docker --version ``` The release automation needs these secrets: - `GITHUB_TOKEN` or a GitHub App token with permission to create releases and upload release artifacts in `jlong/shelbi`. - `TAP_GITHUB_TOKEN`, a fine-scoped token or GitHub App credential that can write only to the confirmed Homebrew tap where possible. - `APT_REPO_TOKEN`, a fine-scoped token or GitHub App credential that can write to the confirmed APT hosting repository. - `APT_GPG_PRIVATE_KEY`, an ASCII-armored private key for APT metadata signing. - `APT_GPG_PASSPHRASE`, the passphrase for the APT private key. - `APT_SIGNING_KEY_ID`, the public fingerprint for the APT metadata signing key. - OIDC permissions for keyless Sigstore signing, or `COSIGN_*` only if the release workflow chooses long-lived cosign secrets. The signing-key owner rotates APT and artifact signing keys. The release owner rotates publishing tokens. Rotate immediately after suspected exposure, staff offboarding, or a failed release where a secret may have been printed in logs. ## Pre-Tag Checks Start from an up-to-date `main`: ```bash git fetch origin main --tags git checkout main git pull --ff-only origin main git status --short ``` `git status --short` must print nothing. Confirm the version before tagging: ```bash VERSION=0.1.0 test "$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "shelbi") | .version')" = "$VERSION" ``` Run the workspace tests and build the exact release profile: ```bash cargo test --workspace cargo build --release --bin shelbi ./target/release/shelbi --version ``` The version output must match the tag you intend to publish: ```bash ./target/release/shelbi --version | grep "shelbi $VERSION" ``` Curate the changelog before tagging. `site/content/docs/changelog.mdx` is hand-maintained and updates nowhere else, so add or finish this version's entry now: a feature-level, newest-first note dated to when the work landed on `main`, folding internal refactors into the capability they enabled. Commit it to `main` before the tag so the published site matches the release. ## GoReleaser Dry Run Validate the GoReleaser configuration: ```bash goreleaser check ``` Build local snapshot artifacts without publishing: ```bash goreleaser release --snapshot --clean ``` Expected snapshot output under `dist/`: - `shelbi_Darwin_x86_64.tar.gz` - `shelbi_Darwin_arm64.tar.gz` - `shelbi_Linux_x86_64.tar.gz` - `shelbi_0.1.0_amd64.deb` - `checksums.txt` - signed checksums or Sigstore bundle, if signing is configured - Homebrew formula output, if the confirmed tap is configured - APT repository metadata output, if the confirmed repository is configured Linux `arm64` and Debian `arm64` artifacts are out of scope until they are built and smoke-tested in CI or on a reliable runner. Inspect the generated files: ```bash find dist -maxdepth 2 -type f | sort grep -E 'shelbi_(Darwin|Linux)|\\.deb$' dist/checksums.txt ``` ## Local Artifact Verification Verify archive contents and checksums: ```bash shasum -a 256 -c dist/checksums.txt tar -tzf dist/shelbi_Linux_x86_64.tar.gz | grep '^shelbi$' tar -xzf dist/shelbi_Linux_x86_64.tar.gz -C /tmp /tmp/shelbi --version rm /tmp/shelbi ``` Verify the Debian package locally: ```bash DEB=$(find dist -name 'shelbi_*_amd64.deb' | head -n 1) dpkg-deb --info "$DEB" dpkg-deb --contents "$DEB" dpkg-deb --field "$DEB" Package Version Architecture Maintainer dpkg-deb --field "$DEB" Package | grep '^shelbi$' dpkg-deb --field "$DEB" Version | grep "^$VERSION" dpkg-deb --field "$DEB" Architecture | grep '^amd64$' ``` Install and execute the package in a clean Debian or Ubuntu container: ```bash docker run --rm -v "$PWD/dist:/dist:ro" debian:bookworm bash -euxo pipefail -c ' apt-get update apt-get install -y /dist/shelbi_*_amd64.deb shelbi --version command -v shelbi ' ``` ## Homebrew Formula Verification After the dry run generates or updates the formula, check the formula in the confirmed tap checkout: ```bash TAP_DIR=/tmp/homebrew-shelbi-tap git clone git@github.com:OWNER/HOMEBREW_TAP_REPO.git "$TAP_DIR" cd "$TAP_DIR" brew audit --strict --online Formula/shelbi.rb brew style Formula/shelbi.rb brew install --build-from-source Formula/shelbi.rb shelbi --version brew test shelbi brew uninstall shelbi ``` Replace `OWNER/HOMEBREW_TAP_REPO` with the confirmed Homebrew tap. If the tap uses a different formula path, update the path before release and commit the runbook correction in the same change. ## APT Repository Verification Verify repository metadata before users can consume it: ```bash APT_ROOT=dist/apt find "$APT_ROOT" -type f | sort test -f "$APT_ROOT/dists/stable/Release" test -f "$APT_ROOT/dists/stable/InRelease" test -f "$APT_ROOT/dists/stable/Release.gpg" gpg --verify "$APT_ROOT/dists/stable/Release.gpg" "$APT_ROOT/dists/stable/Release" gpg --verify "$APT_ROOT/dists/stable/InRelease" grep '^Suite: stable$' "$APT_ROOT/dists/stable/Release" grep '^Codename: stable$' "$APT_ROOT/dists/stable/Release" grep '^Architectures: amd64$' "$APT_ROOT/dists/stable/Release" grep 'pool/.*/shelbi_.*_amd64.deb' "$APT_ROOT/dists/stable/main/binary-amd64/Packages" ``` Then install from the staged repository in a container: ```bash docker run --rm -v "$PWD/dist/apt:/repo:ro" debian:bookworm bash -euxo pipefail -c ' apt-get update apt-get install -y ca-certificates gnupg install -d -m 0755 /etc/apt/keyrings cp /repo/shelbi-archive-keyring.gpg /etc/apt/keyrings/shelbi.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/shelbi.gpg] file:/repo stable main" > /etc/apt/sources.list.d/shelbi.list apt-get update apt-cache policy shelbi apt-get install -y shelbi shelbi --version ' ``` For the live repository, replace the `file:/repo` source with the confirmed HTTPS APT domain and repeat the same container test. ## Tag And Publish Create an annotated tag only after the dry run and local verification pass: ```bash VERSION=0.1.0 git checkout main git pull --ff-only origin main git tag -a "v$VERSION" -m "Shelbi v$VERSION" git push origin "v$VERSION" ``` Publish with GoReleaser: ```bash goreleaser release --clean ``` Verify the GitHub release: ```bash gh release view "v$VERSION" --repo jlong/shelbi gh release download "v$VERSION" --repo jlong/shelbi --dir "/tmp/shelbi-v$VERSION" cd "/tmp/shelbi-v$VERSION" shasum -a 256 -c checksums.txt ``` Expected published artifacts: - Darwin `amd64` archive - Darwin `arm64` archive - Linux `amd64` archive - Debian `amd64` package - `checksums.txt` - Artifact signatures, if signing is enabled - SBOM/provenance files, if configured - Homebrew formula update in the confirmed tap - APT pool package, `Packages`, `Release`, `InRelease`, and `Release.gpg` ## Post-Release Verification Verify install paths after publication: ```bash brew update brew install shelbi shelbi --version brew test shelbi brew uninstall shelbi ``` Verify APT from the live repository: ```bash docker run --rm debian:bookworm bash -euxo pipefail -c ' apt-get update apt-get install -y ca-certificates curl gnupg install -d -m 0755 /etc/apt/keyrings curl -fsSL https://APT_DOMAIN/shelbi-archive-keyring.gpg -o /etc/apt/keyrings/shelbi.gpg echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/shelbi.gpg] https://APT_DOMAIN stable main" > /etc/apt/sources.list.d/shelbi.list apt-get update apt-cache policy shelbi apt-get install -y shelbi shelbi --version ' ``` Replace `APT_DOMAIN` with the confirmed release domain. The installed version must match `VERSION`. ## Rollback ### Bad GitHub Artifacts If an artifact is corrupt before the release is announced, stop package publication, delete the draft or unpublished release, and rerun the release from the same tag: ```bash VERSION=0.1.0 gh release delete "v$VERSION" --repo jlong/shelbi --yes goreleaser release --clean ``` After public announcement or package-manager publication, do not replace or delete GitHub release assets. Publish a patch version from a fix commit: ```bash NEXT_VERSION=0.1.1 git checkout main git pull --ff-only origin main git tag -a "v$NEXT_VERSION" -m "Shelbi v$NEXT_VERSION" git push origin "v$NEXT_VERSION" goreleaser release --clean ``` If the tag points at the wrong commit and the release is not public yet, delete the release and tag, then recreate the tag on the correct commit: ```bash VERSION=0.1.0 gh release delete "v$VERSION" --repo jlong/shelbi --yes git push origin ":refs/tags/v$VERSION" git tag -d "v$VERSION" git checkout CORRECT_COMMIT_SHA git tag -a "v$VERSION" -m "Shelbi v$VERSION" git push origin "v$VERSION" goreleaser release --clean ``` Announce the tag move in the maintainer channel. Never move a tag silently after users may have fetched it. After public announcement, leave the bad tag in place and publish a patch version. ### Bad Homebrew Formula Revert the formula commit in the tap and push the revert: ```bash cd "$TAP_DIR" git pull --ff-only git log --oneline -- Formula/shelbi.rb git revert BAD_FORMULA_COMMIT_SHA brew audit --strict --online Formula/shelbi.rb brew test shelbi git push origin HEAD ``` If the formula points at a bad GitHub artifact, complete the GitHub artifact rollback first, then update the formula checksums and run the formula verification again. ### Bad APT Package Republish APT metadata without the bad package as the candidate version. Leave the bad `.deb` in `pool/` for auditability unless it is actively harmful. The resulting repository must no longer advertise the bad version: ```bash apt-cache policy shelbi ``` Regenerate and sign repository metadata: ```bash apt-ftparchive packages pool > dists/stable/main/binary-amd64/Packages gzip -kf dists/stable/main/binary-amd64/Packages apt-ftparchive release dists/stable > dists/stable/Release gpg --batch --yes --default-key "$APT_SIGNING_KEY_ID" --clearsign -o dists/stable/InRelease dists/stable/Release gpg --batch --yes --default-key "$APT_SIGNING_KEY_ID" -abs -o dists/stable/Release.gpg dists/stable/Release ``` Run the live APT container verification again. Publish a maintainer note telling users to run `sudo apt update` before retrying. APT does not automatically downgrade installed packages; once at least two versions exist, document the manual downgrade: ```bash apt-cache madison shelbi sudo apt install shelbi=PREVIOUS_GOOD_VERSION sudo apt-mark hold shelbi ``` ### APT Key Compromise Treat a suspected signing-key exposure as a release incident: 1. Freeze APT publication and remove repository write credentials from CI. 2. Revoke the compromised key if a revocation certificate exists. 3. Generate a new offline signing key and store its revocation certificate in the maintainer secret store. 4. Export the new public key as `shelbi-archive-keyring.gpg`. 5. Replace `APT_GPG_PRIVATE_KEY`, `APT_GPG_PASSPHRASE`, `APT_SIGNING_KEY_ID`, and any deployment secret that could have accessed the old private key. 6. Re-sign the repository metadata with the new key. 7. Publish the new keyring at the confirmed APT domain. 8. Publish user-facing migration instructions that replace the old keyring file before running `sudo apt update`. Use these local commands for the new key material: ```bash gpg --batch --full-generate-key gpg --list-secret-keys --keyid-format LONG gpg --output shelbi-archive-keyring.gpg --export "$NEW_APT_SIGNING_KEY_ID" gpg --output shelbi-archive-revocation.asc --gen-revoke "$NEW_APT_SIGNING_KEY_ID" ``` Do not delete the old public key from public history. Users need a clear migration path from the old key to the new one. [Source](https://shelbi.dev/docs/maintainers/release) --- # Changelog Major Shelbi features and when they landed on main, newest first. The major, user-facing features Shelbi has grown, each with the date it landed on `main`, newest first. This page is hand-curated: it tracks capabilities, not every commit, so small copy edits and cosmetic tweaks are folded away. Entries are prose with `code` and links where they earn their place. ### August 5, 2026 **v0.7.2. Honest mid-flight comms and workflow-aware handoffs.** **`shelbi message` tells the truth about delivery.** The command used to print a checkmark and exit 0 the instant it wrote the durable record, conflating "queued" with "the worker read it" — and on some hosts the ack never once landed (12 sent, 12 timeouts). Root cause: the worker's ack was gated behind `jq`, absent on stock macOS and minimal Linux, so the ack block was silently skipped. Now the ack extraction is `jq`-free and sends over `nc` (flavour-detected, `-N` on Linux) or a `python3` fallback. The CLI stops over-promising too: a bare push prints `queued … not yet confirmed delivered`, `--wait` blocks and exits non-zero on the ack window elapsing, `shelbi message status <id>` reports the real outcome, and a `done` task or dead pane is reported undeliverable rather than implying a future pickup. **Handoffs follow the workflow's base branch.** The workspace startup prompt hardcoded `git rebase origin/main`, ignoring a task workflow's `git.base_branch`. For fan-out workflows whose base isn't `main` (e.g. `feature/{{feature}}`), agents that followed it produced wrong-base branches the merge guard rightly refuses. The prompt now rebases onto the workflow's resolved base, mirroring the merge side so the handoff and the merge target agree. **Smaller fixes.** `task add`/`task edit` no longer hang reading stdin when the body was already supplied by `--body`/`--body-file`/`-d`; the command palette hides the redundant 'Switch Project' entry from its empty-query menu (it still surfaces on typing); and the generated Homebrew formula drops the redundant `version` stanza that `brew audit --strict` rejects. ### August 4, 2026 **v0.7.1. Feature-branch fan-out you can trust.** **Subtasks land on their parent branch again.** A workflow whose `git.base_branch` is templated — `feature/{{feature}}`, `update/{{update}}`, `task/{{task}}` — now resolves that base from the task's frontmatter at every step instead of silently falling back to `main`. A subtask filed without its parent link is refused at dispatch rather than cut from a degraded base, `{{var}}` placeholders are validated against a workflow's `required_params` at load, and the finish-flow rebase freshens and restacks against the real `origin/<base>` — no more branches quietly rebased onto `main` and polluted with unrelated history. Most important, the handoff-less `merge` is now gated *before* a task is marked done: a merge that fails or integrates nothing leaves the task in-progress and emits a `merge status=failed` event instead of stranding a false `done` with the work landing nowhere. A pre-merge ancestry guard also stops a wrong-base squash from reverting an already-merged sibling. **Reliability.** `shelbi daemon restart` now recovers a launchd job wedged in the loaded-but-never-spawned state with `kickstart -k`, the orchestrator's event feed self-drains and emits keepalives so a dead follower can't silently blind it, and a review slot stranded by a quit or restart no longer re-runs the developer on resume. **Interface.** The dashboard sidebar and review panel now share one user-adjustable width, Add project moved into the projects sidebar, and an awaiting-input workspace shows a yellow `?` badge instead of a speech balloon. ### August 1, 2026 **v0.7.0. Shelbi Zen ships, and a reviewer to match.** **Shelbi Zen is a real autopilot now.** Zen can take a handoff branch all the way to `main` on its own: it runs the project's local pre-merge checks, gates on merge conflicts, diff size, and danger-path edits, then drives the PR create → CI-watch → squash-merge flow to completion. `shelbi zen probe` self-recovers when the hub's primary checkout is holding the task branch, and it flags the cases that used to slip through — a stale base, an already-merged branch, or a squash-merge that would silently revert a sibling PR. Orchestrator self-review is now codified in the shipped defaults, so the old fresh-context evaluator subagent is retired. **A two-column review interface.** Reviewing a task is now a dedicated experience: a left panel with the diff and reviewer actions, a View Diff action alongside Chat with Reviewer, and a reject-reason prompt that opens as a tmux popover with a real textbox and buttons. Queued-for-Review tasks auto-load onto an idle review slot, the Review agent is dispatched there instead of the status's Zen agent, and a review slot stranded by a quit or restart is resumed on the next launch. **The command palette learned about projects.** Switch Project is pinned to the top with inline "Switch to X" entries, each project carries a loaded or unloaded status glyph, and a second column lists your other projects — `Right` to focus it, `Up`/`Down` to move, `Enter` to switch. **A traveling sidebar for window-per-workspace navigation.** The dashboard sidebar now travels with you across per-workspace windows, clamped to its canonical width, and self-heals so a render fault or a process exit can't leave it gone. Activity is a plain-language timeline with multi-line wrapping rows, and project config validation errors surface directly in the sidebar. **System configuration ships as a skill.** Shelbi bundles a system configuration skill with an inventory and an all-surface lint, and injects the system plugin into the built-in runners so a fresh install carries it. **CLI and reliability.** `shelbi quit` tears down a running project cleanly, and `shelbi task edit` gains non-interactive flags for title, body, and field substitutions. SSH grew a stabilized managed `ControlMaster` — keepalive, per-host locking, and mux recovery — and now surfaces the ssh diagnostic behind a blank `exit status: 255` instead of swallowing it. The launchd daemon registers idempotently, and the heartbeat and poller are hardened so a busy board or an unreachable machine can't starve them. ### July 18, 2026 **v0.6.0. Workspaces, project names, and safer worktree integration.** **Workspaces are provisioned by the orchestrator, not hand-edited YAML.** New commands `shelbi workspace add <name>` and `shelbi workspace rm <name>` manage the pool, and a fresh project's workspaces are created through a first-boot interview: the orchestrator asks how many and which naming scheme (phonetic, greek, or toy-story) and creates them on the current machine. `shelbi init` no longer auto-provisions a pool. **Shelbi no longer overwrites your worktree settings.** Its Claude hooks are wired into `.claude/settings.local.json` — which Claude merges additively — instead of clobbering a committed `.claude/settings.json`. A pre-existing user-authored file is left untouched; the orchestrator merges the hooks in. **The default-branch commit guard only fires inside Shelbi.** The `pre-commit` guard that keeps agents from landing work directly on the default branch is now scoped to Shelbi-managed panes, so your own commits from a normal shell are never blocked. Its install is disclosed at `shelbi init`, and `shelbi guard uninstall` removes it. **Project names can be anything.** Name a project `ContextStore` or `My App`; Shelbi slugifies it for the on-disk folder and settings file and shows the human-readable name in the sidebar and command palette. **`git.branch` branch-name templates.** The git block gains a `branch` template key that parameterizes the branch names Shelbi cuts for tasks, rendered with `{{var}}` substitution, and mutually exclusive with the older `branch_prefix`. The shipped `subtask` workflow now cuts its branches from a `branch` template instead of a prefix. See the [git block reference](/docs/configuration/workflow#git). **Zen gained a fresh-context evaluator gate.** Before auto-merging a handoff, Zen spawns a read-only evaluator that checks the diff against the task's acceptance criteria and scope, so a branch that builds but doesn't do what the task asked is caught. Zen integration commits are now authored by you with the worker's real commit message rather than a synthetic bot identity. **Codex orchestrator liveness.** The heartbeat keep-alive that nudges a Codex-native orchestrator was restored, and `shelbi zen probe` now emits a structured JSON error when its own setup fails instead of empty output. ### July 15, 2026 **v0.5.0. Shelbi runs on Codex, not just Claude.** The orchestrator and workers can now run under Codex as a first-class runner alongside Claude. A `RunnerAdapter` capability model unifies per-runner behavior so dispatch, the board, and supervision treat both the same way, backed by a durable event and wake queue that survives restarts; Claude consumes that same durable feed and acknowledges each item so nothing is dropped or replayed. Where a runner has no lifecycle hooks, Shelbi fills the gap: Codex workers receive orchestrator messages by polling, and an idle Codex orchestrator wakes on board events rather than waiting for a prompt. `shelbi status` surfaces per-agent integration health and names the fallback reason when a runner is degraded, and switching a project between the Codex-native and legacy runners is a guided migration rather than a hand edit. **A detected getting-started plan.** `shelbi init` now reads the shape of the project and proposes a tailored setup plan instead of a one-size-fits-all default. **CLI version-mismatch detection.** The hub daemon warns when a workspace's `shelbi` CLI version has drifted from the hub's, so a stale binary on a remote machine shows up as a notice instead of confusing behavior. **"Add project" from the palette.** A new palette flow adds a project through a details dialog, without dropping back to the wizard. **Zen merge-safety hardening.** Zen's PR create and merge steps now match on exact provenance, so they no longer false-fail on Zen's own push or a rebased head, and local checks run under a per-check timeout so a loaded hub fails fast instead of hanging on one wedged check. ### July 10, 2026 **v0.4.0. A user-owned Zen policy that stays in context.** The Zen autopilot policy is now a user-editable `zenmode.md` file, and the hub re-injects its one-line summary into the orchestrator on a heartbeat cadence, so the policy keeps steering even as the orchestrator's context turns over. See [Zen Mode](/docs/concepts/zen-mode). **`shelbi reload <target>`.** Reload one piece in place, a workflow, an agent's instructions, or config, without restarting the whole hub. **Palette and workspace conveniences.** The command palette gains "Edit … Settings" openers, hidden until you query for them, and clicking an idle workspace now opens a user shell in its tmux window instead of doing nothing. **Auto-resume of usage-limit-stalled workers.** A worker parked on an agent usage limit now resumes automatically when its quota window resets, rather than sitting paused until you nudge it. **Sturdier under bad remotes.** `list` and `status` bound their per-workspace probes so a single wedged SSH host can't hang the whole command, and a worker's worktree detaches from its task branch on handoff so the branch is free to merge and clean up. ### July 9, 2026 **v0.3.0. A batteries-included default install.** `shelbi init` now sets up the loop most projects want without any YAML editing. Two things landed. First, six agent presets materialize on first load instead of three: the `orchestrator`, `developer`, and `review` roles that run the core loop, plus three specialized reviewers, `qa` (verifies a change against its acceptance criteria), `security` (a defensive-only diff review), and `adversarial` (an automated skeptic that tries to refute the change). The reviewers ship materialized but unwired, so the default board stays a plain `developer → review` loop and adding a gate is a one-line edit: name `agent: qa` (or `security`, or `adversarial`) on a status. Second, the scaffold ships two workflows instead of one. `task` is the new review-gated default: branch off `main`, `Backlog → Todo → In Progress → Review → Done`, one PR on the `In Progress → Review` edge, squash-merge to `main` on accept, with the `Review` status served on a `review`-tagged workspace by the Reviewer agent. `subtask` is a lighter flow for a piece of a parent task: it branches off and merges into the parent's branch, opens no PR, and has no review. Fresh projects get `default_workflow: task` written in, so the review gate is on out of the box. See the [agents concept](/docs/concepts/agents) and the [workflows guide](/docs/guides/getting-started/workflows#shipped-workflows-task-and-subtask). ### July 8, 2026 **v0.2.0.** The headline fix: launching a pane on a remote machine whose login shell is zsh no longer fails at startup. The tmux targets Shelbi passed over SSH were being eaten by zsh's equals-expansion, so remote workspaces died before the agent ever started; they now come up cleanly. The release also closes the loop on packaging: the release workflow now publishes the Debian package to the signed APT repository as part of the tag-triggered pipeline, so `apt install shelbi` picks up new versions without a manual publish step. And it ships two supervision fixes. The hub poller can now drive a workflow that declares no review/handoff status to completion: when a worker finishes and its status has an outgoing transition that merges, the poller advances the task along that edge (merge, branch cleanup) instead of stranding it in progress. And the hub checkout is guarded against stray commits: a Shelbi-managed pre-commit hook rejects commits made directly on the default branch, backed by dispatch-time branch checks. ### July 7, 2026 **v0.1.0, the first versioned release.** Shelbi now ships as prebuilt binaries, not just source. Pushing a `v*` tag runs a release workflow that builds macOS (Apple Silicon and Intel) and Linux x86_64 binaries, publishes them with SHA256 checksums and GitHub artifact attestations, opens a version-bump PR against the Homebrew tap, and produces a Debian package. `brew tap jlong/shelbi && brew install shelbi` on macOS; a signed APT repository serves Ubuntu. See the [install guide](/docs/guides/getting-started/install) for both paths. ### July 3, 2026 **Usage-limit pause detection.** The workspace poller now recognizes when a worker has stalled on an agent usage limit rather than genuinely working, and surfaces a `⏸` pause badge in the sidebar roster instead of leaving the pane looking busy. You can tell at a glance which workers are blocked on quota and will resume on their own versus which need a hand. **Adaptive hub heartbeats.** The `heartbeat` cadence now backs off when the board is quiet instead of firing at a fixed interval forever. It holds at the standard `interval` (default `3m`) while there's supervisable work in flight, then doubles each idle tick, capped at a new `max` bound (default `60m`), once the board goes quiescent, and snaps back to `interval` on the next real event. The config gains the `max` field: `heartbeat: 3m` still works (bare interval, default cap), as does `heartbeat: off`, and a map `heartbeat: { interval: 3m, max: 60m }` sets both bounds. Every existing project gets back-off with no YAML edit. Backing off on "nothing supervisable in flight" rather than "no log line" is deliberate: a silently-stuck `in_progress` task also emits nothing, and the heartbeat sweep is exactly what catches it. See the [`heartbeat` field reference](/docs/configuration/project#heartbeat) and [the events log](/docs/concepts/events-log#heartbeats). **`shelbi task resume`.** A stalled or killed worker no longer has to lose its task. `shelbi task resume <task>` relaunches the runner in the worker's existing worktree, picking the branch back up where it left off instead of dispatching from scratch. **Crash-safe workspaces.** Shelbi now auto-restarts a managed pane if its agent crashes, and re-submits the in-flight prompt after the restart so the worker resumes the task rather than sitting idle at a fresh prompt. **Self-documenting config on `.yaml`.** New config scaffolds ship with their optional sections present but commented, so the file itself documents every knob you can turn. No need to hunt the reference to discover a setting exists. At the same time every config file standardizes on the `.yaml` extension; existing `.yml` files are migrated automatically on first load. **Wizard simplified.** The onboarding wizard's sole phase is project setup. The orchestrator prompt now reads *"You are the Orchestrator"* statically, with no legacy name placeholder substitution at render time. `~/.shelbi/shelbi.yaml` is now just the per-project `last_launched` index. ### July 2, 2026 **Review workspaces.** Reviewing a finished task now loads its branch into a dedicated, long-lived review workspace with its own agent, rather than a throwaway top-level clone. The orchestrator dispatches the review onto that workspace, a review queue tracks what's waiting, and the sidebar gains `Ready` and `Queued` sections so you can see the pipeline at a glance. The server pane persists across reviews and is reaped on completion. ### July 1, 2026 **In-repo project config.** A project's configuration can now live inside the repo at `.shelbi/project.yaml` instead of only under `~/.shelbi/`, so it can be committed and shared with a team. `shelbi migrate-to-in-repo` converts an existing global project in one step, and resolution is mode-aware about which half of the config is user-local versus shared. ### June 30, 2026 **Two-way worker communication.** Workers can talk back to the orchestrator, not just the other way around. A hub-side Unix socket (reverse-forwarded to remote machines over SSH) carries a per-task message log, and hook-capable workers inject those messages via `SessionStart` + `Stop` hooks; non-hook runners poll for them in the prompt. On top of it sits a clarification loop (a worker can ask a question and block for an acknowledged answer) plus `launchd`/`systemd` supervision so the hub socket survives restarts. ### June 29, 2026 **Custom agents and configurable workflows.** The worker concept was reframed as a *workspace* that runs a named *agent*, and dispatch resolves which agent to run from the task's workflow status, so different columns can hand work to different agents, each with its own context and a shared preamble. A `shelbi agent` CLI quartet (`list`/`show`/`new`/`edit`) manages them. Kanban columns are no longer hardcoded either: `workflows/statuses.yml` defines the status identity the board and CLI render from. **`shelbi open` and marker-free project resolution.** The pane-lifecycle entry point is now the top-level `shelbi open <workspace>` (was `shelbi workspace open`): same behavior, focus a workspace pane and create it if it doesn't exist yet, under a shorter name. Shelbi also stopped dropping a `.shelbi/project` marker into each repo to identify the active project; resolution now scans `~/.shelbi/projects/*.yaml`, collects each project's local `work_dir`, and matches the current directory against them (deepest match wins). Less stray state on disk; the project YAML is the single source of truth. ### June 25, 2026 **Hub heartbeats.** The hub-side workspace-state poller appends a periodic `<ts> project=<name> heartbeat` line to `~/.shelbi/events.log`, giving the orchestrator's `events tail --follow` watch a guaranteed recurring trigger when the board is otherwise quiet. Cadence is set per project via the `heartbeat` key in `project.yaml`: `45s`, `3m`, `1h`, or `"off"` to disable; default `3m`. Bare integers (`heartbeat: 180`) are rejected at load time, so a missing-unit typo can't silently land as seconds. Emission is debounced against any other write to the events log, so active boards don't see padding. Heartbeats also pause while the hub is offline. A quick TCP probe of `1.1.1.1:443` gates each due tick and emission resumes once connectivity is back, so a coffee-shop wifi drop doesn't fill the feed with no-op lines the orchestrator can't act on anyway. Heartbeats are filtered out of the TUI activity feed by default (they'd produce one "nothing happened" row every few minutes) but show up verbatim under `shelbi events tail`. See [the events log](/docs/concepts/events-log#heartbeats) for the line shape and rationale. ### June 24, 2026 **Zen Mode.** An opt-in autopilot that lets the orchestrator promote and merge low-risk work without waiting on you. A per-action confidence bar, configurable project checks, danger-path guards, and a `dry-run` preview keep it honest, and the `ZEN ON` pill in the sidebar (toggle with `Alt+Z`) makes the state unmistakable. Zen auto-disables after an orchestrator crash so it never runs unsupervised. Drive it from the CLI with `shelbi zen on|off|pause|status`. ### June 23, 2026 The first public cut of Shelbi: an open-source agent orchestrator for the terminal, built on tmux. You talk to one orchestrator agent; it delegates work to worker agents running in tmux panes, locally or over SSH, and reports back. This landed the full local-to-remote loop end to end. **Onboarding.** A first run with no projects drops you into a two-phase, idempotent wizard that walks each project through setup. It auto-fills from your environment (repo path, default branch, GitHub URL) and suggests a worker count from available RAM. `shelbi` with no arguments launches the sole project's TUI, or a fuzzy picker when there's more than one. **The TUI.** A two-pane ratatui dashboard: a sidebar with Chat/Tasks navigation, a live worker roster with state badges (working, awaiting input, awaiting permission, review-ready, idle), and a `Ready for Review` queue; beside it a content pane that swaps between the orchestrator chat, a five-column Kanban (Backlog → Todo → In Progress → Review → Done), a Machines view, and a Review list. `Ctrl+P` opens a Nucleo-backed fuzzy command palette as a tmux popup for jumping between projects, workers, and views. **Orchestration.** Move a card to Todo and the orchestrator takes it from there. It watches the column and, as soon as a worker frees up, checks out the highest-priority unblocked card on a fresh branch in that worker's own worktree and starts the runner. `depends_on` blocks a task until its dependencies land in Done. Finished work lands in Review and the worker stops; activating a review checks the branch out, spawns a fresh agent pane to interrogate the diff, and lets you merge into the default branch, or push and open a PR with `--pr`. **Anywhere tmux runs.** Workers run on the hub or on any machine reachable over SSH with `tmux` and an agent CLI installed. Shelbi drives panes with `tmux send-keys` + `capture-pane`, transparently prefixed with `ssh host --` for remotes; a poller reads each pane's `shelbi:<state>` title marker into worker status, which is what lights up the sidebar badges. Agent runners (`claude`, `codex`, or any interactive CLI) are pluggable and declared per project. **State you can read.** No daemons, no servers, no database: every project, task, log, and worker status is a markdown or YAML file under `~/.shelbi/`. The same `shelbi` CLI the orchestrator drives (`task`, `merge`, `reload`, `events`, …) is the one you can run yourself. **The website and docs.** A Next.js marketing site and a contentlayer-backed MDX documentation set (Getting Started, Concepts, and a CLI reference for every `shelbi` subcommand) ship alongside the release. Install from source with `./scripts/install.sh` (builds `--release`, drops the binary at `$HOME/bin/shelbi`, ad-hoc re-signs on macOS). [Source](https://shelbi.dev/docs/changelog)