Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions plugins/developer/skills/worktree/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
name: worktree
description: Create and manage git worktrees for parallel development. Use when user asks to create a worktree, work in parallel on multiple branches, set up isolated development environments, or clean up stale worktrees. Triggers on phrases like "create worktree", "new worktree", "worktree for branch", "/worktree", "clean worktrees", "stale worktrees".
---

# Git Worktree Management

Create worktrees in `<repo-parent>/worktrees/<repo-name>/<branch-name>`.

`SKILL_DIR` = directory containing this SKILL.md

## Creating a Worktree

```bash
"$SKILL_DIR/scripts/create-worktree.sh" <branch-name> [base-branch]
```

- Creates worktree for existing branch, or auto-creates from `origin/main` if branch doesn't exist
- Runs `bin/worktree-init.sh` if present (see below)

## Custom Initialization

Create `bin/worktree-init.sh` in your repo to customize worktree setup:

```bash
#!/usr/bin/env bash
# bin/worktree-init.sh - runs in new worktree, $1 = source repo root
SOURCE_REPO="$1"

# Example: copy env files
cp "$SOURCE_REPO/.env" ./.env

# Example: install dependencies
yarn install
```

The script runs inside the new worktree directory with `$1` set to the source repo root.

## After Creating a Worktree

Always check for stale worktrees and offer to clean them up:

```bash
"$SKILL_DIR/scripts/detect-stale-worktrees.sh"
```

If stale worktrees are found, ask user if they want to remove them. For each one they confirm:

```bash
"$SKILL_DIR/scripts/purge-worktree.sh" "<worktree-path>"
```

## Scripts

### create-worktree.sh

```bash
"$SKILL_DIR/scripts/create-worktree.sh" feature/my-branch # Existing or new branch from origin/main
"$SKILL_DIR/scripts/create-worktree.sh" feature/new-feature develop # New branch from specific base
```

### detect-stale-worktrees.sh

Finds worktrees whose branches no longer exist on remote (merged/deleted PRs).

```bash
"$SKILL_DIR/scripts/detect-stale-worktrees.sh" # Human-readable output
"$SKILL_DIR/scripts/detect-stale-worktrees.sh" --quiet # Just paths (for scripting)
```

Exit code 0 = stale worktrees found, 1 = none found.

### purge-worktree.sh

Removes worktree and deletes local branch.

```bash
"$SKILL_DIR/scripts/purge-worktree.sh" "/path/to/worktree"
"$SKILL_DIR/scripts/purge-worktree.sh" "/path/to/worktree" --keep-branch # Keep local branch
```

## Other Commands

```bash
git worktree list # List all worktrees
git worktree prune # Clean up stale worktree refs (different from stale branches)
```
56 changes: 56 additions & 0 deletions plugins/developer/skills/worktree/scripts/create-worktree.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail

# Configuration - set these before running
BRANCH_NAME="${1:?Usage: $0 <branch-name> [base-branch]}"
BASE_BRANCH="${2:-}" # Optional: base branch for creating new branches

# Compute paths
REPO_ROOT=$(git rev-parse --show-toplevel)
REPO_NAME=$(basename "$REPO_ROOT")
REPO_PARENT=$(dirname "$REPO_ROOT")
WORKTREE_BASE="$REPO_PARENT/worktrees/$REPO_NAME"
WORKTREE_PATH="$WORKTREE_BASE/$BRANCH_NAME"

# Check if worktree already exists
if [ -d "$WORKTREE_PATH" ]; then
echo "Error: Worktree already exists at $WORKTREE_PATH"
exit 1
fi

# Create worktree base directory
mkdir -p "$WORKTREE_BASE"

# Check if branch exists (locally or remotely)
branch_exists() {
git show-ref --verify --quiet "refs/heads/$1" 2>/dev/null || \
git show-ref --verify --quiet "refs/remotes/origin/$1" 2>/dev/null
}

# Create the worktree
if [ -n "$BASE_BRANCH" ]; then
echo "Creating new branch '$BRANCH_NAME' from '$BASE_BRANCH'..."
git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" "$BASE_BRANCH"
elif branch_exists "$BRANCH_NAME"; then
echo "Creating worktree for existing branch '$BRANCH_NAME'..."
git worktree add "$WORKTREE_PATH" "$BRANCH_NAME"
else
echo "Branch '$BRANCH_NAME' not found. Fetching origin/main and creating new branch..."
git fetch origin main
git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" origin/main
fi

# Run repo-specific initialization if available
INIT_SCRIPT="$REPO_ROOT/bin/worktree-init.sh"
if [ -x "$INIT_SCRIPT" ]; then
echo "Running worktree initialization script..."
cd "$WORKTREE_PATH" && "$INIT_SCRIPT" "$REPO_ROOT"
else
echo ""
echo "Tip: Create bin/worktree-init.sh (chmod +x) to customize worktree setup."
echo " It runs in the new worktree with \$1=source_repo_root"
fi

echo ""
echo "Worktree created successfully at: $WORKTREE_PATH"
echo "To start working: cd $WORKTREE_PATH"
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail

# Detect worktrees without corresponding remote branches
# Usage: ./detect-stale-worktrees.sh [--quiet]
#
# Returns exit code 0 if stale worktrees found, 1 if none found
# With --quiet, only outputs paths of stale worktrees (for scripting)

QUIET="${1:-}"
REPO_ROOT=$(git rev-parse --show-toplevel)

# Fetch latest remote refs
git fetch --prune origin 2>/dev/null || true

stale_count=0
stale_worktrees=()

# Parse worktree list
while IFS= read -r line; do
# Extract path and branch info
path=$(echo "$line" | awk '{print $1}')
branch_info=$(echo "$line" | grep -oP '\[.*\]' || echo "")

# Skip if no branch (detached HEAD)
if [[ -z "$branch_info" || "$branch_info" == *"detached"* ]]; then
continue
fi

# Extract branch name from [branch]
branch=$(echo "$branch_info" | sed 's/\[\(.*\)\]/\1/')

# Skip main repo directory
if [[ "$path" == "$REPO_ROOT" ]]; then
continue
fi

# Check if remote branch exists
if ! git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
stale_worktrees+=("$path|$branch")
stale_count=$((stale_count + 1))
fi
done < <(git worktree list)

# Output results
if [[ $stale_count -eq 0 ]]; then
[[ "$QUIET" != "--quiet" ]] && echo "No stale worktrees found."
exit 1
fi

if [[ "$QUIET" == "--quiet" ]]; then
for entry in "${stale_worktrees[@]}"; do
echo "${entry%%|*}"
done
else
echo "Found $stale_count stale worktree(s) without remote branches:"
echo ""
for entry in "${stale_worktrees[@]}"; do
path="${entry%%|*}"
branch="${entry##*|}"
echo " Path: $path"
echo " Branch: $branch"
echo ""
done
fi

exit 0
44 changes: 44 additions & 0 deletions plugins/developer/skills/worktree/scripts/purge-worktree.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail

# Purge a worktree and optionally its local branch
# Usage: ./purge-worktree.sh <worktree-path> [--keep-branch]
#
# Options:
# --keep-branch Keep the local branch after removing the worktree

WORKTREE_PATH="${1:?Usage: $0 <worktree-path> [--keep-branch]}"
KEEP_BRANCH="${2:-}"

# Validate worktree exists
if ! git worktree list | grep -q "^$WORKTREE_PATH "; then
echo "Error: '$WORKTREE_PATH' is not a valid worktree"
exit 1
fi

# Get branch name from worktree
BRANCH_INFO=$(git worktree list | grep "^$WORKTREE_PATH " | grep -oP '\[.*\]' || echo "")
if [[ -z "$BRANCH_INFO" || "$BRANCH_INFO" == *"detached"* ]]; then
BRANCH=""
else
BRANCH=$(echo "$BRANCH_INFO" | sed 's/\[\(.*\)\]/\1/')
fi

echo "Removing worktree: $WORKTREE_PATH"
if [[ -n "$BRANCH" ]]; then
echo "Associated branch: $BRANCH"
fi

# Remove the worktree
git worktree remove "$WORKTREE_PATH" --force

# Delete the local branch if requested and it exists
if [[ -n "$BRANCH" && "$KEEP_BRANCH" != "--keep-branch" ]]; then
if git show-ref --verify --quiet "refs/heads/$BRANCH" 2>/dev/null; then
echo "Deleting local branch: $BRANCH"
git branch -D "$BRANCH"
fi
fi

echo ""
echo "Worktree purged successfully."