# merge-worktree
-Merge a worktree branch into main, rebasing for linear history. Can
-also recover from a deleted worktree.
+Merge the current worktree branch into main, rebasing for linear
+history.
## Instructions
-This command must be invoked from within a worktree (the pre-tool-use
-hooks enforce this). When the user asks to merge a workspace,
-integrate branch changes, or "commit to main":
+This command must be invoked from within a worktree. When the user
+asks to merge a workspace, integrate branch changes, or "commit to
+main":
-1. **Show available workspaces**:
- Run `$CLAUDE_PROJECT_DIR/.claude/scripts/branch-summary` to display
- a markdown table of all non-main branches and their commits.
+1. **Run merge**:
+ Execute `$CLAUDE_PROJECT_DIR/.claude/scripts/merge-worktree`. The
+ script infers the branch from the worktree path.
-2. **Ask which workspace to merge**:
- Use AskUserQuestion (single-select). Present the workspace branch
- names from step 1 as options. If only one non-main branch exists,
- ask to confirm merging that one.
+ Handle its exit code:
-3. **Execute the merge**:
- Run `$CLAUDE_PROJECT_DIR/.claude/scripts/merge-worktree
- <selected-branch>`. The script checks for conflicts without
- touching the working tree. Handle its exit code:
+ - **0 (success):** If run with `--rebase`, the rebase completed —
+ tell the user and offer to re-run without flags to merge.
+ Otherwise the merge completed cleanly — proceed to step 2.
- - **0 (success):** The merge completed cleanly. Proceed to step 5.
+ - **2 (error):** Read the script's stderr message. Always ask the
+ user whether to re-run with `--rebase` (preserves main's
+ commits) or `--force` (discards main's commits), but adjust the
+ question to the error:
- - **1 (error):** Report the error to the user and stop.
+ - **Diverged:** explain that main has commits the branch lacks.
- - **2 (diverged):** The branch has diverged from main, so
- fast-forward is not possible. Ask the user with a single-select
- AskUserQuestion: "Branch <name> has diverged from main. Force-reset
- main to <name>?" Present "Force reset" and "Skip" as options.
- If they choose "Force reset", re-run the merge-worktree script
- with `--force` and proceed to step 5. If they choose "Skip", stop.
+ - **Conflicts:** explain which files conflict and that
+ `--force` skips conflict checking, while `--rebase` will
+ surface conflicts for manual resolution.
- - **3 (conflicts detected):** The script found merge conflicts.
- Proceed to step 4.
+ - **Rebase failed:** explain that the rebase hit conflicts.
+ After the user resolves them and runs
+ `git rebase --continue`, re-run merge-worktree to merge.
- If the worktree directory was deleted out-of-band, the script
- auto-detects this and enters recovery mode (prunes stale
- registration, deletes branch).
+ - For all other errors, report the message and stop.
-4. **Resolve merge conflicts**:
- The script detected that a merge would cause conflicts. Use
- AskUserQuestion (single-select) with three options:
-
- - **"Force reset (override)":** Re-run the merge-worktree script
- as `merge-worktree <selected-branch> --force` to reset main to
- the branch state. After it succeeds, proceed to step 5.
-
- - **"Agent resolves":** In the main worktree at
- `$CLAUDE_PROJECT_DIR`, merge the branch manually:
- 1. `git checkout main`
- 2. `git merge <selected-branch>`
- 3. Resolve all conflicts, then commit.
- After merging, proceed to step 5.
-
- - **"Resolve manually":** Instruct the user to resolve conflicts
- in the main worktree at `$CLAUDE_PROJECT_DIR`:
- 1. `git checkout main`
- 2. `git merge <selected-branch>`
- 3. Fix conflicts and commit.
- After they confirm, proceed to step 5.
-
-5. **Return to the main project directory**:
- Call the `ExitWorktree` tool with `action: "keep"`. This restores
- the session's PWD to the main project directory while preserving
- the worktree on disk for later manual cleanup.
\ No newline at end of file
+2. **Return to the main project**:
+ Call `ExitWorktree` with `action: "keep"`.
# Copyright (c) 2026 Jakub Czajka <jakub@ekhem.eu.org>
# License: GPL-3.0 or later.
#
-# merge-worktree — Merge a worktree branch into main, or recover
-# from a deleted worktree.
-#
-# This script operates on the main worktree (CLAUDE_PROJECT_DIR) as a
-# subprocess, independent of the session's current directory. When the
-# branch is based on main it rebases for linear history then
-# fast-forwards. When the branch has diverged (e.g. after squashing)
-# it can reset main with --force.
-#
-# The script does NOT remove the worktree or branch — that is handled
-# by ExitWorktree (called by the merge-worktree skill after the merge).
-# A subprocess cannot change the parent session's PWD, so letting the
-# harness handle cleanup ensures the session returns to the main
-# project directory.
-#
-# Usage:
-# merge-worktree <branch-name> [--force] # normal merge
-# merge-worktree <branch-name> --recover # cleanup after deletion
-# merge-worktree --list-stale # list orphaned worktrees
-#
-# Exit codes:
-# 0 — success
-# 1 — error (validation, unclean worktree, rebase failure, etc.)
-# 2 — branch diverged from main, needs --force
-# 3 — merge conflicts detected, needs --force or manual resolution
+# merge-worktree — Merge a worktree branch into main.
-set -eu
+set -o errexit -o nounset
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
-# ── --list-stale ───────────────────────────────────────
-
-if [ "${1:-}" = "--list-stale" ]
-then
- echo "=== Orphaned worktree branches ==="
- found=0
- for b in $(git -C "$PROJECT_DIR" branch \
- | sed 's/^[* ]*//' | grep '^worktree-'); do
- wt_path=$(git -C "$PROJECT_DIR" worktree list \
- | grep -F "[$b]" | awk '{print $1}' || true)
- if [ -z "$wt_path" ] || [ ! -d "$wt_path" ]
- then
- echo " $b"
- found=1
- fi
- done
- if [ "$found" -eq 0 ]
- then
- echo " (none)"
- fi
- exit 0
-fi
+# Absolute paths to Guix profile directories, avoiding $PATH lookup.
+GUIX_PROFILE="$HOME/.guix-profile"
+_last="${XDG_DATA_DIRS##*:}"
+SYSTEM_PROFILE="${_last%/share}"
-# ── Arguments ─────────────────────────────────────────
+die() { "$SYSTEM_PROFILE/bin/echo" "Error: $1" >&2; exit 2; }
-branch="${1:-}"
-flag="${2:-}"
+flag="${1:-}"
-if [ -z "$branch" ]
+if [ -n "$flag" ] && [ "$flag" != "--force" ] && [ "$flag" != "--rebase" ]
then
- echo "Usage: merge-worktree <branch> [--force] [--recover]" >&2
- echo " merge-worktree --list-stale" >&2
- exit 1
+ die "Unknown argument: $flag"
fi
-if [ "$branch" = "main" ]
+if [ $# -gt 1 ]
then
- echo "Error: Cannot merge main into itself." >&2
- exit 1
+ die "Unexpected additional arguments: $*"
fi
-# Validate branch.
-if ! git -C "$PROJECT_DIR" rev-parse \
- --verify "$branch" >/dev/null 2>&1
+session_id=$(echo "$PWD" \
+ | "$SYSTEM_PROFILE/bin/sed" --quiet 's|.*/worktrees/\([^/]\{1,\}\).*|\1|p')
+if [ -z "$session_id" ]
then
- echo "Error: Branch '$branch' does not exist." >&2
- exit 1
+ die "Must be run from inside a worktree."
fi
-# Find worktree path.
-worktree_path=$(git -C "$PROJECT_DIR" worktree list \
- | grep -F "[$branch]" | awk '{print $1}')
-
-# ── Explicit recovery ─────────────────────────────────
+branch="worktree-${session_id}"
-if [ "$flag" = "--recover" ]
+if [ -n "$("$GUIX_PROFILE/bin/git" -C "$PWD" status --porcelain)" ]
then
- echo "=== Recovery: worktree '$branch' ==="
- git -C "$PROJECT_DIR" worktree prune 2>/dev/null || true
- git -C "$PROJECT_DIR" branch -D "$branch" 2>/dev/null || true
- echo "=== Recovery complete: '$branch' cleaned up ==="
- exit 0
+ die "Worktree has uncommitted changes. Commit or stash first."
fi
-# ── Auto-detect: worktree directory missing ───────────
-
-if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]
+# Main project must have no uncommitted changes.
+if [ -n "$("$GUIX_PROFILE/bin/git" -C "$PROJECT_DIR" status \
+ --porcelain --untracked-files=no)" ]
then
- echo "=== Worktree directory missing — entering recovery mode ==="
- git -C "$PROJECT_DIR" worktree prune 2>/dev/null || true
- git -C "$PROJECT_DIR" branch -D "$branch" 2>/dev/null || true
- echo "=== Recovery complete: '$branch' cleaned up ==="
- exit 0
+ die "Main worktree has uncommitted changes. Commit or stash first."
fi
-# Check the worktree itself is clean.
-if [ -n "$(git -C "$worktree_path" status --porcelain)" ]
+if [ "$flag" = "--rebase" ]
then
- echo "Error: Worktree has uncommitted changes." >&2
- echo " Commit or stash them first, then re-run." >&2
- exit 1
+ if ! "$GUIX_PROFILE/bin/git" rebase main
+ then
+ die "Rebase failed. Resolve conflicts in this worktree with" \
+ " git rebase --continue and re-run merge-worktree to" \
+ " merge into main."
+ fi
+ exit 0
fi
-# Check main worktree is clean.
-if [ -n "$(git -C "$PROJECT_DIR" \
- status --porcelain --untracked-files=no)" ]
+if [ "$flag" = "--force" ]
then
- echo "Error: Main worktree has uncommitted changes." >&2
- echo " Commit or stash them first." >&2
- exit 1
+ "$GUIX_PROFILE/bin/git" -C "$PROJECT_DIR" checkout -B main "$branch" 2>/dev/null
+ exit 0
fi
-# Ensure main is restored on any exit (rebase failure,
-# interruption).
-trap 'git -C "$PROJECT_DIR" checkout main \
- 2>/dev/null || true' EXIT
+# Restore main checkout on exit for safety.
+trap '"$GUIX_PROFILE/bin/git" -C "$PROJECT_DIR" checkout main 2>/dev/null' EXIT
-# Fetch from origin.
-echo "=== Fetching from origin ==="
-git -C "$PROJECT_DIR" fetch origin
-
-# Determine whether this is a normal fast-forward or a
-# divergent branch that needs a reset.
-if git -C "$PROJECT_DIR" merge-base --is-ancestor \
+if ! "$GUIX_PROFILE/bin/git" -C "$PROJECT_DIR" merge-base --is-ancestor \
main "$branch" 2>/dev/null
then
- # ── Normal case: branch is based on main ──
-
- # ── Conflict pre-check ──
- echo "=== Checking for merge conflicts ==="
- merge_result=$(git -C "$PROJECT_DIR" merge-tree \
- --write-tree main "$branch" 2>&1)
- conflict_files=$(echo "$merge_result" \
- | grep -E '^[0-7]{6} [0-9a-f]{40} [123] ' \
- | awk '{print $4}' | sort -u)
- if [ -n "$conflict_files" ]
- then
- echo "Conflicts detected between main and '$branch'." >&2
- echo "Conflicting files:" >&2
- echo "$conflict_files" >&2
- if [ "$flag" = "--force" ]
- then
- echo "=== Resetting main to '$branch' (override) ==="
- current=$(git -C "$PROJECT_DIR" branch --show-current)
- if [ "$current" != "main" ]
- then
- git -C "$PROJECT_DIR" checkout main
- fi
- git -C "$PROJECT_DIR" reset --hard "$branch"
- trap '' EXIT
- echo "=== Main reset to '$branch' ==="
- exit 0
- else
- echo "Re-run with --force to reset main or resolve" \
- "manually." >&2
- exit 3
- fi
- fi
-
- # Rebase branch onto main.
- echo "=== Rebasing $branch onto main ==="
- if ! git -C "$PROJECT_DIR" \
- rebase main "$branch"
- then
- rc=$?
- echo "Error: Rebase failed (conflicts)." >&2
- echo " Resolve them in the main worktree:" >&2
- echo " cd $PROJECT_DIR" >&2
- echo " git status # see conflicted files" >&2
- echo " # fix conflicts, then:" >&2
- echo " git rebase --continue" >&2
- echo " git checkout main" >&2
- echo " git merge --ff-only $branch" >&2
- exit $rc
- fi
-
- # Switch back to main if needed.
- current=$(git -C "$PROJECT_DIR" branch --show-current)
- if [ "$current" != "main" ]
- then
- echo "=== Switching to main ==="
- git -C "$PROJECT_DIR" checkout main
- fi
-
- # Fast-forward merge.
- echo "=== Merging $branch into main (fast-forward) ==="
- if ! git -C "$PROJECT_DIR" \
- merge --ff-only "$branch"
- then
- rc=$?
- echo "Error: Fast-forward merge failed." >&2
- echo " This should not happen after a successful" \
- "rebase." >&2
- echo " Try: git -C $PROJECT_DIR merge $branch" >&2
- exit $rc
- fi
-
- echo "=== Branch '$branch' successfully merged into" \
- "main ==="
-else
- # ── Divergent case: branch was rewritten ──
+ die "Branch '$branch' diverged from main." \
+ " Re-run with --rebase or --force."
+fi
- if [ "$flag" = "--force" ]
- then
- echo "=== Resetting main to $branch ==="
- current=$(git -C "$PROJECT_DIR" branch --show-current)
- if [ "$current" != "main" ]
- then
- git -C "$PROJECT_DIR" checkout main
- fi
- git -C "$PROJECT_DIR" reset --hard "$branch"
- trap '' EXIT
- echo "=== Main reset to '$branch' ==="
- else
- echo "Branch '$branch' has diverged from main." >&2
- echo "Fast-forward merge is not possible." >&2
- echo "Re-run with --force to reset main to this" \
- "branch." >&2
- exit 2
- fi
+conflicts=$("$GUIX_PROFILE/bin/git" -C "$PROJECT_DIR" merge-tree \
+ --write-tree main "$branch" 2>&1 \
+ | "$SYSTEM_PROFILE/bin/sed" --quiet \
+ 's/^[0-7]\{6\} [0-9a-f]\{40\} [123] //p' \
+ | "$SYSTEM_PROFILE/bin/sort" --unique)
+if [ -n "$conflicts" ]
+then
+ die "Conflicts detected: $conflicts Re-run with --force to override."
fi
-echo "=== Branch '$branch' merged into main. ==="
-echo "=== Use ExitWorktree to return to main. ==="
\ No newline at end of file
+
+"$GUIX_PROFILE/bin/git" -C "$PROJECT_DIR" checkout -B main "$branch"