Skip to content

Commit 4e9ea5e

Browse files
jwaldripclaude
andcommitted
fix(plugin): resolve worktree paths from main repo root, add cleanup
Worktree paths now use `git worktree list` to always resolve to the main repo root instead of `git rev-parse --show-toplevel`, which incorrectly resolves to a worktree root when already inside one. Also adds worktree cleanup to /reset and a new /cleanup skill for removing orphaned worktrees from interrupted sessions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5b65a77 commit 4e9ea5e

File tree

7 files changed

+190
-38
lines changed

7 files changed

+190
-38
lines changed

plugin/hooks/subagent-context.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,8 @@ echo ""
217217
echo "### Branch References"
218218
echo ""
219219
echo "- **Intent branch:** \`ai-dlc/${INTENT_SLUG}/main\`"
220-
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
221-
echo "- **Intent worktree:** \`${PROJECT_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}/\`"
220+
REPO_ROOT=$(git worktree list --porcelain 2>/dev/null | head -1 | sed 's/^worktree //')
221+
echo "- **Intent worktree:** \`${REPO_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}/\`"
222222
echo ""
223223
echo "To access intent-level state from a unit branch:"
224224
echo "\`\`\`bash"
@@ -248,7 +248,7 @@ echo "All bolt work MUST happen in an isolated worktree."
248248
echo "Working outside a worktree will cause conflicts with the parent session."
249249
echo ""
250250
echo "After entering your worktree, verify:"
251-
echo "1. You are in \`${PROJECT_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}-{unit-slug}/\`"
251+
echo "1. You are in \`${REPO_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}-{unit-slug}/\`"
252252
echo "2. You are on the correct unit branch (\`git branch --show-current\`)"
253253
echo "3. You loaded unit-scoped state (see Bootstrap above)"
254254
echo ""

plugin/skills/advance/SKILL.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ After marking a unit as completed, merge behavior depends on `change_strategy`:
102102
103103
```bash
104104
# Load config for merge settings
105-
PROJECT_ROOT=$(git rev-parse --show-toplevel)
105+
REPO_ROOT=$(git worktree list --porcelain | head -1 | sed 's/^worktree //')
106106
source "${CLAUDE_PLUGIN_ROOT}/lib/config.sh"
107107
INTENT_DIR=".ai-dlc/${INTENT_SLUG}"
108108
CONFIG=$(get_ai_dlc_config "$INTENT_DIR")
@@ -149,7 +149,7 @@ EOF
149149
)" 2>/dev/null || echo "PR may already exist for $UNIT_BRANCH"
150150
151151
# Clean up unit worktree
152-
WORKTREE_PATH="${PROJECT_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}-${UNIT_SLUG}"
152+
WORKTREE_PATH="${REPO_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}-${UNIT_SLUG}"
153153
[ -d "$WORKTREE_PATH" ] && git worktree remove "$WORKTREE_PATH"
154154
155155
elif [ "$AUTO_MERGE" = "true" ]; then
@@ -166,7 +166,7 @@ elif [ "$AUTO_MERGE" = "true" ]; then
166166
fi
167167

168168
# Clean up unit worktree
169-
WORKTREE_PATH="${PROJECT_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}-${UNIT_SLUG}"
169+
WORKTREE_PATH="${REPO_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}-${UNIT_SLUG}"
170170
[ -d "$WORKTREE_PATH" ] && git worktree remove "$WORKTREE_PATH"
171171
fi
172172
```
@@ -478,7 +478,6 @@ done
478478
All unit PRs have been created during construction. Review and merge them individually.
479479
480480
To clean up:
481-
git worktree remove .ai-dlc/worktrees/{intent-slug}
482481
/reset
483482
```
484483

@@ -564,6 +563,5 @@ To create PR manually:
564563
gh pr create --base ${DEFAULT_BRANCH} --head ai-dlc/{intent-slug}/main
565564
566565
To clean up:
567-
git worktree remove .ai-dlc/worktrees/{intent-slug}
568566
/reset
569567
```

plugin/skills/cleanup/SKILL.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
---
2+
description: Remove orphaned AI-DLC worktrees
3+
disable-model-invocation: true
4+
---
5+
6+
## Name
7+
8+
`ai-dlc:cleanup` - Remove orphaned AI-DLC worktrees.
9+
10+
## Synopsis
11+
12+
```
13+
/cleanup
14+
```
15+
16+
## Description
17+
18+
**User-facing command** - Run this to clean up stale worktrees left behind by interrupted sessions.
19+
20+
Scans `.ai-dlc/worktrees/` for worktree directories and removes any that are orphaned (the backing git worktree entry is stale or the directory is left over from a crashed session).
21+
22+
This does not:
23+
- Clear AI-DLC state (use `/reset` for that)
24+
- Delete branches or commits
25+
- Affect active worktrees with running sessions
26+
27+
## Implementation
28+
29+
### Pre-check: Reject Cowork Mode
30+
31+
```bash
32+
if [ "${CLAUDE_CODE_IS_COWORK:-}" = "1" ]; then
33+
echo "ERROR: /cleanup cannot run in cowork mode."
34+
echo "Run this in a full Claude Code CLI session."
35+
exit 1
36+
fi
37+
```
38+
39+
If `CLAUDE_CODE_IS_COWORK=1`, stop immediately with the message above. Do NOT proceed.
40+
41+
### Step 1: Discover Worktrees
42+
43+
```bash
44+
REPO_ROOT=$(git worktree list --porcelain | head -1 | sed 's/^worktree //')
45+
WORKTREES_DIR="${REPO_ROOT}/.ai-dlc/worktrees"
46+
47+
if [ ! -d "$WORKTREES_DIR" ]; then
48+
echo "No .ai-dlc/worktrees/ directory found. Nothing to clean up."
49+
exit 0
50+
fi
51+
52+
# List all directories in .ai-dlc/worktrees/
53+
DIRS=$(find "$WORKTREES_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
54+
if [ -z "$DIRS" ]; then
55+
echo "No worktree directories found. Nothing to clean up."
56+
exit 0
57+
fi
58+
```
59+
60+
### Step 2: Identify Orphaned Worktrees
61+
62+
```bash
63+
# Get list of valid worktree paths from git
64+
VALID_WORKTREES=$(git worktree list --porcelain | grep '^worktree ' | sed 's/^worktree //')
65+
66+
ORPHANED=()
67+
ACTIVE=()
68+
69+
for dir in $DIRS; do
70+
if echo "$VALID_WORKTREES" | grep -qF "$dir"; then
71+
ACTIVE+=("$(basename "$dir")")
72+
else
73+
ORPHANED+=("$(basename "$dir")")
74+
fi
75+
done
76+
```
77+
78+
### Step 3: Report and Confirm
79+
80+
Show the user what was found:
81+
82+
```
83+
## AI-DLC Worktree Cleanup
84+
85+
**Active worktrees:** {count}
86+
{list of active worktree names, if any}
87+
88+
**Orphaned worktrees:** {count}
89+
{list of orphaned worktree names, if any}
90+
```
91+
92+
If there are orphaned worktrees, ask the user to confirm removal using `AskUserQuestion`.
93+
94+
If there are no orphaned entries but there are active worktrees, ask whether to force-remove all worktrees (with a warning that this will interrupt any running sessions).
95+
96+
If there is nothing to clean up, output:
97+
98+
```
99+
No orphaned worktrees found. Everything is clean.
100+
```
101+
102+
### Step 4: Remove
103+
104+
```bash
105+
REPO_ROOT=$(git worktree list --porcelain | head -1 | sed 's/^worktree //')
106+
107+
# Remove orphaned directories (no valid git worktree entry)
108+
for name in "${ORPHANED[@]}"; do
109+
rm -rf "${REPO_ROOT}/.ai-dlc/worktrees/${name}"
110+
done
111+
112+
# If user chose to force-remove active worktrees too:
113+
for name in "${FORCE_REMOVE[@]}"; do
114+
git worktree remove --force "${REPO_ROOT}/.ai-dlc/worktrees/${name}" 2>/dev/null
115+
done
116+
117+
# Prune stale git worktree metadata
118+
git worktree prune
119+
```
120+
121+
### Step 5: Confirm
122+
123+
Output:
124+
```
125+
Cleanup complete.
126+
127+
Removed {count} orphaned worktree(s).
128+
```

plugin/skills/construct/SKILL.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,14 @@ elif [ ${#ACTIVE_INTENTS[@]} -gt 1 ]; then
139139
fi
140140

141141
# Ensure we're in the intent worktree
142-
PROJECT_ROOT=$(git rev-parse --show-toplevel)
142+
REPO_ROOT=$(git worktree list --porcelain | head -1 | sed 's/^worktree //')
143143
INTENT_BRANCH="ai-dlc/${INTENT_SLUG}/main"
144-
INTENT_WORKTREE="${PROJECT_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}"
144+
INTENT_WORKTREE="${REPO_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}"
145145

146-
mkdir -p "${PROJECT_ROOT}/.ai-dlc/worktrees"
147-
if ! grep -q '\.ai-dlc/worktrees/' "${PROJECT_ROOT}/.gitignore" 2>/dev/null; then
148-
echo '.ai-dlc/worktrees/' >> "${PROJECT_ROOT}/.gitignore"
149-
git add "${PROJECT_ROOT}/.gitignore"
146+
mkdir -p "${REPO_ROOT}/.ai-dlc/worktrees"
147+
if ! grep -q '\.ai-dlc/worktrees/' "${REPO_ROOT}/.gitignore" 2>/dev/null; then
148+
echo '.ai-dlc/worktrees/' >> "${REPO_ROOT}/.gitignore"
149+
git add "${REPO_ROOT}/.gitignore"
150150
git commit -m "chore: gitignore .ai-dlc/worktrees"
151151
fi
152152

@@ -415,7 +415,7 @@ For EACH ready unit:
415415
UNIT_NAME=$(basename "$UNIT_FILE" .md)
416416
UNIT_SLUG="${UNIT_NAME#unit-}"
417417
UNIT_BRANCH="ai-dlc/${INTENT_SLUG}/${UNIT_SLUG}"
418-
WORKTREE_PATH="${PROJECT_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}-${UNIT_SLUG}"
418+
WORKTREE_PATH="${REPO_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}-${UNIT_SLUG}"
419419

420420
if [ ! -d "$WORKTREE_PATH" ]; then
421421
git worktree add -B "$UNIT_BRANCH" "$WORKTREE_PATH"
@@ -667,7 +667,7 @@ ${TICKET_LINE}
667667
---
668668
*Built with [AI-DLC](https://ai-dlc.dev)*" 2>/dev/null || echo "PR may already exist for $UNIT_BRANCH"
669669

670-
WORKTREE_PATH="${PROJECT_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}-${UNIT_SLUG}"
670+
WORKTREE_PATH="${REPO_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}-${UNIT_SLUG}"
671671
[ -d "$WORKTREE_PATH" ] && git worktree remove "$WORKTREE_PATH"
672672

673673
elif [ "$AUTO_MERGE" = "true" ]; then
@@ -681,7 +681,7 @@ elif [ "$AUTO_MERGE" = "true" ]; then
681681
git merge --no-ff "$UNIT_BRANCH" -m "Merge ${UNIT_NAME} into intent branch"
682682
fi
683683

684-
WORKTREE_PATH="${PROJECT_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}-${UNIT_SLUG}"
684+
WORKTREE_PATH="${REPO_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}-${UNIT_SLUG}"
685685
[ -d "$WORKTREE_PATH" ] && git worktree remove "$WORKTREE_PATH"
686686
fi
687687
```
@@ -873,7 +873,6 @@ done
873873
All unit PRs have been created during construction. Review and merge them individually.
874874
875875
To clean up:
876-
git worktree remove .ai-dlc/worktrees/{intent-slug}
877876
/reset
878877
```
879878

@@ -959,7 +958,6 @@ To create PR manually:
959958
gh pr create --base ${DEFAULT_BRANCH} --head ai-dlc/{intent-slug}/main
960959
961960
To clean up:
962-
git worktree remove .ai-dlc/worktrees/{intent-slug}
963961
/reset
964962
```
965963

@@ -1013,7 +1011,7 @@ fi
10131011
UNIT_NAME=$(basename "$UNIT_FILE" .md) # e.g., unit-01-core-backend
10141012
UNIT_SLUG="${UNIT_NAME#unit-}" # e.g., 01-core-backend
10151013
UNIT_BRANCH="ai-dlc/${intentSlug}/${UNIT_SLUG}"
1016-
WORKTREE_PATH="${PROJECT_ROOT}/.ai-dlc/worktrees/${intentSlug}-${UNIT_SLUG}"
1014+
WORKTREE_PATH="${REPO_ROOT}/.ai-dlc/worktrees/${intentSlug}-${UNIT_SLUG}"
10171015

10181016
# Create worktree if it doesn't exist
10191017
if [ ! -d "$WORKTREE_PATH" ]; then

plugin/skills/elaborate/SKILL.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,21 @@ If no slug provided, or the intent doesn't exist, proceed to Phase 1.
159159
**Start fresh cleanup:** When the user chooses "Start fresh", remove the intent worktree and its branch (if they exist from a prior elaboration attempt), then clean up any leftover `.ai-dlc/{slug}/` directory:
160160

161161
```bash
162-
PROJECT_ROOT=$(git rev-parse --show-toplevel)
163-
INTENT_WORKTREE="${PROJECT_ROOT}/.ai-dlc/worktrees/${slug}"
162+
REPO_ROOT=$(git worktree list --porcelain | head -1 | sed 's/^worktree //')
163+
INTENT_WORKTREE="${REPO_ROOT}/.ai-dlc/worktrees/${slug}"
164164
INTENT_BRANCH="ai-dlc/${slug}/main"
165165

166166
# Remove worktree if it exists
167167
if [ -d "$INTENT_WORKTREE" ]; then
168168
git worktree remove --force "$INTENT_WORKTREE" 2>/dev/null
169169
fi
170170

171+
# Remove any unit worktrees for this intent
172+
for wt in "${REPO_ROOT}/.ai-dlc/worktrees/${slug}-"*; do
173+
[ -d "$wt" ] && git worktree remove --force "$wt" 2>/dev/null
174+
done
175+
git worktree prune
176+
171177
# Delete the intent branch if it exists
172178
git branch -D "$INTENT_BRANCH" 2>/dev/null
173179

@@ -221,11 +227,11 @@ Before beginning technical exploration, create the intent worktree and initializ
221227
You MUST ensure `.ai-dlc/worktrees/` is in `.gitignore` BEFORE creating the worktree. Run this as a **separate Bash command** first:
222228

223229
```bash
224-
PROJECT_ROOT=$(git rev-parse --show-toplevel)
225-
mkdir -p "${PROJECT_ROOT}/.ai-dlc/worktrees"
226-
if ! grep -q '\.ai-dlc/worktrees/' "${PROJECT_ROOT}/.gitignore" 2>/dev/null; then
227-
echo '.ai-dlc/worktrees/' >> "${PROJECT_ROOT}/.gitignore"
228-
git add "${PROJECT_ROOT}/.gitignore"
230+
REPO_ROOT=$(git worktree list --porcelain | head -1 | sed 's/^worktree //')
231+
mkdir -p "${REPO_ROOT}/.ai-dlc/worktrees"
232+
if ! grep -q '\.ai-dlc/worktrees/' "${REPO_ROOT}/.gitignore" 2>/dev/null; then
233+
echo '.ai-dlc/worktrees/' >> "${REPO_ROOT}/.gitignore"
234+
git add "${REPO_ROOT}/.gitignore"
229235
git commit -m "chore: gitignore .ai-dlc/worktrees"
230236
fi
231237
```
@@ -236,12 +242,12 @@ Only after confirming the gitignore entry exists:
236242

237243
```bash
238244
INTENT_SLUG="{intent-slug}"
239-
PROJECT_ROOT=$(git rev-parse --show-toplevel)
245+
REPO_ROOT=$(git worktree list --porcelain | head -1 | sed 's/^worktree //')
240246
INTENT_BRANCH="ai-dlc/${INTENT_SLUG}/main"
241-
INTENT_WORKTREE="${PROJECT_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}"
247+
INTENT_WORKTREE="${REPO_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}"
242248

243249
# Guard: abort if worktrees dir is not gitignored
244-
if ! grep -q '\.ai-dlc/worktrees/' "${PROJECT_ROOT}/.gitignore" 2>/dev/null; then
250+
if ! grep -q '\.ai-dlc/worktrees/' "${REPO_ROOT}/.gitignore" 2>/dev/null; then
245251
echo "ERROR: .ai-dlc/worktrees/ is not in .gitignore. Run Step 1 first." >&2
246252
exit 1
247253
fi

plugin/skills/reset/SKILL.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ Clears all AI-DLC state for the current branch. Use this to:
2222
- Clean up after completing a task
2323
- Abandon a task that's no longer needed
2424

25-
This only clears AI-DLC state. It does not:
25+
This clears AI-DLC state and removes worktrees. It does not:
2626
- Undo code changes
2727
- Delete branches
2828
- Revert commits
2929

30-
The work you did is preserved in git. Only the AI-DLC workflow state is cleared.
30+
The work you did is preserved in git. Only the AI-DLC workflow state and worktrees are cleared.
3131

3232
## Implementation
3333

@@ -89,6 +89,27 @@ TeamDelete()
8989

9090
**Without Agent Teams:** Skip this step entirely. No team exists to clean up.
9191

92+
### Step 1c: Cleanup Worktrees
93+
94+
Remove the intent worktree and any unit worktrees for this intent:
95+
96+
```bash
97+
INTENT_SLUG=$(han keep load intent-slug --quiet 2>/dev/null || echo "")
98+
REPO_ROOT=$(git worktree list --porcelain | head -1 | sed 's/^worktree //')
99+
100+
if [ -n "$INTENT_SLUG" ]; then
101+
# Remove unit worktrees (pattern: {intent-slug}-{unit-slug})
102+
for wt in "${REPO_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}-"*; do
103+
[ -d "$wt" ] && git worktree remove --force "$wt" 2>/dev/null
104+
done
105+
# Remove intent worktree
106+
[ -d "${REPO_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}" ] && \
107+
git worktree remove --force "${REPO_ROOT}/.ai-dlc/worktrees/${INTENT_SLUG}" 2>/dev/null
108+
fi
109+
110+
git worktree prune
111+
```
112+
92113
### Step 2: Delete All AI-DLC Keys
93114

94115
```bash
@@ -112,6 +133,7 @@ Output:
112133
AI-DLC state cleared.
113134
114135
All iteration data, intent, criteria, and notes have been removed.
136+
Worktrees cleaned up.
115137
116138
To start a new task, run `/elaborate`.
117139
```

plugin/skills/resume/SKILL.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,14 @@ starting_hat=$(get_recommended_hat ".ai-dlc/${slug}" "${workflow}")
115115
**CRITICAL: The orchestrator MUST run in the intent worktree, not the main working directory.**
116116

117117
```bash
118-
PROJECT_ROOT=$(git rev-parse --show-toplevel)
118+
REPO_ROOT=$(git worktree list --porcelain | head -1 | sed 's/^worktree //')
119119
INTENT_BRANCH="ai-dlc/${slug}/main"
120-
INTENT_WORKTREE="${PROJECT_ROOT}/.ai-dlc/worktrees/${slug}"
120+
INTENT_WORKTREE="${REPO_ROOT}/.ai-dlc/worktrees/${slug}"
121121

122-
mkdir -p "${PROJECT_ROOT}/.ai-dlc/worktrees"
123-
if ! grep -q '\.ai-dlc/worktrees/' "${PROJECT_ROOT}/.gitignore" 2>/dev/null; then
124-
echo '.ai-dlc/worktrees/' >> "${PROJECT_ROOT}/.gitignore"
125-
git add "${PROJECT_ROOT}/.gitignore"
122+
mkdir -p "${REPO_ROOT}/.ai-dlc/worktrees"
123+
if ! grep -q '\.ai-dlc/worktrees/' "${REPO_ROOT}/.gitignore" 2>/dev/null; then
124+
echo '.ai-dlc/worktrees/' >> "${REPO_ROOT}/.gitignore"
125+
git add "${REPO_ROOT}/.gitignore"
126126
git commit -m "chore: gitignore .ai-dlc/worktrees"
127127
fi
128128

0 commit comments

Comments
 (0)