Skip to content

Project Orchestration Handbook

The encyclopedic guide to running projects with BoB (Big ol’ Brain). For humans and agents.

This handbook is the human-facing how-to. For deep dives on individual subsystems, see the See Also section.


BoB (Big ol’ Brain) is a global Claude Code tooling framework. Skills, agents, slash commands, hooks, scripts, and templates live in a single source-of-truth git repo and get distributed to every project via deploy + symlink.

When someone says Bob, BoB, big brain, or big ol’ brain, they mean this framework. See bob-identity.md for the full identity definition.

BOB_SOURCE (~/projects/bigbrain) <- Source of truth (git repo)
│
│ scripts/deploy.sh (sync)
▼
BOB_HOME (~/.claude/) <- Runtime (Claude Code reads here)
│
│ cdi (per-project init: symlinks)
▼
.claude/ (in each project) <- Project-level entry points
Layer Location Purpose
BOB_SOURCE ~/projects/bigbrain Edit here. One git repo. Single source of truth.
BOB_HOME ~/.claude Where Claude Code reads at runtime. Synced from src.
Project <repo>/.claude/ Per-project symlinks to BOB_HOME items + local items

Everything BoB manages must be declarative, version-controlled, reproducible, idempotent, and observable — with every piece of state owned by exactly one source of truth. Everything BoB concludes or produces must rest on empirical evidence and merit — never on a quota by race, sex, ethnicity or any other immutable trait.

Full text, approved patterns and the evidence-and-merit principle: prime-directive.md.

Every project’s vision lives in docs/vision.md. Everything below the vision lives in GitHub Issues — capabilities, requirements, bugs, decisions, lessons, session summaries, and TODOs. There are no REQ-*.md, BUG-*.md, SOL-*.md, or PM index files. If someone tries to create one, stop them.

Type Count Source path Loading model
Universal skill 9 BOB_SOURCE/skills/ Auto-loaded everywhere by Claude Code
Universal command 18 BOB_SOURCE/commands/ Symlinked into each project’s .claude/commands/ by bob init
Universal agent 3 BOB_SOURCE/agents/ Symlinked into each project’s .claude/agents/ by bob init
Universal runbook 6 BOB_SOURCE/runbooks/ Referenced by absolute path; not symlinked
Registry skill 61 BOB_SOURCE/registry/skills/ Provisioned per-project via manifest (provisions/<project>.json)
Registry command 26 BOB_SOURCE/registry/commands/ Same — opt-in per project
Registry agent 23 BOB_SOURCE/registry/agents/ Same — opt-in per project
Hook 29 BOB_SOURCE/hooks/ Wired up in ~/.claude/settings.json by event (24 wired)
Script — BOB_SOURCE/scripts/ Invoked by absolute path from skills, commands, hooks, or shell
Template — BOB_SOURCE/templates/ Copied (not symlinked) into projects, then customized
Provision manifest the operator repo’s provisions/ (legacy: BOB_SOURCE/provisions/, until 2026-10-31 — #2270) Declares which registry items a project gets

Moved to a tutorial — see Getting Started.

Moved — see Getting Started.

3.2 Deploying changes from BOB_SOURCE → BOB_HOME

Section titled “3.2 Deploying changes from BOB_SOURCE → BOB_HOME”

deploy.sh is the only sanctioned way to update ~/.claude/. Direct edits to ~/.claude/ are anti-pattern — you’d lose them on the next deploy.

Terminal window
scripts/deploy.sh --dry-run # Preview what would change
scripts/deploy.sh # Apply
scripts/deploy.sh --first-run # First-deploy mode (cleans up legacy .git/ in BOB_HOME)
scripts/deploy.sh --force # Skip confirmation prompts

Machine-specific files are protected and never overwritten:

  • ~/.claude/settings.json
  • ~/.claude/settings.local.json
  • ~/.claude/CLAUDE.local.md
Terminal window
cd <project>
bob init # Idempotent — safe to re-run

Creates <project>/.claude/ with symlinks to:

  • commands/ from ~/.claude/commands/
  • agents/ from ~/.claude/agents/
  • hooks/ referenced in settings.json
  • Templates (copied, not symlinked)

Skills are not symlinked — they’re auto-loaded globally from ~/.claude/skills/.

3.4 Provisioning registry items (bob provision + /provision)

Section titled “3.4 Provisioning registry items (bob provision + /provision)”

Each project has a manifest, provisions/<project>.json, declaring which registry skills, commands, and agents it uses (in addition to the universal set). Manifests are operator data (#2270): they live in the operator repo, reached through operatorPath() in scripts/lib/operator-resolve.js — the operator repo first, the legacy BOB_SOURCE/provisions/ second (a deprecation warning, until 2026-10-31); only the product’s _bob-home.json and _default.json stay in BOB_SOURCE.

One engine (#2084). Every bob provision verb runs in a single engine, scripts/orchestrator/provision.js, which sits beside the orchestrator it drives (recommend.js, infer-profile.js, explain.js). bob provision / bob provision (scripts/cli/cdprov) execs it directly. scripts/provision.sh remains for one release cycle as a compatibility shim onto the same engine, so existing callers (bob init, repair-fleet.sh, deployed copies) keep working. The staging machinery stays in scripts/lib/config-branch.sh: the engine calls its functions for the #785 staged route, --now and the #1478 fast path. tests/test-cdprov-golden.sh pins the observable behaviour of every verb (#2083).

Terminal window
bob provision # Apply manifest — create symlinks to match
bob provision --status # What's in manifest vs. what's actually linked
bob provision --diff # Dry run — planned link changes + Recommendation (inferred): why: per item, would-remove
bob provision --refresh # Re-sync symlinks (drop stale, add missing)
bob provision --init # Generate a manifest — profile inferred from vision + code by default (#1878)
bob provision --init --auto # Same, one shot: write + refresh, no prompts; prints a why: line per item (#1879)
bob provision --init --no-infer # Stack-only input (no capabilities) — honoured by --diff and reconcile too
bob provision reconcile [--apply] [--allow-remove] # Re-infer and apply the manifest delta (#1880) — see below
bob provision --init --from-links [--apply [--force]] # Derive the manifest from existing links into BOB_HOME (#2065) — see below

The inference inputs, evidence model, precedence and limits behind --init, --diff and reconcile are explained once in the orchestrator guide — Inference from vision and code, with a flag table for the whole pipeline.

bob provision --init --from-links — adopt a hand-linked project (#2065). A project linked by hand (ln -s ~/.claude/registry/...) has no manifest, so bob provision status, bob ready and the fleet snapshot cannot see it. --from-links inverts the refresh projection instead of inferring anything. It scans .claude/{skills,commands,agents,runbooks}/* and declares every symlink that resolves to a registry item root of that type (under $BOB_HOME/registry/ or the source checkout’s registry/) and is named the way bob provision refresh would name it. It excludes the universal links bob init owns, plus items that have moved to universal (#1872). Everything else is listed as unmapped with a reason: dangling, outside BoB, not an item root, wrong type, or renamed. Real files are authored and never scanned. _meta.project and _meta.path come from the same helpers --init uses; _meta.stack comes from detect_stack. The default is a dry run that prints the proposed manifest and the unmapped list; --json emits the bob-cdprov-from-links/1 document. --apply writes through the same routes as --init — the #785 staged PR by default, --now, or the ungated apply — and is refused when only the BOB_HOME fallback resolved (#1220). An existing manifest is never replaced without --apply --force, and one equal to the proposal is a no-op, so running it twice changes nothing. The apply never refreshes: the mapped links already exist, so bob provision status reports no [unlinked], while a refresh would delete the registry links reported unmapped. Review those first, then run bob provision --diff and bob provision refresh yourself. Tests: make test-cdprov-from-links.

Inside Claude (/provision):

/provision status
/provision add skill cloudflare-dev
/provision add command deploy
/provision add agent code-reviewer
/provision remove skill seo-auditing
/provision diff
/provision init # Bootstrap a manifest for a new project

Manifest shape (provisions/<project>.json):

{
"_meta": { "project": "myproj", "stack": ["cloudflare-workers", "d1"] },
"skills": ["cloudflare-dev", "d1-expert"],
"commands": ["deploy", "release"],
"agents": ["code-reviewer", "code-debugger"],
"runbooks": []
}

Validate against schemas/manifest.schema.json.

Intent-mutating vs pass-through (#784). Every cd* invocation is classified by a single seam (scripts/lib/intent-classify.sh, sourced by bob init and mirrored by the provisioning engine): an invocation is intent-mutating iff it edits provisions/<project>.json — the manifest is the source of truth and symlinks are a deterministic projection of it (#782). Only intent-mutating invocations are routed through the stage-on-branch review gate (#785, below); pass-through invocations (reads and pure projections) always run unchanged.

Invocation Classification Why
bob provision add / remove intent-mutating Edits the manifest’s item lists
bob provision init / interview intent-mutating Creates / (re)writes the manifest
bob provision status / diff / check pass-through Reads only
bob provision reconcile pass-through Dry run: re-infers and prints the manifest delta (#1880)
bob provision reconcile --apply intent-mutating Applies the delta — the same staged PR / fast-path route as add/remove
bob provision refresh pass-through Pure projection manifest → symlinks (the post-merge apply, #789 — stays ungated)
bob provision prune pass-through Touches dangling symlinks, never the manifest
bob init init — manifest step intent-mutating Writes provisions/<project>.json (OQ-05)
bob init init — structural step pass-through .claude/ dir, universal symlinks, dev.json template — applies immediately (OQ-05)
bob link pass-through Symlink-only today: links a global item without touching the manifest (OQ-10). --manifest is reserved — a future manifest-aware bob link classifies intent-mutating
bob dash status / bob dash generate / bob dash serve pass-through Dashboards / generators: pure reads
anything unrecognized intent-mutating Fail-safe: an unknown invocation gets reviewed, never silently applied

The stage-on-branch gate (#785) — gated by default. An intent-mutating bob provision invocation does not write the manifest to disk or any symlink to the target project. Instead (scripts/lib/config-branch.sh, hung off the #784 seam’s route_intent):

  1. Any dirty BOB_SOURCE working tree is stashed (and restored when the flow finishes).
  2. A review branch config/<project>-<action>-<YYYYMMDD> is cut from the default branch. Same-day collisions append an 8-hex hash of the manifest diff — deterministic, so re-running an identical change resolves to the same branch and reports “already staged” instead of duplicating.
  3. The manifest edit is committed on that branch (chore(config): <action> for <project>), pushed, and opened as a PR.
  4. The command prints the PR URL and exits 0. The target project’s .claude/ is byte-identical to before the command — a rejected PR needs no on-disk revert.

What the PR contains: the manifest diff, the symlink projection that bob provision refresh will apply post-merge (each create/remove line, in the same naming the provisioner uses — skills as directories, commands/agents/runbooks as .md), and a link to each affected registry item’s definition. The PR is the canonical answer to “what did BoB just change?”.

Applying (post-merge projection, #789): merge the PR, then run bob provision refresh from the target project — refresh is the ungated post-merge projection. The PR body’s “Apply (post-merge)” section carries the exact copy-paste command for the staging machine (git -C <bob_source> pull --ff-only && bob provision refresh <target-dir>); on another fleet machine, substitute its own paths. Operator-run refresh is the default (OQ-03) — the merge applies nothing by itself, and no CI or fleet job triggers the projection; a CI/fleet auto-trigger is a documented follow-on that would need per-machine target paths (see #415/#446 fleet plumbing). Partial failures are recoverable by re-running refresh: a target path that is a real file is skipped as [local], a permission error on one link is reported as [failed] without aborting the rest of the run, and the summary says what to fix. Re-running re-attempts only what is missing (existing correct links are skipped) and the manifest is never rolled back — it stays the declared truth the next refresh converges toward. BOB_CONFIG_GATE=off remains the internal (test/CI) escape hatch; the user-facing bypass is --now (#788, below).

Config PRs are never auto-merged — with one guarded exception (#788, #1478). Staging is the default automatic step: config_branch_stage opens the review PR and stops. Warp-drive’s L3 direct-merge path merges feature branches (feature/issue-NN-*); it never touches config/* branches. A config change therefore waits for a human to merge its PR, then bob provision refresh projects it — except the single-item fast path below, which is the one code path that merges a config PR, and only under its guards. The immediate-apply alternative remains an explicit, per-invocation opt-in (--now), never the default.

Single-item fast path (#1478). A single bob provision add/remove of a registry item is mechanical, low-risk, and fully schema-validated — a review PR there adds latency without adding review value (the motivating case: a one-line flightplan add took a multi-step PR round-trip). So at automation Level 2+ (resolved through the same promotion decider --now uses; an unresolvable level fails safe to review), the just-staged PR is auto-merged and the apply steps are chained end-to-end: label config-fastpath → squash-merge → bring the operator checkout level with its remote → bob provision refresh (no deploy step since #2270: manifests are read from the operator checkout itself, never deployed into BOB_HOME). “Provision add” then means provisioned, not “merged, now run two more commands”. A config branch is cut from origin/<default>, never the local default branch, so a staged PR carries exactly its own manifest change even when the checkout holds commits the remote lacks (#3912). Bringing the checkout level is a fast-forward when it is only behind; local commits already upstream are dropped by a rebase, unpushed [bypass: --now] commits are rebased and pushed, and any other local commit is named with a resume line that works. The guards (in config_branch_fastpath, measured against origin/<default>): the PR must change exactly provisions/<project>.json, the semantic delta must be exactly one item across the four item arrays (a textual line count would lie — jq’s array formatting rewrites the neighbouring comma line), and both manifest versions must pass the structural schema. Any guard miss degrades gracefully: rc 3, nothing mutated, the PR stays open for normal review — which is also how a #791-accumulated multi-item PR is handled. Level 1 keeps today’s behavior (PR opened, human merges), and BOB_FASTPATH=0 opts out at any level. One exception at every level: a first manifest (#3158). bob provision --init --auto on a project with no manifest yet — the step bob ready runs for a new project — writes a new file of one known shape, so there is nothing for a reviewer to weigh: it is committed straight to the operator repo’s default branch (config_branch_apply_now with the first manifest tag) and the run carries on. A config/<project>-* branch it makes redundant (its only change is that manifest) is finished — its open PR closed with a comment, the branch deleted locally and on origin (config_branch_supersede); a branch carrying anything else is left alone and named. An operator repo that is not a git checkout keeps the in-place write. The audit trail is preserved (Option A): the PR exists, labelled, with the merge commit reachable from the manifest history. A failure at any chained step names the step (MERGE / PULL / REFRESH) and prints the exact resume command; a landed merge is never rolled back — resume forward, don’t restart. The config branch is cut in the operator repo (_cb_root, #2270): a self-contained BOB_SOURCE carrying operator.json counts (the test-fixture shape), otherwise $BOB_OPERATOR → the pointer → the checked default, and with nothing resolved the stage is refused with a #1220-style error naming bob operator init <dir> --link.

The --now bypass (#788). bob provision <intent-mutating-action> --now skips the review PR and applies the change immediately: it commits the manifest edit straight to the default branch (logged [bypass: --now], via config_branch_apply_now) and projects the symlinks in the same run — today’s pre-gate behaviour, now behind a deliberate flag. The commit is pushed to the remote in the same call, rebasing first when the remote has moved; a push that still fails is reported with the command that finishes it (#3912). Every manifest write — --now, the staged branch and the fast path — refuses with exit 6 while the operator repo has a rebase, merge or cherry-pick in progress or unmerged paths, naming the state and the command that ends it (config_branch_busy, #3469); bob provision add/remove pass the 6 through, so they never report success having written nothing. It shares all of the stage path’s carve-outs and safety (BOB_HOME / protected-file refusal, no-delta no-op, dirty-tree stash/restore, staged-set invariant). --now is refused at automation Level 1 with promotion ceiling pr (exit 2, with a message pointing at /automation or /promotion): that stance mandates review, so an immediate apply is disallowed. At any other level×ceiling --now applies. The level+ceiling are resolved through the promotion decider (scripts/promotion/promotion.js); tests pin them via BOB_NOW_LEVEL / BOB_NOW_CEILING.

Warp-drive owns BOB_SOURCE — the concurrency guard (#790, resolves OQ-06). When a warp-drive session is active on BOB_SOURCE itself, an intent-mutating invocation refuses to cut a config branch on top of it (rc 5 from config_branch_stage, non-zero bob provision exit) — the session owns the repo’s branch state (checkouts, stashes, integration/feature branches), and staging underneath it would race those git operations. The signal is an explicit, documented sentinel, never an inference: the warp-drive state file at <BOB_SOURCE>/.claude/.warp-drive-state.json (warp_owns_bob_source in scripts/lib/config-branch.sh), which the state machine creates at session init and deletes at session_ended/reset. The refusal prompt surfaces the session’s phase/issue/branch and the ways forward: finish or merge that work first (bob warp status, /stop-warp-drive), clear stale state from a crashed session (state-machine.js reset), or override this one invocation with --now (config_branch_apply_now is deliberately unguarded — the operator explicitly accepts the risk, contained by the stash/restore + staged-set invariant). A warp-drive session on a target project never trips this — its state file lives in that project’s repo, not the config repo.

Batching policy — per-project accumulating config PR (#791, resolves OQ-01). Multiple intent-mutating commands in one session produce one config PR per project, not N. Every command still stages eagerly — there is no local buffer, so no flush trigger exists or is needed (each command pushes immediately; merging the PR is the flush). But when an open config PR already exists for the project (_cb_pending_branch: candidate config/<project>-* branches, gh-confirmed open — a merged/closed PR’s leftover branch is not pending), the command appends its manifest edit as a new commit on that PR’s branch instead of cutting a sibling PR. Edits compound: the append is computed against the pending manifest, and bob provision add/remove read their edit base from it too (config_branch_pending_manifest), so the second add’s PR diff contains both items rather than two conflicting single-item diffs. Re-adding an item already on the pending PR is a clean no-op (already staged on the pending config PR), as is a no-delta append (rc 4). The alternative — a session accumulator with a bob provision flush — was rejected: it would hold un-versioned pending state outside git (against the Prime Directive) and add a second thing to forget; here the open PR is the accumulator. If gh cannot answer the pending-PR probe, staging fails open to the eager per-command path. --now and the ungated apply route never read the pending base — they operate on the real manifest.

bob provision add / remove and the no-op guard (#786). bob provision add <type> <name> / bob provision remove <type> <name> are the item-level manifest mutations (types: skill, command, agent, runbook; the single-arg form bob provision add <name> infers the type — from the registry for add, from the project manifest for remove). Both flow through the gate above. Before any branch is cut, a no-op guard decides whether the invocation produces a manifest delta at all:

  • item already in the project manifest → already provisioned — no change
  • item already provided globally by BOB_HOME (#412 dedup, via the same item_is_global check the provisioner uses) → already provisioned (global dedup) — no change
  • remove of an item the manifest doesn’t declare → not provisioned — no change

Each no-op exits 0 having created zero git refs — no branch, no PR, nothing to clean up (a fat-fingered bob provision add already-there costs nothing). add/remove require a per-project manifest (provisions/<project>.json); the _default.json fallback is a read-time convenience, never a write target. config_branch_stage keeps its own empty-diff backstop (rc 4) for the init/interview paths.

bob provision reconcile — re-derive the manifest after the vision or code moves (#1880). A manifest written once by bob provision --init drifts as a long-lived project changes: a new capability lands in docs/vision.md, a CI workflow appears, a stack is dropped. bob provision reconcile (alias bob provision --diff --infer) re-runs the #1878 inference and prints the delta the #1879 explain.js document computes against the current manifest — + <kind> <name> — would-add: <why> for recommended-but-undeclared items, - <kind> <name> — would-remove: <reason> for declared items nothing supports — and is a dry run by default (nothing written, no PR; --json emits the explain document). bob provision reconcile --apply builds the new manifest and hands it to exactly the machinery above: the #785 staged review PR, with the #1478 fast path on top — so a single-item delta at automation L2+ is auto-merged, deployed and refreshed end-to-end, a multi-item delta leaves the normal review PR open (the fast-path guard miss), and L1 always stages a PR; --now commits to the default branch, and the #791 pending-PR base applies (the delta is computed against the pending manifest and compounds onto it). It is idempotent: a reconcile immediately after an applied one computes an empty delta and exits 0 having created zero git refs. Removals are never applied by default — would-remove items are withheld (and re-reported on the next run) unless --apply --allow-remove is passed or an interactive run answers yes; _meta.manual items are never removal candidates at all. Note a would-remove whose reason is not a registry item with orchestrator metadata is unevidenced rather than unwanted — check before allowing it. Tests: make test-cdprov-reconcile.

BOB_HOME + protected-file carve-outs (#787). The gate never touches the BOB_HOME surface. The provisioning engine already routes a ~/.claude target to the verify-only path (#411) before the gate is reached; config_branch_stage enforces the same carve-outs itself as defense-in-depth, reusing bob-home-detect.sh (no duplicate detection): it refuses when the staging root resolves to BOB_HOME (covering the BOB_SOURCE-unset fallback, where it would otherwise cut branches inside ~/.claude — operator is pointed at deploy.sh), refuses the _bob-home(.machine/.role) global manifest as a staging target (edited via a normal BOB_SOURCE PR), and refuses a protected basename as a manifest path. A staged-set invariant before the commit guarantees a config branch carries exactly provisions/<project>.json — a touched settings.json, settings.local.json, or CLAUDE.local.md can never ride along (the stash/restore cycle hands it back to the working tree untouched).

3.4a Universal vs registry — and how to promote between tiers

Section titled “3.4a Universal vs registry — and how to promote between tiers”

Every command, skill, and agent lives in exactly one of two tiers. This is the single most important distinction to internalise — most “why isn’t my command showing up?” confusion comes from not knowing which tier an item is in.

Tier Source folder Who gets it How it lands in a project
Universal commands/, skills/, agents/, runbooks/ Every project, automatically bob init symlinks commands/agents in; skills auto-load globally
Registry registry/commands/, registry/skills/, … Only projects that opt in Listed in provisions/<project>.json, then bob provision links it

Registry is an à-la-carte menu: with 30+ registry skills and ~30 registry commands, you don’t want cloudflare-dev loaded into a non-Cloudflare project. So registry items are opt-in, declared per-project. Universal items carry a context cost in every project, so default new items to registry and promote only the ones that are genuinely needed everywhere.

The end-to-end flow — what links what:

edit in BOB_SOURCE → deploy.sh (sync to ~/.claude) → cdi (universal symlinks)
→ cdprov (registry symlinks, per manifest)

deploy.sh copies files into the runtime dir but links nothing into projects. bob init links the universal set. bob provision links the registry items a project’s manifest names. A registry item that no manifest references is linked into no project — even after deploy.

Where the links point (#1475): registry symlinks target the deployed registry — LINK_ROOT = ${BOB_HOME:-~/.claude}/registry — not the source checkout (decision #1484). Manifest reads/writes resolve through BOB_SOURCE (#1220), but link creation and comparison are pinned to LINK_ROOT, so bob provision refresh produces byte-identical targets no matter which environment invoked it. Consequences: an item merged to source but not yet deployed is reported with a “run deploy.sh” hint instead of being linked into the checkout, and moving existing links to a new root is always called out explicitly ([retarget] lines plus a summary count) — a mass retarget can never happen silently. Regression coverage: make test-link-root. Extended to every link BoB writes (#1924): bob init’s universal command/hook/agent links and the git commit-msg hook, and bob provision’s universal retargets, all target $BOB_HOME too — a source-only item is reported [pending] … run deploy.sh, never linked into the checkout — and make check-abs-paths fails the build on any literal home path in a synced or ledgered file, or any absolute-target symlink in a synced tree (see check-abs-paths).

Authored vs. derived — what a consuming repo commits (#1479): commit what is authored in the project (CLAUDE.md, dev.json, seed/, .claude/settings.json, real-file commands/agents/skills); gitignore what is provisioned (every symlink bob provision materializes from the manifest, plus settings.local.json). Provisioned links are derived state — tracking them creates a second owner that fights the manifest, and they embed absolute user paths that dangle for any other clone. bob provision refresh maintains a managed block (# --- BoB-provisioned … ---) in the project’s ignore file, rebuilt from the manifest on every provisioning write so it can never drift from the provision list. It is rewritten where it stands, and appended only when absent, so a block or an authored line below it is never moved above it (#3600). The block also owns two things that are derived by the same argument but not manifest-derived (#1969, #1970): every .claude/* symlink whose target resolves into BOB_HOME — the universal commands, agents and hooks bob init links, so a newly promoted universal item never leaves a permanently dirty symlink behind — and the BoB session-state files declared once in scripts/lib/derived-paths.json (.provision-overlay.json, .last-hook-payload.json, .warp-drive-state.json, .project-ready.json, forge/, settings.local.json). That declaration is shared with the segmentation checkpoint’s dirt classifier, so the ignore block and the checkpoint never disagree about what counts as work. Placement is per-project via the manifest’s _meta.ignore_placement: the committed .gitignore (default) or .git/info/exclude (no BoB fingerprints in client repos); _meta.track_provisioned: true is the explicit opt-out. Two consequences to know: drift detection moves to BoB tooling — a stale link no longer shows in git status; bob provision status flags [unlinked] items and the fleet snapshot carries unlinked_declared — and fresh clones materialize links with bob init + bob provision refresh (links do not arrive via checkout; a just-cloned project showing [unlinked] items is pre-refresh state, not breakage). Coverage: make test-ignore-block.

Migrating a repo that predates #1479 (#2519): a repo provisioned before the rule still tracks its links, so every convergence leaves git dirt — M for a link retargeted to this machine’s BOB_HOME, D for a pruned dead one. bob provision untrack is the one mechanical migration, not a per-repo judgment call. It selects only mode-120000 index entries under .claude/ whose indexed target resolves into BOB_HOME, into the source registry, or to nothing on this machine. It never selects a real file, and it keeps (and reports) a symlink that resolves anywhere else. Without --apply it only reports. With --apply it untracks those entries (the links stay on disk), refreshes the managed ignore block so each one is ignored, and records one chore: untrack provisioned .claude links (#1479) commit on the current branch. It pushes only with --push. It refuses a repo with an active warp-drive state file, a staged index, or a tracked change outside .claude/, saying why. One exception: BoB’s own port settings (dev.json _bob_ports + server.port, and the bob:ports block in vite.config.* / wrangler.toml) do not refuse the repo. The untrack commit carries them. It skips _meta.track_provisioned: true and a repo with no manifest. A second run is a clean no-op.

Terminal window
bob provision untrack # this repo, report only
bob provision untrack --fleet # every active manifest's checkout on this machine
bob provision untrack --fleet --apply # untrack + ignore + commit, one commit per repo

Run it once per fleet machine; the farm cannot reach the laptops (#440). Exit 0 converged, 1 links still to untrack (or residue after --apply — run bob provision prune --apply), 2 usage, 3 a repo refused. --json emits the untrack-links/1 document. Coverage: make test-untrack-links.

Promote registry → universal (the item should be in every project):

Terminal window
cd ~/projects/bigbrain
git mv registry/commands/<name>.md commands/<name>.md # the tier IS the folder
scripts/deploy.sh # sync to ~/.claude
# then re-run `bob init` in each existing project to pick up the new symlink;
# new projects get it automatically. Remove the item from any provisions/*.json
# that listed it — it's now universal, so an explicit opt-in is redundant.

Demote universal → registry (the item is only needed by some projects): reverse the move (git mv commands/<name>.md registry/<name>.md), deploy.sh, then add the item to the manifest of each project that still needs it and bob provision --refresh. Projects not naming it lose the symlink on their next bob init/refresh.

The tier of an item is defined entirely by which folder holds it — promotion and demotion are just a git mv plus a re-link. There is no separate registration step.

Terminal window
cd ~/projects/bigbrain
git pull # Get upstream changes
scripts/deploy.sh --dry-run # Always preview first
scripts/deploy.sh # Apply
make check # Verify nothing broke
  • <project>/.claude/CLAUDE.local.md — project notes that don’t get checked in.
  • ~/.claude/CLAUDE.local.md — global notes that don’t sync.

These are loaded into context but never deployed or committed.

make is the IaC verification layer. Run any of these from BOB_SOURCE:

Target What it checks
make check All of: deps + schemas + symlinks + hooks + provisions
make check-deps node, jq, git, make versions
make check-schemas All *.json configs against JSON Schema definitions
make check-symlinks Every symlink across registered projects resolves
make check-hooks Every hook script in settings.json exists and is executable
make check-provisions Each project’s actual symlink state matches its manifest
make test Schema validator unit tests + warp-drive state machine tests + integration
make ci check + test (what GitHub Actions runs)
make doctor Diagnostic dump for triaging weirdness
make help Print all targets

Current test counts (run make test for the live numbers):

  • Schema validator: 34 unit tests
  • Warp-drive state machine: 99 transition tests
  • Integration tests for all check scripts: 13

make ci is what GitHub Actions runs on push and PR to master. CI skips check-symlinks and check-provisions because those scan local project directories that don’t exist on the runner.

Doc-link sweep: the doc-keeper auditor’s broken_relative_link rule (scripts/doc-keeper/audit.js, run via make docs-check) validates every relative .md/.txt link across README.md, CLAUDE.md, and docs/**/*.md (excluding the frozen docs/audits/**). It supersedes the former standalone check-doc-links.sh, removed in #462 as dead tooling.

BoB runs on a set of machines — the fleet — declared in machines.json (resolver: scripts/fleet/machine.js whoami). scripts/fleet/audit.js audits any managed project on any fleet machine from any other, over Tailscale: audit.js snapshot publishes this machine’s project/provision/health condition to $BOB_FLEET_SNAPSHOTS (default ~/.claude/fleet/snapshots/<id>.json), audit.js pull <id|tailscale-name> reads a remote machine’s audit over Tailscale SSH — its published snapshot when that is fresh, else its snapshot generator run there (how a live pull reads a remote machine, #3609) — and audit.js view aggregates live + last-known-cached state across the fleet, marking a machine stale when its newest snapshot is older than 24 h and never when it has never published one — audit.js snapshot --install-timer keeps every machine publishing on a timer (fleet-snapshot-timer.md, #2067). Each snapshot also carries promotion readiness (#446), mirror condition (mirror-remote.md), harness-defect counts (#1838), the machine’s role default and forbidden session kits (summary.kits, session-kits.md, #2228), and credential presence — what secrets-inventory.json declares on this machine versus what its stores hold, by name, never a value (summary.cred_check, a “Credential presence” table in view; secrets-inventory.md, #2238). It also carries what a Fleet page machine card shows that nothing else does (summary.machine, scripts/fleet/machine-facts.js, #4035): load, free disk, whether Remote Login is on (macOS), and whether a declared CI runner’s process is running — read locally with no forge call, null where not measured, BOB_FLEET_MACHINE_FACTS=0 to opt out. A machine may declare session_limit, the live sessions it is meant to carry, which the Fleet page’s capacity line counts against. Optional host layer (#1871, #1925): when this machine’s machines.json entry declares a host_provider: {id, command} (Fleet on the laptops, thefarm on farm-01 — declared, never discovered from PATH; $BOB_FLEET_HOST_PROVIDER is an override only, and an empty value opts out), the snapshot runs <command> status --json and carries a host section consumed by contract: host-status/1 (schemas/host-status.schema.json, owned here, deployed to BOB_HOME/schemas/; fleet-status/1 is a profile of it). Every provider-defined section lands in host.sections (+ host.section_order) summarised to {ok, drift} — Fleet’s five are also still addressable as host.<name> — plus host.drift_count, and view rolls them up as a “Host drift” table whose columns are the union of sections reported across machines, with a PROVIDER column. No provider means no host key; a declared provider that is missing, fails or times out (BOB_FLEET_HOST_TIMEOUT_MS, default 20000) is recorded as host.error, never a failed snapshot, and summary.host_drift (#1921) is carried either way. The provider’s document wins over its exit code — a provider exits 3 on drift with the full document on stdout (na: true sections are not-measured rather than drift), and a verdict: error document surfaces its error as host.error (paulirv/fleet#7). A declared command may be $HOME/$BOB_HOME-relative (#1924) — expanded for the spawn, recorded as declared. Inbound audits require the target to accept SSH — macOS has Remote Login off by default.

“fleet” vs. “Fleet”. BoB’s fleet is the machine layer above — machines.json, scripts/fleet/{audit,machine,readiness,mirror,runner}.js, BOB_FLEET_SNAPSHOTS. It is not the Fleet project (~/projects/fleet, provisioned via provisions/fleet.json): the host-environment layer beneath BoB for the operator’s Tailscale devices (Homebrew bundle, iTerm2 profiles, repo sync, SSH reachability, fleet status / fleet travel). Fleet composes BoB — it reuses machines.json, scripts/fleet/*, deploy.sh, and mirror.js and never re-declares _bob-home.*; see Fleet’s vision at ~/projects/fleet/docs/vision.md. Both terms are defined in the glossary.


Vision & Strategy (docs/vision.md) /vision
└── Business case (GitHub Issue, business-case) /business-case — justify investment (above cap)
└── Capability (GitHub Issue, label: cap) /capability
└── Requirement (GitHub Issue, req) /requirement
└── Warp-drive chunks automatic — individual commits
Use case (GitHub Issue, label: use-case) /use-case — a scenario; Realizes → cap/req
└── → Capability (broad) or Requirement (narrow)
Level Format Command Contains
Vision docs/vision.md /vision Why, who, where, principles, non-goals, roadmap. Living doc.
Business case GitHub Issue (business-case) /business-case Problem, options, cost/benefit, risk, recommendation, success metrics
Capability GitHub Issue (cap) /capability User stories, success metrics, requirements checklist
Requirement GitHub Issue (req) /requirement Description, acceptance criteria (checkboxes), priority, notes
Use case GitHub Issue (use-case) /use-case Actor, goal, scenario, success criteria; decomposes to cap/req
Bug GitHub Issue (bug) gh issue create --label bug Bug reports
TODO GitHub Issue (todo) auto-created by warp-drive timeout Human action items

Two issue types bracket the capability spine without being part of the linear cap → req decomposition:

  • Business case (business-case, /business-case) sits above capability — it justifies investment before capabilities are spawned. Its recommended option is realized by one or more capabilities, each carrying Part of #NN (where NN is the business case) in its Notes, exactly as a requirement links to its capability. The vision-doc fold-in of an approved recommendation is deferred to grooming; /business-case never edits docs/vision.md.
  • Use case (use-case, /use-case) captures a concrete scenario (actor, goal, main/alternate flows, success criteria) and decomposes into the work that realizes it — a capability (broad: multiple actors/steps) or requirement(s) (narrow: one actor/goal). Each realizing issue carries Realizes #NN (where NN is the use case) in its Notes, and inherits the use case’s area:<slug> so the whole scenario tree is one warp-drivable workstream.

Query the traceability with gh issue list --label use-case, gh issue list --label business-case, and gh issue list --search "Realizes #NN".

Label Meaning
business-case Business case — investment justification above capability
cap Capability
req Requirement
use-case Use case — a scenario that decomposes into cap/req
bug Bug report
todo Human action item
approved Ready to work — picked up by /warp-drive
in-progress Being worked on — on a cap, machine-derived from child-req state (see Capability status)
blocked Waiting on something
completed Done-signal: on a todo, the human performed the action; on a closed cap, every child req closed (derived — closed without it means abandoned)
implemented Code done, awaiting verification
serial-only Excluded from bob fork --from-issues (pinch points)
area:<slug> Workstream tag — batches issues for one module/feature/capability (see Area labels)
decision Architecture decision (created by /journal decision)
lesson Lesson learned (created by /journal lesson)
session-summary Session summary (created by /session-summary)
p1-critical / p2-high / p3-medium / p4-low Priority
risk-status:identified / assessing / monitoring / mitigating Lifecycle status of an open risk issue — one at a time; closing the issue ends it (#3193)
phase-0 … phase-3 Rollout sequence — labels, not Milestones; lower ships first (#3193)

Risk status and rollout phase are standard labels, not per-project inventions: bootstrap-labels.sh declares them, and the label glossary gives their meaning and the old→new mapping for a project that improvised its own.

  1. /capability creates an issue (label cap). Its Requirements checklist is carved by slice, not by layer (#2739): the first requirement is a thin end-to-end path through every layer the capability crosses, and each later one extends it with another behaviour. A checklist reading “schema / API / UI” is the wrong decomposition — no ordering of ACs inside single-layer requirements recovers a working path. Internal, refactor and test-only capabilities, and ones that do not cross layers, are exempt; when the architecture is the open question, a spike precedes the first slice.
  2. Break it into requirements with /requirement (label req, with Part of #NN linking back to the capability). /requirement adds two standing conditional ACs when they apply: the Docs-as-DoD AC for a user-facing surface, and the tracer bullet AC-01 for greenfield or cross-layer work — a thin end-to-end path with no mocks for the layers crossed, followed by ACs ordered by slice rather than by layer (#2734). “No mocks” is AC text and a chunk-report expectation; nothing in the harness inspects a diff for test doubles.
  3. Add approved when a requirement is ready to work.
  4. /warp-drive picks the highest-priority req + approved issue not in progress.
  5. When done, warp-drive flags it implemented and moves on.

Capability status (derived, never hand-set)

Section titled “Capability status (derived, never hand-set)”

Capability status is derived state (#989), machine-reconciled from child-requirement issue state by scripts/warp-drive/cap-status.js — /groom runs it as the reconciler of last resort, and /requirement invokes its reopen gate when linking a new child req. Three states, no new labels:

State Encoding Derivation
Not started Open cap, no status label No child req closed or in-progress (absence of a label is deliberate — one less invariant)
In progress Open cap + in-progress ≥1 child req closed or itself in-progress; removed again when none are
Complete Closed cap + completed Every child req (checklist ∪ Part of #NN, issue state as truth) is closed, and no unfiled slice is left in ## Requirements

A checklist line is a child only when the ref leads it (- [ ] #NN <title>). A #NN mentioned in prose is never a child, and an unchecked line with no leading ref is an unfiled slice that keeps the cap open (#3196).

A cap closed without completed means abandoned/cancelled — the reconciler never produces that state and never touches such a cap, mirroring the todo lifecycle exactly. The one exception is old data (#4297): a cap GitHub records as closed as completed (stateReason: COMPLETED) with every child closed but no completed label — what a bare gh issue close leaves — gains completed from cap-status.js reconcile, and cap-status.js reopen reopens it when a new child req arrives, as it would a labelled cap. It is never re-closed; a cap closed as not planned, or over an open child, is still left alone.

Age is the fourth thing status cannot say (#2064). The three states above answer what a cap is; none answers for how long. A cap can wear in-progress correctly and truthfully for four months while nothing moves — the label is not wrong, it is just silent. cap-status.js audit adds that dimension, report-only:

Finding Condition Clock
long-in-progress open cap + in-progress, no child closed in _workflow.cap_in_progress_stale_days (default 30) the last child close date — a comment or a label edit is not progress, a closed child is
parked open issue + proposed, untouched for over 60 days updatedAt — a proposal has no children to close, so any touch is a sign of life

/groom prints each with a decide-or-park prompt (Step 5e): close it, revive it, or defer it with <!-- parked: YYYY-MM-DD reason --> in the body — a deferral with a 90-day expiry, not a delete. Nothing is closed automatically. The reconciler can prove that every child is closed; it can never prove that a cap with an open child is finished, and that is the judgement the prompt hands back. The audit path is report-only by construction — no age verdict carries an add/remove/close — and age findings never move the exit code, so exit 3 keeps meaning “labels diverge”.

Filter recipes:

Terminal window
gh issue list -l cap,completed -s closed # completed capabilities
gh issue list -l cap,in-progress # in-progress capabilities
gh issue list -l cap --search '-label:in-progress' # not-started capabilities

Within a single repo, issues for distinct workstreams — a new module, a feature, a capability rollout — are intermixed. An area:<slug> label tags every issue belonging to one workstream so the whole cluster can be listed and warp-driven as a unit without hand-picking issue numbers (#260).

  • Convention. Slugs are kebab-case workstream names: area:billing, area:baz, area:seebod. Labels use a reserved colour (#5319E7) and an Area: <Name> description so the namespace is visually distinct and observable. Scope is within a single repo — each project tracks its own issues on its own repo.

  • Hierarchy fit. A capability owns its area:<slug> (derived from its title); its child requirements inherit the same label so the Capability → Requirement tree is warp-drivable as one unit.

  • Idempotent provisioning (Prime Directive). Create/reconcile a label on demand with the helper — it never duplicates or clobbers:

    Terminal window
    ~/.claude/scripts/area-labels.sh ensure billing # create/refresh area:billing (accepts a name too)
    ~/.claude/scripts/area-labels.sh list # list existing area:* labels
    ~/.claude/scripts/area-labels.sh slugify "Billing & Invoicing" # -> billing-invoicing
  • Status overview per workstream (the gh issue list recipe):

    Terminal window
    ~/.claude/scripts/area-labels.sh issues billing # open issues in the area
    ~/.claude/scripts/area-labels.sh issues billing --state all
    # equivalent raw form:
    gh issue list --label "area:billing" --state open
  • Warp-drive batching. /warp-drive --area <slug> narrows discovery to req + approved + area:<slug> — see the warp-drive guide.

  • Expectation & drift check (#527). Every open cap and req should carry an area:<slug> label (other issue types — bugs, todos — take one when a workstream is meaningful, but it is not required). /capability and /requirement assign one at creation; to catch anything that slipped through, run the audit (also wired into /what-next Step 5):

    Terminal window
    ~/.claude/scripts/area-labels.sh audit # lists open cap/req missing area:* (exit 3 on drift)

The following filename patterns are forbidden. They predate the GitHub Issues migration. Do not regenerate them:

  • REQ-*.md, SOL-*.md, BUG-*.md, RISK-*.md, INC-*.md, CR-*.md, UXR-*.md, STATUS-*.md
  • REQUIREMENTS-INDEX.md, SOLUTIONS-INDEX.md, backlog.md, traceability-matrix.md
  • .claude/project-management/, .claude/requirements/, .claude/journal/decisions/ (or lessons/, sessions/)

If a tool emits any of these, treat it as a bug and fix the tool.

Some work cannot be parallelized. Mark these requirements serial-only so bob fork --from-issues skips them:

  • Config / settings sync
  • Lockfile bumps
  • DB migrations
  • Compiled-asset rebuilds
  • Deploy steps

Every project can declare its dev environment in dev.json. The /dev-up command (and bob dev up from a terminal) reads it and brings the environment to a fully testable state — server, migrations, seed data, test users, health check.

Full reference: dev-lifecycle.md.

Lives at the project root. Sample at templates/dev.json. Schema at schemas/dev.schema.json.

{
"server": { "command": "npm run dev", "port": 5173, "health": "/api/health" },
"migrations": { "command": "npm run migrate:dev", "auto_run": true },
"seed": { "directory": "seed/", "runner": "node", "order": "alphabetical" },
"auth": { "adapter": "d1", "users_file": "seed/users.json" },
"access": { "localhost": "http://localhost:5173" }
}

bob init allocates every project a deterministic 10-port band so multiple dev servers coexist without collisions. Two-tier, single source of truth:

  • Ledger (authoritative): the operator repo’s provisions/ports.json (legacy BOB_SOURCE/provisions/ports.json until 2026-10-31, #2270), version-controlled, managed by scripts/port-ledger.js. base = 5200 + slot*10; slot from an FNV-1a hash of the project name. Sub-offsets: +0 vite, +1 wrangler, +2 cds dashboard, +3..9 reserved.
  • Projection (derived): bob init writes a marked, regeneratable block into dev.json (_bob_ports + server.port) and the vite/wrangler configs — never hand-edited; lossless and idempotent.

Name-hash is the default proposal; the ledger only records and arbitrates collisions (deterministic next free band). Explicit non-default pins (e.g. example-awards 5180) are retrofit-backfilled and preserved. The legacy bob dash serve ~/.claude/dashboard-ports.json registry is folded into the ledger and retired. Inspect with bob dash status --ports / bob dash serve --ports. Full reference: dev-lifecycle.md → Port Allocation.

Fleet backfill (#198). Projects provisioned before #197 have no band yet. make port-migrate (or node scripts/port-fleet-migrate.js) is a one-shot, idempotent migration that walks every provisioned project (provisions/*.json), allocates each its band via the same allocator as bob init, writes the ledger, and regenerates the marked projection blocks. It backfills only un-allocated projects, never reassigns an existing band, aborts loudly before any write on a pin collision, and is a verified no-op on a second run. Always review make port-migrate-dry first. deploy.sh runs it idempotently (honouring --dry-run) and non-fatally on each deploy.

Command Purpose
/dev-up Full lifecycle (server + migrations + seed + users + health)
/dev-up --check Health probe only
/dev-up --skip-seed / --skip-users / --skip-server Partial run
/dev-up --regen-seed Discard cached seed and regenerate
bob dev up Same, from a regular terminal
bob dev health Shortcut for bob dev up --check
  • Seed scripts live in seed/ — alphabetical execution order.
  • Seed data must cover all lifecycle states the domain defines (not just fresh records).
  • New features include their seed updates in the same commit.
  • Seed coverage gate: scripts/dev-lifecycle/check-seed-coverage.sh.

seed/users.json declares test users, provisioned via pluggable auth adapters.

Adapter Use when…
d1 Cloudflare D1 (SQLite)
sqlite Local SQLite
supabase Supabase auth
script Custom provisioning script
custom Inline command in provision_command

The superuser is the same on every project an operator runs — admin@test.local / admin123 by default. An operator overrides it once in its profile (operator.json seed.superuser: an email and the NAME of a secrets[] entry holding the password, #2272), never per project; seed-generate.sh writes it into every generated users.json, and bob dev up reports which superuser it provisioned and its source.

When dev.json exists, warp-drive automatically:

  1. Calls bob dev up before the first chunk
  2. Verifies health between chunks
  3. Auto-recovers on dev failure
  4. Treats dev health failure as a blockable event

The day-to-day cycle when you’re at the keyboard.

/start-work → branch off
… code …
/commit → create a commit
/journal → optional: capture a decision/lesson
/pr → optional: open a PR
/finish-work → merge + cleanup
/session-summary → close the session as a GitHub Issue

Creates a new branch with a sensible name. Argument: feature | fix | docs | refactor | tooling | chore.

Stages and commits with a generated message. The commit skill enforces:

  • No commits to master/main (use /start-work first)
  • Pre-commit hooks run (don’t bypass with --no-verify)
  • Commit message reflects the why, not just the what

Pushes branch and opens a PR with requirement traceability: the pr skill’s create-pr.sh adds a Requirements table for every req, bug or task issue the branch’s commits reference, with the AC-NN ids they name.

Subactions: /pr create (default), /pr template (create-pr.sh --dry-run — the description only), /pr link <PR> (create-pr.sh --link <PR> — add the table to an existing PR).

Merges current branch to master (Level 3) or opens a PR (Level 2), runs cleanup, deletes the local branch.

Create a journal entry as a GitHub Issue. Three types:

Subcommand Label
/journal decision <name> decision
/journal lesson <name> lesson
/journal session <name> session-summary

End-of-session writeup as a GitHub Issue. Captures what shipped, what’s next, decisions, lessons.

Built-in Claude Code commands you’ll use alongside

Section titled “Built-in Claude Code commands you’ll use alongside”
Command Purpose
/review Code review of pending changes
/security-review Security review of pending changes
/init Initialize a CLAUDE.md from existing codebase
/clean_gone Clean up local branches that are gone on the remote
/commit-push-pr Commit, push, and open PR in one go (built-in plugin)
/loop Run a slash command on a recurring interval
/schedule Cron-style scheduling for recurring agents

Level Name Start with Default behavior
1 Supervised claude / -a1 Confirm every tool that can change state
2 Trusted Dev claude -a2 Auto-approve safe ops; PRs instead of direct merges
3 Autonomous claude -a3 Minimize prompts; merge directly to master
4 Full-Auto claude -a4 Superset of L3: reversible (two-way-door) decisions auto-made + recorded, irreversible (one-way-door) decisions notify-and-wait or stop; requires the full-auto profile and a cost ceiling to start

Switch mid-session with /automation level <N>. See profiles/{supervised,trusted,autonomous,full-auto}.json for the exact permission profiles. Level 4 is a superset of Level 3 — it inherits the autonomous allow-list and always-blocked floor verbatim, then adds an irreversible-action floor; it only activates when active_level is explicitly 4 and the full-auto profile is present, and it refuses to launch without a spend ceiling. Full behavior table: automation-behavior.md.

7.2 /warp-drive — the autonomous development loop

Section titled “7.2 /warp-drive — the autonomous development loop”

This replaces /auto-loop. /auto-loop is legacy and will be removed.

/warp-drive # Discover next approved requirement, work it
/warp-drive 42 # Work on issue #42 specifically
/stop-warp-drive # Graceful stop

Before the first cycle, prerequisites run two blocking health gates side by side: dev health (bob dev up) and the provisioning preflight (#1267) — provision-preflight.js check grades the project’s manifest adequate / inadequate (missing orchestrator-recommended items, named) / absent / unknown (detect or engine error — failure-safe, never a silent proceed) / skipped (non-BoB or opt-out). A non-adequate verdict is a blockable event resolved per automation level: L1 blocks and asks, L2 requires confirm/deny of the proposed bob provision fix (timeout = block), L3 auto-provisions via bob provision and reports the applied diff at session start and in the session summary. The same gate runs at the other session-start seams — /start-work step 11 and /flightplan Step 0 — so no session begins coding against tooling that can’t support it. Declines and operator overrides are session-scoped (provision-preflight.js waive, cleared at session end).

The preflight’s mid-session complement is gap detection (#1134). When a session is about to hand-roll a procedure, gap-detect.js match --task "<what you're doing>" asks whether an unprovisioned registry item already covers it — matching frontmatter metadata with a deliberately conservative bar (a false “you need skill X” nudge is worse than a missed one) and honouring the #412 dedupe (provisioned and machine-global items are never nudged). A hit emits the exact bob provision add <kind>/<name> --now fix, resolved per automation level: L1 asks, L2 confirms, L3 applies and reports — the provision change is always a visible manifest edit. Its vision-side twin (#1881) is gap-detect.js divergence: instead of a free-text task it takes the project’s inferred profile (vision + README + code, #1878) and nudges — same dedupe, same fix: line, same exit codes — for every registry item the #1879 explain document would-add but the manifest does not declare, each reading vision now mentions <evidence> — registry item <name> covers it. /vision runs it after writing docs/vision.md and /groom once per reconciliation pass (step 2c); both print the nudges verbatim and never apply them — the manifest is edited only by bob provision add / bob provision reconcile --apply (#1880). Gaps that exist nowhere (no registry item at all) are the authoring-gap stream: sessions record them into the evidence ledger (gap-analysis.js observe), and the trace-mining pass batches recurring ones into issue-first authoring recommendations (§9.3) — never auto-authored.

What it does each cycle:

  1. Discover work (gh issue list --label approved --label req)
  2. Plan implementation
  3. Code (chunked: max 3 acceptance criteria per commit)
  4. Test
  5. Update issue (flip ACs, add labels)
  6. Commit + journal
  7. Merge (L3/L4) or open PR (L2)
  8. Ask “continue?” → loop or stop

At Level 4 the loop runs the same cycle, but the human-in-the-loop pauses are resolved by the reversibility decision engine instead of prompting: reversible calls are auto-made and recorded as decision issues, one-way doors notify-and-wait or stop. The continue step auto-continues (bounded), an in-session QA loop gates promotion, and a long run checkpoints and resumes in a fresh context so quality holds over hours — all under a required cost ceiling. See automation-behavior.md §Level 4.

Full deep-dive: warp-drive.md.

7.3 The bob warp CLI (terminal observation)

Section titled “7.3 The bob warp CLI (terminal observation)”

Run from any shell — no Claude session needed:

Terminal window
bob warp status # Phase, current chunk, recent activity
bob warp stop # Abort the loop from outside
bob warp config # Show _workflow settings (incl. session_merge)
bob warp help # All commands

Integration-branch streams (#268). Accumulate several requirements onto one branch and ship them once, instead of a merge per requirement:

Terminal window
bob warp session start integration/feature-x # create + check out the session branch
# ...run /warp-drive over the cluster; each requirement merges into the session branch...
bob warp session status # show resolved branch config
bob warp finalize --dry-run # preview the ship
bob warp finalize # L2: one consolidated PR / L3: rebase+merge to main
bob warp session end # clear the session branch (git branch left intact)

The merging phase switches to local-accumulate automatically when a session branch is set and _workflow.session_merge is local (the default); set it to pr to keep per-requirement PRs. See the warp-drive guide.

7.4 /what-next — discover work without committing

Section titled “7.4 /what-next — discover work without committing”

Reads project state and proposes the next sensible work item. Use it when you’re not sure where to pick up.

/what-next

Inside Claude — graceful stop with proper cleanup. Don’t kill the session with Ctrl-C; let the skill flush state cleanly.

When you’re away from the keyboard, RDB routes all decisions to Telegram via ask_remote/notify_remote.

/rdb on # Decisions go to Telegram
/rdb off # Back to terminal prompts
/rdb status # Current state

Going AFK? Saying “I’m stepping away” is treated as /rdb on automatically.

  • claude -a3 -rdb (or -a2 -rdb)
  • /warp-drive started
  • Phone has Telegram open
  • Laptop plugged in and won’t sleep
  • At least one req + approved issue exists
  • Warp-drive does 3 strikes on failed fixes before escalating.
  • Failures escalate to a Telegram prompt (RDB) or terminal (no RDB).
  • “Type ‘skip’” or “do whatever you think is best” are valid responses.
  • Repeated escalations create a todo-labeled issue for human follow-up.

7.9 Declarative loop primitive (generic “do X until Y”)

Section titled “7.9 Declarative loop primitive (generic “do X until Y”)”

Warp-drive is a loop hardcoded to the dev cycle. For any other iterate-until-done loop — fix-until-tests-pass, refactor-until-lint-clean, or “keep running this agent against this check until it passes” — use the declarative loop primitive (#424): a manifest (goal + agent + evaluator + stop_condition + guardrails) run by a single reusable runner.

Terminal window
node ~/.claude/scripts/loop/run.js ~/.claude/templates/loops/fix-until-tests-pass.json

Guardrails are mandatory (max_iterations at minimum) and breaches halt as blockable events, reusing warp-drive’s budget_exceeded semantics. Cost telemetry reuses the warp-drive token/cost model. Provision it as the skills/loop-primitive registry item. Full reference: loop-primitive.md.


bob fork fans out N parallel /warp-drive sessions, one per branch, in isolated git worktrees + tmux windows. Tmux + git are the state — no daemon, no DB. If the orchestrator dies, your worktrees and commits survive.

Full reference: cdfork.md.

Command Purpose
bob fork fork <branch>... Spawn one warp-drive per named branch
bob fork fork --from-issues [N] Auto-pick N approved requirements; fan out
bob fork status [--json] List worktrees + tmux + warp-drive state
bob fork drop <branch> [--force] Tear down a worktree + tmux window
bob fork drop --all [--force] Tear down every cdfork worktree for this repo
bob fork pair --contract <p> --backend <r> --frontend <r> --branch <n> Cross-repo contract-driven mode
bob pair ... Standalone shape of bob fork pair
bob fork help Show command summary

For a repo at /path/to/<repo>/:

/path/to/<repo>.worktrees/<branch>/

Sibling, not nested — keeps Composer, Bundler, npm, etc. from getting confused.

8.3 When to use cdfork vs. a single warp-drive

Section titled “8.3 When to use cdfork vs. a single warp-drive”
Use cdfork when… Use single /warp-drive when…
Multiple independent issues are approved Single issue or tightly-coupled work
Work clusters don’t touch the same files Issues touch shared config/migrations
You want to maximize throughput while AFK Linear progression matters
You have CPU + RAM to spare Single-threaded resource constraint

8.4 What bob fork --from-issues actually does

Section titled “8.4 What bob fork --from-issues actually does”
  1. Runs gh issue list --label approved --label req (excluding serial-only)
  2. Picks the top N (default 3) by priority + age
  3. Derives a branch name per issue (e.g. req-42-add-auth)
  4. Spawns one worktree + tmux window + warp-drive per branch

Designed for headless splits where backend + frontend share a contract. Anchor use case: example.com Drupal-headless migration.

Terminal window
bob fork pair \
--contract ~/contracts/api-spec.yaml \
--backend ~/Sites/example-api \
--frontend ~/Sites/example-web \
--branch feat/product-listing

Two worktrees, two tmux windows, both warp-drives anchored to the same contract path.

Tool Why Install
git Worktrees brew install git
tmux Window management brew install tmux (NOT installed by default)
claude Spawned in each window Claude Code CLI
gh --from-issues brew install gh
jq Status JSON, issue filtering brew install jq

Preflight runs before any worktree is touched and fails fast on missing deps.


What Command Label
Architecture decision /journal decision <name> decision
Lesson learned /journal lesson <name> lesson
Session writeup /session-summary or /journal session <name> session-summary
Bug report gh issue create --label bug bug
Human action item auto-created by warp-drive timeout todo

Think git fsck for project management. Examines ground truth (git log, file system, codebase) and reconciles all PM artifacts against it.

/rebob

Use it after long absences, before/after major refactors, or when state feels off.

9.3 /trace-mining — outer improvement loop

Section titled “9.3 /trace-mining — outer improvement loop”

Analyzes past session journals to extract failure patterns, identify recurring harness gaps, and propose targeted improvements.

/trace-mining

Output feeds back into BoB — surfacing missing skills, broken hooks, or workflow friction. Pair with gh issue list --label lesson to mine accumulated learnings.

Trace-mining also hosts the authoring-gap analysis (#1134): gap-analysis.js analyze reads the session evidence ledger (#1131) for recurring improvised procedures and no-trigger tasks, clusters lesson issues by shared theme, and applies a configurable recurrence threshold (_workflow.gap_recurrence_count, default 3 occurrences across ≥2 sessions). Cross-project recurrence flags a registry candidate; single-project recurrence a project-docs candidate. gap-analysis.js recommend --apply files the qualifying batch as authoring-gap issues carrying the ledger evidence, routed toward the skill-creator path — zero skills are ever auto-authored, and a dismissed recommendation stays suppressed until new evidence accrues.

9.4 Registry-only continuous-improvement commands

Section titled “9.4 Registry-only continuous-improvement commands”

Provision and use as needed (not universal — opt-in per project):

  • /retrospective — Standard / start-stop-continue / 4Ls retros.
  • /standup — Daily standup format.
  • /status — Status reports at quick / weekly / monthly cadence.

9.5 Automatic self-healing (warp-drive session end)

Section titled “9.5 Automatic self-healing (warp-drive session end)”

At session_ending, warp-drive runs scripts/warp-drive/trace-mine-session.sh to close the improvement loop without manual effort. Since #295 it mines the warp-drive state history, not the issues you filed:

  • What counts as a finding — genuine execution friction recorded by the state machine: tests_failed / merge_failed / push_failed events, retry_count, more than one coding↔testing cycle (coding_cycles), no_work, and phase stalls (longest inter-transition gap over max_phase_minutes). Each finding is tagged with an action bucket: coding reliability (root-cause), merge/process (config), harness/capability (stall), or process.
  • No friction → no issue. A clean session is skipped silently ({"skipped":true,...}) — the common case. This is the key difference from the old behavior, which always emitted an issue that merely re-listed the chunk reports/decisions filed during the session.
  • Context, not findings. Issues filed during the session (decision, risk, lesson, bug-deferred) and the session summary are listed under a Context section for cross-reference — they are no longer presented as findings.
  • When friction is found, the miner files a warp-drive-labelled issue on the BoB repo titled self-healing: session friction from <project> #<req>.

This complements the on-demand /trace-mining (§9.3): the automatic miner flags per-session friction; /trace-mining analyses patterns across many sessions.

9.6 Model tiering in multi-agent workflows

Section titled “9.6 Model tiering in multi-agent workflows”

When you orchestrate work with the Workflow tool (agent() / parallel() / pipeline()), make asymmetric model tiering the default: spend a cheap, fast model on wide exploration and reserve a strong model for selective judgment. This is a convention, not a new capability — agent(prompt, {model, effort}) already accepts per-call overrides. Source: the “Self-Improving Loop” external-validation lesson (#264) — cheap breadth, expensive judgment.

The convention.

Stage role Examples Default tier
Finder / explorer / sweep — wide, shallow, parallel, disposable find bugs, grep logs, enumerate call-sites, map a subsystem cheap: model: 'haiku' or low effort
Verify / judge / critic / synthesize — narrow, deep, decisive adversarially refute a finding, score competing designs, write the final answer strong: model: 'opus' (or omit to inherit) + high effort

Economic rationale. Finders run in bulk and most of what they surface is noise — paying premium per-token to generate candidates is wasteful. Judgment runs on the small filtered set where correctness actually matters, so that is where the strong model earns its cost. A pool of cheap finders feeding a few expensive verifiers gets most of the quality of an all-strong fleet at a fraction of the spend.

Inheritance caveat. When you pass no model/effort, the agent inherits the session model (the resolved main-loop model) and session effort — which is almost always the right default. Only override when you’re confident a different tier fits the stage: drop finders to haiku/low, lift the hardest verify/judge stages to high effort. Don’t set model “just because”; an unnecessary override is how you accidentally run a 200-agent sweep on the expensive tier.

Canonical example — find → verify with explicit per-stage tiers:

// Cheap, parallel finders sweep each dimension; each finding is then verified
// by an expensive, skeptical judge. The pipeline runs verify-as-soon-as-found.
const results = await pipeline(
DIMENSIONS,
// FIND (cheap breadth): wide net, low cost, tolerant of false positives
d => agent(d.prompt, {
label: `find:${d.key}`, phase: 'Find',
model: 'haiku', effort: 'low', // <-- cheap tier
schema: FINDINGS_SCHEMA,
}),
// VERIFY (expensive judgment): adversarially refute each candidate
review => parallel(review.findings.map(f => () =>
agent(`Adversarially verify — try to REFUTE: ${f.title}`, {
label: `verify:${f.file}`, phase: 'Verify',
model: 'opus', effort: 'high', // <-- strong tier
schema: VERDICT_SCHEMA,
}).then(v => ({ ...f, verdict: v }))
))
)
const confirmed = results.flat().filter(Boolean).filter(f => f.verdict?.isReal)

The same shape applies to judge panels (cheap candidate generation → strong scoring), loop-until-dry discovery (cheap finders → strong dedup/synthesis), and multi-modal sweeps (cheap per-modality search → strong completeness critic). Keep the breadth cheap; keep the gate strong.


Moved to reference — see Glossary.

Moved to reference — see Glossary.

Moved to reference — see Glossary.

Moved to reference — see Glossary.

Moved to reference — see Hooks Reference.

The source-repo tree — each top-level directory, what it holds, and its generated component counts — is maintained once, in CLAUDE.md § Architecture. The component table in §1 gives the same counts by loading model.

A typical project has:

my-project/
├── .claude/ # Created by cdi — symlinks into BOB_HOME
│ ├── commands/ # Symlinks
│ ├── agents/ # Symlinks
│ ├── settings.json # Project-specific overrides (optional)
│ └── settings.local.json # Machine-specific, gitignored
├── CLAUDE.md # Project instructions (committed)
├── CLAUDE.local.md # Local notes (gitignored)
├── docs/
│ └── vision.md # The living vision doc
├── dev.json # Dev environment manifest
├── seed/ # Seed scripts + users.json
└── ...

Do not create these — they predate the GitHub Issues migration:

.claude/project-management/ ✗
.claude/requirements/ ✗
.claude/journal/decisions/ ✗ (use /journal decision)
.claude/journal/lessons/ ✗ (use /journal lesson)
.claude/journal/sessions/ ✗ (use /journal session)
REQ-*.md SOL-*.md BUG-*.md ✗
RISK-*.md INC-*.md CR-*.md ✗
UXR-*.md STATUS-*.md ✗
REQUIREMENTS-INDEX.md ✗
SOLUTIONS-INDEX.md ✗
backlog.md ✗
traceability-matrix.md ✗

Derived state is not a forbidden pattern. The rule above bans local PM files — human-authored work records that compete with GitHub Issues as a source of truth. It does not ban tooling-written caches. .claude/forge/ (the #1749 issue read cache) sits under .claude/ and persists across sessions, but it fails every test that makes a PM file a problem: no human writes it, nothing reads it as a source of truth, it is gitignored through the bob provision managed ignore block, and deleting the whole directory costs a re-fetch and nothing else. The distinction to apply is authored vs. derived, not where the file lives or how long it survives.

15.4 BOB_HOME (~/.claude/) — runtime only

Section titled “15.4 BOB_HOME (~/.claude/) — runtime only”

Synced from BOB_SOURCE by deploy.sh. Don’t edit directly. Files protected from overwrite:

  • ~/.claude/settings.json
  • ~/.claude/settings.local.json
  • ~/.claude/CLAUDE.local.md

15.5 Documentation filename conventions (docs/)

Section titled “15.5 Documentation filename conventions (docs/)”

Files under docs/ follow one mechanical naming rule so the directory greps and globs predictably, the docs site renders stable URLs, and contributors (and warp-drive) never have to guess casing.

Rule Decision
Case lowercase only
Word separator - (kebab-case)
Character set [a-z0-9-] only — no underscores, spaces, accents, em-dashes
Extension .md for prose, .json for data. .txt / .markdown are not allowed. An image a doc embeds (.png, .svg) lives in an img/ directory beside it (e.g. explanation/img/bob-viz-dark.png, #2074), and nowhere else. A reference design a doc links to is a static .html mock in docs/design/ (e.g. design/bob-control-mock.html, #3914), and nowhere else
Date stamps ISO 8601 suffix: <topic>-YYYY-MM-DD.md (e.g. doc-audit-2026-05-07.md)
Acronyms lowercased — pm-cheatsheet, iac-audit, bob-identity
Subdirectories lowercase, plural where applicable: audits/, research/
Suffix patterns *-guide.md for “explain X”, *-reference.md for reference docs
Frontmatter title independent of the filename — the docs-site engine uses title for display, the slug for the URL

Reserved names (exempt): README.md, index.md, _index.md, CHANGELOG.md, CONTRIBUTING.md, LICENSE. These follow conventional uppercase and are skipped by the linter.

Canonical regex (basename minus extension): ^[a-z0-9]+(-[a-z0-9]+)*$ with extension in {md, json}.

Why:

  • Slug collapse — under flat serving PM-CHEATSHEET.md and pm-cheatsheet.md can produce the same URL (and engines like Starlight lowercase slugs); lowercasing eliminates the collision and keeps links engine-portable.
  • Case-insensitive filesystems — macOS APFS treats Foo.md and foo.md as the same file. A single casing rule avoids silent git mv no-ops and cross-machine drift. (When renaming, git mv -f through a temp name clears the no-op hurdle.)
  • Greppability — one rule means callers can link and search without second-guessing casing.

Not part of the convention: numbered ordering prefixes (01-foo.md) — use the docs-site engine’s nav-ordering config (e.g. Starlight’s sidebar.order frontmatter) instead; mixing the two is the worst outcome. There is no filename-length budget — long descriptive names like project-orchestration-handbook.md are fine.

Scope & enforcement: This convention covers docs/. It is enforced two ways:

  • make check-doc-naming (script: scripts/checks/check-doc-naming.sh) — wired into make check; fails CI on any non-reserved docs/** file outside the rule.
  • The doc-keeper auditor emits a filename-violation drift category so violations also surface in /doc-audit reports.

The same lowercase-kebab rule is applied to the code-side item directories (agents/, registry/{skills,agents,commands}/, runbooks/, provisions/) by the sibling linter make check-filename-conventions (scripts/checks/check-filename-conventions.sh, #238-241), with framework-reserved carve-outs (SKILL.md, _default.json, …). Skill bundle assets (LICENSE.txt, *.pdf, scripts inside a skill dir) are not policed — only item/dir names are.

15.6 Test & inspection artifacts (.bob-artifacts/)

Section titled “15.6 Test & inspection artifacts (.bob-artifacts/)”

Ephemeral test/inspection output — Playwright screenshots, DOM snapshots, scratch captures — goes in a single per-project directory: .bob-artifacts/. Never the repo root or scattered /tmp paths (#253).

  • bob init idempotently adds .bob-artifacts/ and .playwright-mcp/ (the Playwright MCP default output dir) to the project’s root .gitignore, so these artifacts never get accidentally committed.
  • The webapp-testing skill writes screenshots there; verify/run flows should follow the same convention.
  • Backfill an existing project (or clean stray root screenshots) with scripts/clean-artifacts.sh <project> — idempotent: it relocates loose root-level screenshots into .bob-artifacts/ and ensures the .gitignore entries.

Symptom Run this
“Is BoB healthy?” make check
“Are my project’s symlinks ok?” bob dash status then bob dash status --check <project>
“What is provisioned in this project?” bob provision --status
“Does the manifest match reality?” bob provision --diff
“Why is warp-drive stuck?” bob warp status
“Is the dev env healthy?” bob dev health or /dev-up --check
“Hook script missing?” make check-hooks
“JSON config invalid?” make check-schemas
“Stale state across the system?” /rebob
“Lessons accumulating? Patterns?” /trace-mining
File Owner Notes
<project>/.warp-drive-state.json warp-drive engine Current chunk, phase, history
<project>/.autoloop-state.json (legacy) auto-loop Engine archived (#2079) — safe to delete
~/.claude/settings.local.json machine _rdb.enabled lives here
Problem Fix
Hooks aren’t firing make check-hooks; verify ~/.claude/settings.json references existing scripts
Symlinks point to nowhere bob provision --refresh — since #2314 it also retargets a link whose item moved and prunes one whose item retired (per scripts/lib/retired-items.json), naming each action and reporting an unknown dangler; bob provision --prune for the explicit repair (drop dangling/cross-machine; dry-run first); fleet-wide: make repair-fleet then make check-fleet (#303)
Warp-drive won’t start Check bob warp status; ensure at least one req + approved issue exists
Telegram silent (RDB on) Check ~/.claude/settings.local.json for _rdb.enabled: true; type something in terminal
/auto-loop did weird things Stop using /auto-loop — switch to /warp-drive
Got prompt to create REQ-NNNN.md Stop. That’s a regression. File a bug — work tracking is GitHub Issues
Tests stuck in fix loop Warp-drive 3-strikes-out automatically; let it escalate
bob fork worktree won’t drop bob fork drop <branch> --force; if that fails, manually git worktree prune
Deploy refuses to overwrite something That’s by design — settings.json, settings.local.json, CLAUDE.local.md are protected

Every BoB-managed project can be automatically versioned — conventional-commit-driven semver bumps, git tags, CHANGELOG.md, and GitHub Releases — provisioned from one source of truth rather than hand-copied CI (capability #275).

Tool: commit-and-tag-version (maintained fork of standard-version). release-please was considered and deferred (see the versioning guide).

Enable it — versioning is an opt-in manifest capability; it copies template files rather than symlinking:

// provisions/<project>.json
"versioning": { "enabled": true, "main_branch": "main" }
Terminal window
bob provision refresh # copies auto-release.yml + .versionrc.json + commitlint config,
# merges package.json (release script + devDependency)

Local fallback (#1691) and channels (#2045): bob release --channel stable|nightly runs the one release engine (scripts/release/release.sh, the file auto-release.yml also runs) from a workstation — BoB itself cuts stable weekly and nightly tags on green master, never per push — and the local delivery runbook covers the full no-Actions path (CI gate, release cut, docs deploy).

The orchestrator recommends versioning by default for every project and picks a push trigger-path filter per project type (docroot/** for cms, the full source surface for framework, src/**+package.json for apps/libraries, no filter for research/mixed). An existing versioning block is always preserved on re-run, so enabled:false opt-outs and customizations survive.

A merge to the main branch then bumps the version, rewrites the changelog, tags vX.Y.Z, and publishes a GitHub Release — loop-guarded against the chore(release) commit. Conventional-commit enforcement ships as config (#278).

Heads-up: the workflow pushes the release commit/tag directly to the main branch via GITHUB_TOKEN; enabling branch protection without a bot exception breaks it. Full detail, bump rules, and the per-type path table: versioning.md.



Last updated: 2026-05-07 — reconciled with doc audit (#144). Changelog: disambiguated cdr (Claude Disaster Recovery, not legacy reqs-index) in §13.1; surfaced live test counts and the doc-link sweep script in §3.7; added doc audit and archive subtree to §17 See Also.