Project config
View as markdownThe 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:
| Mode | Where the config lives |
|---|---|
| Global (default) | One flat file: ~/.shelbi/projects/<id>.yaml carries every field. |
| In-repo | A two-file split: the committed <repo>/.shelbi/project.yaml carries the shared fields (SHARED_PROJECT_FIELDS); a per-machine ~/.shelbi/projects/<id>/local.yaml carries the user-local fields (LOCAL_PROJECT_FIELDS). No flat ~/.shelbi/projects/<id>.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 project's sessions, 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.
In the flat global file the name: key is a free-form human label (see
top-level fields); it never sets the id. The committed
in-repo project.yaml is the one exception, described below.
In global mode every field lives in the one flat
~/.shelbi/projects/<id>.yaml that shelbi init --mode global writes. In-repo
mode uses a two-file split instead. shelbi init --mode in-repo writes
both halves; shelbi init --pick-up writes just the user-local half and reads
the already-committed shared half in place:
- Shared fields (
SHARED_PROJECT_FIELDS) —name,display_name,default_branch,default_workflow,orchestrator,agent_runners,runners,agents,zen,heartbeat,git, and the rest — live in the committed<repo>/.shelbi/project.yaml, so every clone shares them. - User-local fields (
LOCAL_PROJECT_FIELDS) —repo,machines,workspaces,editor— live in a per-machine~/.shelbi/projects/<id>/local.yaml.
There is no flat ~/.shelbi/projects/<id>.yaml for an in-repo project. 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.
In the committed <repo>/.shelbi/project.yaml, the name: key is the
project's canonical id: a valid slug that shelbi init --pick-up reads back
to seed a teammate's local alias. The human label rides under display_name:
(only emitted when the entered name was slugified into a different id). This is
the one file where name: carries an id rather than a label. The id Shelbi uses
locally is still the registry directory name (~/.shelbi/projects/<id>/), which
can be a suffixed alias such as myapp-2 when the canonical id is already taken
on your machine.
A present local.yaml is authoritative
Once ~/.shelbi/projects/<id>/local.yaml exists, that split is the project's
config: the loader reads the committed shared half plus your local half, and a
stale flat ~/.shelbi/projects/<id>.yaml left over from a pre-migration global
registration can never shadow it.
shelbi project migrate-to-in-repo
is now just the path that converts an existing flat/global project into this
same split — fresh init --mode in-repo and --pick-up produce it directly.
Minimal example
# ~/.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: ~/Workspaces/myapp
default_branch: main
# Optional: tasks without `workflow:` use workflows/app.yaml instead of
# workflows/task.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 }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. 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 | path | yes | — | Local path to the repository checkout (~ is expanded). shelbi init writes the absolute path. In in-repo mode Shelbi reads <repo>/.shelbi/project.yaml from here. 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 | task | Workflow used by tasks that omit workflow: frontmatter. Names workflows/<name>.yaml; explicit task workflow: still wins. shelbi init writes task. An unset or legacy default value is rewritten to task by shelbi reload, and until then default resolves to task.yaml when no default.yaml exists. |
machines | list of Machine | yes | — | The hosts workspaces run on. User-local. |
orchestrator | Orchestrator | yes | — | Which runner boots the orchestrator. |
agent_runners | map of name → AgentRunner | yes | — | The command lines agents and the orchestrator are launched with. |
runners | map of RunnerKind → ProjectRunner | no | {} | The kind-keyed runner fleet plus an optional house model / reasoning_effort per kind. The evolution of agent_runners; see agents. Additive: empty projects keep resolving through agent_runners. |
agents | map of name → ProjectAgent | no | {} | Per-agent overrides, keyed by the agent's directory name. Pins which runner kind the project runs that agent on. See agents. |
workspaces | list of Workspace | no | [] | Fixed pool of workspace slots. User-local. |
zen | Zen | no | all defaults | Zen Mode check list, CI timeout, and danger paths. |
editor | string | no | — | Reserved; currently unused. shelbi … edit commands use $VISUAL, then $EDITOR, falling back to vi; the editor for "Edit in <editor>" is set in global config. 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. |
workspace_poll_interval_secs | integer | no | 5 | How often the hub poller samples each workspace session for state changes. |
github_reconcile_interval_secs | integer | no | 900 | How often (seconds) the hub poller reconciles review tasks whose PR was merged on GitHub out-of-band, advancing them to done and skipping the local merge. Runs on its own slow clock, never the workspace poll tick. Set to 0 to disable. |
workspace_permissions_mode | string | no | — | Explicit project-wide permission ceiling rendered into the workspace settings template. Unset (the default) passes no --permission-mode and renders no defaultMode, so agents run under your own ~/.claude/settings.json permissions.defaultMode. Set it to clamp every agent's requested mode downward. |
workspace_settings_template | path | no | <config-root>/workspace-settings.json.template | Override path for the workspace settings template (~ is expanded). |
heartbeat | Heartbeat | no | 60s / 5m | Adaptive hub heartbeat written to events.log: the orchestrator's fallback wake-up on a quiet board. Holds at interval while work is in flight, backs off toward max when quiescent. |
disk | Disk | no | 20 / 20 | Hub disk management: the size cap on the shared cargo target/ and the low-disk free-space warning threshold. Both default to 20 GiB and are elided from the wire form when unset. |
git | Git | no | all defaults | Base branch, generated task branch prefix, and merge strategy for the review merge and Zen's auto-merge. |
review | Review | no | all defaults | Review-interface behavior. Today just diff_command, which makes View Diff run a command of your own, such as a tool that reviews a revision range. |
issue_tracker | IssueTracker | no | file_system | Which backend the board lives in: file_system (local markdown, the default) or github (issues in a repo). See GitHub Issues backend. |
session | { raw_output_log } | no | off | raw_output_log: true keeps a full raw output log of every session at ~/.shelbi/sessions/<id>/raw.log. Opt-in, for debugging: the log is mostly repaint noise for full-screen agents and captures anything pasted into a session. |
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 for kind: ssh; defaults to name when omitted. Ignored for local. |
tags | list of string | no | [] | Capability tags every workspace on this machine inherits (e.g. gpu). A tag declared here applies to all of the machine's slots without repeating it per workspace, so a machine-level review tag turns every slot on that machine into a review slot. The legacy shorthands (tag: x, tags: x) still load, but the config-upgrade pass rewrites them to tags: [x]. Elided when empty. |
forward | unix | tcp | no | auto | How the hub reverse-forwards its daemon socket to an ssh machine. Unset (the default) auto-detects: start with a Unix-socket forward and fall back to TCP loopback if the Unix landing socket is unusable (the Tailscale-SSH root-owned-socket case). tcp skips detection and forwards over TCP loopback from the first attempt; unix pins the Unix forward and disables the fallback. Ignored for local. Elided when unset. |
machines:
- name: hub
kind: local
work_dir: ~/Workspaces/myapp
tags: [gpu] # every slot on hub inherits the gpu tag
- name: m2
kind: ssh
host: m2.local
work_dir: ~/work/myapporchestrator
The orchestrator is a single agent that watches the board and dispatches work. See the orchestrator concept.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
runner | string | yes | — | Name of an entry in agent_runners used to boot the orchestrator. |
orchestrator:
runner: claudeTo run the orchestrator with Codex, point orchestrator.runner at a
declared Codex runner:
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 the
worktree's .claude/settings.local.json wires
$CLAUDE_PROJECT_DIR/.shelbi/hooks/*.sh (session-start, stop,
pane-working, pane-idle, pane-blocked) so the session 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 orchestrator session. It does not
change worker slots. A worker slot no longer selects a runner: the
dispatched agent (role) owns runner/model resolution through its
agent.yaml manifest, and the project's runner
fleet supplies the concrete launcher. So a project can run a Codex orchestrator
while dispatching a task agent onto Claude, or run one agent on Opus and another
on Sonnet, all resolved per agent rather than per slot. See
agents for the manifest and the precedence chain.
agent_runners
A map from runner name to the command line agents are launched with.
Referenced by orchestrator.runner, and used as the concrete launcher an
agent resolves onto (kind-keyed lookup, bridged
from this map when no top-level runners fleet is declared).
| 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 session frozen on a dialog. When empty, defaults keyed on command apply. |
prompt_injection | { kind, flag? } | no | by executable basename | How Shelbi delivers the per-task initial prompt. kind is positional-arg, flag-file (with flag, e.g. --message-file), stdin, or paste (for REPL-only harnesses). When omitted, claude and codex get a positional prompt, aider gets --message-file <prompt-file>, and anything else reads the prompt on stdin. |
integration | claude | codex | generic | no | detected from command | Pins the runner adapter, overriding the basename detection, so a wrapper executable Shelbi wouldn't recognize can still get the Claude or Codex contract. |
agent_runners:
claude:
command: claude
flags: []
codex:
command: codex
flags: []Note
Model-in-flags stays supported: an agent_runners entry that bakes
--model into flags keeps working. When an agent
resolves a model, that model takes precedence and the stale baked flag is
replaced, so the launched line always carries exactly one model.
runners
An optional, kind-keyed runner fleet that layers on top of
agent_runners: one entry per runner kind
(claude / codex / generic) carrying the concrete launcher plus an optional
house model / reasoning_effort authoritative for every agent of that
kind. A field it omits falls through to each agent's agent.yaml recommendation.
Empty by default; when empty, a kind resolves through the agent_runners bridge.
See agents for the full shape and
the resolution precedence.
runners:
claude:
command: claude
# model / reasoning_effort omitted -> each agent's manifest decides
codex:
command: codex
model: gpt-5 # authoritative for every codex agentagents
Per-agent overrides keyed by the agent's directory name. Today each entry
carries a single runner: field that pins which runner kind
the project runs that agent on, authoritative over the manifest's
preferred_runner. There is deliberately no per-agent model block: per-agent
differentiation comes from each agent's manifest, and a house model is set once
at the kind level in runners.
agents:
orchestrator: { runner: codex }
review: { runner: claude }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.
| 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 this slot runs on. |
tags | list of string | no | [] | Capability tags for this slot. A workspace's effective tags are these unioned with its machine's tags. Tag-based routing picks a free workspace whose effective tags are a superset of a status's required tags. The legacy shorthands (tag: x, tags: x) still load, but the config-upgrade pass rewrites them to tags: [x]. Elided when empty. |
slot | integer | no | declaration-order index | Numeric slot exported to transition run 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. A workflow's review: recipe uses only an explicit slot: as its port ($SLOT / $PORT), so give each review slot a port-valued slot:. Elided when unset. |
workspaces:
- { name: alice, machine: hub }
- { name: bob, machine: m2 }
- { name: rev, machine: hub, tags: [review], slot: 3000 }A workspace no longer selects a runner
Runner/model/effort are resolved per dispatch from the agent that runs in
the slot (see agents), not from the workspace.
A legacy runner: key on a workspace is accepted-and-ignored on load and
dropped on the next save, so an existing project migrates with no manual step.
To change which runner an agent uses, set agents.<name>.runner (a
runner kind) or a house model in
runners.
Route a slot to review work by giving it the review tag. See
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. Per-workflow overrides live in the workflow file; 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. On a Rust project, add shelbi msrv-check to catch a minimum-Rust-version break before CI does. |
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 takes extend, which keeps the built-in danger list and adds
yours, or override, which replaces the built-ins entirely. A legacy bare list
still loads as extend, but the
config-upgrade pass rewrites it to
{ extend: [...] }:
zen:
checks:
local:
- cargo test --workspace --no-fail-fast
- shelbi msrv-check # build on the Cargo.toml rust-version
- npm --prefix site test
ci_timeout: 1200
danger_paths:
extend: [".env", "infra/**"] # or: override: [...]heartbeat
The recurring line the hub poller writes into events.log so the
orchestrator wakes up to sweep active tasks even when no real transition has
fired. It is the orchestrator's fallback wake-up and the stream's liveness
signal. See
the events log 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
60s → 2m → 4m → 5m, then resets to 60s the moment something
happens.
heartbeat accepts three shapes:
| Form | Meaning |
|---|---|
heartbeat: 60s | Bare duration — sets interval and keeps the default max (5m). |
heartbeat: off | Disables heartbeats entirely. |
heartbeat: { interval: 60s, max: 5m } | Map — sets both bounds explicitly. |
The map form's fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
interval | duration | no | 60s | Standard cadence, used whenever supervisable work is in flight. |
max | duration | no | 5m | 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).
heartbeat:
interval: 60s # standard cadence while work is in flight
max: 5m # back-off cap once the board is quiescentdisk
Hub disk management. Rust build output accumulates in the hub checkout's
shared cargo target/ and can fill the data volume over a week of Zen
builds, and a full volume looks like a wedged hub (probes hang,
heartbeats stop, CLI calls take minutes). These two knobs bound that.
Both default to 20 GiB and apply even when the disk: block is absent,
so an existing project is already protected without any config change.
The block only makes the knobs explicit and tunable, and is elided from
the wire form when left at the defaults.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
target_cap_gib | integer (GiB) | no | 20 | Size cap on the hub checkout's shared cargo target/. Before a Zen probe builds, a target/ larger than this is cleared first, so the baseline rebuilds cold instead of growing without bound. It is never cleared while another probe is building into it. A 0 folds back to the default rather than disabling the cap. |
low_free_gib | integer (GiB) | no | 20 | Free-space threshold on the volume holding the hub machine's work_dir. When free space drops below it, the heartbeat line carries disk_free_gib=<n> disk_low=true and shelbi status shows a visible warning with the free space. Above the threshold both stay quiet. A 0 folds back to the default. |
disk:
target_cap_gib: 20 # clear the hub's shared cargo target/ above this
low_free_gib: 20 # warn when the work_dir volume drops below thisgit
Where workspace branches are based, how generated task branches are named, and how they integrate back. See understanding workflows.
| 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. Mutually exclusive with branch_prefix. |
branch_prefix | string | no | none | Shorthand alternative to branch: when a task omits branch:, Shelbi cuts <branch_prefix>/<task-id>. Use it when a bare prefix is all you need and you don't want to spell out a full {{var}} template. Mutually exclusive with branch — declaring both in one git: block fails the load. |
merge_strategy | squash | merge | rebase | no | squash | How the workflow merge action integrates a reviewed branch. The merge action supports squash and merge; rebase is accepted by the parser but the action rejects it. Zen exact-ref auto-integration currently supports squash; other strategies require human PR merge. |
git:
base_branch: develop
branch: '{{github_user}}/{{id}}'
merge_strategy: mergeAfter substitution, branch must use the same safe characters Shelbi allows
for generated refs: ASCII letters, numbers, -, _, and /. It must not end
with /. A task's branch name resolves in this order:
- An explicit
branch:in the task's frontmatter. - The task's workflow
git:block, when it setsbranchorbranch_prefix. It replaces the project's naming wholesale. - The project's
git.branchtemplate. - The project's
git.branch_prefix, as<branch_prefix>/<id>. <github-login>/<id>, using your authenticated GitHub username, oruser/<id>when it can't be determined.
The shipped task workflow sets branch: '{{github_user}}/{{id}}'.
review
Behavior of the review interface's View Diff action.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
diff_command | string | no | none | Command View Diff runs for this project instead of Shelbi's own diff view or your diff_tool. See below. |
By default View Diff opens Shelbi's own diff view. Set diff_command for a
tool whose CLI takes a revision range (such as skim, invoked skim <base> <head>),
which diff_tool can't express:
that setting hands a tool a pair of files or folders.
The command runs in the review worktree with three placeholders substituted (shell-escaped):
{worktree}: the review workspace worktree,{base}:merge-base(<base branch>, HEAD), the fork point, and{head}:HEAD.
{base} and {head} are the same range the built-in view shows.
review:
diff_command: skim {base} {head}A diff tool that exits without rendering leaves a short message in the pane rather than only git warnings.
The config-upgrade pass currently misreports a
review: block as a removed key (PROJECT_REMOVED_KEY). The block is live:
keep it rather than acting on that finding.
issue_tracker
Which backend the project's board lives in, plus the non-secret connection facts
for a remote backend. An absent block is the default (file_system), so existing
projects keep working. See GitHub Issues backend
for the concept and the full behavior.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
backend | file_system | github | jira | linear | no | file_system | The live backend. file_system (local markdown) and github resolve to a working board today; jira and linear parse and validate but return a "not yet implemented" error. |
github | GithubConnection | when backend: github | — | GitHub connection facts. |
jira | object | when backend: jira | — | { project: KEY }. Stub backend. |
linear | object | when backend: linear | — | { team: KEY }. Stub backend. |
refresh_secs | integer | no | 30 | How often (seconds) the hub daemon re-reads a remote board and rewrites board-index.json. 0 or absent folds to the default. Ignored for file_system, whose local read is free. |
budget | Budget | no | all defaults | Rate-limit governor thresholds for a remote backend. |
Only the selected backend's connection block is validated, so you can stage a
github: block alongside backend: file_system without it failing the load. The
auth token is never a field here: it is resolved out of band from gh (see
prerequisites).
issue_tracker:
backend: github
github:
repo: owner/repo
# Optional, remote-only:
refresh_secs: 30github connection
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
repo | string | yes (when backend: github) | — | owner/repo slug naming the repository the issues live in. Validated as a well-formed slug. |
issue_tracker budget
The rate-limit governor's thresholds for a remote backend. Every field defaults, so an untouched block is the shipped policy. See freshness and rate limits.
| Field | Type | Default | Description |
|---|---|---|---|
graphql_high | integer | 2000 | GraphQL points above which the refresh runs at the configured cadence unthrottled. |
graphql_medium | integer | 500 | Points down to which the daemon slows (rather than pauses) the refresh, using slow_refresh_secs. |
graphql_low | integer | 100 | Accepted but not read today. The governor uses two thresholds: between graphql_medium and graphql_high the refresh slows to slow_refresh_secs, and at or below graphql_medium it pauses until the budget resets. |
slow_refresh_secs | integer | 120 | Throttled refresh cadence for the middle GraphQL band. |
rest_reserve | integer | 100 | REST requests-remaining reserve below which a mutating call is refused up front. |
The PR-body template (pr-template.md)
Alongside the project's YAML, workflows/, agents/, and zenmode.md, each
project carries a pr-template.md. It is the guidance a developer worker
follows when it authors the pull-request body for a finished task. Shelbi
materializes it from a shipped default on shelbi init, self-heals it on
shelbi reload, and the boot-time
config-upgrade pass materializes it for
projects that predate the file. Like zenmode.md and the agent instructions,
it is user-editable and self-heal-preserving: your edits survive a reload
byte-for-byte, and editing the file changes the guidance workers follow for
subsequent PRs.
Not a substitution template
Unlike the git.branch template above, pr-template.md has no
{{var}} placeholders and does no variable substitution. It is instructions to
the worker, not a fill-in-the-blanks form. Only an agent that understands the
change can write a real summary, produce a Mermaid diagram, sketch an ASCII
wireframe, or list a concrete QA checklist, so the worker reads the template and
writes the body itself.
Where it lives
The path is config-mode-aware, resolving next to the rest of the project's config half:
| Mode | Where pr-template.md lives |
|---|---|
| Global (default) | ~/.shelbi/projects/<id>/pr-template.md |
| In-repo | <repo>/.shelbi/pr-template.md |
What the shipped default asks for
The default template tells the worker to write the body for the human reviewer, grounded in the real problem the change solves rather than a copy of the task requirements, in these sections, top to bottom:
- Summary: a few sentences on the feature or fix and the problem it solves, so the reviewer understands the why before the how.
- Technical details: key design decisions and rejected alternatives, tradeoffs, risky or subtle areas, and any migration or rollout notes.
- Data model changes: a conditional section, included only when the change touches a database or schema, with a Mermaid ER or class diagram GitHub renders natively.
- UI changes: a conditional section, included only when the change alters the interface, with a shareable screenshot when one exists or an ASCII wireframe otherwise.
- QA checklist: concrete, checkable items a reviewer or tester can run through, using GitHub task-list syntax.
Conditional sections are omitted entirely when they do not apply, so a non-schema, non-UI change carries only sections 1, 2, and 5.
How a worker uses it at handoff
When a worker finishes a task it reads pr-template.md, writes the finished
body to .shelbi/pr-body.md in its worktree, then emits its ready marker as
usual. The orchestrator picks up .shelbi/pr-body.md when it opens the pull
request. If the file is absent (a worker that genuinely could not produce a
useful description just does not write it), Shelbi falls back to a
task-body-based PR, so a missing description never blocks the handoff.
The body belongs to the task it was written for. Dispatching a different task onto the worktree removes the file, and a pull request only uses a body from the task the worktree is currently on. Sending the same task back to the worktree, after a bounce for example, keeps its body.
See also
- Config modes — global vs. in-repo layout and the shared/user-local field split.
- Config upgrade: the boot-time pass that
materializes
pr-template.mdfor older projects and heals drifted config. - Workspaces and
the orchestrator — the concepts behind
workspaces:andorchestrator:. - Workflow config — the per-project
status pipeline and its per-workflow
zen/gitoverrides. - GitHub Issues backend: the concept behind the
issue_trackerblock and how a GitHub-backed board behaves.