Skip to content

perf(pool): skip merge-state checks for plain slot listing - #184

Open
jadhavgaurav wants to merge 3 commits into
kunchenguid:mainfrom
jadhavgaurav:fix/status-unlanded-176
Open

jadhavgaurav wants to merge 3 commits into
kunchenguid:mainfrom
jadhavgaurav:fix/status-unlanded-176

Conversation

@jadhavgaurav

@jadhavgaurav jadhavgaurav commented Oct 15, 2026 •

Copy link
Copy Markdown

Intent

Fix #176: treehouse status reported an idle, clean slot holding commits that are not merged into the base as available, although get skips such a slot. status now reports it as unlanded, using the same merge-state proof get uses (headLandedForReuse) against the same base get would resolve, so unproven work is never shown as free and the two commands agree. The pool-full error counts the skipped slots and names the base.

Follow-up for the Windows timeout the maintainer reported: the cmd and internal/pool test packages ran into the 10 minute limit. Counting every git invocation over each whole package on macOS, the first version added about 590 calls to go test ./cmd (8989 to 9581) and about 560 to ./internal/pool (10552 to 11113), all light read-only calls. That is a 5 to 7 percent increase in invocations, which is real but small next to main's own Windows spread (cmd 378 to 552 seconds, internal/pool 408 to 587 seconds across three recent main runs). The listing used by enter and return --all does not need the proof, so it no longer runs it.

What Changed

  • status reports a clean, idle slot whose HEAD is not proven merged as unlanded (text and --json), with a hint that get will not reuse it. get and status share headLandedForReuse, and any check that errors counts as unlanded.
  • The base is the one get resolves (pool.ResolveBaseBranch, including a configured base_branch), so a slot merged only into the configured base reads available in both commands.
  • pool.List keeps its pre-existing signature and never judges merge state. status calls the new pool.ListWithMergeState, which resolves the base lazily, at most once, and only when a slot is idle and clean. enter and return --all use the plain listing, which is also how they always saw such a slot (return --all leaves it alone either way).
  • The pool-full error from get counts unlanded slots and names the base.
  • Tests: unlanded reported and skipped by acquire, landed reads available, dirty outranks unlanded, an unlanded slot under the cwd is held only by the cwd, configured base honoured, plain listing never reports unlanded, lazy once-only base resolution, and return --all leaving an unlanded slot alone. docs/design.md and the README describe the split.

Risk Assessment

✅ Low: The change is a well-bounded perf refactor: it extracts the existing merge-state proof into a shared helper, restores pool.List to its pre-PR signature for enter/return --all (which never acted on the unlanded/available distinction anyway), and adds a separate ListWithMergeState with lazy, once-only base resolution for status. The diff matches the stated intent exactly, docs/design.md and README were updated consistently, and the new tests exercise real git repository state (commits, merges, pushes) as independent oracles rather than asserting on source text.

Testing

Built the target-commit binary and exercised it live against a disposable sandbox git repo: simulated an owner-process death leaving a slot with unmerged commits, and confirmed status/get/return --all all treat it as unlanded consistently; also ran the targeted pool and cmd test suites covering List, ListWithMergeState, status, enter, and return --all, all passing. No issues found; sandbox and built binary were torn down after testing.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
status reports an idle, clean slot with unmerged commits as unlanded (not available) ✅ pass live Built treehouse binary from target commit, acquired a slot via bare get (owner-reservation mode) in a background subshell, committed unmerged work on a feature branch inside it, then SIGKILLed the o…
get never resets or reuses an unlanded slot, and reports it distinctly from the generic pool-full message ✅ pass live With max_trees=2 and slot 1 unlanded, treehouse get --lease returned the pool-full error naming 'main', '1 unlanded', and 'unlanded' explicitly, matching the new message format in pool.go's acquire(…
return --all leaves an unlanded slot alone while still returning held slots ✅ pass live treehouse return --all --force returned slot 2 (leased) and reported it skipped the one not-held slot; the subsequent status still showed slot 1 as unlanded with its feature branch intact. See 10_…
pool.List (no merge-state proof) never reports unlanded; enter/return --all use it and treat unlanded identically to available ✅ pass live Ran go test ./internal/pool/... -run TestList -v: TestList_WithoutMergeStateNeverReportsUnlanded and TestListWithMergeState_ResolvesBaseLazilyAndOnce both pass, confirming List's signature reverted…
status and get keep agreeing on a configured (non-default) base_branch ✅ pass live Ran targeted package tests: go test ./internal/pool/... -run TestList (includes TestList_HonorsConfiguredBaseBranch) and go test ./cmd/... -run 'TestStatus|TestReturn|TestEnter' (includes TestStat…
Evidence: status shows unlanded slot with explanation
A new version of treehouse is available: v3.0.0-20261015195810-39a4f3a14c18 → v3.1.4
Run "treehouse update" to update

base  main  (repository default)
1     unlanded     ~/.treehouse/repo-c7056d/1/repo  [feature3]  (holds work not merged into the base; 'treehouse get' will not reuse it)
Evidence: get refuses to reuse unlanded slot; pool-full error names it
A new version of treehouse is available: v3.0.0-20261015195810-39a4f3a14c18 → v3.1.4
Run "treehouse update" to update

🌳 Setting up worktree...
all 2 worktrees are in use, dirty, or hold work not merged into "main" (1 unlanded; 'treehouse status' reports them as unlanded, and get never resets them; max_trees = 2). Merge or return that work, or increase max_trees in treehouse.toml
Evidence: return --all skips the unlanded slot, returns the leased one
A new version of treehouse is available: v3.0.0-20261015195810-39a4f3a14c18 → v3.1.4
Run "treehouse update" to update

🌳 Returning 2 (leased) at ~/.treehouse/repo-c7056d/2/repo
🌳 Returned 1 of 1 held worktree(s); 0 skipped; 1 not held.
Evidence: final status: unlanded slot untouched after return --all
A new version of treehouse is available: v3.0.0-20261015195810-39a4f3a14c18 → v3.1.4
Run "treehouse update" to update

base  main  (repository default)
1     unlanded     ~/.treehouse/repo-c7056d/1/repo  [feature3]  (holds work not merged into the base; 'treehouse get' will not reuse it)
2     available    ~/.treehouse/repo-c7056d/2/repo  (detached)

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
status reports an idle, clean slot with unmerged commits as unlanded (not available) ✅ pass live Built treehouse binary from target commit, acquired a slot via bare get (owner-reservation mode) in a background subshell, committed unmerged work on a feature branch inside it, then SIGKILLed the o…
get never resets or reuses an unlanded slot, and reports it distinctly from the generic pool-full message ✅ pass live With max_trees=2 and slot 1 unlanded, treehouse get --lease returned the pool-full error naming 'main', '1 unlanded', and 'unlanded' explicitly, matching the new message format in pool.go's acquire(…
return --all leaves an unlanded slot alone while still returning held slots ✅ pass live treehouse return --all --force returned slot 2 (leased) and reported it skipped the one not-held slot; the subsequent status still showed slot 1 as unlanded with its feature branch intact. See 10_…
pool.List (no merge-state proof) never reports unlanded; enter/return --all use it and treat unlanded identically to available ✅ pass live Ran go test ./internal/pool/... -run TestList -v: TestList_WithoutMergeStateNeverReportsUnlanded and TestListWithMergeState_ResolvesBaseLazilyAndOnce both pass, confirming List's signature reverted…
status and get keep agreeing on a configured (non-default) base_branch ✅ pass live Ran targeted package tests: go test ./internal/pool/... -run TestList (includes TestList_HonorsConfiguredBaseBranch) and go test ./cmd/... -run 'TestStatus|TestReturn|TestEnter' (includes TestStat…
  • go build ./...
  • go test ./internal/pool/... -run TestList -v
  • go test ./cmd/... -run 'TestStatus|TestReturn|TestEnter' -v
  • manual CLI drive: get (reservation mode) + kill -9 owner process + status/get/return --all against a disposable sandbox git repo and pool
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Issues

Closes #176

status marked an idle, clean slot available even when get skips it
because its HEAD is not merged into the base. Share the reuse proof
between List and acquire, report such slots as unlanded, and count
them in the pool-full error.

Fixes kunchenguid#176
Covers part 1 of kunchenguid#162

Signed-off-by: Gaurav Jadhav <gaurav.vjadhav01@gmail.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T15:43:30.723266Z 92ddab5 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@greptile-apps

greptile-apps Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issue was found.

Reviews (3) · Last reviewed commit: "perf(pool): judge merge state only where..." · Reviewed by Greptile

Comment thread internal/pool/pool.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92ddab5193

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/pool/pool.go Outdated
Comment on lines +857 to +859
branch, err := vcs.DefaultBranchForWorktree(wt.Path)
if err != nil {
return false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back to the slot's recorded base

In the detached local-repository case covered by TestRelease_WithBaseBranchSurvivesUnresolvableDefault, a slot can be clean and parked on a resolvable explicit base such as develop while DefaultBranchForWorktree errors. This early return never checks wt.BaseBranch, so List newly labels that pristine slot unlanded and says get will not reuse it even though treehouse get with base_branch = "develop" can reuse it; fall back to the recorded/configured base before returning false.

AGENTS.md reference: AGENTS.md:L24-L24

Useful? React with 👍 / 👎.

…`pool.List` callers, `enter` and `return --all`) judged a slot's landed state only against the repository default branch via `landedOnDefaultBase`, ignoring a configured `base_branch`. This let `status` promise a slot "unlanded" (get won't reuse it) while `get` actually resolves the configured base and reuses the same slot - the exact main/develop disagreement Greptile described. Fix: exported `pool.resolveBaseBranch` as `pool.ResolveBaseBranch` (the same resolution `Acquire` already uses) and threaded a `configuredBase` parameter through `pool.List` into `landedOnDefaultBase`, so it checks the configured base when one resolves, falling back to the repository default only when none is configured. Updated all three `pool.List` call sites (`cmd/status.go`, `cmd/enter.go`, `cmd/return_cmd.go`'s `returnHeldWorktrees`) to resolve and pass the same base `get` would use, so `status`/`enter`/`return --all` can no longer disagree with `get`. Updated the ~22 unaffected test call sites to pass `""` (preserving existing behavior) and added `TestList_HonorsConfiguredBaseBranch` reproducing Greptile's exact scenario (slot cut from main, merged only into develop, `base_branch=develop`): verified it fails on the prior logic and passes with the fix. Updated docs/design.md's description of this invariant to match. Verified: `go build ./...` and `go vet ./...` clean; `cmd` package `TestStatus*`, `TestReturn*`, `TestEnter*` all pass; the new regression test passes. Two pre-existing failures in `internal/pool` (`TestAcquireBranchRejectedReferenceTransactionPreservesIgnoredOutput`/`...ChangedSeed`, about git reference-transaction hook output) reproduce identically on the unmodified base commit and are unrelated to this change (environment/git-hook dependent, not caused by this PR)
@jadhavgaurav jadhavgaurav changed the title fix(pool): report slots holding unlanded commits as unlanded fix(pool): report slots holding unmerged commits as unlanded in status Oct 15, 2026
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks for picking up #176, @jadhavgaurav. I approved fork CI for tip d8a45fa and read the full diff. No security concerns: no workflow changes, and the new status check reuses the same read-only merge-state proof get already runs. I checked it against VISION.md (status and get agreeing on what's reusable, with unproven work protected) and it fits as a fix to existing status output, not a new default. Your live-test table covers the cases I'd want, and Greptile is 5/5 on this tip. The Codex P2 about the recorded base is moot as far as I can tell: when the repository default can't be resolved, get errors out entirely, so status calling that slot unlanded doesn't contradict anything.

One blocker: test (windows-latest) failed on the 10-minute Go test timeout in two packages, cmd at 605.6s (still in TestReturnAllSkipsASlotLeasedMidRun) and internal/pool at 600.1s (in TestRecoveredAutoFreeGates). macOS, Ubuntu, lint, Nix, and the no-mistakes check are green.

Main's Windows lane is already close to that limit, but on this morning's main run the same packages finished in about 396s (cmd) and 427s (internal/pool), so this branch is well over that. A likely reason, which you should verify: pool.List now runs the merge-state git checks for every idle, clean slot, and process startup is slow on Windows, so every existing test that calls List or status pays for it. If that's what's going on, cutting the git calls per listed slot (or skipping the check when nothing could be unlanded) would probably bring the lane back under the limit. I've also queued a rerun of the failed job to rule out a slow runner. This is waiting on you.

pool.List now takes no base and never runs the merge-state proof, so
`enter` and `return --all`, which treat an unlanded slot exactly like an
available one, are back to the git cost they had before. `status` calls
the new pool.ListWithMergeState, which resolves the configured base
lazily and at most once, and only when a slot is idle and clean.

Counted git invocations over `go test ./cmd` on macOS: 8989 on main,
9581 with the previous tip, 9296 now (the remainder is `status`, which
needs the proof, and the new tests).

Signed-off-by: Gaurav Jadhav <gaurav.vjadhav01@gmail.com>
@jadhavgaurav jadhavgaurav changed the title fix(pool): report slots holding unmerged commits as unlanded in status perf(pool): skip merge-state checks for plain slot listing Oct 15, 2026
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: thanks for the follow-up, @jadhavgaurav. I read the new commit 39a4f3a (plain List for enter/return --all, ListWithMergeState with a lazily resolved base only for status), saw no security concerns and no workflow changes, and approved fork CI on this tip. Greptile is still 5/5, and Ubuntu, macOS, lint, Nix, and no-mistakes are all green.

test (windows-latest) still hits Go's 10-minute per-package timeout, though: cmd at 606.1s (in TestGetUniqueLeafPrecedenceFlagOverEnvOverConfig) and internal/pool at 600.1s (in TestRecoveredSubmoduleChangesRemainLeased). That's essentially the same as the previous tip, and yesterday's reruns of that job failed the same way, so it doesn't look like a one-off slow runner. Main's last Windows job took about 10.5 minutes end to end on the same lane, so the extra cost from this branch is what tips those two packages over.

Since the cut to List didn't move the numbers, it may be worth checking where the remaining extra git calls come from on the status path in the existing cmd and pool tests (for example, tests that call status on pools with several idle, clean slots). Separately, whether to raise the Windows test budget on main is an open question with Kun (the same one #173 is waiting on); if that changes, I'll rerun CI here. For now this is waiting on you.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

treehouse status reports slots holding unmerged commits as available, though get will not reuse them

2 participants