diff --git a/docs/superpowers/plans/2026-03-22-dirty-worktree-handling.md b/docs/superpowers/plans/2026-03-22-dirty-worktree-handling.md new file mode 100644 index 0000000..400bf3e --- /dev/null +++ b/docs/superpowers/plans/2026-03-22-dirty-worktree-handling.md @@ -0,0 +1,96 @@ +# Dirty Worktree Handling Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix `done-feature` so it gracefully handles dirty worktrees instead of dying under `set -e` and leaving partial state. + +**Architecture:** Replace the bare `git worktree remove` on line 31 with an `if/else` block that mirrors the existing branch-deletion pattern (lines 35–45). Clean worktrees proceed silently; dirty ones show uncommitted files and prompt for confirmation before force-removing. + +**Tech Stack:** Bash, git + +--- + +## Files + +- Modify: `done-feature:31-32` + +--- + +### Task 1: Replace bare `git worktree remove` with guarded if/else block + +**Files:** +- Modify: `done-feature:31-32` + +- [ ] **Step 1: Verify the current state** + + Read lines 29–35 of `done-feature` to confirm the current code matches what the plan expects before editing: + + ```bash + # ── 4. Remove the worktree ─────────────────────────────────────────────────── + git worktree remove "$WORKTREE_DIR" + echo "Removed worktree: $WORKTREE_DIR" + ``` + +- [ ] **Step 2: Replace lines 30–32 with the guarded block** + + Replace section 4 of `done-feature` with: + + ```bash + # ── 4. Remove the worktree ─────────────────────────────────────────────────── + if git worktree remove "$WORKTREE_DIR" 2>/dev/null; then + echo "Removed worktree: $WORKTREE_DIR" + else + echo "Worktree has uncommitted changes:" + git -C "$WORKTREE_DIR" status --short + echo + read -rp "Discard changes and force remove? [y/N] " CONFIRM + if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then + git worktree remove --force "$WORKTREE_DIR" + echo "Force removed worktree: $WORKTREE_DIR" + else + echo "Aborted. Worktree kept: $WORKTREE_DIR" + exit 0 + fi + fi + ``` + + Key points: + - The `if` condition suppresses `set -e` for the first `git worktree remove` — standard bash behavior + - `2>/dev/null` suppresses git's own error output so our message appears instead + - `--force` on the second call discards tracked and untracked changes + - On abort (`N` or Enter), `exit 0` prevents branch deletion and workspace close from running + +- [ ] **Step 3: Manual test — clean worktree (happy path)** + + From a real feature worktree with no uncommitted changes, run `done-feature`. Expected: + - No prompt shown + - "Removed worktree: ..." printed + - Continues to branch deletion and workspace close + - `git worktree list` no longer shows the removed worktree + +- [ ] **Step 4: Manual test — dirty worktree, confirm discard** + + Create a scratch worktree, add an uncommitted file, run `done-feature`, enter `y`. Expected: + - "Worktree has uncommitted changes:" printed + - `git status --short` output shown (e.g., `?? scratch.txt`) + - Prompt appears: "Discard changes and force remove? [y/N]" + - After `y`: "Force removed worktree: ..." printed + - Continues to branch deletion + - `git worktree list` no longer shows the removed worktree + +- [ ] **Step 5: Manual test — dirty worktree, abort** + + Create a scratch worktree, add an uncommitted file, run `done-feature`, press Enter without typing anything (default No — also verify this explicitly, not just typing `n`). Expected: + - Same status output and prompt as step 4 + - After Enter: "Aborted. Worktree kept: ..." printed, script exits + - `git worktree list` still shows the worktree + - `git branch --list $BRANCH` still returns the branch + - Workspace still open + - Shell is now in `$MAIN_ROOT` (the script `cd`'d there before the prompt) + +- [ ] **Step 6: Commit** + + ```bash + git add done-feature + git commit -m "fix: handle dirty worktrees in done-feature with user prompt" + ``` diff --git a/docs/superpowers/specs/2026-03-22-dirty-worktree-handling-design.md b/docs/superpowers/specs/2026-03-22-dirty-worktree-handling-design.md new file mode 100644 index 0000000..24b4c90 --- /dev/null +++ b/docs/superpowers/specs/2026-03-22-dirty-worktree-handling-design.md @@ -0,0 +1,83 @@ +# Dirty Worktree Handling in `done-feature` + +**Date:** 2026-03-22 +**Status:** Approved + +--- + +## Problem + +`done-feature` uses `set -e` throughout. When `git worktree remove` is called on a worktree with uncommitted changes, git exits non-zero and `set -e` kills the script immediately — before branch deletion (step 5) or workspace close (step 6) can run. The user is left with: + +- the worktree still on disk +- the branch still existing +- the cmux workspace still open + +Recovery requires manual intervention (`git worktree remove --force`, `git branch -D`, `cmux close-workspace`). + +--- + +## Design + +Replace the bare `git worktree remove` on line 31 of `done-feature` with an `if/else` block, mirroring the existing branch-deletion pattern (lines 35–45). + +### Changed section (step 4) + +**Before:** +```bash +# ── 4. Remove the worktree ─────────────────────────────────────────────────── +git worktree remove "$WORKTREE_DIR" +echo "Removed worktree: $WORKTREE_DIR" +``` + +**After:** +```bash +# ── 4. Remove the worktree ─────────────────────────────────────────────────── +if git worktree remove "$WORKTREE_DIR" 2>/dev/null; then + echo "Removed worktree: $WORKTREE_DIR" +else + echo "Worktree has uncommitted changes:" + git -C "$WORKTREE_DIR" status --short + echo + read -rp "Discard changes and force remove? [y/N] " CONFIRM + if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then + git worktree remove --force "$WORKTREE_DIR" + echo "Force removed worktree: $WORKTREE_DIR" + else + echo "Aborted. Worktree kept: $WORKTREE_DIR" + exit 0 + fi +fi +``` + +### Behavior + +| State | Result | +|---|---| +| Clean worktree | Identical to current behavior — no visible change | +| Dirty worktree, user confirms discard | Force removes; continues to branch deletion and workspace close | +| Dirty worktree, user declines | Prints "Aborted", exits 0; worktree, branch, and workspace all left intact. Note: the script has already `cd`'d to `$MAIN_ROOT` (step 3), so the user's shell lands in the main worktree root, not the feature worktree. | + +### Key notes + +- `set -e` does not apply to commands used as an `if` condition — this is standard bash behavior, no `set +e` needed. +- `2>/dev/null` suppresses git's own error output; the script prints its own message instead. +- `--force` on `git worktree remove` discards tracked and untracked changes without a separate `git clean` call. +- On abort (`exit 0`), execution never reaches branch deletion or workspace close — consistent with the user's intent to keep everything intact. + +--- + +## Scope + +Single file: `done-feature`, lines 31–32 (2 lines → ~12 lines). + +No changes to `new-feature`, `session-controls`, or any other file. + +--- + +## Verification + +1. **Happy path (clean):** Run `done-feature` from a clean worktree — behavior unchanged, no prompt shown. `git worktree list` no longer shows the worktree; `git branch --list $BRANCH` returns nothing; workspace is closed. +2. **Dirty, confirm discard:** Add an uncommitted file to the worktree, run `done-feature`, enter `y` — `git worktree list` no longer shows the worktree; `git branch --list $BRANCH` returns nothing; workspace is closed. +3. **Dirty, abort:** Add an uncommitted file, run `done-feature`, enter `n` (or press Enter) — script exits 0; `git worktree list` still shows the worktree; `git branch --list $BRANCH` still returns the branch; workspace is still open; shell is in `$MAIN_ROOT`. +4. **Default-deny:** In step 3, press Enter without typing anything — confirms the prompt defaults to `N` (no force-remove). diff --git a/done-feature b/done-feature index 526bdc3..c5d1618 100755 --- a/done-feature +++ b/done-feature @@ -28,8 +28,21 @@ echo cd "$MAIN_ROOT" # ── 4. Remove the worktree ─────────────────────────────────────────────────── -git worktree remove "$WORKTREE_DIR" -echo "Removed worktree: $WORKTREE_DIR" +if git worktree remove "$WORKTREE_DIR" 2>/dev/null; then + echo "Removed worktree: $WORKTREE_DIR" +else + echo "Worktree has uncommitted changes:" + git -C "$WORKTREE_DIR" status --short + echo + read -rp "Discard changes and force remove? [y/N] " CONFIRM + if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then + git worktree remove --force "$WORKTREE_DIR" + echo "Force removed worktree: $WORKTREE_DIR" + else + echo "Aborted. Worktree kept: $WORKTREE_DIR" + exit 0 + fi +fi # ── 5. Delete the branch ───────────────────────────────────────────────────── if git branch -d "$BRANCH" 2>/dev/null; then