Skip to content

Build master @ bc50cd5bunreleased — 31 commits past v1.282.0Last reviewed Last changed Grade aging (aging >90d, stale >180d)Source / History

Branch Configuration (_branch) ​

Branch detection and merge behavior, configured per-project in .claude/settings.local.json under the _branch key.

Script: ~/.claude/scripts/branch-detect.sh — single source of truth for branch resolution across all commands and hooks.

Schema ​

FieldTypeDefaultDescription
_branch.mainstring|nullauto-detectOverride main branch name
_branch.sessionstring|nullunset (no stream)The declared integration/session branch — written by bob warp session start / /start-work, never inferred from HEAD (#2313)
_branch.merge_strategy"ff-only" | "merge-commit" | "squash""ff-only"Merge strategy for /finish-work and warp-drive
_branch.scope{"area": "<slug>"} | {"flightplan": "<N>"} | absentnoneStream↔scope binding (#1177): the warp-drive scope bound to the session branch

Auto-Detection (no config) ​

When _branch is not set, branch-detect.sh auto-detects:

  1. Main branch: origin/HEAD → local main → local master → error
  2. Session branch: the _branch.session pin, otherwise empty — declared, never inferred (#2313)
  3. Merge target: session branch if declared, otherwise main
  4. Merge strategy: ff-only

The session branch is never taken from HEAD. Until #2313, Step 6 adopted any checked-out non-main branch as the session when no pin was set, so a per-item feature/issue-N-x branch became merge_target and an L3 run stacked every later item on it — 24 commits that never reached master (flight plan #2092). Now an unpinned run on any branch resolves merge_target to main; a stream exists only when bob warp session start <branch> (or /start-work) has written the pin. /warp-drive init warns when HEAD is a non-main branch without a pin, naming the branch and the merge target it will use (recorded as branch_warning on the run state and shown by bob warp status). tests/test-branch-detect.sh pins the rule.

Examples ​

Default (no config needed) ​

Most repos need no configuration — detection just works.

json
{}

Output on a main-based repo while on feature/foo (no pin — the feature branch is not the session, and the merge target is main):

json
{"main":"main","session":"","current":"feature/foo","on_main":false,"merge_target":"main","merge_strategy":"ff-only","scope":""}

Explicit session branch ​

Pin session branch so it persists even when on main (e.g., long-running dev branch):

json
{
  "_branch": {
    "session": "dev/sprint-3"
  }
}

Stream↔scope binding (#1177) ​

Bind a warp-drive scope (an area workstream or a flight-plan issue) to the integration branch when opening the stream, so every kickoff in the checkout lines up with it automatically:

bash
bob warp session start dev/warp-stream --area warp-drive     # or: --flightplan 1078
json
{
  "_branch": {
    "session": "dev/warp-stream",
    "scope": { "area": "warp-drive" }
  }
}

The binding is enforced at /warp-drive kickoff by resolveScopeBinding() (scripts/warp-drive/kickoff.js), feeding the run's normal session.kickoff state so scope precedence stays in kickoffScope() (#992):

  • No scope flags → the kickoff defaults to the bound scope; an unscoped run on a bound stream can never reach the full approved queue.
  • Explicit matching scope → passes through unchanged.
  • Explicit --issue → allowed; a named issue is explicit operator intent strictly narrower than the binding.
  • Different area/plan (or mixed kinds) → refused with a message naming the bound stream and how to re-bind or end it.

Because it lives in settings.local.json, the binding survives session end, segmentation, and machine restart — and it is per-checkout, so cdfork worktrees each carry their own binding and parallel streams on different areas coexist. bob warp session end and bob warp finalize clear it together with the branch pin; to run a different scope, re-bind (bob warp session start <branch> --area <slug>) or end the stream first. A scope recorded without a session pin is inert.

branch-detect.sh reports the binding as a scope field ("area:<slug>" / "flightplan:<N>", empty when unbound; also via --field scope); bob warp session status and bob warp status display it omit-when-empty.

Custom main branch (develop) ​

For repos using develop as the integration branch:

json
{
  "_branch": {
    "main": "develop"
  }
}

Squash merging ​

For projects that prefer squash merges:

json
{
  "_branch": {
    "merge_strategy": "squash"
  }
}

Merge Strategy → Git Flag Mapping ​

StrategyGit flagBehavior
ff-only--ff-onlyFast-forward only, fails if diverged
merge-commit--no-ffAlways create a merge commit
squash--squashSquash all commits into one

Unified freshness/finalize policy (#1178/#1179) ​

Integration-branch streams follow one sync policy in both directions, and it never rebases:

  • During the stream (kickoff, segment resume, bob warp session start on an existing branch): main is merged into the stream by scripts/warp-drive/stream-freshness.sh. A pushed stream must never be rebased — rewriting shared history breaks every checkout tracking it.
  • At bob warp finalize (L3): the stream is freshened one last time (same merge-in engine — no rebase-onto-main step), then shipped to main using the configured merge_strategy flag above. Because a freshened stream strictly descends from main, the ff-only default ships cleanly with any absorbed sync-merge commits preserved in history; set merge-commit if you want an explicit --no-ff ship commit marking the stream boundary. The policy is carried entirely by merge_strategy — there is no hidden finalize-specific override. When the stream lives in a worktree and main is checked out in another worktree (the farm-01 layout), git refuses to check main out there, so finalize lands the stream from a temporary detached worktree of origin/<main> (a squash is committed there) and pushes it; the other checkout is left alone, and this one is left detached at the new origin/<main> (#3543). Where the project's manifest declares _meta.branch_protection enforced for admins, finalize pushes neither: it lands the stream through one pull request and merge-on-green (How finalize lands, #4184).

When does rebase apply? Never inside warp tooling. Rebasing is acceptable only as a manual choice on a branch that has never been pushed (nothing shared to rewrite); once a stream has an upstream, merge-in is the only sync direction.

Consumer Commands ​

Command / ScriptUses
/start-workDetects main for checkout; writes _branch.session after branch creation
bob warp session start/end, bob warp finalizeWrite/clear _branch.session + _branch.scope (stream↔scope binding, #1177)
kickoff.js (resolveScopeBinding)Applies _branch.scope at /warp-drive kickoff — bound default, conflict refusal
/finish-workDetects main for rebase/merge target; reads --field session (the declared pin only) for the mismatch check
/warp-driveDetects main, session, merge_target, merge_strategy for all merge operations — session is the declared pin or empty, so an unpinned run merges into main (#2313)
check-branch.sh, session-branch-guard.sh, promotion-ceiling-guard.sh hooksDetect main for branch enforcement; the session guard is keyed on the _branch.session pin it reads itself
stream-freshness.shReads the _branch.session pin directly; without one it reports skipped and never checks a branch out
swarm/integrate.jsIntegration target is --target or the declared session; with neither it refuses (exit 2) rather than merging into the checked-out branch
state-machine.jsStores branch fields in warp-drive state; records branch_warning when HEAD is a non-main branch without a pin

Every consumer above was audited for #2313 (AC-02): none derives a session from HEAD — each reads the pin (directly or through branch-detect.sh), and tests/test-branch-detect.sh drives bob warp finalize, bob warp session status, stream-freshness.sh and swarm/integrate.js on an unpinned feature branch to pin their behaviour.