Skip to content

fix(bin): pool task worktrees per home and refuse a worktree of another clone - #4222

Open
Alberto-Codes wants to merge 3 commits into
kunchenguid:mainfrom
Alberto-Codes:fm/fm-secondmate-treehouse-pool-collision-k19
Open

Alberto-Codes wants to merge 3 commits into
kunchenguid:mainfrom
Alberto-Codes:fm/fm-secondmate-treehouse-pool-collision-k19

Conversation

@Alberto-Codes

Copy link
Copy Markdown

Intent

The captain's word on 2026-09-11, choosing this from the queued work: "go." He picked it over several other firstmate defects because real work is stacked behind it, and because the near-miss below is worse than an inconvenience.

WHAT IS BROKEN. Each persistent second mate gets its own clone of a project. Treehouse keys its worktree pool by repository IDENTITY, not by clone path or by home. Both clones carry the same origin URL, so a spawn from a second mate's home is handed a worktree of the MAIN home's clone. The second mate's dispatch is blocked for every item in its domain as a result.

WHY IT IS URGENT RATHER THAN ANNOYING. The only place the worktree-to-clone relationship is checked anywhere in the spawn path is Claude's trust pre-registration, which refuses because the worktree is not of the clone that was asked for. That refusal is an accident of one harness's trust step, not a guard. A Cursor spawn SAILS PAST IT AND LAUNCHES INTO THE WRONG CLONE. Observed cleanly on 2026-09-06: a second mate's Cursor spawn launched into the main home's clone, was caught by the mate itself rather than by any check, and left an empty branch in the main home's clone plus a task stuck in flight. Nothing was lost only because the mate was watching. A worker that actually committed would have written into another home's clone silently.

WHY IT IS OURS AND NOT TREEHOUSE'S. Treehouse does what it documents. Firstmate is the side that decided each home owns a clone while calling a name-keyed shared pool with no home in the key.

TWO CORRECTIONS THAT COST TIME ALREADY, recorded on 2026-09-06 after a proposed interim was tested and failed. Do not repeat either.

FIRST: TREEHOUSE_ROOT IS NOT THE ANSWER AND MAY NOT EVEN BE REACHABLE. bin/fm-spawn.sh sends the literal text treehouse get INTO the spawned pane, so the pool resolves in that pane's environment and an exported TREEHOUSE_ROOT never reaches it. And the root would not have helped regardless, because the pool key is repository identity rather than path: a different root yields a different EMPTY directory, not a different pool.

SECOND, AND THIS IS THE METHODOLOGICAL WARNING: firstmate's own verification of that interim was invalid. It observed an empty pool under a fresh root and concluded the pool had moved - but an empty directory and a relocated pool look identical to that check. Do not accept a check that cannot distinguish the outcome you want from the outcome you fear.

THE INTERIM CURRENTLY IN PLACE is that the second mate was told to prefix its spawns by hand. That is what this task exists to replace.

What Changed

  • bin/fm-spawn.sh now sends treehouse get --root '<home>' into the task pane instead of a bare treehouse get, with the root resolved from FM_HOME by a new fm_treehouse_pool_root helper in bin/fm-wake-lib.sh, so each home draws task worktrees from its own <home>/.treehouse/ pool rather than a $HOME-rooted pool keyed only by clone basename and origin hash; the root travels as literal command text because an exported TREEHOUSE_ROOT never reaches the pane, and an unresolvable home aborts the spawn.
  • spawn_worktree_isolated additionally compares the candidate worktree's git common dir against the spawning project's on every non-Orca backend, refusing a worktree of another clone before any harness launches, and validate_spawn_worktree now prints the specific SPAWN_WT_REASON in its refusal.
  • bin/fm-install-treehouse.sh bumps the CI pin from v2.0.1 to v2.3.0 (new per-platform SHA-256s) for --root support, bin/fm-bootstrap.sh replaces its --lease-only probe with treehouse_missing_flag that probes --lease and --root and names the missing flag in the MISSING: line, CI installs the pinned Treehouse for the portable-serial shards with --fail-on-gate-skip 'treehouse not found', and .treehouse/ is gitignored.
  • Adds tests/fm-spawn-pool-home.test.sh (per-home pool root and clone guard) and tests/fm-treehouse-pool-root.test.sh (pool-key and --root/return facts against the real binary), registers both in bin/fm-test-run.sh lanes, updates existing fakes to advertise --root under Global Flags via FM_FAKE_TREEHOUSE_GET_FLAGS, and refreshes the architecture, configuration, scripts, AGENTS, skill, and runtime-backends verification docs.

🤖 Generated with Claude Code

Risk Assessment

⚠️ Medium: The collision fix is correct, structurally verified against the real Treehouse binary (pool key formula, per-root separation, and rootless return all reproduced independently), and the pinned v2.3.0 checksums match the published release assets exactly; the residual risk is that the chosen approach reverses a correction the captain explicitly wrote down, plus two small mechanical cleanups.

Testing

Reproduced the reported defect end-to-end and then showed it fixed, driving the real fm-spawn.sh and the real treehouse v2.3.0 binary through the two-homes/one-origin shape: on the base commit the second mate's spawn launched into the main home's clone and published a task, while on the change each pane is sent treehouse get --root &#39;&lt;its own home&gt;&#39;, each home pools under itself, each mate lands in its own clone, and rootless treehouse return still releases the slot; with the root sabotaged the new clone-membership guard refuses before any harness launches. The Treehouse pin bump was measured rather than inferred - v2.0.1 rejects --root on both get and status, v2.3.0 advertises it - and the real bootstrap names the missing flag for a genuine v2.0.1 build. Both new suites fail on the base tree and pass on the change, the full bootstrap suite and seven adjacent touched suites pass, and CI lane wiring was checked semantically so neither new test can silently skip. No UI surface is involved (all CLI and shell), so evidence is CLI transcripts rather than screenshots. One unrelated herdr liveness case fails identically on the base commit and is reported as informational only.

Evidence: End-to-end: second mate dispatched into the main home's clone, before and after (real fm-spawn + real treehouse)

Source: End-to-end: second mate dispatched into the main home's clone, before and after (real fm-spawn + real treehouse)

Persistent second mate dispatched into the MAIN HOME's clone: reproduction and fix
Driven end-to-end through the real bin/fm-spawn.sh and the real treehouse binary.

================ BEFORE - base e0d269e (shared $HOME pool) ================
fm-spawn.sh under test : /tmp/fm-base-0lmQST/bin/fm-spawn.sh
treehouse              : v2.3.0
one machine, one user  : HOME=<tmp>/user
main home 'alpha'      : clone at <tmp>/homes/mate-alpha/projects/proj
second mate 'bravo'    : clone at <tmp>/homes/mate-bravo/projects/proj  (same basename, same origin)

----- 1) the main home dispatches a task -----
warning: <tmp>/homes/mate-alpha/data/demo-alpha/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/proj-6cdc08/1/proj. Run 'treehouse return ~/.treehouse/proj-6cdc08/1/proj' to release it.
spawned demo-alpha harness=codex kind=ship mode=no-mistakes yolo=off window=firstmate:fm-demo-alpha worktree=<tmp>/user/.treehouse/proj-6cdc08/1/proj
pane received  : treehouse get
worktree       : <tmp>/user/.treehouse/proj-6cdc08/1/proj
  is a worktree of: <tmp>/homes/mate-alpha/projects/proj

----- 1b) that task finishes; teardown returns the slot (no --root needed) -----
🌳 Worktree returned to pool.

----- 2) the SECOND MATE dispatches a task in its own domain -----
warning: <tmp>/homes/mate-bravo/data/demo-bravo/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/proj-6cdc08/1/proj. Run 'treehouse return ~/.treehouse/proj-6cdc08/1/proj' to release it.
spawned demo-bravo harness=codex kind=ship mode=no-mistakes yolo=off window=firstmate:fm-demo-bravo worktree=<tmp>/user/.treehouse/proj-6cdc08/1/proj
pane received  : treehouse get
worktree       : <tmp>/user/.treehouse/proj-6cdc08/1/proj
  is a worktree of: <tmp>/homes/mate-alpha/projects/proj
  VERDICT: *** the second mate LAUNCHED INTO THE MAIN HOME'S CLONE - the reported defect ***

----- pools on disk -----
<tmp>/user/.treehouse/.gitignore
<tmp>/user/.treehouse/proj-6cdc08
<tmp>/user/.treehouse/proj-6cdc08/1
<tmp>/user/.treehouse/proj-6cdc08/treehouse-state.json
<tmp>/user/.treehouse/proj-6cdc08/treehouse-state.lock


================ AFTER - cecfe14 (per-home pool root) ================
fm-spawn.sh under test : ~/.no-mistakes/worktrees/d58a82b70cc7/01M28PTS5E5W9VFGM26FFXCK4V/bin/fm-spawn.sh
treehouse              : v2.3.0
one machine, one user  : HOME=<tmp>/user
main home 'alpha'      : clone at <tmp>/homes/mate-alpha/projects/proj
second mate 'bravo'    : clone at <tmp>/homes/mate-bravo/projects/proj  (same basename, same origin)

----- 1) the main home dispatches a task -----
warning: <tmp>/homes/mate-alpha/data/demo-alpha/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
🌳 Setting up worktree...
🌳 Leased worktree at <tmp>/homes/mate-alpha/.treehouse/proj-c75cf2/1/proj. Run 'treehouse return <tmp>/homes/mate-alpha/.treehouse/proj-c75cf2/1/proj' to release it.
spawned demo-alpha harness=codex kind=ship mode=no-mistakes yolo=off window=firstmate:fm-demo-alpha worktree=<tmp>/homes/mate-alpha/.treehouse/proj-c75cf2/1/proj
pane received  : treehouse get --root '<tmp>/homes/mate-alpha'
worktree       : <tmp>/homes/mate-alpha/.treehouse/proj-c75cf2/1/proj
  is a worktree of: <tmp>/homes/mate-alpha/projects/proj

----- 1b) that task finishes; teardown returns the slot (no --root needed) -----
🌳 Worktree returned to pool.

----- 2) the SECOND MATE dispatches a task in its own domain -----
warning: <tmp>/homes/mate-bravo/data/demo-bravo/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
🌳 Setting up worktree...
🌳 Leased worktree at <tmp>/homes/mate-bravo/.treehouse/proj-c75cf2/1/proj. Run 'treehouse return <tmp>/homes/mate-bravo/.treehouse/proj-c75cf2/1/proj' to release it.
spawned demo-bravo harness=codex kind=ship mode=no-mistakes yolo=off window=firstmate:fm-demo-bravo worktree=<tmp>/homes/mate-bravo/.treehouse/proj-c75cf2/1/proj
pane received  : treehouse get --root '<tmp>/homes/mate-bravo'
worktree       : <tmp>/homes/mate-bravo/.treehouse/proj-c75cf2/1/proj
  is a worktree of: <tmp>/homes/mate-bravo/projects/proj
  VERDICT: the second mate is working in ITS OWN clone, task published

----- pools on disk -----
<tmp>/homes/mate-alpha/.treehouse/.gitignore
<tmp>/homes/mate-alpha/.treehouse/proj-c75cf2
<tmp>/homes/mate-alpha/.treehouse/proj-c75cf2/1
<tmp>/homes/mate-alpha/.treehouse/proj-c75cf2/treehouse-state.json
<tmp>/homes/mate-alpha/.treehouse/proj-c75cf2/treehouse-state.lock
<tmp>/homes/mate-bravo/.treehouse/.gitignore
<tmp>/homes/mate-bravo/.treehouse/proj-c75cf2
<tmp>/homes/mate-bravo/.treehouse/proj-c75cf2/1
<tmp>/homes/mate-bravo/.treehouse/proj-c75cf2/treehouse-state.json
<tmp>/homes/mate-bravo/.treehouse/proj-c75cf2/treehouse-state.lock


================ AFTER - cecfe14, second layer: pane still lands in the shared pool ================
fm-spawn.sh under test : ~/.no-mistakes/worktrees/d58a82b70cc7/01M28PTS5E5W9VFGM26FFXCK4V/bin/fm-spawn.sh
treehouse              : v2.3.0
pane treehouse          : --root deliberately DROPPED (stale pool simulation)
one machine, one user  : HOME=<tmp>/user
main home 'alpha'      : clone at <tmp>/homes/mate-alpha/projects/proj
second mate 'bravo'    : clone at <tmp>/homes/mate-bravo/projects/proj  (same basename, same origin)

----- 1) the main home dispatches a task -----
warning: <tmp>/homes/mate-alpha/data/demo-alpha/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/proj-166f36/1/proj. Run 'treehouse return ~/.treehouse/proj-166f36/1/proj' to release it.
spawned demo-alpha harness=codex kind=ship mode=no-mistakes yolo=off window=firstmate:fm-demo-alpha worktree=<tmp>/user/.treehouse/proj-166f36/1/proj
pane received  : treehouse get --root '<tmp>/homes/mate-alpha'
worktree       : <tmp>/user/.treehouse/proj-166f36/1/proj
  is a worktree of: <tmp>/homes/mate-alpha/projects/proj

----- 1b) that task finishes; teardown returns the slot (no --root needed) -----
🌳 Worktree returned to pool.

----- 2) the SECOND MATE dispatches a task in its own domain -----
warning: <tmp>/homes/mate-bravo/data/demo-bravo/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/proj-166f36/1/proj. Run 'treehouse return ~/.treehouse/proj-166f36/1/proj' to release it.
error: treehouse get did not enter an isolated worktree within 60s (last seen '<tmp>/user/.treehouse/proj-166f36/1/proj': it is a worktree of another clone (common git dir '<tmp>/homes/mate-alpha/projects/proj/.git'), not of the spawning project's clone (common git dir '<tmp>/homes/mate-bravo/projects/proj/.git'); spawning project '<tmp>/homes/mate-bravo/projects/proj'); inspect window firstmate:fm-demo-bravo
pane received  : treehouse get --root '<tmp>/homes/mate-bravo'
task metadata  : none published (<tmp>/homes/mate-bravo/state/demo-bravo.meta does not exist)
  VERDICT: the spawn REFUSED before launching any harness

----- pools on disk -----
<tmp>/user/.treehouse/.gitignore
<tmp>/user/.treehouse/proj-166f36
<tmp>/user/.treehouse/proj-166f36/1
<tmp>/user/.treehouse/proj-166f36/treehouse-state.json
<tmp>/user/.treehouse/proj-166f36/treehouse-state.lock
Evidence: Treehouse --root capability measured on v2.0.1 vs v2.3.0, and the bootstrap line an operator sees with each real binary

Source: Treehouse --root capability measured on v2.0.1 vs v2.3.0, and the bootstrap line an operator sees with each real binary

Treehouse --root capability: measured on the real binaries, not inferred

$ bin/fm-install-treehouse.sh (base commit pin) -> v2.0.1
$ /tmp/th201/bin/treehouse --version
v2.0.1
$ /tmp/th201/bin/treehouse get --help  | grep -E '\-\-root|Global Flags'
(no --root advertised anywhere in the help)
$ /tmp/th201/bin/treehouse get --root /tmp
unknown flag: --root
$ /tmp/th201/bin/treehouse status --root /tmp
unknown flag: --root

$ bin/fm-install-treehouse.sh (this change's pin) -> v2.3.0  [checksum asserted by the installer]
$ /tmp/th230/bin/treehouse --version
v2.3.0
$ /tmp/th230/bin/treehouse get --help | grep -A1 'Global Flags'
Global Flags:
      --root string   Worktree root directory, overriding TREEHOUSE_ROOT and config; relative paths (e.g. "." for an in-project pool) resolve from the repo root

=> v2.0.1 cannot serve the per-home pool root; the pin bump to v2.3.0 was required.

What the operator sees at session start, with each REAL binary on PATH
(bin/fm-bootstrap.sh with everything else faked; scratch-FM_ROOT git noise filtered):

--- bootstrap with real treehouse v2.0.1 ---
MISSING: treehouse (installed build's 'treehouse get' lacks --root; install: curl -fsSL https://kunchenguid.github.io/treehouse/install.sh | sh)

--- bootstrap with real treehouse v2.3.0 ---
(silent - bootstrap raises nothing about treehouse)
Evidence: Regression direction: the two new suites on the base tree vs the fix

Source: Regression direction: the two new suites on the base tree vs the fix

Regression direction: the two new suites against the base commit vs the fix
(same test files in both trees; only bin/ differs)

############ tests/fm-spawn-pool-home.test.sh ############
--- on base e0d269e (pre-fix) ---
not ok - spawn launched into a worktree of another clone
warning: /tmp/fm-spawn-pool-home.9vSlpD/foreign/home/data/pool-home-foreign-k1/launch-brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
spawned pool-home-foreign-k1 harness=codex kind=ship mode=no-mistakes yolo=off window=firstmate:fm-pool-home-foreign-k1 worktree=/tmp/fm-spawn-pool-home.9vSlpD/foreign/pool-other/proj
--- on cecfe14 (fixed) ---
ok - a worktree of another clone of the same origin is refused at the deadline, naming both clones
ok - the project's own worktree launches and the pane is told to pool under this home
ok - the pool root is the physical home even when FM_HOME is a symlink
ok - a single quote in the home path is quoted for the pane
# all fm-spawn-pool-home tests passed

############ tests/fm-treehouse-pool-root.test.sh (real treehouse v2.3.0, no stubs) ############
--- on base e0d269e (pre-fix) ---
ok - the pool key is the clone basename plus the first six sha256 hex digits of its origin URL, rooted at HOME
ok - under a shared root the second clone of the same origin is handed the first clone's worktree
tests/fm-treehouse-pool-root.test.sh: line 47: fm_treehouse_pool_root: command not found
tests/fm-treehouse-pool-root.test.sh: line 47: fm_treehouse_pool_root: command not found
not ok - fm_treehouse_pool_root did not return the physical home (expected '/tmp/fm-treehouse-pool-root.tvucTN/home-a', got '')
--- on cecfe14 (fixed) ---
ok - the pool key is the clone basename plus the first six sha256 hex digits of its origin URL, rooted at HOME
ok - under a shared root the second clone of the same origin is handed the first clone's worktree
ok - under each home's own root every clone is handed a worktree of itself
ok - treehouse return locates the pool from the slot path, without --root
# all fm-treehouse-pool-root tests passed

############ tests/fm-bootstrap.test.sh (--lease / --root probe cases) ############
ok - bootstrap reports treehouse --lease/--root + tasks-axi/quota-axi bootstrap contracts
ok - bootstrap: session-provider backends require their own CLI + jq + treehouse, never tmux
ok - bootstrap: the treehouse lease check follows the resolved backend's worktree provider
Evidence: The defect and the fix, side by side (excerpt)
BEFORE - base e0d269e (shared $HOME pool)
----- 2) the SECOND MATE dispatches a task in its own domain -----
spawned demo-bravo harness=codex kind=ship mode=no-mistakes yolo=off worktree=<tmp>/user/.treehouse/proj-6cdc08/1/proj
pane received : treehouse get
worktree : <tmp>/user/.treehouse/proj-6cdc08/1/proj
is a worktree of: <tmp>/homes/mate-alpha/projects/proj
VERDICT: *** the second mate LAUNCHED INTO THE MAIN HOME'S CLONE - the reported defect ***

AFTER - cecfe14 (per-home pool root)
----- 2) the SECOND MATE dispatches a task in its own domain -----
spawned demo-bravo harness=codex kind=ship mode=no-mistakes yolo=off worktree=<tmp>/homes/mate-bravo/.treehouse/proj-c75cf2/1/proj
pane received : treehouse get --root '<tmp>/homes/mate-bravo'
worktree : <tmp>/homes/mate-bravo/.treehouse/proj-c75cf2/1/proj
is a worktree of: <tmp>/homes/mate-bravo/projects/proj
VERDICT: the second mate is working in ITS OWN clone, task published

AFTER - cecfe14, pane still lands in the shared pool (--root dropped)
error: treehouse get did not enter an isolated worktree within 60s (last seen '<tmp>/user/.treehouse/proj-166f36/1/proj': it is a worktree of another clone (common git dir '<tmp>/homes/mate-alpha/projects/proj/.git'), not of the spawning project's clone (common git dir '<tmp>/homes/mate-bravo/projects/proj/.git'))
task metadata : none published
VERDICT: the spawn REFUSED before launching any harness
- Outcome: ⚠️ 1 info across 1 run (14m20s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 3 issues (2 warnings, 1 info)
  • ⚠️ tests/fm-spawn-pool-home.test.sh:176 - test_pool_root_quoting_survives_a_single_quote cannot fail for the bug it claims to guard. The expected string is built with ${home_phys//\&#39;/\&#39;\\\&#39;\&#39;} - character for character the same parameter expansion bin/fm-spawn.sh:3115 uses to build the text it sends. The test therefore asserts expand(home) == expand(home), which holds no matter what escaping scheme the implementation uses; replacing the implementation's &#39;\&#39;&#39; with a broken \&#39; would leave the test green. This is the exact methodological trap the intent warns about ("Do not accept a check that cannot distinguish the outcome you want from the outcome you fear"). Fix: derive the expectation independently of the implementation's expression - extract the recorded send-keys text from $REC and run it through a shell (e.g. eval &#34;set -- ${text%% Enter}&#34; or a fake treehouse on PATH that records "$@"), then assert the resulting --root argument string-equals "$home_phys".
  • ⚠️ .github/workflows/ci.yml:82 - The new CI steps wire tests/fm-treehouse-pool-root.test.sh into all three portable lane jobs via bin/fm-install-treehouse.sh, which pins Treehouse v2.0.1 (bin/fm-install-treehouse.sh:15) and asserts that exact pin post-install. But every fact the test pins - global --root on get and status, the pool key &lt;basename&gt;-&lt;sha256(origin)[:6]&gt; under &lt;root&gt;/.treehouse/, the shared-root collision, and rootless return - was verified against v2.3.0 (docs/verification/runtime-backends.md:1497), three minor versions newer, and v2.3.0 is what is installed on the operating machine. If v2.0.1 lacks the global --root flag or uses a different key shape the suite fails outright; if it merely differs in a detail the suite passes while proving nothing about the version the fleet actually runs, which is the shape of check the intent explicitly warns against. The remedy - bumping the pinned third-party version and its four platform checksums, which also changes what the real-Herdr lane exercises - extends the change beyond its stated intent, so it needs your call: bump the pin to v2.3.0, or record in the verification section why v2.0.1 is equivalent for these four facts.
  • ⚠️ bin/fm-bootstrap.sh:910 - Every ship/scout spawn now depends on Treehouse's global --root flag, but the only capability probe is treehouse_supports_lease, which greps treehouse get --help for --lease alone. An installed Treehouse that has --lease but not --root passes bootstrap as healthy, then fails inside the pane: treehouse get --root &#39;&lt;home&gt;&#39; errors, the pane's shell never leaves the project, and the poll at bin/fm-spawn.sh:3153 burns the full 60s before refusing with "treehouse get did not enter an isolated worktree within 60s" - a message that never names the flag, so the operator has no path from the symptom to "upgrade treehouse". The smallest honest remedy adds a new capability probe and MISSING/upgrade branch alongside the lease probe, which extends this change with a new gate rather than correcting what it does, so it needs your authorization; the alternative is to confirm the --root flag predates the repo's supported Treehouse floor and leave the probe alone.
  • ⚠️ .github/workflows/ci.yml:90 - Simplification: neither new test lands in the parallel lanes. bin/fm-test-run.sh --list --lane portable-parallel-1|-2 contains neither fm-spawn-pool-home.test.sh nor fm-treehouse-pool-root.test.sh - both resolve to portable-serial - and no other test in those two lanes emits a "treehouse not found" skip (the four tests that do are all family real-herdr-gated). So the "Install pinned Treehouse" step and the --fail-on-gate-skip &#39;treehouse not found&#39; flag added to the shard-1 and shard-2 jobs are not required by the intent: they download and checksum a release on two jobs that never invoke it, and guard a skip string those lanes cannot produce. Recommend removing both from the parallel shard jobs and keeping them only on the portable serial job (ci.yml:206/228). If they are deliberate insurance against the lane rebalancing this repo does periodically, say so in the step comment instead.
  • ℹ️ bin/fm-home-seed.sh:396 - Sibling-path note, not a defect in this change: acquire_treehouse_home still runs treehouse get --lease with no --root, so firstmate HOME leases keep drawing from the default $HOME pool keyed by basename+origin. Within the fleet model this is safe - a treehouse-leased home is a linked worktree of the same root clone (same common dir), and a git-cloned secondmate home takes the parent's path as its origin, so both yield either the correct clone or a distinct pool key. The shared-pool shape would only bite if one machine ever held two separate firstmate clones of the same origin, which is outside the stated intent (task worktrees for second-mate dispatch). Recorded so the scoping is explicit, not as work to do.

🔧 Fix: bump pinned Treehouse to v2.3.0 and probe --root support
3 issues (2 warnings, 1 info) still open:

  • ⚠️ bin/fm-spawn.sh:3116 - Intent conformance: the change's fix IS the Treehouse root, which the intent's first correction marks as a mistake not to repeat. Quoting the criterion: "FIRST: TREEHOUSE_ROOT IS NOT THE ANSWER AND MAY NOT EVEN BE REACHABLE. ... And the root would not have helped regardless, because the pool key is repository identity rather than path: a different root yields a different EMPTY directory, not a different pool." The contradicting hunk is spawn_send_text_line &#34;$WT_TARGET&#34; &#34;treehouse get --root &#39;$spawn_pool_root_quoted&#39;&#34; plus fm_treehouse_pool_root (bin/fm-wake-lib.sh:1194). The change does satisfy the reachability half honestly - the root travels as literal pane text, not as an exported TREEHOUSE_ROOT - and it disproves the second half with exactly the evidence standard the intent's SECOND correction demanded: docs/verification/runtime-backends.md and tests/fm-treehouse-pool-root.test.sh read the git common dir of the worktree actually handed out (clone B under root B gets COMMON_B; under the shared root it gets COMMON_A), never whether a directory is empty. I independently confirmed the premise the correction rested on is false for the installed binary: the pool key is &lt;clone basename&gt;-&lt;sha256(origin)[:6]&gt; (verified: this repo's origin hashes to b8697d, matching the live ~/.treehouse/firstmate-b8697d pool), so a distinct root yields a distinct pool whose first slot is created from the invoking clone. This is a well-evidenced reversal, not a repeat of the earlier mistake - but it reverses a correction the captain wrote down, so it needs their explicit acceptance rather than a reviewer's.
  • ⚠️ bin/fm-wake-lib.sh:1197 - fm_treehouse_pool_root runs CDPATH=&#39;&#39; cd -- &#34;$home&#34; 2&gt;/dev/null &amp;&amp; pwd -P directly in the caller's shell, so calling it outside a command substitution silently moves the caller's cwd (and OLDPWD). The sole production caller, bin/fm-spawn.sh:3111, uses SPAWN_POOL_ROOT=$(fm_treehouse_pool_root &#34;$FM_HOME&#34;), so nothing breaks today - but this is a helper in a library sourced by fm-spawn, fm-teardown, hooks, and the wake path, and every other path-resolving function in the same file wraps its cd in a subshell (lines 1160, 1173, 1215, 1222). Failure scenario: a later caller writes if fm_treehouse_pool_root &gt;/dev/null; then ... and fm-spawn.sh silently continues from inside the home instead of its original cwd, changing what every subsequent relative git -C/path resolution sees. Remedy is mechanical and behavior-preserving: ( CDPATH=&#39;&#39; cd -- &#34;$home&#34; 2&gt;/dev/null &amp;&amp; pwd -P ).
  • ℹ️ bin/fm-teardown.sh:3226 - The comment above the task-worktree return still reads "treehouse resolves the pool from the working directory, so run it from the project." That is no longer the mechanism teardown depends on: with the per-home root, the slot lives under <home>/.treehouse/<key>/<n>/<repo> while the project clone's default root is $HOME, so a working-directory-derived pool lookup would miss it. What actually makes the rootless treehouse return work is that treehouse resolves the pool from the SLOT PATH - the fact bin/fm-spawn.sh:203 asserts and tests/fm-treehouse-pool-root.test.sh's test_return_needs_no_root pins against the real binary. Failure scenario: a maintainer reading this comment concludes the cd is what locates the pool and either drops it or adds a --root &#34;$HOME&#34;, breaking return for every slot under a per-home root. Update the comment to name slot-path resolution and point at the pinning test.
⚠️ **Test** - 1 info
  • ℹ️ tests/fm-secondmate-liveness.test.sh:165 - tests/fm-secondmate-liveness.test.sh fails on this machine at the case 'Herdr pane state unknown should map to unreadable, got missing'. Pre-existing and unrelated to this change: it fails identically on the base commit e0d269e, and the case stubs only fm_backend_herdr_pane_agent_state, so the 'unknown' branch falls through to fm_backend_herdr_server_running_state, which probes the ambient herdr server (0.8.2 installed, not running) and yields 'missing'. The change touched this file only to rename FM_FAKE_TREEHOUSE_LEASE_HELP to FM_FAKE_TREEHOUSE_GET_FLAGS. No action needed for this change; remote CI owns it.
  • bash tests/fm-spawn-pool-home.test.sh - 4/4 ok on the change; on a base-commit tree with the same test file it fails with spawned pool-home-foreign-k1 ... worktree=.../pool-other/proj (launched into another clone)
  • bash tests/fm-treehouse-pool-root.test.sh - 4/4 ok against the real installed treehouse v2.3.0 (no stubs); on the base tree it fails at fm_treehouse_pool_root: command not found
  • bash tests/fm-bootstrap.test.sh - full suite passes, including the new rows 'treehouse without --lease reports an upgrade naming that flag' and 'treehouse without --root reports an upgrade naming that flag'
  • Manual end-to-end: two firstmate homes on one $HOME, each with its own clone of one origin at the same basename; fake tmux records the pane text and actually EXECUTES the treehouse get ... line against the real binary, reporting the leased worktree back as the pane cwd. Run three ways - base fm-spawn.sh (reproduces the defect), fixed fm-spawn.sh (each mate in its own clone), fixed fm-spawn.sh with the pane's --root dropped (guard refuses, no task metadata published)
  • Manual measurement: installed treehouse v2.0.1 and v2.3.0 to temp dirs via bin/fm-install-treehouse.sh (base and target pins; installer asserts sha256 and version). treehouse get --help, treehouse get --root /tmp, treehouse status --root /tmp on v2.0.1 -> unknown flag: --root; v2.3.0 advertises --root under Global Flags
  • Manual: ran the real bin/fm-bootstrap.sh with each genuine treehouse binary on PATH (rest of toolchain faked from the bootstrap suite's own helpers) - v2.0.1 yields MISSING: treehouse (installed build&#39;s &#39;treehouse get&#39; lacks --root; install: ...), v2.3.0 is silent
  • git check-ignore -v .treehouse/proj-abc/1/proj - git itself confirms the new .gitignore rule covers a per-home pool inside a firstmate home checkout
  • bin/fm-test-run.sh --list --lane portable-serial|portable-parallel-1|portable-parallel-2 plus a YAML parse of .github/workflows/ci.yml - both new tests resolve to portable-serial, the only lane carrying the Install pinned Treehouse step and the --fail-on-gate-skip &#39;treehouse not found&#39; flag
  • Adjacent touched suites: bash tests/fm-spawn-worktree-settle.test.sh, tests/fm-tangle-guard.test.sh, tests/fm-secondmate-sync.test.sh, tests/fm-session-start.test.sh, tests/fm-startup-memory-budget.test.sh, tests/fm-secondmate-harness.test.sh, tests/fm-bootstrap-network-parallel.test.sh - all pass
  • bash tests/fm-secondmate-liveness.test.sh on both the change and the base commit - same single failure in both, pre-existing
⚠️ **Document** - 1 info
  • ℹ️ AGENTS.md:46 - Judgment call, deliberately not changed: the two summary enumerations of captain-private gitignored paths (AGENTS.md:46 and CONTRIBUTING.md:41, both listing .env, data/, state/, config/, projects/, .no-mistakes/) do not name the new .treehouse/ pool directory. I left them alone rather than synchronizing a third and fourth copy of the same fact: the home-layout tree at AGENTS.md:96 and docs/configuration.md:17 are the authoritative owners and both now describe .treehouse/, and .gitignore already prevents any accidental commit. Raising it here only so the omission reads as intentional placement rather than an oversight.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…er clone

Treehouse keys a pool by the clone's directory basename plus the first six
sha256 hex digits of its origin URL, under <root>/.treehouse/ with the root
defaulting to $HOME, never by the clone the pool was created from. Verified
against treehouse v2.3.0: two same-named clones of one origin under one root
resolve to one pool, and the second clone is handed the first clone's
worktree. Each secondmate home clones its projects under the same names and
origins as the main home, so a secondmate spawn was handed a worktree of the
main home's clone and vice versa. Only Claude's trust pre-registration ever
refused that shape, and only for claude; a Cursor spawn launched straight into
the other home's clone.

Two changes, both in the spawn path:

- fm-spawn now sends `treehouse get --root '<home>'` into the task pane, the
  home resolved from FM_HOME by fm-wake-lib's fm_treehouse_pool_root as the
  home's physical path, so every home draws worktrees only from its own
  clones. The root travels as literal command text because an exported
  TREEHOUSE_ROOT never reaches the pane's own shell. Teardown needs no root:
  treehouse return locates the pool from the slot path, so in-flight slots
  under the old shared root still return.
- The worktree isolation predicate additionally requires the worktree's git
  common dir to be the spawning project's, on every backend Treehouse serves
  and independent of harness, at discovery, validation, and relaunch. A pane
  settled on a worktree of another clone is refused at the settle deadline
  naming both clones and publishes no task. Orca owns its own worktree shape
  and is exempt rather than guessed at. The Claude trust step is unchanged.

tests/fm-spawn-pool-home.test.sh drives the real spawn with a fake terminal:
the refusal case reproduces the incident shape and passes only with the
guard (against the unpatched script the same fixture "spawned" into the other
clone's worktree), the own-clone case launches, and the recorded pane text
carries the physical home as --root, through a symlinked FM_HOME and a path
containing a single quote. tests/fm-treehouse-pool-root.test.sh pins the pool
key, the collision, the per-home separation, and rootless return against the
installed treehouse binary, so the facts the fix rests on fail loudly if
treehouse changes them; the portable CI lanes now install the pinned binary so
that suite cannot merely skip there. The dated evidence is recorded under
docs/verification/runtime-backends.md "Treehouse pool root".

Existing pools under ~/.treehouse are left in place: in-flight tasks tear down
unchanged, and idle slots there are no longer handed out and can be destroyed
from their owning clone once nothing runs in them.
Copilot AI lite review requested due to automatic review settings September 11, 2026 18:04

Copilot AI 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.

🟡 Changes recommended

There are two correctness/robustness issues in the changed code/tests (a library helper that can mutate caller cwd, and a regression test that can’t detect broken quoting) that should be fixed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Captain, this PR fixes cross-home Treehouse worktree pool collisions by making task worktree allocation pool per FM_HOME (via treehouse get --root <home>) and adding a backend-independent guard that refuses a worktree belonging to a different clone before any harness launches.

Changes:

  • Route Treehouse pooling through a per-home root and hard-refuse “worktree of another clone” during spawn isolation checks.
  • Bump CI’s pinned Treehouse version to support --root, and improve bootstrap diagnostics to name missing Treehouse flags.
  • Add and wire regression tests that pin Treehouse pool-key/root facts and verify per-home pooling behavior.
File summaries
File Description
tests/fm-treehouse-pool-root.test.sh New integration test that pins Treehouse pool-key/root/return behavior against the installed binary.
tests/fm-tangle-guard.test.sh Updates spawn send-keys assertion to require treehouse get --root '<physical home>'.
tests/fm-startup-memory-budget.test.sh Updates Treehouse fake help output to advertise global --root.
tests/fm-spawn-pool-home.test.sh New spawn regression tests for per-home pool rooting and foreign-clone worktree refusal.
tests/fm-session-start.test.sh Updates Treehouse fake help output to include global --root.
tests/fm-secondmate-sync.test.sh Updates Treehouse help fakes and bootstrap flag fake plumbing (FM_FAKE_TREEHOUSE_GET_FLAGS).
tests/fm-secondmate-liveness.test.sh Updates Treehouse fake help output to include global --root.
tests/fm-secondmate-harness.test.sh Updates Treehouse fake help output to include global --root.
tests/fm-bootstrap.test.sh Enhances bootstrap suite to model --lease and --root capability separately and assert missing-flag messaging.
tests/fm-bootstrap-network-parallel.test.sh Updates bootstrap network-parallel fixture to advertise both Treehouse flags.
docs/verification/runtime-backends.md Documents and evidences Treehouse pool-root/key behavior and the --root version floor.
docs/scripts.md Notes Treehouse installer is used for pool-root regression coverage in CI.
docs/configuration.md Documents .treehouse/ and clarifies FM_HOME as the Treehouse pool root.
docs/architecture.md Updates worktree isolation doc to include per-home pooling and foreign-clone guard plus tests.
bin/fm-wake-lib.sh Adds fm_treehouse_pool_root helper for resolving a home’s physical path for pooling.
bin/fm-test-run.sh Registers new tests into families/lanes.
bin/fm-spawn.sh Sends treehouse get --root '<home>' to panes and adds clone-membership verification in worktree isolation.
bin/fm-install-treehouse.sh Bumps CI pin to Treehouse v2.3.0 and updates checksums/docs.
bin/fm-bootstrap.sh Replaces lease-only probe with treehouse_missing_flag for --lease and --root, reporting the missing flag.
AGENTS.md Documents .treehouse/ as a home-local pool directory.
.gitignore Ignores .treehouse/.
.github/workflows/ci.yml Installs pinned Treehouse for the portable serial job and fails CI if the “treehouse not found” gate-skip occurs.
.agents/skills/stuck-crewmate-recovery/SKILL.md Updates guidance to use treehouse status --root <home> for per-home pools.
.agents/skills/bootstrap-diagnostics/SKILL.md Clarifies Treehouse “MISSING” may mean an installed build lacks a required flag (named in the message).
Review details
  • Files reviewed: 18/24 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +176 to +182
expected="treehouse get --root '${home_phys//\'/\'\\\'\'}' Enter"

out=$(run_pool_spawn "$id" "$OWN_WT")
status=$?
expect_code 0 "$status" "spawn from a home path containing a quote should succeed"$'\n'"$out"
grep -F -- "$expected" "$REC" >/dev/null \
|| fail "the pane did not receive a correctly quoted pool root; expected: $expected"$'\n'"$(cat "$REC")"
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: first look on fork PR #4222 (Alberto-Codes).

Head 1ff50b8e618bb7b863c1e7042e027addb2fad903 (fm/fm-secondmate-treehouse-pool-collision-k19) vs main 31f062d43d25775f8010480cbcba56134aac1998. MERGEABLE / BLOCKED (checks settling). Author not blocked. Surface: bin/fm-spawn.sh per-home treehouse get --root + foreign-clone common-dir refuse, fm_treehouse_pool_root, Treehouse CI pin v2.0.1→v2.3.0 (SHA-256 verified against kunchenguid/treehouse v2.3.0 release assets), bootstrap --lease/--root probe, CI serial Treehouse install, tests + docs. ci.yml delta is install+fail-on-gate-skip only — no secrets / pull_request_target / privilege widen.

Attestation: MATCH (body <!-- no-mistakes-pipeline-attestation:v1 head_sha binds tip 1ff50b8e…).

Contract-class: restore. Unconfigured spawn already promised home-owned clones / isolated worker copies; shared Treehouse identity-keyed pool handed secondmates a worktree of another clone (observed Cursor near-miss). Per-home --root + common-dir refuse restore that isolation contract. Treehouse floor bump is the dependency honesty that restore needs, not a new product default.

VISION.md per-rule

  • One captain, one interface — aligns (mechanics stay below deck; refusal reasons more legible).
  • Authority is explicit and never inferred — aligns (no new consent assumption; refuse wrong clone rather than sail past).
  • Scripts own the mechanics, agents own the judgment — aligns (deterministic pool root + common-dir compare; no agent adjudication).
  • A restart is a non-event — aligns (no durable chat-only state; pool keyed by home path).
  • Delegation with a spine — aligns (workers stay in the spawning home's clone; isolation restored).
  • The fleet outlives any vendor — aligns (Treehouse --root is a documented vendor flag; Orca path skipped rather than guessed).
  • Scope — aligns (command-layer spawn isolation; workshop unchanged).

CI: First-time fork workflows approved this pass → CI 34631207780, Require no-mistakes 34631207796 (were action_required). Waiting on green tip CI. Merge-eligible N until CI green (restore + MATCH + safe review otherwise satisfied). Firstmate flag: no.

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