Skip to content

fix(bin): lease a secondmate's worktree from its own clone - #64

Merged
tiago-peixoto merged 2 commits into
mainfrom
fm/firstmate-secondmate-claude-trust-pool
Sep 23, 2026
Merged

tiago-peixoto merged 2 commits into
mainfrom
fm/firstmate-secondmate-claude-trust-pool

Conversation

@tiago-peixoto

Copy link
Copy Markdown
Owner

Problem

A second mate cannot start or relaunch any worker on the claude harness.
Spawns fail with:

error: refusing to pre-register Claude trust: '<pool>/artemis-b0df83/1/artemis' is not a worktree of project '<secondmate home>/projects/artemis'

Upstream issue: kunchenguid#4348 (duplicate report: kunchenguid#4045).

Root cause

Treehouse names a pool <basename>-<sha256(origin URL)[:6]> (internal/config/config.go ResolvePoolDir in treehouse v2.3.0), not by clone path.
The main home's projects/artemis and the second mate's projects/artemis have the same basename and origin, so they share ~/.treehouse/artemis-b0df83.
Its reuse scan (internal/pool/pool.go acquire) hands out any idle slot regardless of which clone it is linked to, and every existing slot there is linked to the main home's clone.
The trust refusal is correct: that copy is not the spawning project's worktree, and the slot claim and teardown's project lock also skip it.

Fix

The fix is in lease acquisition in bin/fm-spawn.sh, which every Treehouse spawn goes through.
Trust registration still refuses these copies.

  • A leased copy whose Git common directory is not the spawning clone's is held leased while acquisition retries, the same way an occupied copy already is.
    Treehouse then cannot hand it out again and creates a copy from the spawning clone.
    The held copies are returned when acquisition finishes, whether it succeeds or fails.
  • Relaunch only: a task already recorded in a copy of another clone with an identical origin URL registers trust against that clone, and fm-claude-trust.sh still checks that the worktree is linked to it.
    Without this, work already in flight in the Artemis home could not move to claude.
    A copy of an unrelated repository is still refused.

I considered a per-home treehouse --root (kunchenguid#4222) and rejected it here.
It moves every home's pool, including the main home's pools that hold live leases, and it raises the treehouse version floor.
This fix needs neither.
Trade-off: the two clones still share max_trees capacity, and a spawn may reset a few of the other clone's idle copies while it retries.

Evidence

Reproduced first, then fixed (tests/fm-spawn-worktree-lease.test.sh):

  • test_spawn_with_real_treehouse_launches_in_its_own_clone runs installed treehouse 2.3.0 end to end.
    The main clone leaves an idle copy in the shared pool, then a claude spawn runs from a second-mate-shaped home with its own clone.
    Before the fix it failed with the exact reported refusal; after the fix it launches in a copy of its own clone and returns the main clone's copy.
    It skips when treehouse is not installed.
  • test_spawn_skips_a_copy_of_another_clone covers the same case with the fake pool, so it also runs in CI.
  • test_relaunch_trusts_a_recorded_copy_of_another_clone failed before the fix and passes after it.
    test_relaunch_still_refuses_a_copy_of_an_unrelated_repository pins the refusal that stays.

Also run: bin/fm-lint.sh, bin/fm-doc-audience-check.sh, tests/fm-claude-trust.test.sh, and all tests/fm-spawn-*.test.sh, plus tests/fm-secondmate-safety.test.sh.
All passed.
tests/fm-spawn-compact-adviser-disable-remote.test.sh failed once while another suite ran concurrently, then passed 3 of 3 reruns.
It does not touch Treehouse.

Treehouse names a pool by directory name and origin URL, not by clone
path, so a secondmate home's clone of a project shares the pool of the
main home's clone of the same origin. A spawn from the secondmate home
was handed a copy linked to the main home's clone, and Claude trust
pre-registration refused it ("is not a worktree of project"), so no
claude worker could start there.

Acquisition now holds such a copy leased while it retries, so Treehouse
cannot hand it out again and creates a copy from the spawning clone,
then returns the held copies. The trust refusal is unchanged for fresh
spawns. A relaunch of a task already recorded in another clone's copy
of the same origin registers trust against that clone, so existing work
can move to claude; an unrelated repository's copy is still refused.

Refs kunchenguid#4348
The free-or-dead-lock case made its dead pid with `sleep 30 &` followed
at once by `kill`. A TERM that reaches the forked child before it drops
the suite's TERM trap runs fm_test_cleanup there, and the child shares
the suite's cleanup registry, so it deleted the fixture root while the
test kept running. CI saw the second extension load fail with
ERR_MODULE_NOT_FOUND. A child that exits on its own yields a dead pid
without sending any signal.
@tiago-peixoto
tiago-peixoto merged commit 1006b0f into main Sep 23, 2026
19 checks passed
tiago-peixoto added a commit that referenced this pull request Sep 25, 2026
* fix(bin): lease a secondmate's worktree from its own clone

Treehouse names a pool by directory name and origin URL, not by clone
path, so a secondmate home's clone of a project shares the pool of the
main home's clone of the same origin. A spawn from the secondmate home
was handed a copy linked to the main home's clone, and Claude trust
pre-registration refused it ("is not a worktree of project"), so no
claude worker could start there.

Acquisition now holds such a copy leased while it retries, so Treehouse
cannot hand it out again and creates a copy from the spawning clone,
then returns the held copies. The trust refusal is unchanged for fresh
spawns. A relaunch of a task already recorded in another clone's copy
of the same origin registers trust against that clone, so existing work
can move to claude; an unrelated repository's copy is still refused.

Refs kunchenguid#4348

* test(pi): stop a killed fork from deleting the dead-lock fixture

The free-or-dead-lock case made its dead pid with `sleep 30 &` followed
at once by `kill`. A TERM that reaches the forked child before it drops
the suite's TERM trap runs fm_test_cleanup there, and the child shares
the suite's cleanup registry, so it deleted the fixture root while the
test kept running. CI saw the second extension load fail with
ERR_MODULE_NOT_FOUND. A child that exits on its own yields a dead pid
without sending any signal.
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.

1 participant