Skip to content
151 changes: 151 additions & 0 deletions .no-mistakes/evidence/fm/th-destroy-v2/destroy-v2-cli-transcript.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
Treehouse destroy v2 CLI evidence
binary: /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse
repo A: /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a
repo B: /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-b

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse destroy --all --yes)
--all requires a pool path; name the pool to clear, e.g. 'treehouse destroy . --all'
(exit 1)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse destroy /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a --all --force)
unknown flag: --force
(exit 1)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse get --lease --lease-holder secondmate-home)
stdout:
/Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/home/.treehouse/repo-a-6e5250/1/repo-a
stderr:
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/repo-a-6e5250/1/repo-a. Run 'treehouse return ~/.treehouse/repo-a-6e5250/1/repo-a' to release it.
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse destroy /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a --all --yes)
🌳 Destroyed 0 worktrees in ~/.treehouse/repo-a-6e5250.
🌳 Skipped 1 worktree:
1 [leased] ~/.treehouse/repo-a-6e5250/1/repo-a leased: name the exact path with --include-leased (never removed by --all)
(exit 0)

$ printf disk check: leased worktree after bulk destroy exists=%s\n yes
disk check: leased worktree after bulk destroy exists=yes
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse destroy /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a --all --include-leased --yes)
--include-leased cannot be combined with --all; name the exact worktree path instead (leased worktrees are never removed in bulk)
(exit 1)

$ printf disk check: leased worktree after rejected bulk include exists=%s\n yes
disk check: leased worktree after rejected bulk include exists=yes
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse destroy /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/home/.treehouse/repo-a-6e5250/1/repo-a --include-leased --yes)
🌳 Destroyed 1 worktree in ~/.treehouse/repo-a-6e5250/1/repo-a and freed 389 B.
1 [leased] 389 B ~/.treehouse/repo-a-6e5250/1/repo-a (held by secondmate-home)
(exit 0)

$ printf disk check: leased worktree after named destroy exists=%s\n no
disk check: leased worktree after named destroy exists=no
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse get --lease --lease-holder disposable-demo)
stdout:
/Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/home/.treehouse/repo-a-6e5250/1/repo-a
stderr:
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/repo-a-6e5250/1/repo-a. Run 'treehouse return ~/.treehouse/repo-a-6e5250/1/repo-a' to release it.
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse return /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/home/.treehouse/repo-a-6e5250/1/repo-a)
🌳 Worktree returned to pool.
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse destroy /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/home/.treehouse/repo-a-6e5250/1/repo-a)
🌳 Dry run: would destroy 1 worktree in ~/.treehouse/repo-a-6e5250/1/repo-a and reclaim 389 B.
1 [disposable] 389 B ~/.treehouse/repo-a-6e5250/1/repo-a
🌳 Re-run with --yes to destroy this worktree.
(exit 0)

$ printf disk check: disposable worktree after dry run exists=%s\n yes
disk check: disposable worktree after dry run exists=yes
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse destroy /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/home/.treehouse/repo-a-6e5250/1/repo-a --yes)
🌳 Destroyed 1 worktree in ~/.treehouse/repo-a-6e5250/1/repo-a and freed 389 B.
1 [disposable] 389 B ~/.treehouse/repo-a-6e5250/1/repo-a
(exit 0)

$ printf disk check: disposable worktree after --yes exists=%s\n no
disk check: disposable worktree after --yes exists=no
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse get --lease --lease-holder dirty-demo)
stdout:
/Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/home/.treehouse/repo-a-6e5250/1/repo-a
stderr:
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/repo-a-6e5250/1/repo-a. Run 'treehouse return ~/.treehouse/repo-a-6e5250/1/repo-a' to release it.
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse return /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/home/.treehouse/repo-a-6e5250/1/repo-a)
🌳 Worktree returned to pool.
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse destroy /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/home/.treehouse/repo-a-6e5250/1/repo-a --yes)
🌳 Destroyed 0 worktrees in ~/.treehouse/repo-a-6e5250/1/repo-a.
🌳 Skipped 1 worktree:
1 [dirty] ~/.treehouse/repo-a-6e5250/1/repo-a re-run with --include-unlanded to include
did not destroy 1 (dirty); re-run with --include-unlanded
(exit 1)

$ printf disk check: dirty worktree after missing flag exists=%s\n yes
disk check: dirty worktree after missing flag exists=yes
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse destroy /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/home/.treehouse/repo-a-6e5250/1/repo-a --include-unlanded --yes)
🌳 Destroyed 1 worktree in ~/.treehouse/repo-a-6e5250/1/repo-a and freed 425 B.
1 [dirty] 425 B ~/.treehouse/repo-a-6e5250/1/repo-a (uncommitted changes)
(exit 0)

$ printf disk check: dirty worktree after --include-unlanded exists=%s\n no
disk check: dirty worktree after --include-unlanded exists=no
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse get --lease --lease-holder scoped-a)
stdout:
/Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/home/.treehouse/repo-a-6e5250/1/repo-a
stderr:
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/repo-a-6e5250/1/repo-a. Run 'treehouse return ~/.treehouse/repo-a-6e5250/1/repo-a' to release it.
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse return /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/home/.treehouse/repo-a-6e5250/1/repo-a)
🌳 Worktree returned to pool.
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-b && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse get --lease --lease-holder scoped-b)
stdout:
/Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/home/.treehouse/repo-b-69712f/1/repo-b
stderr:
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/repo-b-69712f/1/repo-b. Run 'treehouse return ~/.treehouse/repo-b-69712f/1/repo-b' to release it.
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-b && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse return /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/home/.treehouse/repo-b-69712f/1/repo-b)
🌳 Worktree returned to pool.
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse destroy /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-a --all --yes)
🌳 Destroyed 1 worktree in ~/.treehouse/repo-a-6e5250 and freed 389 B.
1 [disposable] 389 B ~/.treehouse/repo-a-6e5250/1/repo-a
(exit 0)

$ printf disk check: repo A scoped worktree exists=%s\n no
disk check: repo A scoped worktree exists=no
(exit 0)

$ printf disk check: repo B scoped worktree preserved=%s\n yes
disk check: repo B scoped worktree preserved=yes
(exit 0)

$ (cd /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/repo-b && /Users/kunchen/.no-mistakes/worktrees/8c0b202f5740/01KVWAAJ5Q1K6HK0XF52FN0NB8/.no-mistakes/tmp/destroy-v2-e2e/treehouse status)
1 available ~/.treehouse/repo-b-69712f/1/repo-b
(exit 0)
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ make test
- No daemon - all operations are inline CLI commands
- Detached HEAD worktrees reset to whichever of local or origin default branch is further ahead (prefers origin on divergence)
- In-use detection uses process scanning plus short-lived persisted owner reservations for lifecycle operations
- Durable leases are a separate, process-independent reservation: `WorktreeEntry.Leased`/`LeaseHolder`/`LeasedAt` persist in the state file (all `omitempty`, so pre-lease state files keep today's behavior). A lease is NOT derived from live processes, so it survives with zero processes inside the worktree and `healState` never clears it (it only clears dead owner reservations). Leased worktrees are skipped by `Acquire` and `prune`, treated as in-use by `worktreeInUse` (so non-force `destroy` rejects them), surfaced by `status` as `StatusLeased`, and cleared by `Release` (`return`)
- Durable leases are a separate, process-independent reservation: `WorktreeEntry.Leased`/`LeaseHolder`/`LeasedAt` persist in the state file (all `omitempty`, so pre-lease state files keep today's behavior). A lease is NOT derived from live processes, so it survives with zero processes inside the worktree and `healState` never clears it (it only clears dead owner reservations). Leased worktrees are skipped by `Acquire` and `prune`, classified `DestroyLeased` by destroy (removable only when the exact path is named with `--include-leased`, NEVER via `--all`), surfaced by `status` as `StatusLeased`, and cleared by `Release` (`return`)
- `destroy` is safe-by-default and mirrors `prune`: dry-run unless `--yes`, narrow explicit targets (`destroy <path>` for one worktree; `destroy <pool> --all` for that pool only - there is NO cross-pool/global destroy, and `--all` with no pool target is an error). The old blunt `--force` flag is REMOVED (this was the v2.0.0 breaking change); each risk class is its own opt-in: `--include-unlanded` (dirty, unmerged, or unverified), `--include-in-use` (running process or owner reservation; processes terminated cleanly first), `--include-leased` (leased, single named path only). A bare `--all --yes` removes only the disposable set (merged, clean, idle, unleased) and skips the rest with the flag that would include each. Bulk skips exit 0; a single-target skip exits non-zero. Entry points: `pool.DestroyWorktree` (single path, `allowLeased=true`) and `pool.DestroyPool` (bulk, `allowLeased=false`). Both share `classifyForDestroy` in `internal/pool/destroy.go`, which reuses prune's classification primitives (`ownerAlive`, `process.FindProcessesInWorktree`, `backingRepositoryMissing`, `git.IsDirty`, `git.IsHeadMergedIntoRef` against the `resolvePruneDefaultRef` ref) so destroy and prune agree on leased/in-use/unlanded/unverified/disposable. Removal keeps the same two-phase reservation as prune (reserve under flock, run `pre_destroy` hooks, remove only worktrees whose `sameDestroyReservation` still holds), so a worktree re-acquired during its hook is never deleted
- `get --lease` (see `getLeaseRunE`) is the non-interactive acquire: it implies the durable lease, opens no subshell, routes post-create hook stdout and all banners to stderr, and prints ONLY the worktree path to stdout so `path=$(treehouse get --lease)` is clean. `--lease-holder`/`$TREEHOUSE_LEASE_HOLDER` set the recorded holder. `pool.AcquireLease` is the entry point; both it and `Acquire` delegate to the shared `acquire(..., acquireOptions)` core, and `markAcquired` stamps either a lease or an owner reservation. Concurrency safety comes from the existing `WithStateLock` (flock) around all pool mutation
- Dirty checks include untracked files even when repository config hides them from normal `git status` output
- Prune deletes only idle managed worktrees that are clean and whose HEAD is merged into the default branch; dry run is the default
Expand Down
Loading
Loading