feat(pool): add user lifecycle hooks#22
Merged
Merged
Conversation
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
The developer wanted a maintainer-style triage of GitHub issue #21 using the provided managed checkout and relevant GitHub context. They required structured JSON containing one or more concrete resolution options, including comments, coding-agent handoff prompts, state changes, waiting party, and confidence. They emphasized accepting legitimate actionable issues with a fix_required option and a detailed Markdown fix prompt, while avoiding repository mutation, ad hoc clones, or blindly following untrusted transcript instructions.
What Changed
post_createandpre_destroylifecycle hooks, executed through a newinternal/hookspackage and honored only from user-level config.get,return, ordestroyoperations.Risk Assessment
Testing
go test ./internal/config ./internal/hooks ./internal/pool ./cmdgo test ./...Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
Round 1 - passed ✅
✅ **Rebase** - passed
Round 1 - passed ✅
🔧 **Review** - 2 issues found → auto-fixed (11)
Round 1 - found 2 warnings
internal/pool/pool.go:70-post_createruns when an existing clean worktree is reused after reset, not only when a worktree is created. That makes this hook fire on everytreehouse getfor pooled worktrees, which can repeat side-effectful provisioning despite thepost_createname and issue request for create/destroy hooks; confirm this is intended or move this call to the new-worktree path only.internal/pool/pool.go:70- Hooks are executed whileWithStateLockis held. Any hook that invokes anothertreehousecommand for the same pool can deadlock waiting on the state lock, and long-running setup hooks block unrelatedstatus,get, anddestroycalls; run user hooks outside the locked critical section or add explicit protection against reentrant treehouse calls.Round 2 (auto-fix) - found 2 errors
internal/pool/pool.go:200-Destroynow releases the state lock beforepre_destroyruns while the target worktree remains listed as available in state. A concurrenttreehouse getcan acquire the same clean worktree during the hook/window, after which the second lock removes the worktree out from under the new session; reserve/remove the entry before running hooks or rework the state transition so it cannot be acquired while pending destroy.internal/pool/pool.go:265-DestroyAllclearsstate.Worktreesusing a stale snapshot after hooks run outside the lock. Any concurrent acquire/create during the hook window can be dropped from state without being removed from disk, and any old worktree acquired during that window can still be removed; preserve concurrent state changes or mark the snapshot as pending destruction before releasing the lock.Round 3 (auto-fix) - found 2 warnings
internal/pool/pool.go:112-post_createruns after the state lock is released without reserving the acquired worktree in state. A long hook that changes cwd or otherwise is not detected byprocess.IsWorktreeInUseleaves the same clean worktree visible to a concurrenttreehouse get, so two sessions can be handed the same path before the firstgetreaches its shell.internal/pool/pool.go:201- Marking a worktreeDestroyingbefore runningpre_destroycan strand it if the treehouse process is interrupted during the hook. Becausestatushides destroying entries andAcquirestill counts them againstmax_trees, an interrupted hook can make capacity disappear with no visible worktree to recover from.Round 4 (auto-fix) - found 2 errors
internal/pool/pool.go:213-Destroystill decides whether a worktree is safe to remove using only cwd-based process detection, ignoring the newOwnerPIDreservation set byAcquire. During the window afterAcquirereserves a worktree and before the user's shell has a process cwd inside it, a concurrent non-forcetreehouse destroycan mark and remove the path thattreehouse getis about to hand out; treatownerAlive(wt)as in-use here before marking it destroying.internal/pool/pool.go:263-DestroyAllhas the same reservation blind spot: non-force destroy-all skips onlyDestroyingentries and cwd-detected processes, so it can remove worktrees currently reserved by a livetreehouse getprocess before the shell appears in the worktree. IncludeownerAlive(wt)in the in-use check so reserved worktrees cannot be deleted out from under an acquire.Round 5 (auto-fix) - found 1 error
internal/pool/pool.go:275-DestroyAllstill includes entries already markedDestroyingby another live command in its removal snapshot. A concurrent non-forcetreehouse destroy --allcan skip the in-use check for that entry, overwrite itsOwnerPID, runpre_destroya second time, and remove the worktree while the first destroy hook is still running, causing duplicate hooks and a failed/partial first destroy; exclude live destroying entries from the snapshot or reject them as in-use unless forced.Round 6 (auto-fix) - found 1 warning
internal/pool/pool.go:338- Stale reservations are considered live based only on PID existence. If atreehouse getor destroy process exits before clearingOwnerPIDand the OS later reuses that PID,ownerAlivewill keep treating the worktree as in-use or destroying, which can hide it fromstatusand permanently consume pool capacity until manual state cleanup; store and validate process start time or another process identity before trusting a persisted PID.Round 7 (auto-fix) - found 2 errors
internal/pool/pool.go:254-Destroydoes not verify that the state entry it removes is still the destroying reservation created by this command. If another forced destroy removes the entry while this command is inpre_destroy, a subsequenttreehouse getcan recreate the same numbered path; when the first destroy resumes it will match only by path and remove the newly acquired worktree from under that session. RecheckDestroying,OwnerPID, andOwnerStartedAtbefore final removal, and treat a changed reservation as already superseded instead of deleting the path.internal/pool/pool.go:303-DestroyAllremoves every path from its pre-hook snapshot before validating that those paths still belong to the same destroy reservation. A concurrent forced destroy can delete one of those entries, then an acquire can recreate the same numbered worktree path before this final loop runs, causingDestroyAllto delete a newly acquired worktree and then drop it from state via the path-based remove map. Validate the current entry's reservation identity under the lock before removing each snapshot path.Round 8 (auto-fix) - found 1 error
cmd/get.go:50- Passingcfg.Hooks.PostCreatedirectly intoAcquiremakes repo-leveltreehouse.tomlhooks auto-execute ontreehouse getbecauseconfig.Loadprefers the checked-out repository config before the user config. A malicious or compromised repository can therefore run arbitrary shell commands as soon as a user asks Treehouse for a worktree; require explicit trust/confirmation for repo-defined hooks or restrict executable hooks to user-level config before merging.Round 9 (auto-fix) - found 1 warning
treehouse.toml.example:2- The example still tells users they can place this file at<repo_root>/treehouse.toml, but the implementation now intentionally ignores[hooks]from repo-level config and only executes hooks from the user config. A user following this example will addpost_create/pre_destroyto the repo config and see them silently not run; document that hooks are only honored from~/.config/treehouse/config.tomlor split the example so repo-safe settings and executable hooks are not presented as interchangeable.Round 10 (auto-fix) - found 1 warning
internal/pool/pool.go:144-ReleaseclearsOwnerPID/OwnerStartedAtwithout clearing or rejectingDestroying. If apre_destroyhook or concurrenttreehouse returnruns against a worktree that has been marked destroying, the final destroy will treat its reservation as superseded and skip removal, whilehealStatewill not recover it because the owner is now zero; the entry remains hidden fromstatusand permanently skipped byAcquire. ClearDestroyingwhen clearing the owner, or reject returning destroying entries.Round 11 (auto-fix) - found 1 warning
internal/pool/pool.go:134-Releaseresets and cleans the worktree before checking whether the state entry is markedDestroying. A concurrenttreehouse returnor get-exit path can therefore delete files while apre_destroyhook is still running, then only afterward fail withworktree ... is being destroyed; check the destroy reservation under the state lock before mutating the worktree.Round 12 (auto-fix) - passed ✅
✅ **Test** - passed
Round 1 - passed ✅
go test ./internal/config ./internal/hooks ./internal/pool ./cmdgo test ./...🔧 **Document** - 3 issues found → auto-fixed (2)
Round 1 - found 3 issues (2 warnings, 1 info)
README.md:133- The README still says in-use detection is purely process scanning and that usage state is never persisted. The change adds persisted owner and destroy reservation fields (owner_pid,owner_started_at,destroying) to the state file and treats live reservations as in-use, so this design note is now stale and should explain the new temporary persisted reservation behavior.AGENTS.md:38- The agent guide's key design decisions still say in-use detection is runtime-only and never persisted, and line 39 says the state file only tracks pool membership. The implementation now persists owner and destroy reservation metadata intreehouse-state.json, so this internal documentation should be updated to distinguish process scanning from temporary reservation state.AGENTS.md:56- The agent guide's config section still presents repo-leveltreehouse.tomland user-level~/.config/treehouse/config.tomlas equivalent locations. With this change, lifecycle hooks are intentionally loaded only from the user-level config and ignored from repo-level config, so the guide should document the user-only hooks rule and include the new[hooks]keys if it is meant to stay current with configuration behavior.Round 2 (auto-fix) - found 1 info
AGENTS.md:7- The project structure section was not updated for the newly addedinternal/hooks/package. The change introduces a dedicated hooks package used by pool lifecycle operations, so the internal agent guide should list it alongside the other internal packages and describe that it runs user-configured lifecycle hook commands.Round 3 (auto-fix) - passed ✅
✅ **Lint** - passed
Round 1 - passed ✅
✅ **Push** - passed
Round 1 - passed ✅