feat(pool): add durable worktree leases#35
Merged
Conversation
Add a persistent, process-independent worktree reservation so a caller can permanently hold a pooled worktree as a home without keeping a live process inside it. This replaces the fragile process-based hold that consumers like firstmate's persistent secondmate homes relied on. - New WorktreeEntry.Leased/LeaseHolder/LeasedAt persistent state, all omitempty so pre-lease state files keep today's behavior. - `treehouse get --lease` acquires without a subshell, marks the worktree leased, and prints only its absolute path to stdout (all banners and hook output go to stderr), so `path=$(treehouse get --lease)` is clean in scripts. `--lease-holder` / $TREEHOUSE_LEASE_HOLDER records the holder. - Leased worktrees are skipped by get and prune regardless of running processes, require --force to destroy, and healState never clears them. - `treehouse return <path>` clears the lease and returns the worktree, keeping its existing lingering-process termination behavior. - `treehouse status` shows a distinct `leased` state with the holder. - Concurrency is guarded by the existing WithStateLock (flock); both Acquire and AcquireLease delegate to a shared acquire() core. Tests cover: lease prints only the path to stdout with info on stderr; a leased worktree is skipped by get and prune with no process inside; return releases it; status shows the leased state; concurrent acquires never double-lease. README and AGENTS.md document the flag and the lease concept.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Add a durable worktree lease to treehouse so a caller (firstmate's persistent secondmate homes) can permanently reserve a pooled worktree without keeping a live process inside it. Today treehouse decides in-use vs available from live processes plus short-lived owner reservations, and 'get' opens an interactive subshell whose lifetime IS the hold - so a process-less home is unreclaimable-safe.
Design decisions:
Back-compat: default interactive 'get' subshell behavior is unchanged. Tests added at pool level (lease marking, skipped-by-get, skipped-by-prune with no process, return clears lease, status shows leased, healState preserves lease, non-force destroy rejected, concurrent acquires never double-lease - the last verified under -race) and e2e CLI level (stdout-only path, holder recorded, leased skipped by get and prune, return releases). README and AGENTS.md document the flag and the lease concept. Windows/Linux cross-builds, go vet, and gofmt all clean.
What Changed
treehouse get --lease, including holder metadata, path-only stdout for scripting, and persisted lease state that survives without live processes.treehouse return <path>.Risk Assessment
Testing
No separate baseline output was supplied; I ran the lease-focused Go tests, the concurrent acquire race test, the full Go suite, and an isolated CLI workflow that demonstrates clean stdout capture, persisted holder state without an owner PID, skip-by-get, survival through prune, non-force destroy rejection, and release plus reuse. All checks passed, and the reviewer-visible transcript is saved in the requested evidence directory.
Evidence: durable lease CLI E2E transcript
Source: durable lease CLI E2E transcript
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed (3) ✅
internal/pool/state.go:26-omitemptydoes not omit a zerotime.Time, so cleared or never-leased entries will still write"leased_at": "0001-01-01T00:00:00Z"afterWriteState. That contradicts the intended sparse lease metadata and leaves stale-looking lease fields in the state file. Useomitzeroor make this a*time.Timeso non-leased entries omit it.🔧 Fix: Omit cleared lease timestamps from state
1 warning still open:
cmd/get.go:122- The lease banner advertisestreehouse return <path>as the release command, butreturnstill derives the repo and pool from the caller's cwd rather than from the provided path. A script that captures a leased path and later runs this command outside the original repo, or from another repo, cannot release the lease. Either make explicit-path return resolve config from that worktree path, or document the cwd requirement here.🔧 Fix: Fix return-by-path lease release
1 warning still open:
cmd/return_cmd.go:29-return <path>now resolves the pool from the managed worktree's main repo, butget --leasestill creates the pool from the caller'sgit.FindRepoRoot()result. If the lease was acquired while running inside a linked worktree whose directory name or relative config root differs from the main checkout, return-by-path from another directory will look in a different pool and report the path as unmanaged. Use the same canonical repo root on acquire and return, or add a deliberate fallback for existing linked-worktree pool identities.🔧 Fix: Fix return-by-path pool lookup
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
TREEHOUSE_NO_UPDATE_CHECK=1 go test ./internal/pool ./cmd -run 'Test(AcquireLease|Release_ClearsLease|GetLease|LeasedWorktree|Return.*Lease)' -count=1TREEHOUSE_NO_UPDATE_CHECK=1 go test -race ./internal/pool -run TestAcquireLease_ConcurrentAcquiresNeverDoubleLease -count=1TREEHOUSE_NO_UPDATE_CHECK=1 go test ./...Manual CLI E2E: builttreehouse, then in an isolated fixture repo rantreehouse get --lease --lease-holder secondmate-home,treehouse status,SHELL=<true> treehouse get,treehouse prune --yes,printf 'y\n' | treehouse destroy <leased-path>,treehouse return <leased-path>, andSHELL=<true> treehouse get; transcript saved to.no-mistakes/evidence/fm/treehouse-lease-t4/lease-cli-e2e.txt.✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.