---
name: finishing-a-development-branch
description: Use when development is complete and verified — lands the feature branch (merge-to-main or open-a-PR, per project) and cleans up the worktree via the tested shared brain. Resolves conflicts by agent judgment; NEVER prompts the user.
---

# Finishing a Development Branch

audience: AI coding agents first. Imperative, BLUF.

MUST follow `/home/user/Projects/0 DOCS/GIT_FATIGUE.md` §12. Delivery controller owns workspace, validation, publication, and cleanup. Agents MUST NOT run raw publication commands or surface source-control/CI choices.

Land a verified branch by driving the TESTED shared brain — never by hand-typing git. The agent
resolves every conflict by judgment (the rubric); the user is NEVER involved in git. Land mode
(merge-to-main vs open-a-PR) is FROZEN per project, not decided here.

## Land it — one command, never raw git

```bash
bash <PROJECT_ROOT>/.claude/scripts/ship.sh land <branch> <worktree>
```

This is the ONLY way to land. DO NOT hand-type `git checkout main && git merge … && git push && git
worktree remove …` — the wrapper drives the tested primitives (`~/.claude/workflows/lib/finish-branch.sh`)
which already encode every footgun: cwd-correct `git -C`, receipt-backed publication, and ordered cleanup
(worktree-remove BEFORE branch-delete). Re-typing the cascade re-opens every bug below.

### Read the exit code — ladder, stop at the rung that fires

- **exit 0** → published + cleaned. Emit only `Preview ready`. Proceed to `learn-from-mistakes`.
- **exit 20** → AGENT ACTION NEEDED. The last stdout line is JSON with `stage`/`next`/`detail`. Act,
  then RE-RUN the exact same command (re-entry is safe — stages are idempotent):
  - `stage:"preflight"` → main is not clean. Clear each `blockers[].recovery` (dirty main = inspect:
    src/ contamination → `git -C <main> checkout -- <file>`; coherent WIP → stash/commit. pending
    merge → complete or `merge --abort`). Re-run.
  - `stage:"sync-base"` `status:"conflict"` → resolve per the rubric (below), commit in the worktree,
    re-run.
  - `stage:"deps-refresh"` → the worktree dep reconcile failed (a base sync bumped the lockfile and the
    install errored). Fix deps in the worktree (read `detail`), re-run.
  - `stage:"land-merge"` (tests-failed / conflict-markers / dirty-tree / not-ff) → fix in the
    worktree. Read current-tree receipt/log first; invalidated receipt → designated executor runs one replacement gate after all fixes. Re-run controller.
  - `stage:"cleanup"` `status:"assets-present"` → expensive gitignored assets live ONLY in the
    worktree (removal is irreversible). `cp` anything worth keeping to `<main>/tmp`, then re-run WITH
    the override: `ship.sh land <branch> <worktree> --assets-ok`.
- **exit 3** → drift: the project's frozen facts changed (base gone, deploy mechanism moved). Re-init
  the wrapper (re-sniff), then land again.

## Resolving conflicts — the agent decides, never the user

Follow `~/.claude/workflows/lib/merge-conflict-rubric.md` EXACTLY. Core: integrate both by default;
delete a side only after proving it carries zero unique behavior; on accidental double-work the
better version wins (tests/completeness, NOT "newer"); green tests over uncovered lines → say
"resolved but unverified at `<file>:<line>`".

- **NEVER** blanket `git merge -X theirs` or `git checkout --theirs -- <file>` to clear a conflict —
  the silent-loss move the rubric exists to kill.
- **NEVER** escalate a conflict to the user.

## No project wrapper yet? GENERATE one (the right way)

`<PROJECT_ROOT>/.claude/scripts/ship.sh` absent → run the generator; it sniffs the facts and
freezes them into the wrapper. Two phases (fail-closed on the land-mode call):

```bash
bash ~/.claude/workflows/lib/ship-init.sh <PROJECT_ROOT>                 # Phase A: PROPOSE (exit 10), writes nothing
bash ~/.claude/workflows/lib/ship-init.sh <PROJECT_ROOT> --mode pr|merge-to-main [--testcmd "…"]   # Phase B: emit wrapper
```

Phase A prints proposed internal mode + evidence. Resolve internally; NEVER surface PR/merge/push choice to owner.
Pass `--testcmd` only when no valid current-tree receipt exists and project controller requires a canonical gate command.
Phase B writes the frozen wrapper + a smoke test + a local exclude (when `.claude` is tracked). Then
land via the wrapper above.

One-off (no wrapper wanted) → drive the orchestrator directly (same ladder, same exit codes):
```bash
bash ~/.claude/workflows/lib/finish-branch.sh land --root R --base B --mode M --anchor KIND:VAL --testcmd T [--depcmd D] -- <branch> <wt>
```

## Red Flags — NEVER

- Hand-type the merge/push/worktree cascade instead of running the wrapper.
- Blind `-X theirs` / whole-file `--theirs` (see rubric).
- Escalate a conflict to the user.
- Force-push main (no history rewrite on a shared branch).
- Land with failing tests — the `land-merge` gate enforces this; do not bypass it.

## Integration

**Called by:** `ship` — final step, replaces the old push/deploy + merge tail.
**Pairs with:** `using-git-worktrees` — cleans up the worktree it created.

## Learned rules — now enforced mechanically by `finish-branch.sh`

These hard-won footguns are why the cascade is now tested code, not prose. Each is enforced by a
named primitive; kept here as the WHY so no future editor re-inlines raw git.

- **worktree-remove-before-branch-delete** → `cleanup` orders push → worktree-remove --force →
  branch -d. (`branch -d` before worktree-remove fails "used by worktree".)
- **merge-from-worktree-cwd / merge-from-main-not-worktree** → every primitive uses `git -C <dir>`;
  merges target the right repo, never "Already up to date" from a wrong cwd.
- **dirty-src-before-merge / wip-feature-work-blocks-merge** → `preflight` reports a dirty main
  worktree (src/ contamination AND coherent WIP) before any land; the agent clears it.
- **pending-merge-on-main-blocks-checkout** → `preflight` detects MERGE_HEAD / unmerged paths and
  blocks until the agent completes or aborts it.
- **worktree-remove-destroys-gitignored-assets** → `cleanup` refuses to remove a worktree holding
  non-empty `tmp/.cache/.turbo/dist/coverage` until the agent preserves them (`--assets-ok`).
- **merge-theirs-drops-imports / merge-conflict-resolution-missing-vars** → the blind `-X theirs`
  path is DELETED; conflicts go to the rubric (read both sides, integrate, verify with tests).
