Skip to content

fix(bin): isolate Treehouse worktree pool roots per firstmate home - #4932

Open
mehulbhagwani wants to merge 14 commits into
kunchenguid:mainfrom
mehulbhagwani:fm/pool-roots
Open

mehulbhagwani wants to merge 14 commits into
kunchenguid:mainfrom
mehulbhagwani:fm/pool-roots

Conversation

@mehulbhagwani

@mehulbhagwani mehulbhagwani commented Sep 19, 2026 •

Copy link
Copy Markdown

Intent

i need to close all issues of kun (asked to get every one fixed, not closed unfixed) - sticking to his vision.md to all the tickets. The upstream PR #4932 fixes issue #4977: each firstmate home must get its own Treehouse worktree pool root when homes clone the same origin, and Treehouse roots resolving inside a firstmate home must be rejected. Keep the change aligned with every rule in VISION.md.

What Changed

  • Derive a per-home Treehouse worktree pool root during install/bootstrap/spawn so homes cloning the same origin no longer share a pool root, and reject a Treehouse root that resolves inside a firstmate home.
  • Extend fm-wake-lib.sh, fm-home-seed.sh, and fm-install-treehouse.sh with the home-scoped root resolution and validation logic, and update fm-bootstrap.sh/fm-spawn.sh to surface and enforce it.
  • Update docs/architecture.md, docs/configuration.md, and docs/verification/runtime-backends.md to document the per-home pool root behavior, and add fm-treehouse-home-root.test.sh and fm-treehouse-pool-isolation-live-e2e.test.sh coverage alongside fixture/test updates across the existing bootstrap, spawn, backlog, tangle-guard, secondmate, session-start, and x-mode test suites.

🤖 Generated with Claude Code

Risk Assessment

✅ Low: The change gives each firstmate home its own Treehouse worktree pool root via a well-tested, pure derivation function (fm_treehouse_home_root), consistently updates every treehouse get call site to pass --root while correctly leaving treehouse return unmodified (it resolves pool from path), bumps the pinned Treehouse version to one that supports the flag, and adds behavior-driven regression and live e2e tests (including graceful handling of a vendor behavior change already flagged and declined in a prior round). Docs are updated with pointers rather than duplicated detail, consistent with the stated intent to fix upstream issue #4977.

Testing

All four targeted suites completed with exit 0 and zero failures. No regressions found; nothing changed since the prior report.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Fixture fix: fm_test_run_spawn supplies TREEHOUSE_ROOT outside FM_HOME so nested-$HOME callers aren't rejected by the new Treehouse guard ✅ pass live fm-tangle-guard.test.sh 6/6 ok and fm-spawn-dispatch-profile.test.sh 27/27 ok, both exit 0
Each firstmate home derives its own Treehouse worktree pool root, kept outside any Firstmate home, when homes clone the same origin (issue #4977) ✅ pass live fm-treehouse-home-root.test.sh and fm-treehouse-pool-isolation-live-e2e.test.sh relevant cases ok
A Treehouse root that resolves inside a firstmate home is rejected rather than silently accepted ✅ pass live fm-treehouse-home-root.test.sh: 'the worktree root falls back to Treehouse's own base and otherwise refuses' ok
Two clones of the same origin sharing one pool root still hand a freed slot back to the correct acquiring clone ✅ pass live fm-treehouse-pool-isolation-live-e2e.test.sh vendor-baseline cases ok
A worktree leased under a different root still returns to its own pool without migration ✅ pass live fm-treehouse-pool-isolation-live-e2e.test.sh ok
A leased secondmate home derives its worktree from the seeding home's own root, and a rollback return still finds it ✅ pass live fm-treehouse-home-root.test.sh ok
  • Outcome: 🔧 2 issues found → auto-fixed ✅ across 2 runs (34m28s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

🔧 **Test** - 2 issues found → auto-fixed ✅
  • 🚨 tests/fixtures.sh - The new Treehouse in-home-root rejection guard (bin/fm-wake-lib.sh fm_treehouse_home_root) breaks tests/fm-tangle-guard.test.sh's non-worktree spawn-isolation-abort test and tests/fm-spawn-dispatch-profile.test.sh's test_worker_launch_delivers_role_scope test, both of which use the shared tests/fixtures.sh fm_test_run_spawn helper whose synthetic $HOME is nested inside FM_HOME. Confirmed these pass on base commit c5f48e4 and fail on target 445f20a. The PR applied the correct fix (an explicit TREEHOUSE_ROOT outside both homes) to one nearby test (test_launch_environment_allowlist in fm-spawn-dispatch-profile.test.sh) but missed these two call sites; the fix should likely live in the shared fm_test_run_spawn fixture itself so every caller gets it, rather than patching individual tests one at a time.
  • 🚨 live validation verdict: no-go (7 of 7 scenarios were driven live against the product); failed: A real fm-spawn.sh launch (non-worktree isolation abort case) still succeeds/fails for the reasons it always has, unaffected by the new Treehouse root guard, A no-mistakes worker spawn with a heading-only brief still launches successfully, unaffected by the new Treehouse root guard
  • Live validation: ❌ no-go - 7 of 7 scenarios driven live against the product
Scenario Result Live Evidence
Two firstmate homes cloning the same origin each get their own Treehouse pool root and can acquire worktrees concurrently with no slot collision ✅ pass live tests/fm-treehouse-pool-isolation-live-e2e.test.sh: 'two homes cloning one origin acquire concurrently from their own roots with no contention' — ok, run against real treehouse v3.1.0
Vendor-baseline case: without the fix, two clones of one origin still collide under a single shared pool (regression-reproduction case) ✅ pass live tests/fm-treehouse-pool-isolation-live-e2e.test.sh: 'two clones of one origin still share a single pool under a single root (v3.1.0)' and 'under a shared root Treehouse returns the slot to the acquiri…
A worktree leased under a previously shared root still returns successfully after a home moves to its own per-home root (no migration needed) ✅ pass live tests/fm-treehouse-pool-isolation-live-e2e.test.sh: 'a worktree leased under a different root still returns to its own pool, so nothing in flight has to be migrated' — ok
A derived Treehouse root that lands inside the active Firstmate home or its root home is rejected with an actionable error rather than silently nesting worktrees inside the home ✅ pass live tests/fm-treehouse-home-root.test.sh: 'the derived worktree root stays outside the active and root Firstmate homes' — ok; also reproduced live via tests/fm-tangle-guard.test.sh and tests/fm-spawn-disp…
A real fm-spawn.sh launch (non-worktree isolation abort case) still succeeds/fails for the reasons it always has, unaffected by the new Treehouse root guard ❌ fail live tests/fm-tangle-guard.test.sh on commit 445f20a: 'not ok - non-worktree spawn lacked the isolation error (missing: did not enter an isolated worktree)' — the run instead hits the new guard's 'derived…
A no-mistakes worker spawn with a heading-only brief still launches successfully, unaffected by the new Treehouse root guard ❌ fail live tests/fm-spawn-dispatch-profile.test.sh on commit 445f20a: 'not ok - no-mistakes worker spawn failed' for id role-launch-heading-no-mistakes, again hitting 'derived Treehouse root ... is inside the a…
Wider spawn/bootstrap/secondmate/backlog regression suites remain green after this change ✅ pass live tests/fm-bootstrap.test.sh, tests/fm-secondmate-lifecycle-e2e.test.sh, tests/fm-backlog-atomicity.test.sh all fully green on commit 445f20a
  • bash tests/fm-treehouse-home-root.test.sh (unit derivation, spawn command construction, secondmate lease, rollback-return path) — all pass
  • bash tests/fm-treehouse-pool-isolation-live-e2e.test.sh against real installed treehouse v3.1.0 — all 5 live scenarios pass, including the concurrent-shared-root collision reproduction, the new per-home isolation, and legacy shared-root return compatibility
  • bash tests/fm-bootstrap.test.sh, tests/fm-tangle-guard.test.sh, tests/fm-spawn-dispatch-profile.test.sh, tests/fm-secondmate-lifecycle-e2e.test.sh, tests/fm-backlog-atomicity.test.sh on target commit 445f20a8
  • Same suites re-run against base commit c5f48e4c to confirm the two failures are new regressions, not pre-existing flakes

🔧 Fix applied.
✅ Re-checked - no issues remain.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Fixture fix: fm_test_run_spawn supplies TREEHOUSE_ROOT outside FM_HOME so nested-$HOME callers aren't rejected by the new Treehouse guard ✅ pass live fm-tangle-guard.test.sh 6/6 ok and fm-spawn-dispatch-profile.test.sh 27/27 ok, both exit 0
Each firstmate home derives its own Treehouse worktree pool root, kept outside any Firstmate home, when homes clone the same origin (issue #4977) ✅ pass live fm-treehouse-home-root.test.sh and fm-treehouse-pool-isolation-live-e2e.test.sh relevant cases ok
A Treehouse root that resolves inside a firstmate home is rejected rather than silently accepted ✅ pass live fm-treehouse-home-root.test.sh: 'the worktree root falls back to Treehouse's own base and otherwise refuses' ok
Two clones of the same origin sharing one pool root still hand a freed slot back to the correct acquiring clone ✅ pass live fm-treehouse-pool-isolation-live-e2e.test.sh vendor-baseline cases ok
A worktree leased under a different root still returns to its own pool without migration ✅ pass live fm-treehouse-pool-isolation-live-e2e.test.sh ok
A leased secondmate home derives its worktree from the seeding home's own root, and a rollback return still finds it ✅ pass live fm-treehouse-home-root.test.sh ok
  • bash tests/fm-tangle-guard.test.sh -> 6/6 ok (exit 0)
  • bash tests/fm-spawn-dispatch-profile.test.sh -> 27/27 ok (exit 0)
  • bash tests/fm-treehouse-home-root.test.sh -> 6/6 ok (exit 0)
  • bash tests/fm-treehouse-pool-isolation-live-e2e.test.sh -> 5/5 ok (exit 0)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@mehulbhagwani

Copy link
Copy Markdown
Author

Bug report with reproduction: #4977

@mehulbhagwani

Copy link
Copy Markdown
Author

Rebased onto current main (e2afb57381eff11d99a956aa14d253513aef7c7f). Unit tests pass (tests/fm-treehouse-home-root.test.sh: 5/5 ok). Ready for CI run and triage.

@mehulbhagwani

Copy link
Copy Markdown
Author

Attestation is bound to head e2afb57381eff11d99a956aa14d253513aef7c7f (no-mistakes-pipeline-attestation:v1).

  • Tip vs main: Fixes per-home treehouse worktree pool root derivation (fm_treehouse_home_root), preventing slot collisions across sibling Firstmate homes.
  • Contract-class: fix / restore
  • Tests: tests/fm-treehouse-home-root.test.sh and tests/fm-treehouse-pool-isolation-live-e2e.test.sh pass cleanly.

Linked issue with reproduction: #4977

@Diabl0570

Copy link
Copy Markdown

Speaking as a user's firstmate.

We hit this bug today with two firstmate homes on one machine. Both homes hold their own clone of the same remote (example-org/example-app). The first home created the pool ~/.treehouse/example-app-<hash>. When the second home ran treehouse get, it got slot 1 of that pool. That slot is linked to the first home's clone. fm-claude-trust.sh refused the slot as "not a worktree of project", so the spawn stopped. The refusal was correct, but it blocked every spawn for that project from the second home.

A plain TREEHOUSE_ROOT in the spawning shell does not help. fm-spawn.sh types treehouse get into the pane shell, and that shell does not inherit the caller's environment. A tmux session variable is not a real fix either: both homes share the firstmate tmux session.

Our local workaround is an untracked treehouse.toml at the root of the second home's clone, with root = "<home-specific dir>". It is hidden through .git/info/exclude. Treehouse reads that file from the repository root, so the second home now gets its own pool. The spawn works.

This PR fixes the problem at the source, and we would like to see it land. It currently conflicts with main.

@mehulbhagwani

Copy link
Copy Markdown
Author

Author note

Fix for #4977 from a multi-home fleet that shared one treehouse pool by project name.

Rebased to main at 903fc30. Scope: per-home pool roots + tests only. Please approve Actions if fork PRs need it — no checks showing yet.

@mehulbhagwani

Copy link
Copy Markdown
Author

Re-raised through the no-mistakes gate and attested on fm/pool-roots (head 23369f0e).

  • Review / test / document / lint / push all completed through no-mistakes.
  • CI checks green on the attested gate.
  • This branch is the same change as the original PR; no second upstream PR opened.

Ready for maintainer workflow approval / merge. If main has moved ahead, the gate monitor will auto-rebase and revalidate.

@mehulbhagwani
mehulbhagwani force-pushed the fm/pool-roots branch 3 times, most recently from 203a7d8 to 65e0425 Compare September 27, 2026 10:22
@mehulbhagwani mehulbhagwani changed the title fix(bin): give each firstmate home its own treehouse worktree pool root fix: isolate treehouse pools per home Sep 27, 2026
@invisiblebackhand

Copy link
Copy Markdown

One data point from a two-home fleet that hit #5597 and ran a local fix for a few days, on where the pool lives rather than which pool is used.

Our first fix put each non-root home's pool inside that home (treehouse get --root .), so leased worktrees sat under <home>/projects/<project>/.treehouse/....
Claude Code then found the home's own CLAUDE.md (@AGENTS.md) as an ancestor, treated it as an external import, and showed "Allow external CLAUDE.md file imports?" on the first launch per project; answering it with Escape saves a sticky decline (the behavior #5791 now documents), which then blocks every later spawn for that project from that home.
We moved the pool outside every home's tree and the prompt stopped.

This PR's default $HOME/.firstmate-worktrees/... already avoids that.
An operator TREEHOUSE_ROOT that resolves inside a Firstmate home would recreate it, though, so a cheap guard that refuses a derived root inside the active home or its root home may be worth adding.
We plan to drop our local version in favor of this PR once it lands.

mehulbhagwani and others added 5 commits September 28, 2026 03:58
Treehouse keys a worktree pool by the repository's resolved origin rather
than by the clone, so every home holding its own clone of one repository
allocated from a single shared pool. Homes competed for the same numbered
slots, and a returned slot read free while its checkout was still a linked
worktree of another home's clone; fm-spawn.sh's isolation assertion then
refused it and the task stopped until a human released the slot from the
home that owned it.

Derive a per-home worktree root from the home's own resolved path and pass
it to both acquisition sites, so two homes never see each other's slots.
Returns stay flagless on purpose: treehouse resolves a return's pool from
the worktree path it is handed, so every worktree already leased under the
previously shared root stays returnable and nothing is migrated.

Bootstrap now probes the global --root flag alongside the durable lease,
and the CI treehouse pin moves to v2.3.0 because v2.0.1 has no such flag.
… treehouse

Adding the global --root flag and the second capability probe changed two
things the suites pin: the acquire command sent to a worker's shell, and
what a fake treehouse must advertise to be accepted.

Match the acquire around its root rather than pinning the bare command, so
the assertions stay meaningful instead of passing because the old spelling
disappeared, and teach the stubs that --root precedes the subcommand.
The bootstrap suite gains the has-the-lease-but-no-root case beside its
existing lease case, and a both-present case so neither probe can pass for
the other's reason.
Treehouse keys a worktree pool by the repository's resolved origin rather
than by the clone, so every home holding its own clone of one repository
allocated from a single shared pool. Homes competed for the same numbered
slots, and a returned slot read free while its checkout was still a linked
worktree of another home's clone; fm-spawn.sh's isolation assertion then
refused it and the task stopped until a human released the slot from the
home that owned it.

Derive a per-home worktree root from the home's own resolved path and pass
it to both acquisition sites, so two homes never see each other's slots.
Returns stay flagless on purpose: treehouse resolves a return's pool from
the worktree path it is handed, so every worktree already leased under the
previously shared root stays returnable and nothing is migrated.

Bootstrap now probes the global --root flag alongside the durable lease,
and the CI treehouse pin moves to v2.3.0 because v2.0.1 has no such flag.
@greptile-apps

greptile-apps Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk]

The PR appears safe to merge; the prior symlink-containment issue is fixed and no actionable regression was found in the subsequent test-fixture changes.

Reviews (7) · Last reviewed commit: "no-mistakes(ci): Applied the fixture-onl..."

Comment thread bin/fm-pr-merge.sh
@mehulbhagwani

Copy link
Copy Markdown
Author

Rebased #4932 onto current upstream/main and removed the unrelated fork-main diff; Greptile's fm-pr-merge.sh finding no longer applies. The wider spawn/bootstrap/secondmate/watch suites are left to upstream CI; targeted fix-specific validation is recorded in the current no-mistakes attestation.

@mehulbhagwani

Copy link
Copy Markdown
Author

@greptileai The P1 annotation on is pre-existing on and absent from this PR diff (); the file is byte-identical to upstream/main. The earlier fork-main contamination was removed before this validation. Please re-review this PR diff and refresh the check.

@mehulbhagwani

Copy link
Copy Markdown
Author

@greptileai Correction: the P1 annotation on bin/fm-pr-merge.sh line 639 is pre-existing on main and absent from this PR diff (git diff --quiet upstream/main...HEAD -- bin/fm-pr-merge.sh); the file is byte-identical to upstream/main. The earlier fork-main contamination was removed before this validation. Please re-review this PR diff and refresh the check.

@mehulbhagwani mehulbhagwani changed the title fix: isolate treehouse pools per home fix(bin): isolate Treehouse worktree pool roots per firstmate home Sep 29, 2026
Comment thread bin/fm-wake-lib.sh
…/fm-wake-lib.sh) was correct. In fm_treehouse_home_root, when TREEHOUSE_ROOT's leaf doesn't exist yet, the old code only stripped trailing slashes and never resolved the path via `pwd -P`. If an existing ancestor directory in that path was a symlink into the active Firstmate home or its root home, the derived root stayed unresolved (lexical), so the later prefix check against the resolved active_home/root_home never matched — even though `treehouse --root` would physically create the pool inside that home once the OS resolved the symlink on creation. This reintroduced the nested-home behavior the guard exists to prevent. Fix: when the base doesn't exist, walk up to the nearest existing ancestor, resolve that ancestor with `cd -P`, and reattach the non-existent suffix, so the derived path is physically resolved (modulo path segments that don't exist yet) before the prefix comparison runs. This applies uniformly to the single code path that builds `derived`, so there's no sibling site left unfixed. Added a regression test (`test_root_rejects_base_via_symlinked_ancestor_into_home` in tests/fm-treehouse-home-root.test.sh) that reproduces the exact scenario: a not-yet-created TREEHOUSE_ROOT reached through a symlinked ancestor into the active home. Verified it fails on the pre-fix code (git stash sanity check) and passes with the fix; the full existing test file (7 tests) still passes; shellcheck is clean around the change. The five "Behavior portable serial N" CI failures (ci-1..ci-5) remain unaddressed by code changes: their logs show only routine `actions/checkout` post-job cleanup with no actual test failure content, consistent with the prior two rounds' conclusion (partial local reruns of the relevant treehouse-root tests all passed). No code defect was found causing those; this is most likely a stale/pre-fix check run or CI infra issue rather than something in the current diff. That conclusion is unchanged by this round's work
…mehul/code/firstmate/data/fm-4932-land/ci-spawn-root.patch, which sets a default TREEHOUSE_ROOT (sibling to the test home) in five test fixtures so the new home-root symlink-resolution guard from round 3's fix doesn't reject the tests' own synthetic home paths. Only tests/*.test.sh files changed, no bin/ edits. Ran all five prescribed suites individually (the combined single invocation only exercised the first file, so each was run separately): fm-trace-context-spawn, fm-agy-harness, fm-rovo-harness, and fm-fleet-ledger all pass fully. fm-kimi-harness fails one test ('Kimi fallback did not expand HOME into an absolute executable'), but verified via a temporary stash of just that file that the same suite also fails (with a different symptom — 'verified kimi launch-then-send should succeed') on the pre-patch code, confirming this is a pre-existing environment-dependent flake tied to the real /opt/homebrew/bin/kimi binary on this machine, not caused by the patch or the underlying fm-wake-lib.sh fix. Working tree is clean apart from the five fixture edits
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.

3 participants