Global vs Repo Config

View as markdown

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: issue 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.

Which mode?

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

PieceGlobal modeIn-repo mode (fresh init / --pick-up)
Shared config (name, default_branch, orchestrator, agent_runners, zen, heartbeat, git — SHARED_PROJECT_FIELDS)~/.shelbi/projects/<name>.yaml (one flat file)Committed <repo>/.shelbi/project.yaml (with config_mode: in-repo)
User-local config (repo path, machines, workspaces, editor — LOCAL_PROJECT_FIELDS)Same flat ~/.shelbi/projects/<name>.yamlPer-machine ~/.shelbi/projects/<name>/local.yaml (no flat <name>.yaml)
Shared directories (workflows/, agents/, workspace-settings.json.template)~/.shelbi/projects/<name>/<repo>/.shelbi/
State (state.json, tasks/, HANDOFF.md, .claude/, workspaces/, events.log)~/.shelbi/projects/<name>/ (+ ~/.shelbi/events.log)~/.shelbi/projects/<name>/ (+ ~/.shelbi/events.log)
DiscoveryReverse-lookup: cwd matched against every registered project's local work_dirWalk up from cwd for <dir>/.shelbi/project.yaml, then require a matching ~/.shelbi/projects/<name>/local.yaml
Shared with clones?No: every teammate reruns shelbi initYes: everything under <repo>/.shelbi/ (the shared project.yaml plus the workflows/agents/settings directories, minus the gitignored state) travels with git clone; each teammate adds their own local.yaml via --pick-up

A present local.yaml is authoritative

Fresh shelbi init --mode in-repo and shelbi init --pick-up produce this split directly: the shared half is committed at <repo>/.shelbi/project.yaml and your machines live in ~/.shelbi/projects/<name>/local.yaml. Once that local.yaml exists the loader treats it as authoritative — it reads the committed shared half plus your local half, and a stale flat ~/.shelbi/projects/<name>.yaml (a pre-migration global registration, a hand copy) can never shadow it. migrate-to-in-repo is now just the path that converts an existing flat/global project into the same split.

The full field-by-field bucket lists are in shelbi_core::model under SHARED_PROJECT_FIELDS and LOCAL_PROJECT_FIELDS; a committed 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/<name>.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 <name> 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 with a single command.

In-repo mode

In-repo mode commits the project's shared config — <repo>/.shelbi/project.yaml plus the shared workflows/, agent prompts, and settings template under <repo>/.shelbi/ — so everyone who clones the repo shares the same declarative setup. Each teammate still has their own machines and workspace pool; those user-local fields live in a per-user ~/.shelbi/projects/<name>/local.yaml that never gets committed. Fresh shelbi init --mode in-repo writes both halves of that split; shelbi init --pick-up writes just the local.yaml half against a shared config a teammate already committed.

On-disk layout (fresh init / --pick-up)

<repo>/
  .shelbi/
    project.yaml              # ← committed shared half: name, display_name,
                              #   default_branch, orchestrator, agent_runners,
                              #   zen, heartbeat, git, 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/
    <name>/
      local.yaml              # ← user-local half: repo, machines, workspaces,
                              #   editor (no flat <name>.yaml for in-repo)
      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 <repo>/.shelbi/project.yaml, or a zen: block inside local.yaml, raises an error identifying the misplaced field and naming the file it belongs in. Migrating an existing global project produces the identical layout — see migrating a project to in-repo mode.

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 committed project.yaml, Shelbi reads the name: field out of it and checks that you've registered the project locally (normally a ~/.shelbi/projects/<name>/local.yaml). If you haven'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). Pick-up writes that user-local local.yaml and reads the committed shared half in place; it never rewrites the committed file.

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 <repo>/.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 <repo>/.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 <repo>/.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. The local alias is fixed at pick-up time; there is no rename command, so if you want a different alias, re-pick-up the clone (or edit the registry filename and its entry by hand).

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/<name>.yaml into a committed <repo>/.shelbi/project.yaml (shared half, with config_mode: in-repo) and a per-machine ~/.shelbi/projects/<name>/local.yaml.
  2. Moves workflows/, agents/, and workspace-settings.json.template from ~/.shelbi/projects/<name>/ into <repo>/.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. Retires the original ~/.shelbi/projects/<name>.yaml by renaming it to <name>.yaml.migrated (it is not deleted — the copy stays as a rollback source), 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

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

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 <repo>/.shelbi/ to its pre-migration state.
  2. Restore the global-mode YAML: rename the retained ~/.shelbi/projects/<name>.yaml.migrated copy back to ~/.shelbi/projects/<name>.yaml (or reconstruct it from ~/.shelbi/projects/<name>/local.yaml plus the committed shared half).

A merged migration commit is expensive to undo. --dry-run first.

Picking up a teammate's project

When a teammate has committed <repo>/.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 (in-repo): /Users/you/.shelbi/projects/shelbi/local.yaml
✓ wrote workspace settings template: /Users/you/work/shelbi/.shelbi/workspace-settings.json.template
✓ created agent workspace: agents/orchestrator/
✓ created agent workspace: agents/developer/
✓ wrote project statuses: /Users/you/work/shelbi/.shelbi/workflows/statuses.yaml
 
✓ picked up `shelbi` from /Users/you/work/shelbi/.shelbi/project.yaml.
 
next:
  1. add machines/workspaces to ~/.shelbi/projects/shelbi/local.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 <repo>/.shelbi/project.yaml, reads the canonical name, and writes a user-local ~/.shelbi/projects/<name>/local.yaml that points your machines at the checkout — it reads the committed shared half in place and never rewrites it. (If the committed file is a legacy name-only stub that predates the shared/local split, pick-up instead falls back to a flat ~/.shelbi/projects/<name>.yaml registration so the project still loads.) 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 (in-repo): /Users/you/.shelbi/projects/shelbi-2/local.yaml

✓ picked up `shelbi` from /Users/you/work/shelbi/.shelbi/project.yaml as local alias `shelbi-2`.
 
next:
  1. add machines/workspaces to ~/.shelbi/projects/shelbi-2/local.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 <repo>/.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, issues, 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 <repo>/.shelbi/project.yaml or local.yaml (in-repo mode) or ~/.shelbi/projects/<name>.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-issue; 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 issue, reload, then re-dispatch.

See also

  • Project config — 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 — the detected, one-confirmation global-mode setup.
  • shelbi 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.