---
name: worktree-manager
effort: medium
description: >
  Manage fully independent git worktrees for parallel coding agents.
  Commands: /nw (new worktree) creates an isolated workspace with its own
  node_modules, .next cache, and dev server port; /mw (merge worktree) commits,
  pushes, creates PR to the trunk branch (git.trunk_branch), merges, and cleans up; /lw (list worktrees)
  shows status of all active worktrees; /cw (cleanup worktrees) removes stale
  or already-merged worktrees without the full PR flow.
  Also used programmatically by /new orchestrator for batch card worktree ops.
  Trigger on: /nw, /mw, /lw, /cw, "new worktree", "merge worktree",
  "list worktrees", "cleanup worktree".
---

# Worktree Manager

**Project Context (resolved from `baldart.config.yml`)**:
- `git.trunk_branch` — the integration trunk branch (resolved value, default autodetected). Every worktree branches from `origin/<git.trunk_branch>` and merges back into it. NEVER hardcode `develop` or `main`.
- `git.merge_strategy` — `pr` (default, GitHub PR via `gh`) or `local-push` (direct fast-forward to `origin/<git.trunk_branch>`). Drives `/mw` step 4c.
- `paths.backlog_dir` — used for syncing untracked backlog cards (`/nw` step 3b).
- `paths.metrics` — JSONL telemetry dir (default `docs/metrics`) used by the rebase conflict-resolution table.
- Protocol reference: `framework/agents/project-context.md`. Skills must ASK when a needed key is missing — never assume.

## Effort

**Baseline:** `effort: medium` (frontmatter). **Inline override:** pass
`effort=<low|medium|high|xhigh|max>` anywhere in the invocation to scale
reasoning depth for this run — detect it once at kickoff and strip the token
before consuming user input. Level→behavior mapping, parsing contract, and
precedence caveats: `framework/agents/effort-protocol.md`.


**IMMEDIATE EXECUTION**: When invoked via `/nw`, `/mw`, `/lw`, or `/cw`, do NOT explain the process. Start executing the matching command flow immediately:

- `/nw` → Ask for **slug** (required) and optionally a **card ID**, then execute all steps (pre-flight → create → install → build → report). If the user provided args, skip questions and use them directly. Supports three forms:
  - `/nw FEAT-0500 menu-fix` — with card ID and slug
  - `/nw menu-fix` — slug only (no card, branch: `feat/menu-fix`)
  - Add `--dev` to also start the dev server (e.g., `/nw --dev menu-fix`).
- `/mw` → Read registry, identify worktree, start merge flow.
- `/lw` → Read registry + `git worktree list`, show status table.
- `/cw` → Scan for removable worktrees, present options.

---

Four commands for fully isolated parallel workspaces + programmatic API for orchestrators.

## Programmatic API (for /new orchestrator)

### /nw programmatic

Input: `{ cards: string[], groupParent: string|null, slug: string, branch?: string }`
Output: `{ path: string, branch: string, port: number }`

### /mw programmatic

Input: `{ worktreePath: string, checksAlreadyPassed?: boolean }`
Output: `{ merged: boolean, developVerified: boolean, prNumber: number|null, mergeCommit: string, strategy: "pr"|"local-push" }`

## Programmatic API — docs mode (for /prd orchestrator)

The docs mode is the **lean** counterpart to `/nw` and `/mw`. It is intentionally
stripped down for skills that produce **documentation artefacts only** — PRDs,
backlog cards, design references — and never touch source code.

What docs mode SKIPS vs the standard flow:
- No `npm install` (no `node_modules/` inside the worktree)
- No port allocation (no dev server)
- No `.env`/`.env.local` copy
- No `npm run build` baseline verification
- No lint / tsc baseline checks
- No `--dev` server bootstrap
- No build / lint / tsc gates at merge time

What docs mode KEEPS:
- Pre-flight `git fetch origin <git.trunk_branch>` (read-only on the main repo).
- `git worktree add` branched from `origin/<git.trunk_branch>`.
- Registry entry with `kind: "docs"` so `/lw` and `/cw` can list and clean docs worktrees alongside code worktrees.
- Safety commit at merge time (uncommitted files are never lost).
- Rebase onto `origin/<git.trunk_branch>` before merging (prevents PR/FF conflicts).
- The configured `git.merge_strategy` (`pr` or `local-push`).
- Cleanup of worktree + local branch + remote branch after merge.

Cost target: **< 5 seconds** to create a docs worktree on a typical machine (no
network beyond the `fetch`, no install, no build). Compare to `/nw` which can
run for minutes due to `npm install` + build.

### nw-docs programmatic

Input:
```json
{
  "slug": "string",            // REQUIRED — kebab-case feature slug
  "branchPrefix": "string"     // OPTIONAL — defaults to "prd/"
}
```

Output:
```json
{
  "path": "string",            // absolute worktree path
  "branch": "string",          // e.g. "prd/menu-nutritional-info"
  "kind": "docs"
}
```

**Steps (run exactly these, nothing else):**

```bash
# 0. Pre-flight: .gitignore safety check (MANDATORY — Safety Rules
#    "Always verify .worktrees/ is in .gitignore before creating").
#    Without this, the worktree IS git-tracked: `git status` on the main
#    repo explodes with every file inside the worktree, exactly defeating
#    the parallel-isolation purpose of the docs-mode.
MAIN_ROOT="$(git rev-parse --show-toplevel)"
if [ ! -f "$MAIN_ROOT/.gitignore" ] || ! grep -qE '^\.worktrees/?$' "$MAIN_ROOT/.gitignore"; then
  echo "ERROR: .worktrees/ is not in $MAIN_ROOT/.gitignore" >&2
  echo "Add it before running nw-docs:  echo '.worktrees/' >> .gitignore" >&2
  echo "(or invoke this skill with --auto-ignore to append it for you — not yet implemented)" >&2
  exit 1
fi

# 0b. Resolve the trunk branch (R7). Prefer the config key; if the consumer
#     updated to >=4.0.0 without re-running `configure`, `git.trunk_branch` may
#     be absent — in that case AUTODETECT the repo's real default branch
#     (origin/HEAD → local develop/main/master) instead of hard-failing. This is
#     NOT "assume develop": it resolves the branch the repo actually uses, the
#     same logic configure.js applies. Only abort when even autodetection finds
#     nothing (bare repo / no branches). Without this fallback a config missing
#     the v4.0.0 key silently blocked every `/prd` worktree (regression v4.0.0).
TRUNK=$(grep -A40 '^git:' "$MAIN_ROOT/baldart.config.yml" 2>/dev/null \
  | grep 'trunk_branch:' | head -1 \
  | sed -E 's/.*trunk_branch:[[:space:]]*([A-Za-z0-9._\/-]+).*/\1/' || true)
if [ -z "$TRUNK" ]; then
  TRUNK=$(git -C "$MAIN_ROOT" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null \
    | sed -E 's#^refs/remotes/origin/##' || true)
  if [ -z "$TRUNK" ]; then
    for cand in develop main master; do
      if git -C "$MAIN_ROOT" show-ref --verify --quiet "refs/heads/$cand"; then TRUNK="$cand"; break; fi
    done
  fi
  if [ -z "$TRUNK" ]; then
    echo "ERROR: git.trunk_branch not set in baldart.config.yml and no trunk branch could be autodetected (origin/HEAD / develop / main / master). Run 'npx baldart configure' to set git.trunk_branch, or create the trunk branch first." >&2
    exit 1
  fi
  echo "WARN: git.trunk_branch missing from baldart.config.yml — autodetected '$TRUNK'. Run 'npx baldart configure' to persist it and silence this warning." >&2
fi

# 1. Pre-flight — read-only fetch, NEVER touches main repo HEAD
git fetch origin "$TRUNK"

# 2. Duplicate check — read registry; if an entry with the same slug AND kind=docs
#    exists, return the existing { path, branch, kind } instead of creating a new
#    one (resume semantics — Step 0 of /prd needs this).
#    If an entry with same slug AND kind != "docs" exists (code worktree
#    collision), fail loudly — never silently coexist.

# 3. Create the worktree. The path returned MUST be ABSOLUTE — Claude Code's
#    Write/Edit require absolute paths, and downstream skills (/prd, ui-design,
#    prd-card-writer) interpolate this path verbatim.
#    SLUG must be kebab-case + deterministic (R8): the same slug always maps to
#    the same worktree path, so a resumed /prd run finds the existing worktree.
SLUG="<slug>"
BRANCH_PREFIX="${branchPrefix:-prd/}"
BRANCH="${BRANCH_PREFIX}${SLUG}"
WORKTREE_REL=".worktrees/prd-${SLUG}"      # deterministic: derived from slug only, no timestamp
WORKTREE_PATH="$MAIN_ROOT/$WORKTREE_REL"   # absolute, anchored to main repo root
git -C "$MAIN_ROOT" worktree add "$WORKTREE_REL" -b "$BRANCH" "origin/$TRUNK"

# 4. Registry entry — kind: "docs", port: null, buildVerified: null
#    Same registry file (.worktrees/registry.json) as code worktrees.
#    The `path` field MUST hold $WORKTREE_PATH (absolute), not $WORKTREE_REL.
#    R6: persist $MAIN_ROOT, slug, branch AND the resolved trunk on the entry
#    (fields: path, slug, branch, trunkBranch) so every later consumer reads them
#    from the registry with a presence guard — never relies on in-context vars.
```

**Registry entry shape for docs worktrees:**

```json
{
  "card": null,
  "cards": [],
  "groupParent": null,
  "slug": "menu-nutritional-info",
  "branch": "prd/menu-nutritional-info",
  "trunkBranch": "develop",
  "mainRoot": "/abs/repo-root",
  "path": "/abs/.worktrees/prd-menu-nutritional-info",
  "port": null,
  "kind": "docs",
  "createdAt": "2026-05-27T10:00:00Z",
  "envSyncedAt": null,
  "buildVerified": null
}
```

**Forbidden in docs mode**: `npm install`, port scan, `.env*` copy, `npm run build`,
`npx tsc`, `npx eslint`, dev server start, `git checkout`/`switch`/`branch` on main.

### mw-docs programmatic

Input:
```json
{
  "worktreePath": "string",    // REQUIRED — absolute path of the docs worktree
  "commitMessage": "string"    // OPTIONAL — defaults to "[PRD] <slug>: <generated summary>"
}
```

Output:
```json
{
  "merged": true,
  "mergeCommit": "string",                 // SHA of the merge commit (or branch tip in local-push)
  "prNumber": null,                         // populated only when strategy = "pr"
  "strategy": "pr" | "local-push",
  "worktreeRemoved": true
}
```

**Steps (run exactly these, nothing else):**

```bash
# Presence guard (R6): $WORKTREE_PATH and the resolved $TRUNK MUST be read from
# the registry entry (fields path / trunkBranch) and be non-empty before any op.
if [ -z "$WORKTREE_PATH" ]; then
  echo "ERROR: \$WORKTREE_PATH unset — read it from the registry entry before mw-docs." >&2
  exit 1
fi
# If $TRUNK is unset, read the `trunkBranch` field from this worktree's registry
# entry in .worktrees/registry.json (do NOT assume 'develop').
if [ -z "$TRUNK" ]; then
  echo "ERROR: trunk branch unresolved — read trunkBranch from the registry entry (do NOT assume 'develop')." >&2
  exit 1
fi

cd "$WORKTREE_PATH"

# 1. Safety commit (MANDATORY — same as /mw step 3, never skippable).
#    Stage every changed and untracked file by explicit name, then commit.
#    For docs worktrees the staged set is typically:
#      - ${paths.prd_dir}/<slug>/PRD.md
#      - ${paths.prd_dir}/<slug>/design.html (if exists)
#      - ${paths.prd_dir}/<slug>/mockups/** (if any)
#      - ${paths.prd_dir}/sessions/YYYY-MM-DD-<slug>-state.md
#      - ${paths.backlog_dir}/FEAT-XXXX-*.yml (all cards)
#      - ${paths.references_dir}/ssot-registry.md  (if updated by Step 7)
#      - ${paths.references_dir}/project-status.md (if updated by Step 7)
#    NEVER `git add .` or `git add -A`. NEVER `git stash` (see Safety Rules).

# 2. Rebase onto the latest trunk branch ($TRUNK = git.trunk_branch). Reuses the
#    /mw step 4b conflict-resolution table (additive auto-strip for
#    docs/yaml/jsonl-in-${paths.metrics}; abort on structured registries when
#    validation fails; abort on code/tests/fixtures).
#
#    NO `git stash` in mw-docs (or anywhere in a worktree). The safety commit in
#    step 1 above guarantees a clean working tree. Running `git stash push` from a
#    worktree violates the Safety Rule "NEVER use git stash in worktrees"
#    (refs/stash is shared across all worktrees via git.commondir — see the
#    FEAT-0522 incident note). Two parallel runs that both stash would race on the
#    shared stash list and could pop into the wrong tree.
#
#    Direct rebase, no stash. If the working tree is unexpectedly dirty here
#    (someone wrote between step 1 and step 2), STOP and report — a second
#    safety commit would be the right answer, never a stash.
DIRTY=$(git -C "$WORKTREE_PATH" status --porcelain 2>/dev/null)
if [ -n "$DIRTY" ]; then
  echo "ERROR: working tree dirty in $WORKTREE_PATH after safety commit." >&2
  echo "Files: $DIRTY" >&2
  echo "Investigate — never stash to recover (refs/stash shared across worktrees)." >&2
  exit 1
fi
git -C "$WORKTREE_PATH" fetch origin "$TRUNK"
git -C "$WORKTREE_PATH" rebase "origin/$TRUNK"
# (apply /mw 4b conflict-resolution table if rebase reports conflicts)

# 3. Land on trunk — reuse /mw step 4c verbatim. Read git.merge_strategy from
#    baldart.config.yml. NEVER `git checkout <trunk>` on the main repo.
#    - pr        → gh pr create + gh pr merge --merge --delete-branch
#    - local-push → git push origin <branch>:$TRUNK (fast-forward only)
#
#    Note: many consumers prefer `local-push` for docs worktrees specifically,
#    because a PRD is rarely worth a GitHub PR roundtrip. The CLI does not
#    differentiate — it respects the project's configured strategy. If the user
#    wants per-skill strategy, they should override via overlay (.baldart/overlays/prd.md).

# 4. Cleanup — reuse /mw step 6 verbatim (worktree remove + branch -d + prune).
#    Skip /mw step 5 (post-merge `npm run build`) — docs worktrees have nothing
#    to build.

# 5. Sync local trunk ref — reuse /mw "Common" block: ff-only pull when main
#    repo HEAD is already $TRUNK; otherwise just fetch.

# 6. Remove the registry entry for this worktree, then release its backlog-ID
#    reservations (best-effort, high-water mark untouched):
#    .claude/skills/worktree-manager/scripts/allocate-id.sh release "$WORKTREE_PATH" 2>/dev/null || true
```

**Forbidden in docs merge**: pre-merge `npm run build`, `npx eslint`, `npx tsc`, `npm run test`, post-merge `npm run build`. The whole point of docs mode is that these gates are inapplicable.

**Failure handling**: if the safety commit, rebase, or merge fails, STOP and
report — do NOT proceed to cleanup. The worktree must stay on disk so the user
can investigate. Cleanup runs only after a successful merge.

---

The merge path depends on `git.merge_strategy` in `baldart.config.yml`:
- `pr` (default, since v3.4.0): merge via `gh pr create` + `gh pr merge`. `prNumber` is the PR number, `mergeCommit` is the SHA returned by the GitHub merge API.
- `local-push` (since v3.9.0): direct fast-forward push of the rebased feature branch to `origin/<git.trunk_branch>` — no PR, no `gh`. `prNumber` is `null`, `mergeCommit` is the local SHA of the feature branch tip.

Neither path runs `git checkout` / `git switch` / `git branch` on the main repo. The orchestrator can branch on `strategy` if it needs PR linking (only meaningful in `pr` mode).

### /cw programmatic

Input: `{ worktreePath: string, force?: boolean }`
Output: `{ removed: boolean }`

---

Registry file: `.worktrees/registry.json` — tracks all active worktrees.

---

## Registry

All worktree operations read/write `.worktrees/registry.json`:

```json
{
  "worktrees": [
    {
      "card": "FEAT-0200",
      "cards": ["FEAT-0200", "FEAT-0201"],
      "groupParent": "FEAT-0200",
      "slug": "menu-ranking",
      "branch": "feat/FEAT-0200-menu-ranking",
      "trunkBranch": "develop",
      "mainRoot": "/absolute/path",
      "path": "/absolute/path/.worktrees/feat-FEAT-0200-menu-ranking",
      "port": 3001,
      "createdAt": "2026-03-09T10:00:00Z",
      "envSyncedAt": "2026-03-09T10:00:00Z",
      "buildVerified": true
    }
  ]
}
```

- `card`: primary card ID (first card or group parent), or `null` for slug-only worktrees
- `cards`: all card IDs sharing this worktree (for batches), or `[]` for slug-only
- `groupParent`: from backlog card `group.parent` field, or `null` for standalone/slug-only
- `trunkBranch`: the resolved `git.trunk_branch` this worktree was created from (R6 — persisted so `/mw`/`/cw` never re-guess or assume `develop`)
- `mainRoot`: absolute path of the main repo root (R6 — persisted so `$MAIN` is read from the registry, not from in-context state that does not survive compaction)
- `buildVerified`: true if `npm run build` passed after setup

Create the file if it doesn't exist. Update it on every `/nw`, `/mw`, `/cw` operation.

---

## ID Allocation — atomic backlog-ID reservation across parallel worktrees

When several PRD / card sessions run in parallel on sibling worktrees, each
branched from the same trunk, a plain `max(^id: PREFIX-) + 1` scan makes them all
land on the **same next integer**: the other session's card is in flight on an
unmerged sibling branch, invisible to both the local backlog and the trunk
merge-base. They collide at rebase/merge time.

`scripts/allocate-id.sh` closes that race **on the same machine**. Every worktree
shares one main repo root (resolved from `git rev-parse --git-common-dir`), and
`.worktrees/` lives there and is gitignored — so it is the natural shared
coordination point, exactly like `registry.json`. The allocator anchors a lock
and a per-prefix high-water-mark there:

```bash
# Reserve the next free integer for a prefix (FEAT, BUG, UI, DOC, PERF, …).
# Prints the integer zero-padded to 4 digits on stdout; diagnostics on stderr.
.claude/skills/worktree-manager/scripts/allocate-id.sh reserve FEAT <slug>

# Release a finished worktree's reservations (best-effort prune; never blocks).
.claude/skills/worktree-manager/scripts/allocate-id.sh release <worktree-path>
```

Shared files, all under `$MAIN/.worktrees/` (already gitignored):
- `.id-alloc.lock/` — cross-process mutex (atomic `mkdir`, stale-stolen after 30s
  via the dir's own mtime; it is a directory, NOT a git ref, so it does not touch
  the shared `refs/stash` that `git stash` in worktrees is forbidden over).
- `.id-hwm-<PREFIX>` — monotonic high-water mark per prefix. **This is the
  correctness anchor**: bumped under the lock, never decremented.
- `id-reservations.jsonl` — append-only `{prefix,id,worktree,slug,ts}` log for
  observability + `release` pruning. NOT load-bearing for correctness.

Inside the lock, `reserve` takes `NEXT = max(high-water file, local backlog, every
sibling worktree's backlog from registry.json, reservations log, trunk
merge-base) + 1`. The `max()` against the real backlog makes it **self-healing**:
if `.id-hwm-*` is ever lost or a card was created bypassing the allocator, the
next reservation still skips occupied numbers. Abandoned reservations leave
**gaps**, which is fine — backlog IDs need not be contiguous, and "reserve the
whole integer, never reuse" is already the rule.

**Scope: same machine only.** The shared file cannot reach separate cloud agents;
the trunk merge-base scan inside `reserve` is the best-effort cross-machine guard.
Callers (`prd-card-writer`, the `prd` agent) invoke `reserve` when the script is
present and **fall back to their inline merge-base scan + `[ID-RACE-RISK]` note**
when it is absent (older installs) — the allocator is additive, never required.

`release` is wired into the cleanup path of `/mw`, `mw-docs`, and `/cw` (see those
steps) so a merged/cleaned worktree's reservations are pruned.

---

## /nw — New Worktree

Supports three modes:
- **Interactive** (user invokes `/nw`): asks for slug (required) and optionally card ID
- **Slug-only** (user invokes `/nw <slug>`): creates a worktree without a backlog card
- **Programmatic** (called by `/new` orchestrator): receives card IDs, group.parent, and branch name

### 1. Pre-flight checks

```bash
# Resolve $MAIN (main repo root) and $TRUNK (git.trunk_branch) up front, and
# persist both onto the registry entry created in step 6 (R6). Every later step
# reads them from the registry with a presence guard — never from in-context state.
MAIN="$(git rev-parse --show-toplevel)"
# Prefer git.trunk_branch; autodetect the repo's real default branch if the
# config key is absent (consumer updated to >=4.0.0 without re-running
# `configure`). Same fallback as nw-docs — never hard-fail on a resolvable repo.
TRUNK=$(grep -A40 '^git:' "$MAIN/baldart.config.yml" 2>/dev/null \
  | grep 'trunk_branch:' | head -1 \
  | sed -E 's/.*trunk_branch:[[:space:]]*([A-Za-z0-9._\/-]+).*/\1/' || true)
if [ -z "$TRUNK" ]; then
  TRUNK=$(git -C "$MAIN" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null \
    | sed -E 's#^refs/remotes/origin/##' || true)
  if [ -z "$TRUNK" ]; then
    for cand in develop main master; do
      if git -C "$MAIN" show-ref --verify --quiet "refs/heads/$cand"; then TRUNK="$cand"; break; fi
    done
  fi
  if [ -z "$TRUNK" ]; then
    echo "ERROR: git.trunk_branch not set in baldart.config.yml and no trunk branch could be autodetected (origin/HEAD / develop / main / master). Run 'npx baldart configure' to set git.trunk_branch, or create the trunk branch first." >&2
    exit 1
  fi
  echo "WARN: git.trunk_branch missing from baldart.config.yml — autodetected '$TRUNK'. Run 'npx baldart configure' to persist it." >&2
fi

# Fetch latest trunk from remote — read-only, does NOT touch the main repo's HEAD.
# We branch the new worktree from `origin/<trunk>` directly (step 3), so we never
# need to checkout the trunk on the main repo. This is the absolute rule:
# /nw must not run `git checkout`, `git switch`, `git checkout -b`, or
# `git branch` on the main repo (it's shared across parallel terminals; many
# consumer projects declare this as a hard rule in CLAUDE.md and the classifier
# will block the call preemptively).
git fetch origin "$TRUNK"
```

(There is no need to check `git status --porcelain` on the main repo here — we
do not touch it. If the user has uncommitted work on the main repo it stays
exactly where it is.)

Check registry for duplicate:
- If a card ID is provided and a worktree for the same card ID already exists, WARN and ask to confirm.
- If a worktree for the same `groupParent` exists, WARN — cards should share the existing worktree.
- If slug-only and a worktree with the same slug already exists, WARN and ask to confirm.

### 2. Determine branch name

**Interactive mode** — ask the user for:
- **Short slug** (e.g. `menu-ranking`) — REQUIRED
- **Backlog card ID** (e.g. `FEAT-0200`, `BUG-0500`) — OPTIONAL

**Slug-only mode** (no card ID):
- Branch name: `feat/<slug>` (e.g. `feat/menu-ranking`)
- Registry `card` field: `null`
- Registry `cards` field: `[]`

**Programmatic mode** (from `/new` orchestrator) — apply grouping logic:
1. Check each card's `group.parent` field in backlog YAML.
2. If all cards share the same `group.parent` → ONE worktree, branch from parent card.
3. If cards have no `group.parent` but share a common ID prefix → suggest grouping.
4. If cards are unrelated → each gets its own worktree.
5. Read parent card's `git_strategy.branch` if set; otherwise derive: `feat/<PARENT-ID>-<slug>`.
6. If `group.parent` is set AND parent card has `git_strategy.branch` → use it directly, no questions.

Branch name: `feat/<CARD-ID>-<slug>` (with card) or `feat/<slug>` (slug-only). Use `hotfix/` prefix if card specifies hotfix.

### 3. Create the worktree

Base the new worktree on `origin/<$TRUNK>` (the remote tip we just fetched in step 1) rather than the local trunk branch — that way we never need the local trunk to be checked out or up to date on the main repo.

The worktree path is **deterministic** (R8): derived from the card ID + kebab-case slug only, never a timestamp — so a resumed run resolves to the same path and the duplicate check in step 1 is meaningful.

```bash
# With card ID:
WORKTREE_PATH=".worktrees/feat-<CARD-ID>-<slug>"
git worktree add "$WORKTREE_PATH" -b "feat/<CARD-ID>-<slug>" "origin/$TRUNK"

# Slug-only (no card):
WORKTREE_PATH=".worktrees/feat-<slug>"
git worktree add "$WORKTREE_PATH" -b "feat/<slug>" "origin/$TRUNK"
```

### 3b. Sync untracked backlog cards from main repo

Backlog cards created during `/prd` sessions may exist as untracked files in the main repo
but are NOT on the trunk branch yet. The worktree (branched from the trunk) won't have them.

```bash
# BACKLOG_DIR = value of paths.backlog_dir from baldart.config.yml (fallback `backlog`
# when the key is absent — emit it resolved, never the literal ${paths.backlog_dir}
# token, which is not valid bash). Same resolution as /new's card-scoped diff block.
BACKLOG_DIR="<value of paths.backlog_dir, or 'backlog' if the key is absent>"
MAIN_ROOT="$(git -C "$WORKTREE_PATH" rev-parse --show-superproject-working-tree 2>/dev/null || pwd)"

# For each card in the batch, check if its YAML exists in the main repo but not in the worktree
for CARD_FILE in $(ls "$MAIN_ROOT/$BACKLOG_DIR"/*.yml 2>/dev/null); do
  BASENAME=$(basename "$CARD_FILE")
  if [ ! -f "$WORKTREE_PATH/$BACKLOG_DIR/$BASENAME" ]; then
    cp "$CARD_FILE" "$WORKTREE_PATH/$BACKLOG_DIR/$BASENAME"
    echo "Synced untracked card: $BASENAME"
  fi
done
```

This prevents "File not found" errors when Phase 4 tries to mark cards as DONE.

### 4. Full isolation setup

Run ALL of these inside the worktree directory:

```bash
cd "$WORKTREE_PATH"

# 1. Own node_modules (full independent install)
npm install

# 2. Own .next cache — already isolated since each worktree has its own
#    directory tree. The default .next inside the worktree is sufficient.

# 3. Copy environment files from main repo root
#    Build requires Firebase env vars — this copy is critical.
MAIN_ROOT="$(git -C .. rev-parse --show-toplevel 2>/dev/null || echo '../..')"
cp "$MAIN_ROOT/.env.local" .env.local 2>/dev/null || true
cp "$MAIN_ROOT/.env" .env 2>/dev/null || true

# 4. Pick a free dev server port (avoid 3000 used by main)
PORT=3001
while lsof -i :$PORT -sTCP:LISTEN >/dev/null 2>&1; do
  PORT=$((PORT + 1))
  if [ $PORT -gt 3099 ]; then echo "ERROR: No free port in 3001-3099" >&2; exit 1; fi
done
# Write PORT to .env.local (append or replace existing PORT line)
if grep -q "^PORT=" .env.local 2>/dev/null; then
  sed -i '' "s/^PORT=.*/PORT=$PORT/" .env.local
else
  echo "PORT=$PORT" >> .env.local
fi

# 5. ASSERT git hooks are ACTIVE — not just readable.
#    The worktree shares core.hooksPath via .git/commondir, so the same
#    resolution applies here as in the main checkout. Reading the value
#    (the old `git config core.hooksPath || true`) is NOT enough: it never
#    catches the failure mode where core.hooksPath points at an empty dir
#    while the repo's versioned hooks live elsewhere (pre-commit / pre-push
#    silently inactive). Detect the versioned-hooks dir and assert it is served.
HOOK_SRC=""
for d in .husky .githooks scripts/git-hooks githooks; do
  if [ -d "$d" ] && ls "$d" 2>/dev/null | grep -qE '^(pre-commit|pre-push|commit-msg)$'; then
    HOOK_SRC="$d"; break
  fi
done
if [ -n "$HOOK_SRC" ]; then
  ACTIVE="$(git config --get core.hooksPath || echo .git/hooks)"
  # Lean literal-string match (HOOK_SRC is relative). Full path resolution +
  # canonicalization lives in `baldart doctor` (src/utils/githooks.js); an
  # absolute core.hooksPath here would warn spuriously — rare, run doctor to confirm.
  case "$ACTIVE" in
    "$HOOK_SRC"|"$HOOK_SRC"/*|.husky/_) : ;;  # active dir serves the versioned hooks (incl. husky v9 .husky/_)
    *)
      echo "⚠️  WARNING: git hooks INACTIVE — the versioned hooks live in '$HOOK_SRC' but core.hooksPath = '$ACTIVE'." >&2
      echo "    Re-activate them with:  git config core.hooksPath $HOOK_SRC" >&2
      ;;
  esac
fi
```

### 5. Verify baseline

```bash
# TypeScript + lint (fast)
npx tsc --noEmit
npx eslint --max-warnings=0 src/

# Full build verification (required — confirms worktree is functional)
npm run build
```

If build fails → STOP and report. Do NOT continue — the worktree is broken.
If only tsc/lint fails → report but continue (the trunk branch should be clean, may be a transient issue).

### 6. Update registry

Add entry to `.worktrees/registry.json` with all fields: card, cards, groupParent, slug, branch, `trunkBranch` (the resolved `$TRUNK`), `mainRoot` (the resolved `$MAIN`), absolute path, port, `createdAt`, `envSyncedAt`, and `buildVerified`. Persisting `trunkBranch` + `mainRoot` (R6) lets `/mw`/`/cw` read them back with a presence guard instead of re-deriving or assuming `develop`.

### 7. Auto-start dev server (if `--dev` flag or `/nw --dev`)

If the user passed `--dev` (e.g., `/nw --dev FEAT-0500 menu-fix`), start the dev server in background:

```bash
cd "$WORKTREE_PATH"
nohup npm run dev > .next/dev.log 2>&1 &
DEV_PID=$!
# Wait a few seconds and verify it's running
sleep 3
if kill -0 $DEV_PID 2>/dev/null; then
  echo "Dev server started (PID: $DEV_PID) on port $PORT"
else
  echo "WARNING: Dev server failed to start. Check .next/dev.log" >&2
fi
```

### 8. Report to user

```
Worktree ready:
  Path:   <full-absolute-path>
  Branch: feat/<CARD-ID>-<slug> (or feat/<slug> if no card)
  Port:   <PORT>
  Card:   <CARD-ID> (or "none" if slug-only)
  Build:  verified ✓
  Dev:    http://localhost:<PORT> (PID: <DEV_PID>)  ← only if --dev

Start dev server: cd <path> && npm run dev  ← only if NOT --dev
The worktree has its own node_modules and .next cache — fully independent.
```

---

## /mw — Merge Worktree

Supports two modes:
- **Interactive** (user invokes `/mw`): full flow with PR creation + merge + cleanup
- **Programmatic** (called by `/new` orchestrator): can be invoked with specific worktree path and skip-checks flag if checks already passed in orchestrator pipeline

### 1. Identify worktree

Read `.worktrees/registry.json`. If multiple entries exist or user didn't specify, list them and ask which to merge.

Fallback: `git worktree list` if registry is missing.

Determine, reading them from the worktree's registry entry (R6):
- `WORKTREE_PATH`, `BRANCH`, `CARD` (or `CARDS` for batch worktrees)
- `TRUNK` (the entry's `trunkBranch`) and `MAIN` (the entry's `mainRoot`)

These were persisted at `/nw` time. Before any step below, presence-guard them — if `WORKTREE_PATH`, `TRUNK`, or `MAIN` is empty, HALT with a clear message (never assume `develop` or re-derive `$MAIN` silently):

```bash
if [ -z "$WORKTREE_PATH" ] || [ -z "$BRANCH" ] || [ -z "$TRUNK" ] || [ -z "$MAIN" ]; then
  echo "ERROR: missing WORKTREE_PATH/BRANCH/TRUNK/MAIN — read them from the registry entry (fields path/branch/trunkBranch/mainRoot)." >&2
  exit 1
fi
```

### 2. Env sync check

Compare main repo `.env.local` modification time with registry `envSyncedAt`.
If main `.env.local` is newer, WARN:

```
Warning: Main repo .env.local has changed since this worktree was created.
Consider updating the worktree's .env.local before merging.
```

### 3. Pre-merge safety commit + checks inside the worktree

**CRITICAL — Safety commit (MUST run before anything else)**:
Files lost after rebase are unrecoverable. ALWAYS commit everything before proceeding.

```bash
cd "$WORKTREE_PATH"

# 1. Check for uncommitted changes (staged, unstaged, and untracked)
CHANGED=$(git diff --name-only HEAD 2>/dev/null)
UNTRACKED=$(git ls-files --others --exclude-standard 2>/dev/null)

if [ -n "$CHANGED" ] || [ -n "$UNTRACKED" ]; then
  echo "⚠️ Uncommitted files detected — creating safety commit before merge"

  # Stage all changed files by EXPLICIT name (never git add -A)
  for f in $CHANGED $UNTRACKED; do
    git add "$f"
  done

  # Verify what we staged
  git diff --staged --name-only

  # Safety commit
  git commit -m "[safety] Auto-commit uncommitted files before merge

Files were uncommitted when /mw started. This safety commit
prevents file loss during rebase. Squash-merge will fold this
into the final commit on the trunk branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"

  echo "✅ Safety commit created — all files preserved"
fi

# 2. Run quality checks
# Lint only changed files (vs the trunk branch)
npx eslint --max-warnings=0 $(git diff --name-only "origin/$TRUNK" -- '*.ts' '*.tsx')
npx tsc --noEmit

# Full build (required before PR per AGENTS.md)
npm run build

# Tests
npm run test 2>/dev/null || true
```

If lint, tsc, or build fails → report and STOP. Do NOT proceed to PR.

**Programmatic mode**: If the orchestrator already ran these checks (Phase 2 self-healing + Phase 3.5 QA), it may pass `checksAlreadyPassed: true` to skip re-running quality checks. The safety commit step ALWAYS runs regardless — it is never skippable.

### 4. Commit & push

**Interactive mode**: Stage and commit all pending changes.

```bash
cd "$WORKTREE_PATH"

# Stage only changed files (explicit names, NEVER git add . or git add -A)
git add <list-changed-files-explicitly>

# Verify staged files
git diff --staged --name-only

# Commit directly — NO stash in worktrees (stashes are globally shared via refs/stash)
git commit -m "[<CARD-ID>] <description>"
```

**Programmatic mode**: The orchestrator handles per-card commits in Phase 4. `/mw` only needs to rebase and push (see steps 4b and 4c below).

### 4b. Rebase onto latest trunk (MUST — prevents PR merge conflicts)

Before pushing, ALWAYS rebase the feature branch onto the latest trunk branch to avoid merge conflicts at PR merge time. The trunk branch is `git.trunk_branch` from `baldart.config.yml` (resolved below).

```bash
# Presence guard (R6): $MAIN and $WORKTREE_PATH MUST already be set + non-empty
# (written to the registry at /nw time; re-read from there if absent). Never
# proceed with an undefined $MAIN — it would resolve git operations against the
# wrong repo.
if [ -z "$MAIN" ] || [ -z "$WORKTREE_PATH" ]; then
  echo "ERROR: \$MAIN and/or \$WORKTREE_PATH unset — read them from .worktrees/registry.json before step 4b." >&2
  exit 1
fi

# Resolve the trunk branch from config (default: read by the caller; no bare
# 'develop' literal). TRUNK MUST be non-empty.
if [ -z "$TRUNK" ]; then
  echo "ERROR: \$TRUNK (git.trunk_branch) unset — resolve it from baldart.config.yml before step 4b." >&2
  exit 1
fi

cd "$WORKTREE_PATH"

# 1. NO `git stash` in a worktree (refs/stash is shared across all worktrees via
#    git.commondir — see FEAT-0522 incident + Safety Rules). The safety commit in
#    step 3 already guarantees a clean working tree. If the tree is unexpectedly
#    dirty here (someone wrote between step 3 and step 4b), STOP and report — the
#    right recovery is a SECOND safety commit, never a stash.
DIRTY=$(git status --porcelain 2>/dev/null)
if [ -n "$DIRTY" ]; then
  echo "ERROR: working tree dirty in $WORKTREE_PATH after the step-3 safety commit." >&2
  echo "Files: $DIRTY" >&2
  echo "Make a second safety commit (explicit file names) — NEVER stash (refs/stash shared across worktrees)." >&2
  exit 1
fi

# 2. Fetch latest trunk
git fetch origin "$TRUNK"

# 3. Rebase onto origin/<trunk>
git rebase "origin/$TRUNK" 2>&1
REBASE_EXIT=$?
```

**If rebase succeeds** (exit 0): continue straight to push — there is no stash to restore.

**If rebase fails with conflicts**: apply the auto-resolution protocol below.

#### Conflict auto-resolution protocol

```bash
CONFLICTED=$(git diff --name-only --diff-filter=U)
```

Categorize each conflicted file and resolve. **Per-file rule of thumb**:
auto-resolve only when "keep both sides" is semantically additive (markdown
bullet lists, YAML lists, JSONL append-only logs); abort on anything with
internal structure (tables, key/value blocks, code).

| Category | Patterns | Action | Note |
|----------|----------|--------|------|
| **Structured registries** | `project-status.md`, `ssot-registry.md`, `field-registry.*`, `traceability-matrix.md`, `REGISTRY.md` | Strip markers, then **structural validation** — if valid, accept; if invalid, abort | The auto-strip "keep both sides" usually produces an additive result (new bullet rows appended to a table or section). The validation step catches the rare case where the two sides modified the same row. See `validate_structured_md` below. |
| **JSONL logs under `${paths.metrics}/`** | `${paths.metrics}/*.jsonl`, `${paths.metrics}/**/*.jsonl` | Auto-resolve: strip markers, keep both sides | JSONL is line-oriented and additive — duplicating both branches' lines yields a valid file. (`paths.metrics` default `docs/metrics`.) |
| **Other `.jsonl`** | `*.jsonl` outside `${paths.metrics}/` | STOP — abort and report | Fixtures, seeds, training data: auto-strip can corrupt them. The user must inspect. |
| **Docs/config** | `${paths.docs_dir}/**/*.md`, `${paths.backlog_dir}/*.yml`, `*.md` outside `src/`, `*.json`/`*.yml` outside `src/` (excluding rows above) | Auto-resolve: strip markers, keep both sides | The canonical additive case. |
| **Code** | `src/**/*.ts`, `*.tsx`, `*.js`, `*.jsx` | STOP — abort rebase, report to user | |
| **Tests** | `*.test.*`, `*.spec.*` | Manual only — report to user | |

The "Structured registries" row replaces the v3.22.0-initial blanket-abort
behaviour. The blanket abort regressed the `/new` flow (every multi-card batch
that updates `ssot-registry.md` would fail rebase); the validation-based
approach restores the additive merge for the routine case while still
catching genuine corruption.

```bash
# Helper: validate that a structured markdown file is still parseable after
# auto-strip. Cheap heuristics; returns 0 if file looks structurally intact.
validate_structured_md() {
  local f="$1"
  # Reject if any residual conflict markers (shouldn't happen after sed)
  if grep -qE '^(<{7}|={7}|>{7})' "$f"; then return 1; fi
  # Reject if a top-level markdown table got duplicated header rows back-to-back
  # (two `|---|---|` separator lines in a row = malformed table).
  if awk 'prev~/^\|[ -:|]+\|$/ && $0~/^\|[ -:|]+\|$/{found=1; exit} {prev=$0} END{exit !found}' "$f"; then
    return 1
  fi
  # Reject if a YAML frontmatter block (--- ... ---) got duplicated.
  if [ "$(grep -cE '^---[[:space:]]*$' "$f")" -gt 2 ]; then return 1; fi
  return 0
}

for FILE in $CONFLICTED; do
  BASENAME=$(basename "$FILE")

  # 1. Code / tests — always abort
  case "$FILE" in
    src/*.ts|src/*.tsx|src/*.js|src/*.jsx|*.test.*|*.spec.*)
      git rebase --abort
      # No stash to restore: step 3's safety commit leaves the tree clean, and
      # step 4b NEVER stashes (refs/stash is shared across worktrees).
      echo "CONFLICT in $FILE — cannot auto-resolve" >&2
      exit 1 ;;
  esac

  # 2. Structured registries — strip + structural validation
  case "$BASENAME" in
    project-status.md|ssot-registry.md|traceability-matrix.md|REGISTRY.md)
      sed -i '' '/^<<<<<<< /d; /^=======/d; /^>>>>>>> /d' "$FILE"
      if ! validate_structured_md "$FILE"; then
        git checkout --conflict=merge "$FILE" 2>/dev/null || true
        git rebase --abort
        echo "CONFLICT in structured registry $FILE produced invalid structure after auto-strip — manual resolution required." >&2
        exit 1
      fi
      git add "$FILE"
      continue ;;
  esac
  case "$BASENAME" in
    field-registry.*)
      # field-registry.json typically — strict JSON, can't auto-strip safely
      git rebase --abort
      echo "CONFLICT in structured registry $FILE — JSON cannot be auto-stripped." >&2
      exit 1 ;;
  esac

  # 3. JSONL — only auto-resolve under ${paths.metrics}/
  case "$FILE" in
    ${paths.metrics}/*.jsonl|${paths.metrics}/**/*.jsonl)
      sed -i '' '/^<<<<<<< /d; /^=======/d; /^>>>>>>> /d' "$FILE"
      git add "$FILE"
      continue ;;
    *.jsonl)
      git rebase --abort
      echo "CONFLICT in $FILE (.jsonl outside ${paths.metrics}/ — may be fixture/seed) — manual resolution required." >&2
      exit 1 ;;
  esac

  # 4. Generic docs/config — additive merge
  case "$FILE" in
    ${paths.docs_dir}/*|${paths.backlog_dir}/*|*.md|*.json|*.yml)
      sed -i '' '/^<<<<<<< /d; /^=======/d; /^>>>>>>> /d' "$FILE"
      git add "$FILE" ;;
  esac
done

# Continue rebase + verify build (no stash to restore — step 4b never stashes)
git rebase --continue
npm run build
```

If build fails after rebase → STOP and report. The rebase introduced incompatibilities.

### 4c. Land the feature branch onto the trunk branch (strategy-branched)

**IMPORTANT — NO local checkout/switch/branch on the main repo, regardless of strategy.** Consumer projects share the main repo as a parallel resource across multiple terminals; running `git checkout <trunk>` on it preempts whatever the other terminals are doing and breaks worktree-driven workflows. Both strategies below operate from the worktree and the main repo's `HEAD` is never touched.

Read `git.merge_strategy` from `baldart.config.yml` (default: `pr`):

```bash
# $MAIN and $TRUNK were resolved in step 1 (read from the registry entry, R6) and
# presence-guarded. Fall back to deriving $MAIN from git-common-dir ONLY if it is
# still unset — never silently re-derive over a value the registry already supplied.
if [ -z "$MAIN" ]; then
  MAIN=$(git -C "$WORKTREE_PATH" rev-parse --git-common-dir)/..
  MAIN=$(cd "$MAIN" && pwd)
fi

# Resolve strategy from baldart.config.yml (default: pr)
STRATEGY=$(grep -A40 '^git:' "$MAIN/baldart.config.yml" 2>/dev/null \
  | grep 'merge_strategy:' | head -1 \
  | sed -E 's/.*merge_strategy:[[:space:]]*([a-z-]+).*/\1/' || echo "pr")
STRATEGY=${STRATEGY:-pr}
```

If neither `pr` nor `local-push` resolves, ASK the user once which to use and suggest persisting the choice (per the always-ask, never-assume contract in `framework/agents/project-context.md`).

---

#### Strategy A — `pr` (default, GitHub PR via `gh`)

Use this when the trunk branch is protected on origin (required status checks, reviews, merge queue) — a direct push would be rejected.

```bash
# 1. Push the feature branch to origin (force-with-lease in case of rebase in 4b).
git -C "$WORKTREE_PATH" push --force-with-lease origin "$BRANCH"

# 2. Open / find the PR.
PR_NUMBER=$(gh -R "$(gh repo view --json nameWithOwner -q .nameWithOwner)" pr list \
  --head "$BRANCH" --state open --json number -q '.[0].number' 2>/dev/null || true)

if [ -z "$PR_NUMBER" ]; then
  PR_URL=$(gh pr create \
    --base "$TRUNK" \
    --head "$BRANCH" \
    --title "[<CARD-ID>] $(echo "$BRANCH" | sed 's|.*/||')" \
    --body "Auto-merged by /mw after final review + QA passed." \
    --repo "$(gh repo view --json nameWithOwner -q .nameWithOwner)")
  PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
fi
echo "PR #$PR_NUMBER ready to merge."

# 3. Merge via gh CLI. --delete-branch removes the remote branch on success.
MERGE_OUTPUT=$(gh pr merge "$PR_NUMBER" --merge --delete-branch 2>&1) || MERGE_RC=$?
echo "$MERGE_OUTPUT"

# 3b. Fallback: if `gh pr merge` failed AND the PR is not yet merged, fall back
#     to the REST API (works even when the CLI errors on local cleanup).
PR_STATE=$(gh pr view "$PR_NUMBER" --json state -q .state)
if [ "$PR_STATE" != "MERGED" ]; then
  echo "Falling back to REST API merge..."
  REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
  gh api -X PUT "repos/$REPO/pulls/$PR_NUMBER/merge" \
    -f merge_method=merge \
    -f commit_title="[<CARD-ID>] Merge $BRANCH into $TRUNK"
  PR_STATE=$(gh pr view "$PR_NUMBER" --json state -q .state)
fi

if [ "$PR_STATE" != "MERGED" ]; then
  echo "❌ PR #$PR_NUMBER did not merge. Inspect manually: $PR_URL"
  exit 1
fi
echo "✅ PR #$PR_NUMBER merged to $TRUNK."
```

If `gh` is not installed in the project, STOP and ask the user to install GitHub CLI (`brew install gh` or equivalent) and run `gh auth login`. Do NOT fall back to a local checkout — it violates terminal isolation.

If both gh CLI and REST API merge fail → STOP and report. Suggest switching to `merge_strategy: local-push` only if the trunk branch is NOT protected on origin.

---

#### Strategy B — `local-push` (direct FF push, no PR)

Use this when the trunk branch is NOT protected on origin and the user wants to skip the GitHub PR roundtrip. After step 4b the feature branch is rebased onto `origin/$TRUNK`, so it can fast-forward the trunk on origin in a single push.

```bash
# 1. Sanity: confirm the feature branch is strictly ahead of origin/$TRUNK
#    (i.e. the rebase in step 4b succeeded and we can FF the trunk on origin).
git -C "$WORKTREE_PATH" fetch origin "$TRUNK" --quiet
AHEAD=$(git -C "$WORKTREE_PATH" rev-list --count "origin/$TRUNK..$BRANCH")
BEHIND=$(git -C "$WORKTREE_PATH" rev-list --count "$BRANCH..origin/$TRUNK")
if [ "$BEHIND" -ne 0 ]; then
  echo "❌ local-push aborted: $BRANCH is $BEHIND commit(s) behind origin/$TRUNK." >&2
  echo "   Re-run step 4b (rebase onto origin/$TRUNK), then retry." >&2
  exit 1
fi
if [ "$AHEAD" -eq 0 ]; then
  echo "ℹ️ Nothing to push — $BRANCH is identical to origin/$TRUNK. Skipping."
else
  echo "Fast-forwarding origin/$TRUNK by $AHEAD commit(s) via local-push…"
fi

# 2. Direct fast-forward push to the trunk on origin. No PR.
#    If the trunk is protected on origin, this fails — surface the hint and stop.
if ! PUSH_OUTPUT=$(git -C "$WORKTREE_PATH" push origin "$BRANCH:$TRUNK" 2>&1); then
  echo "$PUSH_OUTPUT"
  if echo "$PUSH_OUTPUT" | grep -qiE "protected branch|required status|review required"; then
    echo "❌ local-push rejected: '$TRUNK' is protected on origin." >&2
    echo "   Switch baldart.config.yml → git.merge_strategy: pr, or remove the protection." >&2
  fi
  exit 1
fi
echo "✅ $BRANCH merged into $TRUNK on origin (fast-forward, no PR)."

# 3. Delete the remote feature branch (best-effort — never fatal).
git -C "$WORKTREE_PATH" push origin --delete "$BRANCH" 2>/dev/null || true

MERGE_SHA=$(git -C "$WORKTREE_PATH" rev-parse HEAD)
PR_NUMBER=""
```

---

#### Common — sync local trunk ref (both strategies)

```bash
# Sync the local trunk ($TRUNK) ONLY if the main repo's HEAD is already on it.
# NEVER `git checkout "$TRUNK"` here. If the user (or another terminal) had the
# trunk checked out, ff-only updates it cleanly. Otherwise we leave their HEAD
# alone and they'll pull next time. $TRUNK and $MAIN were resolved + presence-
# guarded in step 1 (read from the registry, R6).
CURRENT_BRANCH=$(git -C "$MAIN" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
if [ "$CURRENT_BRANCH" = "$TRUNK" ]; then
  if git -C "$MAIN" pull --ff-only origin "$TRUNK" 2>/dev/null; then
    echo "✅ Main repo $TRUNK fast-forwarded to origin/$TRUNK."
  else
    # FF blocked. NEVER leave this as a passive "Leaving as-is" TODO for the user
    # (v3.39.0 — the finalizer MUST conclude). The cause is almost always an
    # uncommitted append to the framework-owned telemetry log left by a prior run.
    # Partition the dirty tree:
    #   - framework-owned artifacts → auto-reconcile (commit + rebase) so the ff
    #     completes with zero data loss. Two sub-classes:
    #       (a) telemetry: ${paths.metrics}/skill-runs.jsonl
    #       (b) BALDART-managed install output left by `baldart update`/`add`/`push`:
    #           .baldart/generated/ (overlay-merged agents/commands), .baldart/state.json
    #           (version ledger), .baldart/skill-conflicts.json — these ARE meant to be
    #           committed; a reinstall just leaves them uncommitted until the next sync.
    #           .baldart/overlays/ is NOT owned here — an overlay edit is genuine work.
    #   - ANY foreign file → emit a decision marker the orchestrator MUST convert
    #     into ONE explicit user gate. Never auto-commit work this run did not own.
    METRICS_LOG="${paths.metrics}/skill-runs.jsonl"   # paths.metrics default: docs/metrics
    DIRTY=$(git -C "$MAIN" status --porcelain --untracked-files=no | sed 's/^...//')
    FOREIGN=""
    OWNED=""
    for f in $DIRTY; do
      case "$f" in
        "$METRICS_LOG") OWNED="$OWNED $f" ;;
        .baldart/generated/*|.baldart/state.json|.baldart/skill-conflicts.json) OWNED="$OWNED $f" ;;
        *) FOREIGN="$FOREIGN $f" ;;
      esac
    done
    if [ -n "$FOREIGN" ]; then
      echo "[SYNC-NEEDS-DECISION] main repo $TRUNK cannot fast-forward; foreign uncommitted files:${FOREIGN} — orchestrator MUST raise ONE explicit user gate (commit / user-handles), never a passive note."
    elif [ -n "$OWNED" ]; then
      # Only framework-owned artifacts (telemetry + BALDART-managed install output) are
      # dirty → reconcile autonomously, lossless.
      git -C "$MAIN" add $OWNED
      git -C "$MAIN" commit -m "chore(baldart): reconcile framework-managed artifacts before $TRUNK sync" --quiet
      if git -C "$MAIN" pull --rebase origin "$TRUNK" 2>/dev/null; then
        git -C "$MAIN" push origin "$TRUNK" 2>/dev/null \
          && echo "✅ Reconciled framework-managed artifacts + synced $TRUNK to origin." \
          || echo "✅ Reconciled framework-managed artifacts (local $TRUNK 1 commit ahead; syncs on next push). Tree clean."
      else
        git -C "$MAIN" rebase --abort 2>/dev/null || true
        echo "[SYNC-NEEDS-DECISION] framework-managed reconcile rebase conflict on $TRUNK — orchestrator MUST raise ONE explicit user gate."
      fi
    else
      echo "[SYNC-NEEDS-DECISION] main repo $TRUNK cannot fast-forward and the working tree is clean (diverged?) — orchestrator MUST raise ONE explicit user gate."
    fi
  fi
else
  echo "ℹ️ Main repo HEAD is '$CURRENT_BRANCH' — not switching. Next manual \`git pull\` on $TRUNK will sync."
  git -C "$MAIN" fetch origin "$TRUNK" --quiet
  # Structured marker for upstream orchestrators (e.g. /new Phase 6c).
  # `/mw` standalone keeps fetch-only behavior (terminal isolation); orchestrators
  # parse this marker and route reconciliation through an explicit user gate.
  echo "[SYNC-DEFERRED] main repo HEAD=${CURRENT_BRANCH}, local $TRUNK not fast-forwarded — orchestrator workspace-hygiene phase must reconcile."
fi
```

### 5. Post-merge verification

```bash
npx tsc --noEmit
npm run build
```

If post-merge build fails → STOP and report. Do NOT cleanup worktree (may need to investigate).

### 6. Cleanup worktree

Only after post-merge verification passes:

Run each command INDIVIDUALLY (do NOT chain with `&&` or `;` — permission systems
may block destructive commands when chained):

```bash
# 1. Remove the worktree (this also unlocks the branch from worktree use)
git worktree remove "$WORKTREE_PATH" --force
```

```bash
# 2. Delete the local branch — use -d (safe delete, branch is already merged)
#    NOT -D (force delete) which gets blocked by permission systems
git branch -d "$BRANCH"
```

```bash
# 3. Delete the remote branch. The branch is ALREADY MERGED into the trunk at this
#    point, so this is routine cleanup — NOT the gated "branch deletion" that needs
#    owner approval (see AGENTS.md § branch deletion). Report the outcome; never
#    degrade a failure into a passive "you can delete it by hand" note handed to the user.
if git push origin --delete "$BRANCH" 2>/dev/null; then
  echo "✅ Remote branch '$BRANCH' deleted (merged → routine cleanup)."
else
  echo "ℹ️ Remote branch '$BRANCH' not deleted (already gone, or the deletion was denied)."
  echo "   If denied: this is a merged branch — retry the delete; only if it is STILL"
  echo "   denied must the orchestrator raise ONE explicit user gate, never a passive TODO."
fi
```

```bash
# 4. Prune stale references
git worktree prune
```

### 7. Update registry

Remove the entry from `.worktrees/registry.json`.

Also release this worktree's backlog-ID reservations (best-effort; the high-water
mark stays untouched so numbering remains monotonic):

```bash
.claude/skills/worktree-manager/scripts/allocate-id.sh release "$WORKTREE_PATH" 2>/dev/null || true
```

### 8. Report

```
Merge complete:
  Card:     <CARD-ID>
  Branch:   <BRANCH> (deleted on remote — merged branch deletion is routine cleanup, never gated)
  Strategy: <pr | local-push>
  PR:       #<PR_NUMBER> merged via `gh pr merge`    ← only in `pr` strategy
  Push:     fast-forward to origin/<trunk>           ← only in `local-push` strategy
  Worktree: <WORKTREE_PATH> (removed)
  <trunk>:  remote up to date; local main-repo synced (auto-reconciled framework
            telemetry if it blocked the ff). Any [SYNC-NEEDS-DECISION] /
            [SYNC-DEFERRED] marker above is for the orchestrator to gate on —
            NOT a residue to hand the user as a manual TODO.
```

---

## /lw — List Worktrees

Show status of all active worktrees. Read `.worktrees/registry.json` and cross-reference with `git worktree list`.

For each worktree, display:

```
Active worktrees:

  1. FEAT-0200 menu-ranking (batch: FEAT-0200, FEAT-0201)
     Branch: feat/FEAT-0200-menu-ranking
     Path:   /abs/path/.worktrees/feat-FEAT-0200-menu-ranking
     Port:   3001
     Age:    2 days
     Status: 3 uncommitted changes
     Build:  verified ✓
     Env:    in sync | STALE (main .env.local changed)

  2. BUG-0500 fix-auth
     Branch: feat/BUG-0500-fix-auth
     Path:   /abs/path/.worktrees/feat-BUG-0500-fix-auth
     Port:   3002
     Age:    8 days (stale — consider merging or cleaning up)
     Status: clean
     Build:  verified ✓
     Env:    in sync
```

**Stale detection**: Worktrees older than 7 days get a warning.
**Merged detection**: If the branch has been merged into the trunk branch — read each entry's `trunkBranch`, e.g. `git branch -a --merged "$TRUNK"` — flag as "already merged — safe to /cw".
**Orphan detection**: Worktrees in `git worktree list` but NOT in registry get flagged as "unregistered".

---

## /cw — Cleanup Worktrees

Remove worktrees without the full PR/merge flow. Use for:
- Worktrees whose branches were already merged
- Abandoned worktrees
- Worktrees created outside this skill

### 1. Scan

```bash
git worktree list
```

Cross-reference with registry. For each worktree, check (using its `trunkBranch`):
- Is the branch merged into the trunk branch? → "safe to remove"
- Is the branch pushed to remote? → "has remote backup"
- Are there uncommitted changes? → "has uncommitted work — DANGEROUS"

### 2. Present options

```
Worktrees available for cleanup:

  1. feat/FEAT-0200-menu-ranking — merged into trunk ✓ safe
  2. feat/BUG-0300-old-fix — not merged, pushed to remote
  3. feat/FEAT-0100-abandoned — not merged, uncommitted changes ✗ DANGEROUS

Remove which? (comma-separated numbers, or "all-safe" for merged-only)
```

### 3. Cleanup selected

For each selected worktree:

```bash
git worktree remove "<path>" --force
```

Also delete the remote branch if it exists:

```bash
git push origin --delete "<branch-name>" 2>/dev/null || true
```

If the worktree has uncommitted changes and user confirmed removal, proceed.
If user did NOT confirm, SKIP and warn.

After all removals:

```bash
git worktree prune
```

### 4. Update registry

Remove cleaned entries from `.worktrees/registry.json`. For each removed
worktree, also release its backlog-ID reservations (best-effort):

```bash
.claude/skills/worktree-manager/scripts/allocate-id.sh release "<removed-worktree-path>" 2>/dev/null || true
```

### 5. Report

```
Cleanup complete:
  Removed: feat/FEAT-0200-menu-ranking, feat/BUG-0300-old-fix
  Skipped: feat/FEAT-0100-abandoned (uncommitted changes)
  Registry: updated
```

---

## Safety Rules

- **NEVER `git checkout`, `git switch`, `git checkout -b`, or `git branch` on the main repo from inside `/mw` / `/nw` / `/cw`.** The main repo is a shared resource across parallel terminals; touching its `HEAD` preempts other agents and breaks worktree-driven workflows. Many consumer projects declare this as an absolute rule in CLAUDE.md and Claude Code's classifier will pattern-match and block these commands preemptively. Use the configured `git.merge_strategy` for trunk-branch merges and `git -C <main> pull --ff-only` (only when `HEAD = git.trunk_branch` already) for sync. See `/mw` step 4c.
- NEVER merge to a production/release branch — only to the configured trunk branch (`git.trunk_branch`), via the configured `git.merge_strategy` (`pr` → `gh pr merge`; `local-push` → `git push origin <feat>:<git.trunk_branch>`). NEVER via local checkout.
- NEVER force push from a worktree (`--force-with-lease` on the feature branch after rebase is the only allowed force variant).
- NEVER `git add .` or `git add -A` — always explicit file names.
- NEVER remove a worktree with uncommitted changes without explicit user confirmation.
- NEVER delete a branch before successful merge verification.
- NEVER cleanup a worktree before confirming the trunk branch is stable post-merge.
- If build/lint/tsc fails during /mw, STOP and report. Do not skip checks.
- If merge conflicts during /mw, STOP and ask the user. Do not auto-resolve.
- Always verify `.worktrees/` is in `.gitignore` before creating it (the nw-docs pre-flight in step 0 enforces this; add it before running if absent).
- Commit lock protocol: Each worktree has its own `COMMIT_LOCK` in its git dir — no cross-worktree interference.
- Port persistence: On dev server restart, reuse the port from registry (grep .env.local for PORT=) instead of picking a new one.
- **NEVER use `git stash` in worktrees.** Stashes are globally shared across all worktrees (`refs/stash` via `git.commondir`). A stash created in one worktree or in the main repo can be popped in another, causing conflicts, data loss, and cascading merge failures (see FEAT-0522 incident). Use explicit file staging only — the file ownership map ensures no overlap between cards. The stash pattern in AGENTS.md/CLAUDE.md applies ONLY to the main repo working tree.
