---
name: worktree-manager
description: "Manage git worktrees for isolated concurrent work on products/projects. Prevents work from being overwritten when multiple people request changes simultaneously. Use when: modifying ANY product code, multiple requests to same product, merge conflicts. Keywords: concurrent work, isolation, merge conflict, apps/main."
---

# Worktree Manager Skill

Manage git worktrees for isolated concurrent work on products/projects. Prevents work from being overwritten when multiple people request changes simultaneously.

## When to Use

- Any time you're about to modify code in a product or project
- Always use worktrees for consistency (even for single tasks)

## CRITICAL: Orchestration Pattern (MAIN SESSION)

**Subagents don't automatically know about worktrees.** The MAIN SESSION must orchestrate:

### For ANY Code Modification Request

```bash
# 1. Main session creates worktree and gets spawn context
SKILL_DIR="/home/aria/.openclaw/workspace/skills/worktree-manager/scripts"
SPAWN_CTX=$($SKILL_DIR/spawn-code-task.sh "<slug>" "<requester>" "<requester-id>" "<description>")

# 2. Extract the task instructions
TASK_ID=$(echo "$SPAWN_CTX" | jq -r '.taskId')
WORK_DIR=$(echo "$SPAWN_CTX" | jq -r '.workDir')
SPAWN_TASK=$(echo "$SPAWN_CTX" | jq -r '.spawnTask')
```

### Then Spawn Subagent

```python
sessions_spawn({
    "task": SPAWN_TASK,  # Includes work dir, rules, commit instructions
    "label": f"code-{TASK_ID}"
})
```

### After Subagent Completes

```bash
# Main session handles merge
$SKILL_DIR/merge-worktree.sh "<slug>" "$TASK_ID"

# If MERGE_SUCCESS: deploy from apps/main
# If MERGE_CONFLICT: notify requesters, wait for human decision
```

### Why This Pattern

| Problem | Solution |
|---------|----------|
| Subagents don't read AGENTS.md | Task string includes ALL rules |
| Subagents might skip worktree | They're GIVEN a directory, no choice |
| Conflict handling needs context | Main session handles merge with full context |
| Lost work from race conditions | Isolated branches, explicit merge |

### Example: Full Orchestration

```bash
# Request: "Change greeting to Hello Teachers in AI Teacher Assistant"

# Step 1: Create worktree
SPAWN_CTX=$($SKILL_DIR/spawn-code-task.sh "ai-teacher-assistant" "raven" "470..." "Change greeting to Hello Teachers")
TASK_ID=$(echo "$SPAWN_CTX" | jq -r '.taskId')

# Step 2: Spawn subagent (they work in isolated worktree)
# sessions_spawn with the spawnTask from SPAWN_CTX

# Step 3: Wait for completion, then merge
$SKILL_DIR/merge-worktree.sh "ai-teacher-assistant" "$TASK_ID"

# Step 4: If clean, rebuild and deploy from apps/main
cd /home/aria/products/ai-teacher-assistant/apps/main
pnpm build && pnpm start -p <port>
```

## Directory Structure

```
/home/aria/products/<slug>/
├── apps/main/              ← deployed state (main branch only)
├── worktrees/
│   └── <task-id>/          ← ephemeral worktree for active task
└── .worktree-state.json    ← state tracking (gitignored)
```

## Workflow

### 1. Before Starting Work

```bash
cd /home/aria/products/<slug>/apps/main

# Generate task ID (use job ID, or timestamp-based)
TASK_ID="task-$(date +%s)"

# Create worktree
git fetch origin
git worktree add ../worktrees/$TASK_ID -b task/$TASK_ID origin/main
```

Then update state:
```bash
# Read/create state file
STATE_FILE="/home/aria/products/<slug>/.worktree-state.json"

# Add entry (use jq or write directly)
```

### 2. Do the Work

Work in the worktree directory:
```bash
cd /home/aria/products/<slug>/worktrees/<task-id>
# Make changes, commits, etc.
```

### 3. Merge Back

```bash
cd /home/aria/products/<slug>/apps/main
git fetch origin

# Attempt merge
git merge task/<task-id> --no-edit

# If clean merge:
#   - Push to origin
#   - Cleanup worktree
#   - Remove state entry

# If conflict:
#   - Do NOT force
#   - Notify relevant parties
#   - Wait for human decision
```

### 4. Cleanup

```bash
cd /home/aria/products/<slug>/apps/main
git worktree remove ../worktrees/<task-id>
git branch -d task/<task-id>
```

## State File Format

Location: `<product-or-project>/.worktree-state.json`

```json
{
  "active": {
    "task-1708012345": {
      "branch": "task/task-1708012345",
      "path": "worktrees/task-1708012345",
      "requester": "raven",
      "requester_id": "470619903855034369",
      "description": "Add dark mode toggle",
      "started": "2026-02-18T10:00:00Z",
      "base_commit": "abc123def"
    }
  },
  "recent_merges": [
    {
      "task_id": "task-1708012300",
      "requester": "jim",
      "requester_id": "123456789",
      "description": "Update header",
      "merged_at": "2026-02-18T09:30:00Z",
      "commit": "def456abc",
      "files": ["src/components/Header.tsx"]
    }
  ]
}
```

### Key Fields

- **base_commit**: The main branch HEAD when worktree was created (for divergence detection)
- **recent_merges**: Last 10 merges (for conflict context)

**IMPORTANT:** Add to `.gitignore`:
```
.worktree-state.json
worktrees/
```

## Conflict Handling

### Merge Script Exit Codes

| Code | Status | Meaning |
|------|--------|---------|
| 0 | MERGE_SUCCESS | Clean merge, deployed |
| 1 | MERGE_CONFLICT | Git-level conflict |
| 2 | ERROR | Script error |
| 3 | DIVERGENCE_CONFLICT | Same files modified by concurrent task |
| 4 | DIVERGENCE_SAFE | Different files, auto-merged |

### Divergence Detection (Concurrent Work)

Before merging, the script checks if `main` has changed since the worktree was created:

```
base_commit (when we started) vs current HEAD (now)
```

**If diverged:**
1. Compare which files changed in main vs our branch
2. If **overlap** (same files): DIVERGENCE_CONFLICT → notify both parties
3. If **no overlap** (different files): DIVERGENCE_SAFE → auto-merge

### Notification Format

**For DIVERGENCE_CONFLICT (same files):**

```
⚠️ Concurrent changes detected on **ProductName**

Your task: "Add dark mode toggle" (requested by @Raven)
  → Changed: src/app/page.tsx, src/components/Header.tsx

While you were working, another task merged:
  → "Update header layout" (by @Jim) at 12:25 AM
  → Changed: src/components/Header.tsx

**Same file modified: src/components/Header.tsx**

Options:
1. Merge yours on top (your changes win for conflicts)
2. Discard yours (keep Jim's version)
3. Manual review (I'll show both versions)

Waiting for decision...
```

**For MERGE_CONFLICT (git conflict):**

```
⚠️ Merge conflict on **ProductName**

Your task: "Add dark mode toggle" (requested by @Raven)

Git couldn't auto-merge these files:
• src/components/Header.tsx

Options:
1. Keep your version
2. Keep main version
3. Manual merge (I'll show the diff)

Waiting for decision...
```

### Handling Flow

1. **Do NOT force merge or pick sides automatically**
2. **Identify conflicting parties** from state file + recent_merges
3. **Notify via Discord** (both parties if concurrent, or just requester)
4. **Wait for human decision** before proceeding
5. **Once decided**, perform the merge as instructed and cleanup

## Helper Scripts

### Create Worktree

```bash
#!/bin/bash
# create-worktree.sh <product-slug> <task-id> <requester> <requester-id> <description>

SLUG=$1
TASK_ID=$2
REQUESTER=$3
REQUESTER_ID=$4
DESC=$5

PRODUCT_DIR="/home/aria/products/$SLUG"
MAIN_DIR="$PRODUCT_DIR/apps/main"
WORKTREE_DIR="$PRODUCT_DIR/worktrees/$TASK_ID"
STATE_FILE="$PRODUCT_DIR/.worktree-state.json"

cd "$MAIN_DIR"
git fetch origin
git worktree add "$WORKTREE_DIR" -b "task/$TASK_ID" origin/main

# Initialize state file if needed
if [ ! -f "$STATE_FILE" ]; then
  echo '{"active":{}}' > "$STATE_FILE"
fi

# Add entry to state
jq --arg id "$TASK_ID" \
   --arg branch "task/$TASK_ID" \
   --arg path "worktrees/$TASK_ID" \
   --arg req "$REQUESTER" \
   --arg req_id "$REQUESTER_ID" \
   --arg desc "$DESC" \
   --arg started "$(date -Iseconds)" \
   '.active[$id] = {branch: $branch, path: $path, requester: $req, requester_id: $req_id, description: $desc, started: $started}' \
   "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"

echo "$WORKTREE_DIR"
```

### Merge Worktree

```bash
#!/bin/bash
# merge-worktree.sh <product-slug> <task-id>

SLUG=$1
TASK_ID=$2

PRODUCT_DIR="/home/aria/products/$SLUG"
MAIN_DIR="$PRODUCT_DIR/apps/main"
STATE_FILE="$PRODUCT_DIR/.worktree-state.json"

cd "$MAIN_DIR"
git fetch origin

# Attempt merge
if git merge "task/$TASK_ID" --no-edit; then
  echo "MERGE_SUCCESS"
  git push origin main
  
  # Cleanup
  git worktree remove "$PRODUCT_DIR/worktrees/$TASK_ID"
  git branch -d "task/$TASK_ID"
  git push origin --delete "task/$TASK_ID" 2>/dev/null || true
  
  # Remove from state
  jq --arg id "$TASK_ID" 'del(.active[$id])' "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
else
  echo "MERGE_CONFLICT"
  git merge --abort
  # List conflicting files
  git merge --no-commit "task/$TASK_ID" 2>/dev/null || true
  git diff --name-only --diff-filter=U
  git merge --abort
fi
```

### List Active Worktrees

```bash
#!/bin/bash
# list-worktrees.sh <product-slug>

SLUG=$1
STATE_FILE="/home/aria/products/$SLUG/.worktree-state.json"

if [ -f "$STATE_FILE" ]; then
  jq -r '.active | to_entries[] | "\(.key): \(.value.description) (by \(.value.requester))"' "$STATE_FILE"
else
  echo "No active worktrees"
fi
```

### Cleanup Stale Worktrees

```bash
#!/bin/bash
# cleanup-stale.sh <product-slug> <max-age-hours>

SLUG=$1
MAX_AGE=${2:-24}

STATE_FILE="/home/aria/products/$SLUG/.worktree-state.json"
NOW=$(date +%s)

if [ ! -f "$STATE_FILE" ]; then
  exit 0
fi

# Find stale entries
jq -r --argjson now "$NOW" --argjson max "$((MAX_AGE * 3600))" \
  '.active | to_entries[] | select(($now - (.value.started | fromdateiso8601)) > $max) | .key' \
  "$STATE_FILE"
```

## Ensuring .gitignore

When creating worktrees for a product, always verify `.gitignore` includes:

```bash
# Check and add if missing
GITIGNORE="/home/aria/products/<slug>/.gitignore"
grep -q "worktree-state.json" "$GITIGNORE" 2>/dev/null || echo ".worktree-state.json" >> "$GITIGNORE"
grep -q "worktrees/" "$GITIGNORE" 2>/dev/null || echo "worktrees/" >> "$GITIGNORE"
```

## Integration with Existing Workflows

### Before (direct work on main)
```bash
cd /home/aria/products/<slug>/apps/main
# make changes
git commit && git push
```

### After (worktree-based)
```bash
# 1. Create worktree
WORK_DIR=$(bash create-worktree.sh <slug> <task-id> <requester> <id> "description")

# 2. Do work
cd "$WORK_DIR"
# make changes
git add . && git commit -m "feat: description"
git push -u origin task/<task-id>

# 3. Merge back
bash merge-worktree.sh <slug> <task-id>
# Handle conflicts if any
```

## Stale Worktree Policy

- **>24h no activity**: Notify requester
- **>48h no activity**: Auto-cleanup (with notification)
- **Check during heartbeat**: Run cleanup-stale.sh on active products
