Skip to content

fix(bin): isolate per-home Treehouse pool roots and read null required checks as empty - #4932

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

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

Conversation

@mehulbhagwani

@mehulbhagwani mehulbhagwani commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Make every open mehulbhagwani PR on kunchenguid/firstmate clean for Kun to merge.

PRs: #5954 #5953 #5952 #5951 #5919 #5709 #4932

For each: checkout that PR head branch on origin/fork, rebase onto current upstream main, fix red CI (behavior portable serial jobs), ensure no-mistakes attestation head_sha MATCHES PR head, push to the PR branch (update existing PR in place — do NOT open new PRs), confirm CI green and mergeable CLEAN.

Priority: 5954 and 5952 already green—verify still clean; then 5953, 5951 red serial jobs; 5919 conflicts; 5709 attestation mismatch; 4932 two serial fails.

Do not merge on Kun's repo (no authority). Leave PRs ready. Report final status table.

Fix all 7 fast.

This run validates and updates existing PR #4932 in place (branch fm/pool-roots on the mehulbhagwani fork): each firstmate home derives its own Treehouse worktree pool root so homes cloning the same origin never share a pool, and a TREEHOUSE_ROOT that resolves inside a firstmate home (including through an existing ancestor that is a symlink into the active or root home) is rejected. bin/fm-pr-merge.sh: a protected branch whose readable protection object carries an explicit required_status_checks: null has no classic required checks (readable emptiness, the merge is not refused for that source), while a protected payload with no protection object or no required_status_checks key, or a non-object value, stays unreadable and refuses. The branch was rebased onto main, so the push must update the existing PR branch (force with lease) and the PR body attestation must bind to the new head. Do not open a new PR.

What Changed

  • Each firstmate home now derives its own Treehouse worktree pool root, so homes cloning the same origin no longer share a pool. A TREEHOUSE_ROOT that resolves inside a firstmate home is rejected, including when it reaches the home through an existing ancestor that is a symlink. The derivation lives in bin/fm-wake-lib.sh and is used by fm-spawn.sh, fm-bootstrap.sh, fm-home-seed.sh, fm-install-treehouse.sh and fm-test-run.sh. Docs and the bootstrap-diagnostics skill are updated to match.
  • bin/fm-pr-merge.sh: a protected branch whose readable protection object has an explicit required_status_checks: null is treated as having no classic required checks, so the merge is not refused on that basis. A protected payload with no protection object, no required_status_checks key, or a non-object value is still unreadable and still refuses.
  • Tests: new fm-treehouse-home-root and live pool-isolation e2e suites, new fm-pr-merge cases, and fixture updates that set a default TREEHOUSE_ROOT outside the test home so existing suites pass the new guard.

Risk Assessment

⚠️ Medium: The change is well tested and matches the stated intent. The remaining risk is that every spawn and seed now depends on Treehouse v2.3.0's --root flag and a hand-written path-normalization routine, and I did not run anything.

Testing

Ran three targeted executable suites, which drive the real scripts and the real treehouse binary in disposable homes. All exited 0, including the symlinked-ancestor rejection and protected+null required-checks cases. No UI surface, so no visual artifacts.

  • Live validation: ✅ go - 3 of 3 scenarios driven live against the product
Scenario Result Live Evidence
Per-home pool roots: two homes cloning one origin acquire from separate roots without contention ✅ pass live tests/fm-treehouse-pool-isolation-live-e2e.test.sh exit 0
TREEHOUSE_ROOT resolving inside a home, including via a symlinked ancestor, is rejected ✅ pass live tests/fm-treehouse-home-root.test.sh exit 0
fm-pr-merge: protected branch with required_status_checks null is not refused; missing structure still refuses ✅ pass live tests/fm-pr-merge.test.sh exit 0

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - medium risk

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 3 of 3 scenarios driven live against the product
Scenario Result Live Evidence
Per-home pool roots: two homes cloning one origin acquire from separate roots without contention ✅ pass live tests/fm-treehouse-pool-isolation-live-e2e.test.sh exit 0
TREEHOUSE_ROOT resolving inside a home, including via a symlinked ancestor, is rejected ✅ pass live tests/fm-treehouse-home-root.test.sh exit 0
fm-pr-merge: protected branch with required_status_checks null is not refused; missing structure still refuses ✅ pass live tests/fm-pr-merge.test.sh exit 0
  • bash tests/fm-treehouse-home-root.test.sh
  • bash tests/fm-pr-merge.test.sh
  • bash tests/fm-treehouse-pool-isolation-live-e2e.test.sh
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@mehulbhagwani

Copy link
Copy Markdown
Contributor Author

Bug report with reproduction: #4977

@mehulbhagwani

Copy link
Copy Markdown
Contributor 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
Contributor 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
Contributor 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
Contributor 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.

@greptile-apps

greptile-apps Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High risk] Upgrades Treehouse dependency and changes worktree pool isolation.

The PR appears safe to merge; the previous findings are fully addressed and no actionable new failures remain.

Reviews (10) · Last reviewed commit: "no-mistakes(ci): Fixed ci-2: an absent T..."

Comment thread bin/fm-pr-merge.sh Resolved
mehulbhagwani and others added 15 commits October 2, 2026 19:33
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.
…/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
A branch protected only by review rules can report protected:true with
required_status_checks:null. That is readable emptiness for classic checks,
not an unreadable producer that must refuse the merge. Keep non-object
non-null shapes as unreadable.
Only an explicit required_status_checks: null inside a readable protection
object means a protected branch has no classic required checks. A protected
branch payload with no protection object, or a protection object without the
required_status_checks key, stays an unreadable source and refuses the merge.
Restore the missing-protection refusal case and cover the missing-key case.
@mehulbhagwani mehulbhagwani changed the title fix(bin): isolate Treehouse worktree pool roots per firstmate home fix(bin): isolate per-home Treehouse pool roots and read null required checks as empty Oct 2, 2026
Comment thread bin/fm-wake-lib.sh Resolved
…..' segment is now refused in fm_treehouse_home_root (error names the path and says to use a path without dot segments), before any containment check or derivation. An existing base keeps its pwd -P normalization. Added test_root_refuses_dot_segments_in_absent_base covering the '..' case, the './' case, and an ordinary absent base still deriving. tests/fm-treehouse-home-root.test.sh passes in full (10 tests), shellcheck reports only info-level notes. ci-1 (shellcheck OOM upstream) left untouched as instructed
@mehulbhagwani

Copy link
Copy Markdown
Contributor Author

The two red jobs here are known upstream flakes this PR does not touch: Behavior portable serial 4 is tests/fm-remote-delta-read.test.sh (#6404), and Lint 2 is shellcheck running out of memory on bin/fm-teardown.sh (#6410). Could you re-run those two failed jobs? I don't have rerun rights on this repo.

@mehulbhagwani

Copy link
Copy Markdown
Contributor Author

Closing in favor of #7341 and #7347

@mehulbhagwani

Copy link
Copy Markdown
Contributor Author

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