# CB-301-ext — Worktree provisioning + config-parity overlay **Status:** ✅ shipped — implemented at commit `97ecc71` (per-worker git worktree + config-parity overlay). As-built: `session/GitWorktrees.java` behind the `Worktrees` port, wired in `Bridged.main` and configurable via `worktreeRoot` / per-profile `parityOverlay` (see `bridged.example.yaml`). Branch/worktree surface in `bridge_list` landed with CB-304 (`9fe04bf`); the worker-opened-PR checkpoint landed as CB-302 (`64e70ef`). **Extends:** [CB-301 Session Manager](CB-301-Session-Manager.md) (shipped, commit `54d907c`). **Realizes:** the config-parity requirement in [Worker Git Workflow](Worker-Git-Workflow.md). **Grounded in:** `SessionManager`, `WorkerService.spawn/effectiveCwd`, `BridgedConfig.Worker`, `inject/…LsofPeerPidLookup` (the `ProcessBuilder` exec pattern). ## Problem CB-301 gives each worker a session record but every worker still runs in the **primary's own working tree** (`cwd = callerCwd`). One worker at a time is safe; two **parallel implementers** would stomp each other. We need each implementer session to get an **isolated git worktree on its own branch** — *without* degrading the worker: a bare worktree checks out **tracked files only**, so it silently drops the untracked/local config (`.claude/settings.local.json`, the locally-modified `.mcp.json`, `.env`) that makes a session a full peer of the primary. **CB-301-ext provisions the worktree AND hydrates it to config parity**, so a worker differs from the primary only in the LLM provider. ## Decisions (locked) 1. **Opt-in, not default.** A worktree is provisioned **only** when the caller requests one. Absent a request, `acquire` behaves exactly as it does today (shared primary tree) — auditors, smoke tests, and conversational workers are unaffected. **Backward compatibility is a hard requirement.** 2. **Copy-overlay + `--skip-worktree`, not symlink.** Each parity file is **copied** primary→worktree (isolation-friendly, no symlink type-change noise on tracked files). For a *tracked* overlay file (`.mcp.json`) the worktree copy is then marked `git update-index --skip-worktree`, so the worker's commits can **never** include the parity overlay. Ignored files (`settings.local.json`) stay ignored in the worktree (shared `info/exclude`), so no marking is needed. 3. **Branch persists; worktree is disposable.** `release` runs `git worktree remove --force` (the working dir is throwaway) but **never deletes the branch** — the branch holds the worker's commits and its PR (CB-302). Teardown of the checkout ≠ teardown of the work. 4. **Git behind a seam.** SessionManager depends on a `Worktrees` interface (production impl shells `git` via `ProcessBuilder`; tests use a fake). No live `git` in unit tests — mirrors the `WorkerService`/`FakeHerdr` seam. ## Design ### `WorktreeRequest` (new, nullable = "no worktree") ```java package dev.ltms.bridged.session; /** Ask acquire() to provision an isolated worktree. null ⇒ run in the shared primary tree. */ public record WorktreeRequest(String ticketSlug, String baseRef) { // ticketSlug seeds the branch name; baseRef null/blank ⇒ current HEAD of the repo. } ``` ### `WorkerSession` — two nullable fields added | Field | Notes | |---|---| | `worktree` | absolute path of the provisioned worktree; `null` ⇒ shared tree | | `branch` | the worker's branch (`worker/-`); `null` ⇒ shared tree | Add to the record + `withState`. A `null` worktree keeps every existing test and the shared-tree path untouched. ### `Worktrees` seam (new) ```java package dev.ltms.bridged.session; public interface Worktrees { /** git -C worktree add -b . Returns the worktree path. */ String add(String repoRoot, String branch, String baseRef); /** git -C worktree remove --force . Idempotent (already-gone tolerated). */ void remove(String repoRoot, String worktreePath); /** Copy each existing overlay path repoRoot→worktree; mark tracked ones --skip-worktree. */ void overlayParity(String repoRoot, String worktreePath, List overlay); /** git -C rev-parse --show-toplevel — the repo root that owns cwd. */ String repoRoot(String cwd); } ``` - **Production impl** `GitWorktrees implements Worktrees` — `ProcessBuilder` per path, `redirectErrorStream(true)`, non-zero exit → a `WorktreeException`. Worktree location = `/` where `worktreeRoot` is a daemon setting (default: sibling `../.bridged-worktrees` of the repo root — **outside** the repo, never nested). - `overlayParity` per file: skip if absent in `repoRoot`; else copy into the worktree; if `git -C ls-files --error-unmatch ` succeeds (tracked), run `git -C update-index --skip-worktree `. ### `acquire` — extended, old signature preserved ```java // existing (unchanged): shared tree WorkerSession acquire(String profile, String requestedCwd, String callerCwd, String ownerTerminal); // new overload: provision a worktree when wt != null WorkerSession acquire(String profile, String requestedCwd, String callerCwd, String ownerTerminal, WorktreeRequest wt); ``` When `wt != null`: 1. `repoRoot = worktrees.repoRoot(firstNonBlank(requestedCwd, callerCwd))`. 2. `branch = "worker/" + slug(wt.ticketSlug()) + "-" + nonce`. 3. `path = worktrees.add(repoRoot, branch, wt.baseRef())`. 4. `worktrees.overlayParity(repoRoot, path, cfg.parityOverlay())`. 5. `spawn(profile, path, callerCwd)` — **the worktree path becomes the worker's cwd** (highest precedence in `WorkerService.resolveCwd`). 6. register the session with `worktree=path, branch=branch`. 7. **On any failure in 1–5, unwind**: if the worktree was added, `remove` it; do not leave a dangling registry entry. (Guard/spawn already throw before herdr on a bad base_url — unchanged.) ### `release` — remove the worktree, keep the branch ```java public void release(String paneId) { WorkerSession s = registry.remove(paneId); workerService.stop(paneId); // existing if (s != null && s.worktree() != null) { worktrees.remove(worktrees.repoRoot(s.cwd()), s.worktree()); // branch is NOT deleted } } ``` ### Config — `BridgedConfig.Worker.parityOverlay` + a `worktreeRoot` - Add `List parityOverlay` to the `Worker` record (12th field). Compact-constructor default when null/empty: `[".mcp.json", ".claude/settings.local.json", ".env", ".envrc"]` (missing paths are silently skipped, so the default is safe across repos). Update `withProfile`. - Add a top-level daemon setting `worktreeRoot` (String, nullable → `/.bridged-worktrees`). - `@JsonIgnoreProperties(ignoreUnknown = true)` already set → additive, no parser breakage. ### Surface: MCP + REST - `bridge_spawn` gains an optional `worktree` arg: `true`, or a ticket slug string. Truthy ⇒ build a `WorktreeRequest(slug, null)` and call the 5-arg `acquire`. - `POST /workers` gains `worktree` (+ optional `ticket`) in the body/query, same mapping. - `workerView`/`view(WorkerSession)` include `worktree` and `branch` **when non-null** (omit for shared-tree sessions, so existing response assertions for shared-tree spawns are unchanged). ## Flow ```mermaid flowchart TD A["acquire(profile, ..., WorktreeRequest?)"] --> B{"worktree
requested?"} B -->|"no (default)"| C["spawn(cwd = callerCwd)
— shared tree, unchanged"] B -->|yes| D["repoRoot = rev-parse --show-toplevel"] D --> E["git worktree add path -b branch base"] E --> F["overlayParity: copy local config in;
--skip-worktree the tracked ones"] F --> G["spawn(cwd = worktree path)"] G --> H["register worktree + branch on the session"] E -.->|"add/overlay/spawn fails"| X["unwind: remove worktree,
no dangling registry entry"]:::warn C --> R["worker is a full peer of the primary"]:::goal H --> R classDef warn fill:#b7791f,stroke:#7b341e,color:#ffffff; classDef goal fill:#2b6cb0,stroke:#2a4365,color:#ffffff; ``` *Green = the invariant: whether shared-tree (parity for free) or worktree (parity via overlay), the worker matches the primary. Amber = the failure-unwind path.* ## Acceptance (fake `Worktrees`, no live git) 1. **Backward-compat:** `acquire` with **no** `WorktreeRequest` makes **zero** `Worktrees` calls, spawns with `cwd = callerCwd`, and records `worktree == null` / `branch == null`. (Every CB-301 test still passes.) 2. **Provision:** `acquire(..., new WorktreeRequest("cb-999", null))` calls `add(repoRoot, "worker/cb-999-", null)`, then `spawn` receives the returned worktree path as `requestedCwd`; the session records that path + branch. 3. **Overlay:** `overlayParity` is invoked with the profile's `parityOverlay` (default list when unset); the fake asserts tracked paths were `--skip-worktree`'d and missing paths skipped. 4. **Release removes worktree, keeps branch:** releasing a worktree session calls `Worktrees.remove(repoRoot, path)` and performs **no** branch-delete; a shared-tree session's release makes no `Worktrees` calls. 5. **Failure unwind:** a fake `add` that throws ⇒ `acquire` throws, the session is **not** registered, and no worker is left running (spawn not reached / torn down). 6. **Distinct worktrees:** two worktree acquires yield **distinct** branches and paths (no collision). ## Constraints & exclusions (standing, non-negotiable) - **Only a worker sets `ANTHROPIC_BASE_URL`.** Worktrees touch cwd + files only; env path is unchanged — the guard still runs before any herdr call. - **Worker cwd stays inside the primary repo** (the worktree is a checkout of it) — never `$HOME`. - **`.mcp.json` and `wiki/` never enter a worker commit.** `.mcp.json` is overlaid for *reference* but `--skip-worktree`'d so it can't be staged; `wiki/` is a submodule the worker must not touch. The CB-302 commit step (and the implementer skill) exclude both. - **The overlay list stays explicit + minimal** (trust: local secrets flow to an off-subscription worker). No blanket tree copy. ## Seams left for later - **CB-302** — the worker commit → push → PR checkpoint runs *inside* the worktree on its branch. - **CB-304** — `roster()` rows surface `worktree`/`branch` for the fleet view.