fix: record real leased worktree in fm-spawn meta; harden fm-teardown#264
fix: record real leased worktree in fm-spawn meta; harden fm-teardown#264prodempsey wants to merge 1 commit into
Conversation
fm-spawn recorded worktree=<firstmate home> (the launch cwd) in every crew's meta instead of the crew's real treehouse worktree. It sent `treehouse get` into the pane and polled `pane_current_path` until it differed from PROJ_ABS, taking that first differing path as the worktree - but the pane can briefly sit in the session's default working directory (the firstmate home) before the treehouse subshell is entered, so WT latched onto that spurious path. The bad worktree broke fm-teardown (`treehouse return` on a non-pool path), merge/PR detection, and branch resolution, forcing hand-teardown of every crew. fm-spawn now leases the worktree authoritatively with `treehouse get --lease`, whose stdout is exactly the leased path, then sends `cd <worktree>` into the pane. Secondmate and Orca paths are unchanged. Teardown's existing `treehouse return --force` releases the lease as before. fm-teardown is also hardened: a missing, stale, or non-treehouse worktree in the meta (e.g. a pre-fix meta) now warns and continues - killing the window and clearing volatile state - instead of aborting when `treehouse return` refuses an unmanaged path. A safe-worktree guard prevents branch cleanup / return from ever touching the runtime home for a firstmate-on-itself meta. Tests: new fm-spawn-worktree-lease asserts the meta records the leased path; fm-teardown gains graceful-degradation and non-fatal-return cases; the existing spawn mocks (fm-backend, fm-tangle-guard, fm-grok-harness, fm-secondmate-harness, fm-spawn-dispatch-profile) are updated for the --lease flow, and the fm-backend spawn conformance check now asserts the new mechanism.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 999b82f802
ℹ️ 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".
| # the worktree-path fix, which recorded the launch cwd instead of the leased | ||
| # worktree) must not brick teardown - warn and fall through so the window is | ||
| # still killed and volatile state cleared. | ||
| if [ -n "$WT" ] && [ -d "$WT" ] && safe_task_worktree "$PROJ" "$WT"; then |
There was a problem hiding this comment.
Skip invalid recorded worktrees before safety checks
In the normal non-Orca teardown path, this new guard is only reached after the dirty/unpushed checks have already run against $WT. When a pre-fix meta recorded the launch cwd or firstmate home and that directory is a git checkout with dirty or unpushed work, fm-teardown.sh exits with REFUSED before it reaches this graceful skip, so the window and meta remain stuck even though the recorded path is not safe to return. Apply the same safe_task_worktree decision before the landed-work checks, or otherwise bypass those checks for invalid recorded worktrees.
Useful? React with 👍 / 👎.
fix: record real leased worktree in fm-spawn meta; harden fm-teardown
Summary
bin/fm-spawn.shrecorded the wrong worktree path in every crew's meta, which broke teardown, merge/PR detection, and branch resolution and forced hand-teardown of every crew.This change replaces the fragile pane-cwd polling heuristic with an authoritative
treehouse get --leaseacquire, whose stdout is exactly the leased worktree path, and hardensbin/fm-teardown.shso a missing, stale, or non-treehouse worktree in the meta degrades gracefully instead of bricking teardown.Root cause
The old spawn path sent
treehouse getinto the task pane and then polled tmux's#{pane_current_path}, taking the first value that differed from the project path as the worktree.That is a fork/exec race: between sending
treehouse getand the treehouse subshell actually entering the pooled worktree, the pane can transiently report the session's default working directory - the firstmate home wherefm-spawnitself runs.The poll latched onto that spurious transient cwd, so
worktree=<firstmate home>was written into the meta instead of the real leased worktree.Downstream,
fm-teardownthen rantreehouse returnon a non-pool path (refused), and merge/PR detection and branch resolution operated on the wrong directory, so every crew had to be torn down by hand.The fix
fm-spawnnow leases the worktree out-of-band withtreehouse get --lease --lease-holder "fm-<id>", run from the project so treehouse resolves the correct pool.--leaseprints only the leased worktree's absolute path to stdout (all banners go to stderr), so the captured value is authoritative rather than scraped.If no worktree is leased, spawn exits 1 immediately - no 60s poll, no timeout, no chance of a spurious path.
The pane is then moved into the leased worktree with an explicit
cd <worktree>send.The leased worktree is protected from
treehouse prunefor the task's lifetime, and teardown's existingtreehouse return --force "$WT"releases the lease exactly as it did for an interactively-acquired worktree.The secondmate and Orca spawn paths are unchanged.
Why
--leasebeats a hardened pollA hardened poll (longer timeout, cwd sanity checks) would still be inferring the worktree from an observable side effect that races with the pane.
--leaseremoves the inference entirely: treehouse returns the path it reserved, so there is exactly one source of truth and no window in which a transient cwd can be mistaken for the worktree.It also makes the reservation durable against
treehouse prune, which the poll never did.This design - authoritative acquire plus fail-fast exit 1 - is Captain-approved.
Teardown hardening
fm-teardownaddssafe_task_worktree(), which permits branch cleanup andtreehouse returnonly when the recorded worktree is a real, registered worktree of the task's project AND is neither the firstmate repo root nor the active firstmate home.The second guard matters for firstmate-on-itself tasks whose meta was written before this fix: such a meta could record the runtime home (itself a genuine, registered worktree of the firstmate repo) as the worktree, and teardown must never detach, branch-delete, or hand the runtime home to
treehouse return.Teardown now also degrades gracefully: a missing, stale, or non-treehouse worktree warns on stderr and continues - the window is still killed and volatile state cleared - instead of aborting when
treehouse returnrefuses an unmanaged path.A failed
treehouse returnlikewise warns rather than aborting, so a stuck pool slot never blocks the rest of teardown.Tests
tests/fm-spawn-worktree-lease.test.shasserts the spawn meta records the leased path, that the worktree is leased withtreehouse get --leaserather than sent into the pane, and that the pane is driven withcd <worktree>.tests/fm-teardown.test.shgains graceful-degradation and non-fatal-return cases (non-treehouse worktree skipped, failed return warns but continues).test_spawn_conformance_old_vs_newintests/fm-backend.test.shdrops the byte-identical old-vs-new tmux-log comparison (no longer meaningful after the mechanism change) and instead asserts the new--leasemechanism, meta recording, and pane drive.tests/fm-grok-harness.test.sh,tests/fm-secondmate-harness.test.sh,tests/fm-spawn-dispatch-profile.test.sh, andtests/fm-tangle-guard.test.share updated to echo a fake leased path for the--leaseflow.Test evidence
Verified in the crew worktree on 2026-07-04, rebased onto
origin/mainatdc37d58.shellcheck bin/*.sh bin/backends/*.sh tests/*.sh(the exact CI command): clean, exit 0.bash -non every touched script: clean.fm-spawn-worktree-lease.test.sh,fm-teardown.test.sh, and the rewrittenfm-backend.test.shconformance case.origin/main(dc37d58) baseline run in the same environment.The branch is strictly better than baseline:
fm-backend.test.shgoes FAIL -> PASS, because itstest_spawn_conformance_old_vs_newwas already failing onorigin/main(an upstreamGOTMPDIRchange had broken the byte-identical old-vs-new assertion) and this change rewrites that test to the new mechanism.origin/main, identical on both baseline and this branch, and all live in files this change does not touch:fm-backend-cmux,fm-backend-orca,fm-backend-zellij(backend-binary/home-hash env dependencies),fm-gotmp,fm-fleet-sync,fm-session-start,fm-turnend-guard,fm-secondmate-safety, andfm-secondmate-lifecycle-e2e(environment/tooling/seed dependencies).They are unrelated to this change.
Note for the pusher
The commit preserves its original
Co-Authored-By: Claude Opus 4.8 (1M context)trailer from the archivedfm/fmspawn-worktree-fixcommita5f477c.The firstmate repo house style forbids an agent co-author trailer, so consider
git commit --amendto strip it before pushing.