Skip to content

fix(bin): reconcile treehouse pool path spellings on a symlinked home - #2692

Open
Inthuson wants to merge 5 commits into
kunchenguid:mainfrom
Inthuson:fm/teardown-worktree-path-p6
Open

fix(bin): reconcile treehouse pool path spellings on a symlinked home#2692
Inthuson wants to merge 5 commits into
kunchenguid:mainfrom
Inthuson:fm/teardown-worktree-path-p6

Conversation

@Inthuson

@Inthuson Inthuson commented Aug 20, 2026

Copy link
Copy Markdown

Intent

Stop firstmate having to hand-edit a recorded worktree path before every crewmate or scout cleanup on this host.

On this machine /home/inthuson is a symlink to /local/home/inthuson, so treehouse records pool worktree paths using its as-written $HOME-rooted spelling (/home/inthuson/...) while bin/fm-spawn.sh records the fully resolved physical path (/local/home/inthuson/...). Treehouse matches its managed inventory by string rather than by resolved path, so "treehouse return" refused with "is not managed by treehouse" and cleanup aborted at bin/fm-teardown.sh:2453 after it had already reaped the worktree processes, leaving the task half cleaned. Nine occurrences across two days, and every cleanup on this host needed a manual rewrite of worktree= in state/.meta first.

Required approach, as specified by the task: reconcile the two spellings rather than picking one, comparing paths after resolving both sides so a symlinked home stops being a special case. Explicitly ruled out: simply switching firstmate to the $HOME spelling without checking every other consumer of worktree=, because the physical path is what the liveness and worktree-isolation checks compare against, and breaking isolation detection to fix a cleanup annoyance would be a bad trade. The new bin/fm-treehouse-lib.sh is the single owner of the reconciliation and only ever offers spellings that provably resolve to the identical physical directory; retry and lock patience policy deliberately stays with the caller. bin/fm-home-seed.sh needed the same reconciliation because the identical defect silently leaked a durable lease during seed rollback.

Explicitly out of scope and deliberately not fixed here: teardown-shared-slot-guard-s2, the separate hazard that cleanup returns a pool slot without checking whether another task holds it.

Verification owed and completed: a real pooled worktree torn down on this host with no hand-editing; and a colocated regression in tests/fm-teardown.test.sh covering the symlinked-home spelling on both sides, since a happy-path-only test would be worthless here when the bug is a comparison that is wrong only when the two spellings differ.

The third owed item was making tests/fm-backend-herdr-presentation-e2e.test.sh complete rather than abort 77 lines before the code it exists to exercise, which was a silent coverage hole rather than a green tick. The abort is gone: the suite now reaches and passes five checks, including the previously unreachable opted-out spawn and teardown. It then failed on an unrelated assertion because this host runs herdr 0.8.2 while CI pins 0.7.4 exactly. On 0.8.2, closing a tab's last pane respawns a replacement pane, so a projected workspace never becomes empty, the exactly-one-tab-and-one-pane test in fm_backend_herdr_emptying_close_plan never matches, a projected workspace is therefore never removed by a pane close, and the leftover fixture workspace trails the ordering label list. That was confirmed by direct probe through the guarded Herdr lab helper, not inferred, and it is a real product defect whose captain-visible symptom is that every projected cleanup on 0.8.2 orphans a workspace in their own window.

Accepted decision on that item: bound the suite to the supported pin so a non-pinned host skips rather than fails, which keeps the signal honest without masking anything. Do not fix the 0.8.2 cleanup defect in this change; it is filed separately as herdr-082-projection-orphan-o4 at priority 1. Adapting or deleting the fixture to go green was considered and rejected outright, because it would hide the orphan defect behind a green tick. Downgrading herdr locally to the pin was also rejected, because it would disturb the live session holding the captain's own terminal and two parked validation runs. The pin gate reads the version from bin/fm-install-herdr.sh so that file remains the single owner of the pin, and an unreadable pin or unreadable installed version exits non-zero rather than skipping, because a skip nobody can trust would quietly disable this whole required lane.

The PR body must state four things: that unblocking this test immediately exposed a real 0.8.2 defect which CI's 0.7.4 pin hides, since that is the strongest argument for the change; that a correct path makes teardown-shared-slot-guard-s2's wrong return succeed rather than abort, so this change slightly raises the stakes on that separate task; that the reconciliation is bounded to the $HOME root alias plus the fully physical reverse direction and does not cover an arbitrary symlinked custom root in a repo's treehouse.toml; and that no docs/verification record is owed because nothing here is harness-dependent.

Constraints: this is firstmate's own shared tracked material, so firstmate-coding-guidelines applies; shellcheck-clean at the pinned 0.11.0; no em-dashes anywhere, including commit messages and the PR body; never add an agent name as a commit co-author.

Note for review: four failures in the changed-file test selection are pre-existing and unrelated to this change, proven by byte-identical failures on a pristine 1cb900c baseline clone: fm-backlog-handoff, fm-on, fm-remote-backlog-handoff, and fm-secondmate-lifecycle-e2e.

What Changed

  • Added bin/fm-treehouse-lib.sh as the single owner of pool path spelling reconciliation between firstmate's recorded physical worktree paths and treehouse's string-matched inventory. It offers only spellings that provably resolve to the identical physical directory (recorded spelling first, then the $HOME-rooted rewrite, then the fully resolved path) and exposes fm_treehouse_return_force. bin/fm-teardown.sh's teardown_treehouse_return now tries each equivalent spelling before spending its git index.lock patience window, headlining the recorded path's own refusal on total failure while still surfacing any other spelling's genuinely different failure; bin/fm-home-seed.sh's seed rollback uses the same reconciliation so a refused return no longer leaks a durable lease behind a warning. Retry and lock patience policy stays with each caller.
  • tests/fm-backend-herdr-presentation-e2e.test.sh no longer aborts before the code it exists to exercise: it now reaches and passes its five checks, including the previously unreachable opted-out spawn and teardown, and it gates on the exact Herdr pin read from the new bin/fm-install-herdr.sh --required-version flag so a non-pinned host skips rather than fails. An unreadable pin or unreadable installed version exits non-zero instead of skipping. That suite plus the autodetect, launcher-workspace, and workspace-per-home real-Herdr suites return pool worktrees through the shared helper.
  • Test and wiring updates: the fake treehouse in tests/secondmate-helpers.sh gained FM_FAKE_TREEHOUSE_MANAGED string matching so fixtures mimic the real inventory refusal; tests/fm-teardown.test.sh adds four colocated cases (reconciled return on a symlinked home, a genuinely unmanaged worktree still failing as the recorded path, a managed spelling's real failure surviving the recorded refusal, and a transient lock retried under the recognized spelling), tests/fm-secondmate-safety.test.sh covers the seed rollback path, bin/fm-test-run.sh maps the new library to the pr-forge, secondmate, and real-herdr-gated families, and tests/fm-gotmp.test.sh links it into its fake roots.

Notes

  • Unblocking the presentation suite immediately exposed a real Herdr 0.8.2 defect that CI's exact 0.7.4 pin hides, which is the strongest argument for this change. On 0.8.2, closing a tab's last pane respawns a replacement pane, so a projected workspace never becomes empty, the exactly-one-tab-and-one-pane test in fm_backend_herdr_emptying_close_plan never matches, and a projected workspace is never removed by a pane close. The captain-visible symptom is that every projected cleanup on 0.8.2 orphans a workspace in their own window. Confirmed by direct probe through the guarded Herdr lab helper, filed separately as herdr-082-projection-orphan-o4 at priority 1, and deliberately not fixed here.
  • This slightly raises the stakes on teardown-shared-slot-guard-s2, which is out of scope here: with a correct path, cleanup's pool slot return now succeeds rather than aborting, so a return that never checks whether another task holds the slot will actually go through.
  • The reconciliation is bounded to the $HOME root alias plus the fully physical reverse direction. It does not cover an arbitrary symlinked custom root configured in a repo's treehouse.toml.
  • No docs/verification record is owed, because nothing here is harness dependent. The docs/verification/runtime-backends.md edits only annotate that the presentation suite is now pin gated and that the dated runs on other releases predate that gate.
  • The pin gate's first implementation was wrong, and that is a review miss rather than a late tightening. It recovered the pinned version by regexing bin/fm-install-herdr.sh for its FM_HERDR_CI_VERSION assignment. The goal of keeping one owner of the pin was right and the gate was accepted on that basis, but nobody caught that the mechanism breaks this repo's own rule that a test must never assert implementation source bytes, including through a regex, so a behavior preserving edit to that assignment could have silently disabled a required lane. The --required-version flag added here is that correction rather than a tidy-up: it publishes the pin through an executable interface in the shape bin/fm-lint.sh --required-version already established, and the pin still has exactly one owner.
  • The presentation suite's five checks are confirmed only by the pinned CI lane, not by any run on the host this was developed on. That host runs Herdr 0.8.2 while the suite is bound to 0.7.4, so with the gate in place the suite skips there, and what was actually exercised locally is the gate itself: the skip path on a non-pinned host, plus both loud failure modes, an unreadable pin and an unreadable installed version, each exiting non-zero rather than skipping. The five checks were seen reaching and passing on 0.8.2 before the gate existed, which is how the orphan defect above surfaced at all, but that is not the same as the committed suite passing at the pin. Downgrading Herdr locally to 0.7.4 would have closed that gap and was refused on purpose, because it would disturb a live session holding a working terminal and two parked validation runs. Pull requests from this fork also wait on a maintainer to approve workflow runs before they start, so that lane's output cannot be attached here before merge.

Risk Assessment

✅ Low: The reconciliation is bounded by a provable same-physical-directory invariant, dedups to a single byte-identical attempt on non-symlinked hosts, every treehouse return call site in bin/ now routes through the one library, all five prior findings are verifiably fixed with genuine regression tests, and the only stakes-raising interaction (teardown-shared-slot-guard-s2) is explicitly acknowledged and separately tracked in the intent.

Testing

I reproduced the reported failure and its fix with the real treehouse binary and the real bin/fm-teardown.sh on a symlinked-home fixture: treehouse refuses the physically recorded pool path and keeps the lease, pre-fix teardown aborts at the return, and post-fix teardown completes with no hand-edited worktree= and the pool slot released. I then probed the claimed reconciliation boundaries against the real binary (reverse physical-root direction, recorded-first ordering, no $HOME candidate for a directory outside $HOME, single attempt for a path that no longer resolves). On the automated side I ran the changed-file portable suites that this host can run (fm-teardown, fm-secondmate-safety, fm-gotmp, fm-test-run, plus a spot check of fm-backlog-handoff which gate-skips here), proved both colocated regressions fail on a 1cb900c baseline tree carrying the new tests, added one focused teardown case for the reconciliation-plus-lock-patience interaction and mutation-checked it, and exercised the runner's changed-file mapping through bin/fm-test-run.sh --list --changed, which selects the consumer families for the new library where the pre-fix runner dies as unmapped. The Herdr presentation suite skips cleanly on this host's herdr 0.8.2 against the 0.7.4 pin and exits non-zero rather than skipping when either the pin owner or the installed version cannot be read, and it does not skip when the reported version equals the pin. This change is shell and CLI only with no rendered surface, so the evidence is CLI transcripts and treehouse's own pool status rather than screenshots. All tests and checks I ran passed; scratch trees are removed, the captain's herdr session and the $HOME treehouse pool are untouched, and the only working-tree change is the added test case.

Evidence: Real treehouse + real fm-teardown.sh on a symlinked home (the fix)

== the bug: real treehouse return --force with the recorded physical path == worktree /tmp/fm-th-e2e-lQmVLi/physhome/.treehouse/proj-c2b672/1/proj is not managed by treehouse exit=1 pool status: [{"path":".../home/.treehouse/proj-c2b672/1/proj","status":"leased","lease_holder":"fm-evidence"}] == recorded task meta (worktree= is the physical spelling, unedited) == worktree=/tmp/fm-th-e2e-lQmVLi/physhome/.treehouse/proj-c2b672/1/proj == the fix: real bin/fm-teardown.sh on that recorded path == 🌳 Worktree returned to pool. teardown: worktree return matched treehouse's pool path /tmp/fm-th-e2e-lQmVLi/home/.treehouse/proj-c2b672/1/proj for recorded worktree /tmp/fm-th-e2e-lQmVLi/physhome/.treehouse/proj-c2b672/1/proj (same directory, different spelling) teardown task-e2e complete (window firstmate:fm-task-e2e, worktree /tmp/fm-th-e2e-lQmVLi/physhome/.treehouse/proj-c2b672/1/proj) fm-teardown.sh exit=0 == treehouse's own pool status after teardown == [{"path":".../home/.treehouse/proj-c2b672/1/proj","status":"available","lease_id":"","lease_holder":""}]


== host-shaped fixture: a home that is a symlink ==
HOME as written : /tmp/fm-th-e2e-lQmVLi/home
HOME physical   : /tmp/fm-th-e2e-lQmVLi/physhome
treehouse       : /home/linuxbrew/.linuxbrew/bin/treehouse (2.1.1)

== treehouse get --lease (real pool acquire) ==
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/proj-c2b672/1/proj. Run 'treehouse return ~/.treehouse/proj-c2b672/1/proj' to release it.
treehouse's own pool spelling : /tmp/fm-th-e2e-lQmVLi/home/.treehouse/proj-c2b672/1/proj
fm-spawn's recorded worktree= : /tmp/fm-th-e2e-lQmVLi/physhome/.treehouse/proj-c2b672/1/proj

== the bug: real treehouse return --force with the recorded physical path ==
worktree /tmp/fm-th-e2e-lQmVLi/physhome/.treehouse/proj-c2b672/1/proj is not managed by treehouse
exit=1
pool status: [{"name":"1","path":"/tmp/fm-th-e2e-lQmVLi/home/.treehouse/proj-c2b672/1/proj","status":"leased","lease_id":"64b261d690d78832ca4150836c1fefd0","lease_holder":"fm-evidence","leased_at":"2026-08-20T19:24:23.044538541Z","processes":[]}]

== recorded task meta (worktree= is the physical spelling, unedited) ==
window=firstmate:fm-task-e2e
endpoint_task_id=task-e2e
worktree=/tmp/fm-th-e2e-lQmVLi/physhome/.treehouse/proj-c2b672/1/proj
project=/tmp/fm-th-e2e-lQmVLi/physhome/proj
kind=ship
mode=no-mistakes

== the fix: real bin/fm-teardown.sh on that recorded path ==
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🌳 Worktree returned to pool.
teardown: worktree return matched treehouse's pool path /tmp/fm-th-e2e-lQmVLi/home/.treehouse/proj-c2b672/1/proj for recorded worktree /tmp/fm-th-e2e-lQmVLi/physhome/.treehouse/proj-c2b672/1/proj (same directory, different spelling)
/tmp/fm-th-e2e-lQmVLi/physhome/proj: STUCK: on branch main with uncommitted changes, 0 commits behind origin/main - needs attention
teardown task-e2e complete (window firstmate:fm-task-e2e, worktree /tmp/fm-th-e2e-lQmVLi/physhome/.treehouse/proj-c2b672/1/proj)
Backlog: task-e2e just finished. Update data/backlog.md - move task-e2e to Done, keep Done to the 10 most recent, then re-scan Queued and dispatch only work whose blockers are gone and date is due.
fm-teardown.sh exit=0

== treehouse's own pool status after teardown ==
[{"name":"1","path":"/tmp/fm-th-e2e-lQmVLi/home/.treehouse/proj-c2b672/1/proj","status":"available","lease_id":"","lease_holder":"","leased_at":null,"processes":[]}]
Evidence: Same real e2e against the pre-fix 1cb900c tree (the reported symptom)

== the fix: real bin/fm-teardown.sh on that recorded path == worktree /tmp/fm-th-e2e-bSX1si/physhome/.treehouse/proj-eecd16/1/proj is not managed by treehouse error: treehouse return failed for worktree /tmp/fm-th-e2e-bSX1si/physhome/.treehouse/proj-eecd16/1/proj; teardown aborted fm-teardown.sh exit=1 == treehouse's own pool status after teardown == [{"path":".../home/.treehouse/proj-eecd16/1/proj","status":"leased","lease_holder":"fm-evidence"}]


== host-shaped fixture: a home that is a symlink ==
HOME as written : /tmp/fm-th-e2e-bSX1si/home
HOME physical   : /tmp/fm-th-e2e-bSX1si/physhome
treehouse       : /home/linuxbrew/.linuxbrew/bin/treehouse (2.1.1)

== treehouse get --lease (real pool acquire) ==
🌳 Setting up worktree...
🌳 Leased worktree at ~/.treehouse/proj-eecd16/1/proj. Run 'treehouse return ~/.treehouse/proj-eecd16/1/proj' to release it.
treehouse's own pool spelling : /tmp/fm-th-e2e-bSX1si/home/.treehouse/proj-eecd16/1/proj
fm-spawn's recorded worktree= : /tmp/fm-th-e2e-bSX1si/physhome/.treehouse/proj-eecd16/1/proj

== the bug: real treehouse return --force with the recorded physical path ==
worktree /tmp/fm-th-e2e-bSX1si/physhome/.treehouse/proj-eecd16/1/proj is not managed by treehouse
exit=1
pool status: [{"name":"1","path":"/tmp/fm-th-e2e-bSX1si/home/.treehouse/proj-eecd16/1/proj","status":"leased","lease_id":"7166fe5eb5d4fb171694b65054e15ca3","lease_holder":"fm-evidence","leased_at":"2026-08-20T19:24:34.296056307Z","processes":[]}]

== recorded task meta (worktree= is the physical spelling, unedited) ==
window=firstmate:fm-task-e2e
endpoint_task_id=task-e2e
worktree=/tmp/fm-th-e2e-bSX1si/physhome/.treehouse/proj-eecd16/1/proj
project=/tmp/fm-th-e2e-bSX1si/physhome/proj
kind=ship
mode=no-mistakes

== the fix: real bin/fm-teardown.sh on that recorded path ==
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
worktree /tmp/fm-th-e2e-bSX1si/physhome/.treehouse/proj-eecd16/1/proj is not managed by treehouse
error: treehouse return failed for worktree /tmp/fm-th-e2e-bSX1si/physhome/.treehouse/proj-eecd16/1/proj; teardown aborted
fm-teardown.sh exit=1

== treehouse's own pool status after teardown ==
[{"name":"1","path":"/tmp/fm-th-e2e-bSX1si/home/.treehouse/proj-eecd16/1/proj","status":"leased","lease_id":"7166fe5eb5d4fb171694b65054e15ca3","lease_holder":"fm-evidence","leased_at":"2026-08-20T19:24:34.296056307Z","processes":[]}]
Evidence: Reconciliation boundaries against the real treehouse (reverse direction, safety invariant, non-resolving record)

A. reverse direction: pool root physical, record $HOME-rooted raw treehouse return --force -> "is not managed by treehouse", exit=1, lease still held fm_treehouse_return_force -> 🌳 Worktree returned to pool. exit=0, pool status "available" B. recorded dir outside $HOME: only its own spelling offered; every spelling resolves to the recorded directory: yes B2. under a symlinked $HOME: physhome/wt then home/wt (recorded first) C. record that no longer resolves: attempts made: 1, refusal unchanged


== A. reverse direction: pool root is physical, firstmate records the $HOME spelling ==
treehouse's own pool spelling : /tmp/fm-recon-probe-adBidr/physhome/.treehouse/proj-12e57c/1/proj
the recorded spelling         : /tmp/fm-recon-probe-adBidr/home/.treehouse/proj-12e57c/1/proj
both name one directory       : /tmp/fm-recon-probe-adBidr/physhome/.treehouse/proj-12e57c/1/proj

raw treehouse return --force "$RECORDED":
worktree /tmp/fm-recon-probe-adBidr/home/.treehouse/proj-12e57c/1/proj is not managed by treehouse
exit=1
lease still held: [{"name":"1","path":"/tmp/fm-recon-probe-adBidr/physhome/.treehouse/proj-12e57c/1/proj","status":"leased","lease_id":"38c862e47fab0b9b2f9b1960c99f96a6","lease_holder":"fm-evidence","leased_at":"2026-08-20T19:29:02.92035286Z","processes":[]}]

fm_treehouse_return_force "" "$RECORDED" (from the project dir):
🌳 Worktree returned to pool.
exit=0
pool after: [{"name":"1","path":"/tmp/fm-recon-probe-adBidr/physhome/.treehouse/proj-12e57c/1/proj","status":"available","lease_id":"","lease_holder":"","leased_at":null,"processes":[]}]

== B. safety invariant: a directory outside $HOME gets no $HOME-rooted candidate ==
HOME              : /tmp/fm-recon-probe-adBidr/home -> /tmp/fm-recon-probe-adBidr/physhome
recorded (outside): /tmp/fm-recon-probe-adBidr/elsewhere/wt
spellings offered :
  /tmp/fm-recon-probe-adBidr/elsewhere/wt
every spelling resolves to the recorded directory: yes

== B2. same-directory spellings under a symlinked $HOME are offered, recorded first ==
  /tmp/fm-recon-probe-adBidr/physhome/wt
  /tmp/fm-recon-probe-adBidr/home/wt

== C. a recorded path that no longer resolves: exactly one attempt, refusal unchanged ==
worktree /tmp/fm-recon-probe-adBidr/physhome/gone-forever is not managed by treehouse
exit=1
attempts made: 1
attempted path: /tmp/fm-recon-probe-adBidr/physhome/gone-forever
Evidence: Colocated regressions fail before the fix, pass after

against 1cb900c code: not ok - symlinked-home-pool-path: teardown should return the worktree by treehouse's own spelling of the same directory: expected exit 0, got 1 not ok - seed rollback reported a leaked lease instead of reconciling the two spellings against this change: ok - a physically recorded worktree is returned by treehouse's own $HOME-rooted spelling of it ok - a genuinely unmanaged worktree still fails, reported as the recorded path ok - an alternative spelling's genuinely different failure survives the recorded path's refusal ok - a transient lock under treehouse's own spelling is retried under that spelling, not the refused one ok - home seed rollback returns a physically recorded home by treehouse's own $HOME-rooted spelling

Pre-fix baseline = 1cb900c source tree + this change's new test cases.

== tests/fm-teardown.test.sh, the three reconciliation cases, against 1cb900c code ==
not ok - symlinked-home-pool-path: teardown should return the worktree by treehouse's own spelling of the same directory: expected exit 0, got 1
exit=1

== tests/fm-secondmate-safety.test.sh, the seed-rollback case, against 1cb900c code ==
not ok - seed rollback reported a leaked lease instead of reconciling the two spellings

== the same cases against this change (c6fb6e5 + this phase's added case ac) ==
ok - a physically recorded worktree is returned by treehouse's own $HOME-rooted spelling of it
ok - an alternative spelling's genuinely different failure survives the recorded path's refusal
ok - a transient lock under treehouse's own spelling is retried under that spelling, not the refused one
ok - home seed rollback returns a physically recorded home by treehouse's own $HOME-rooted spelling
Evidence: Herdr pin gate: honest skip here, loud failure when the pin or version is unreadable, no skip at the pin

pin published by bin/fm-install-herdr.sh --required-version : 0.7.4 installed herdr : herdr 0.8.2 real run on this host: skip: herdr 0.8.2 is not the suite-verified pin 0.7.4; install it with bin/fm-install-herdr.sh (exit=0) pin owner cannot answer: not ok - could not read the exact Herdr pin from bin/fm-install-herdr.sh --required-version (exit=1) installed version unreadable: not ok - could not read the installed Herdr version from ... (exit=1) version equals the pin: no skip line; the suite proceeds past the gate into lab provisioning

== this host ==
pin published by bin/fm-install-herdr.sh --required-version : 0.7.4
installed herdr                                            : herdr 0.8.2

== real run of tests/fm-backend-herdr-presentation-e2e.test.sh on this host ==
skip: herdr 0.8.2 is not the suite-verified pin 0.7.4; install it with bin/fm-install-herdr.sh
exit=0

== gate failure mode 1: the pin owner cannot answer (must not skip) ==
not ok - could not read the exact Herdr pin from bin/fm-install-herdr.sh --required-version
exit=1

== gate failure mode 2: the installed version cannot be read (must not skip) ==
not ok - could not read the installed Herdr version from /tmp/fm-pin-gate-zSn7VX/fakebin/herdr
exit=1

== gate is not vacuous: a herdr reporting the pin 0.7.4 is NOT skipped ==
fm-herdr-lab: fleet-state tripwire requires exactly one running default session
not ok - could not provision the isolated Herdr lab
(no 'skip:' line: the suite proceeded past the pin gate into lab provisioning)
Evidence: Mutation check for the case I added (reconciliation x lock patience)

control (unmutated c6fb6e5): ok - a transient lock under treehouse's own spelling is retried under that spelling, not the refused one mutated (dir=$spelling removed): not ok - managed-spelling-transient-lock: teardown should succeed once the lock under treehouse's own spelling clears: expected exit 0, got 1

Mutation check for the added case (ac): drop 'dir=$spelling' from
teardown_treehouse_return so the lock patience window keeps the refused
recorded spelling instead of the one treehouse recognized.

== control: unmutated c6fb6e5 ==
ok - a transient lock under treehouse's own spelling is retried under that spelling, not the refused one

== mutated ==
not ok - managed-spelling-transient-lock: teardown should succeed once the lock under treehouse's own spelling clears: expected exit 0, got 1
Evidence: Changed-file selection for bin/fm-treehouse-lib.sh alone (fixed runner selects pr-forge, secondmate, real-herdr-gated; pre-fix runner dies unmapped)

pre-fix runner: fm-test-run: no changed-test mapping for source path: bin/fm-treehouse-lib.sh (exit=2) fixed runner selects 35 scripts, including tests/fm-teardown.test.sh, tests/fm-secondmate-safety.test.sh, tests/fm-backend-herdr-presentation-e2e.test.sh

tests/fm-pr-check-security.test.sh
tests/fm-pr-merge.test.sh
tests/fm-review-diff.test.sh
tests/fm-teardown.test.sh
tests/fm-x-mode.test.sh
tests/fm-backlog-handoff.test.sh
tests/fm-on.test.sh
tests/fm-remote-backlog-handoff.test.sh
tests/fm-remote-doctor.test.sh
tests/fm-remote-job-orphan-reap.test.sh
tests/fm-remote-job.test.sh
tests/fm-remote-reply.test.sh
tests/fm-remote-secondmate-lifecycle-e2e.test.sh
tests/fm-remote-secondmate-trace-context.test.sh
tests/fm-secondmate-harness.test.sh
tests/fm-secondmate-lifecycle-e2e.test.sh
tests/fm-secondmate-liveness.test.sh
tests/fm-secondmate-safety.test.sh
tests/fm-secondmate-sync.test.sh
tests/fm-send-secondmate-marker.test.sh
tests/fm-shared-captain-inheritance.test.sh
tests/fm-startup-memory-budget.test.sh
tests/fm-stow-cascade.test.sh
tests/fm-afk-inject-herdr-e2e.test.sh
tests/fm-afk-launch.test.sh
tests/fm-backend-autodetect-smoke.test.sh
tests/fm-backend-herdr-eventwait-smoke.test.sh
tests/fm-backend-herdr-launcher-workspace-e2e.test.sh
tests/fm-backend-herdr-presentation-e2e.test.sh
tests/fm-backend-herdr-prune-safety-e2e.test.sh
tests/fm-backend-herdr-respawn-idem-e2e.test.sh
tests/fm-backend-herdr-smoke.test.sh
tests/fm-backend-herdr-workspace-per-home-e2e.test.sh
tests/fm-control-herdr-smoke.test.sh
tests/fm-herdr-session-cleanup-e2e.test.sh
Evidence: Suite log: tests/fm-teardown.test.sh (reconciliation cases z, aa, ab, ac)
ok - local-only worktree with HEAD on a fork remote is torn down (fix holds)
ok - teardown prompts tasks-axi backlog refresh when compatible
ok - teardown honors config/backlog-backend=manual even when tasks-axi is compatible
ok - local-only worktree with truly unpushed work is refused (safety preserved)
ok - local-only worktree with work merged into local main is torn down (no regression)
ok - no-mistakes worktree with HEAD on origin is torn down (no regression)
ok - no-mistakes worktree with genuinely unlanded work is refused (safety preserved)
ok - local-only worktree with unpushed work is torn down under --force (escape hatch)
ok - teardown completes when an exact busy-state sidecar is already absent
ok - herdr teardown removes pane-owned escalation dedupe state
ok - herdr flat teardown refuses before returning the isolated copy under lock contention and the retry completes cleanly
ok - herdr flat teardown never erases records when pane presence is unparseable
ok - herdr flat teardown preflight refuses before every destructive change
ok - forced secondmate teardown preflights every Herdr child before cleanup mutation
ok - forced secondmate teardown holds every descendant lifecycle and metadata lock
ok - forced secondmate teardown retains Herdr child identity until exact pane disappearance
ok - forced teardown retains a nested secondmate home and its grandchild's Herdr identity when the grandchild close is unconfirmed
ok - herdr projection teardown retires its journal only after confirming the exact recorded pane is gone
ok - herdr projection teardown retains every record when post-close presence is unknown
ok - herdr projection teardown surfaces failed focus restoration without turning confirmed cleanup into a hard failure
ok - squash-merged + deleted-branch worktree (PR merged) is torn down (the fix)
ok - squash-merged PR accepts a local HEAD that is an ancestor of the final PR head
ok - teardown discovers a merged PR by branch name and tears down when no pr= was ever recorded
ok - squash-merged PR accepts replayed unpushed local patches contained in the PR head
ok - merged PR does not allow teardown after a later local commit
ok - fm-pr-check does not refresh PR head after HEAD moves
ok - fm-pr-check records the remote PR head when the local worktree lags
ok - worktree whose content already landed in the default branch is torn down (content fallback)
ok - content fallback refreshes origin default before comparing trees
ok - dirty worktree is refused even when its committed work has landed (dirty always wins)
ok - gh lookup error with content not in default refuses (fail-safe)
ok - provably-stale worktree index.lock (old, no live holder) is cleared and teardown succeeds
ok - live-held worktree index.lock is never removed and teardown refuses
ok - lsof errors leave worktree index.lock in place and refuse teardown
ok - stale lock cleanup rechecks and refuses dirty worktree before return
ok - normal repo index.lock is resolved from the worktree and cleared when stale
ok - lock mtime read failures leave worktree index.lock in place and refuse teardown
ok - transient index.lock cleared after first failed return is retried successfully without force-remove
ok - persistent index.lock exhausts retries and refuses without force-removing the lock
ok - a physically recorded worktree is returned by treehouse's own $HOME-rooted spelling of it
ok - a genuinely unmanaged worktree still fails, reported as the recorded path
ok - an alternative spelling's genuinely different failure survives the recorded path's refusal
ok - a transient lock under treehouse's own spelling is retried under that spelling, not the refused one
ok - empty retry wait overrides use the default without aborting teardown
ok - fractional legacy retry wait remains supported without arithmetic
ok - a task's own parked no-mistakes run is aborted, not orphaned, before the worker is removed
ok - teardown refuses before reap or removal when a task-owned run remains parked
ok - a different run cannot confirm the targeted abort
ok - empty post-abort status is not accepted as confirmation
ok - the CLI's exact run-not-found signal confirms completion
ok - a parked run on another branch is never aborted by this task's teardown (ownership is precise)
ok - a task-owned autonomous running step is left alone rather than aborted
ok - a leaked descendant process rooted under the task's worktree is reaped by teardown, not left surviving
ok - a leaked descendant process rooted under the task's per-task tasktmp is reaped by teardown too
ok - missing lsof falls back to reaping the tmux pane process group
ok - an erroring lsof scan refuses teardown and preserves the task
ok - a reused pid with a different start time is never force-killed
ok - an exec change preserves birth identity and the process is reaped
ok - a process spawned during grace is reaped on a later pass
ok - persistent leaked processes refuse teardown after bounded retries
ok - a process exiting during identity lookup does not block teardown
ok - the run abort and the leaked-process reap both complete before the destructive worktree return
Evidence: Suite log: tests/fm-secondmate-safety.test.sh (seed-rollback reconciliation case)
ok - FM_HOME parameterizes data and state paths
ok - fm-lock status is scoped per home
ok - seed allows overlapping project clone lists and drops the owns/owner routing
ok - home-seed validation rejects registry records no operational parser can consume
ok - home seeding refuses broken registry symlinks before provisioning
ok - home seeding refuses unreadable registries before provisioning
ok - home seed validation rejects duplicate home routes
ok - home seed validation rejects duplicate id routes
ok - home seed validation rejects nested home routes
leased worktree for dash
ok - home seeding durably leases treehouse-acquired dash homes under the secondmate id
ok - home seeding returns rejected acquired homes through treehouse
ok - home seed rollback returns a physically recorded home by treehouse's own $HOME-rooted spelling
ok - home seed rollback warns when treehouse-acquired return fails
ok - home seeding leaves unsafe acquired active homes untouched
ok - home seeding rolls back failed clone attempts without residue
ok - home seeding refuses direct seed without filled charter text
ok - home seeding refuses unfilled placeholder charters
ok - home seeding refuses empty normalized charter fields
ok - home seeding scaffolds, registers, and spawns a project-less home end to end
ok - secondmate spawn resolves home validation and projects from punctuated registry fields
ok - secondmate spawn refuses ambiguous, supplied-home, and metadata-home registry bindings
ok - home seeding validates reused project-less charters before mutation
ok - home seeding refuses project-less conversion of a populated home
ok - home seeding refuses project-less homes whose projects directory cannot be inspected
ok - home seeding refuses project-less homes with symlinked projects directories
ok - home seeding refuses project-less homes with non-directory projects paths
ok - home seeding refuses project-less homes whose project registry cannot be inspected
ok - home seeding fails loudly on accidental project omission and rejects mixed --no-projects
ok - home seeding refuses local-only projects
ok - home seeding refuses registry delimiter home paths
ok - home seeding refuses active home and repo root
ok - home seeding refuses homes marked for another id
ok - home seeding refuses homes registered to another id
ok - home seeding refuses same-id reassignment to a different home
ok - home seeding refuses registered home overlaps
ok - remote-backed subhome seeding requires a source origin
ok - remote-backed subhome seeding validates existing destination origins
ok - home seeding resolves relative source origins against the source project
ok - home seeding skips initialized existing no-mistakes clones
ok - home seeding refuses uninitialized existing no-mistakes clones
ok - home seeding refuses project destinations outside the subhome
ok - home seeding refuses operational directories outside the subhome
ok - home seeding refuses symlinked and non-regular leaf files
ok - home reseeding preserves and enforces the durable parent binding
ok - secondmate spawn validates homes before launch
ok - secondmate spawn refuses operational directories outside the subhome
ok - fm-send refuses a bare firstmate window with no metadata in this home
ok - secondmate teardown retires empty homes and releases routing
ok - secondmate teardown refuses ambiguous and identity-mismatched registry bindings
ok - normal secondmate teardown sweeps process events before removal
ok - secondmate teardown preserves state when process-event sweeping is unavailable
ok - later teardown refusals preserve active process-event sources
ok - force teardown sweeps nested secondmate homes before deletion
ok - force teardown preserves nested process-event restoration status and recovery state
ok - secondmate teardown refuses to hide failed leased-home return
ok - secondmate teardown raw-removes plain-clone homes
ok - secondmate force teardown discards child work
ok - secondmate force teardown prevalidates child quarantine cleanup without following symlinks
ok - secondmate force teardown preserves child worktree after unproven lock refusal
ok - force teardown allows non-state operational directory symlinks inside the subhome
ok - force teardown refuses operational directory symlinks outside the subhome
ok - secondmate teardown refuses homes containing registered nested homes
ok - secondmate teardown refuses nested homes from the child registry
ok - force teardown validates subhome before child cleanup
ok - forced teardown refuses a non-directory descendant state path
ok - forced teardown refuses a symlinked descendant state path
ok - forced teardown locks a descendant whose state directory was absent
ok - forced teardown refuses while a fresh task is being published in the home
ok - a fresh spawn refuses to publish while a forced teardown owns the task set
ok - a fresh remote secondmate spawn refuses while the task set is owned
ok - force teardown refuses child worktrees inside the active home
ok - force teardown refuses child worktrees inside the firstmate repo
ok - force teardown refuses unregistered child worktree paths
ok - secondmate teardown path-boundary matrix refuses unmarked/ancestor/active-descendant/repo-descendant homes
ok - idle kind=secondmate pane is healthy and not stale
ok - secondmate charter brief is idle by default and does not self-initiate work
ok - fm-backlog-handoff aborts atomically on unmatched, in-flight, and unregistered targets
ok - fm-backlog-handoff refuses Done items under whitespace section headings and unsafe homes
Evidence: Reproducible demo scripts used for the manual verification
#!/usr/bin/env bash
# Manual end-to-end verification for teardown-worktree-path.
#
# Uses the REAL treehouse binary and the REAL bin/fm-teardown.sh on a home that
# is a symlink (the shape of this host, where /home/inthuson -> /local/home/inthuson).
# Treehouse leases the pool worktree under its as-written $HOME root; firstmate
# records the fully resolved physical path. The demo shows:
#   1. real treehouse refusing the recorded physical spelling ("is not managed by
#      treehouse") with the lease still held  -- the reported bug
#   2. real fm-teardown.sh completing the return with no hand-edited worktree=,
#      and the lease released in treehouse's own pool status
#
# Usage: real-treehouse-teardown-demo.sh <firstmate-repo-root>
set -u

ROOT=${1:?usage: real-treehouse-teardown-demo.sh <firstmate-repo-root>}
SCRATCH=$(mktemp -d "${TMPDIR:-/tmp}/fm-th-e2e-XXXXXX")
PHYS="$SCRATCH/physhome"
LINK="$SCRATCH/home"
mkdir -p "$PHYS"
ln -s "$PHYS" "$LINK"
export HOME="$LINK"
export FM_GATE_REFUSE_BYPASS=1

hr() { printf '\n== %s ==\n' "$1"; }

hr "host-shaped fixture: a home that is a symlink"
printf 'HOME as written : %s\n' "$HOME"
printf 'HOME physical   : %s\n' "$(cd "$HOME" && pwd -P)"
printf 'treehouse       : %s (%s)\n' "$(command -v treehouse)" "$(treehouse --version 2>&1 | head -1)"

# A project with an origin, so the committed work is provably landed and teardown
# is allowed to make the destructive pool return.
mkdir -p "$PHYS/proj"
git init -q -b main "$PHYS/proj"
git -C "$PHYS/proj" config user.email t@t
git -C "$PHYS/proj" config user.name t
echo hello > "$PHYS/proj/file.txt"
git -C "$PHYS/proj" add -A
git -C "$PHYS/proj" commit -q -m "seed work"
git init -q --bare "$PHYS/origin.git"
git -C "$PHYS/proj" remote add origin "$PHYS/origin.git"
git -C "$PHYS/proj" push -q -u origin main
printf 'max_trees = 4\nroot = ""\n' > "$PHYS/proj/treehouse.toml"

hr "treehouse get --lease (real pool acquire)"
WT_ASWRITTEN=$( cd "$PHYS/proj" && treehouse get --lease --lease-holder fm-evidence ) || exit 1
WT_PHYS=$(cd "$WT_ASWRITTEN" && pwd -P)
printf "treehouse's own pool spelling : %s\n" "$WT_ASWRITTEN"
printf "fm-spawn's recorded worktree= : %s\n" "$WT_PHYS"

hr "the bug: real treehouse return --force with the recorded physical path"
( cd "$PHYS/proj" && treehouse return --force "$WT_PHYS" ) 2>&1
printf 'exit=%s\n' "$?"
printf 'pool status: %s\n' "$( cd "$PHYS/proj" && treehouse status --json )"

# Record the task exactly as bin/fm-spawn.sh does: resolved physical paths, no
# hand-editing of worktree=.
CASE="$SCRATCH/case"
mkdir -p "$CASE/state" "$CASE/config" "$CASE/fakebin"
{
  echo "window=firstmate:fm-task-e2e"
  echo "endpoint_task_id=task-e2e"
  echo "worktree=$WT_PHYS"
  echo "project=$PHYS/proj"
  echo "kind=ship"
  echo "mode=no-mistakes"
} > "$CASE/state/task-e2e.meta"

# Terminal / GitHub / no-mistakes stubs only. treehouse is deliberately NOT
# stubbed, so the return below goes to the real binary.
for stub in tmux gh gh-axi no-mistakes; do
  printf '#!/usr/bin/env bash\nexit 0\n' > "$CASE/fakebin/$stub"
  chmod +x "$CASE/fakebin/$stub"
done

hr "recorded task meta (worktree= is the physical spelling, unedited)"
cat "$CASE/state/task-e2e.meta"

hr "the fix: real bin/fm-teardown.sh on that recorded path"
HOME="$LINK" \
FM_ROOT_OVERRIDE="$ROOT" \
FM_STATE_OVERRIDE="$CASE/state" \
FM_CONFIG_OVERRIDE="$CASE/config" \
PATH="$CASE/fakebin:$PATH" \
  "$ROOT/bin/fm-teardown.sh" task-e2e
printf 'fm-teardown.sh exit=%s\n' "$?"

hr "treehouse's own pool status after teardown"
( cd "$PHYS/proj" && treehouse status --json )

rm -rf "$SCRATCH"
- Outcome: ⚠️ 2 warnings across 1 run (17m20s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • 🚨 bin/fm-teardown.sh:169 - fm-teardown.sh now sources a new sibling, $SCRIPT_DIR/fm-treehouse-lib.sh, but tests/fm-gotmp.test.sh runs the real teardown from a hand-built fake bin/ that symlinks each lib teardown sources one by one (tests/fm-gotmp.test.sh:63 and :143) and has no entry for the new file. SCRIPT_DIR resolves to that fake bin/ (the fixture exists precisely because BASH_SOURCE[0] is not dereferenced), so the source fails and, under teardown's set -eu, the script exits 1 before it ever parses its argument. Failure: bash &#34;$fake/bin/fm-teardown.sh&#34; td-rm-z2 prints "fm-treehouse-lib.sh: No such file or directory" and exits 1, so both test_teardown_removes_tasktmp_dir and test_teardown_skips_gracefully_without_tasktmp fail with "teardown exited non-zero with a valid tasktmp". bin/fm-teardown.sh maps only to the pr-forge family, so the changed-file selection never selects fm-gotmp.test.sh (pure-contract-unit), but CI's portable lanes run it. Fix: add ln -s &#34;$ROOT/bin/fm-treehouse-lib.sh&#34; &#34;$fake/bin/fm-treehouse-lib.sh&#34; at both fixture sites, matching the fm-lock-lib.sh line above each.
  • ⚠️ bin/fm-treehouse-lib.sh:1 - The new lib has no entry in bin/fm-test-run.sh's families_for_changed_path, so it falls through the bin/* case to families_for_test_reference on its basename. Only the four real-Herdr suites source it by name, and all four are family real-herdr-gated, so a future change to bin/fm-treehouse-lib.sh selects only suites that gate-skip on any host without pinned herdr, and never selects pr-forge (tests/fm-teardown.test.sh, which owns this change's reconciliation regression) or secondmate (tests/fm-secondmate-safety.test.sh, which owns the home-seed rollback return). Failure: editing the spelling-candidate order in fm_treehouse_path_spellings and running the changed-file selection runs zero assertions about it on a non-pinned host, reporting green. bin/fm-nm-run-lib.sh at bin/fm-test-run.sh:937 is the existing precedent for a shared lib mapping to each consumer's family; add bin/fm-treehouse-lib.sh emitting pr-forge, secondmate, and real-herdr-gated.
  • ⚠️ bin/fm-teardown.sh:1120 - On total failure only the first attempt's output is reported. That is right when later attempts are "not managed by treehouse" refusals, but it also discards a later attempt's genuinely different failure. Failure: on the symlinked home this change targets, the recorded physical path is refused as unmanaged (first_out), then the $HOME spelling that treehouse does manage fails for a real reason such as an unreadable worktree or a failed reset; teardown prints only "worktree /local/home/... is not managed by treehouse" plus "teardown aborted", so the operator is pointed at the spelling defect this change just fixed instead of the actual cause, and the real error text is lost entirely. bin/fm-treehouse-lib.sh:120 has the same shape, where the seed-rollback warning then claims only that the lease may still be held. Suggest keeping first_out as the headline but still emitting any attempt output that is not an unmanaged-path refusal; flagging rather than changing it because the intent states this reporting rule deliberately.
  • ⚠️ bin/fm-treehouse-lib.sh:96 - fm_treehouse_return_force is a separate ~25-line loop from teardown's inline one, and no test covers it succeeding on an alternative spelling. The two new tests in tests/fm-teardown.test.sh drive teardown_treehouse_return's own loop, not this function. tests/fm-secondmate-safety.test.sh:327 only exercises the total-failure branch with an always-failing fake, and the four real-Herdr suites call it in best-effort cleanup behind || true and gate-skip without pinned herdr. Failure: invert the candidate order or drop the resolved = physical guard in this function and every portable suite still passes, while the seed-rollback lease leak the intent names as the reason for touching fm-home-seed.sh silently returns. Mirror test_home_seed_returns_treehouse_acquired_home_on_assignment_failure with a symlinked HOME and a string-matching treehouse fake, the shape add_pool_spelling_treehouse already provides.
  • ⚠️ tests/fm-backend-herdr-presentation-e2e.test.sh:38 - The pin gate recovers the expected version by regexing bin/fm-install-herdr.sh's source with sed -n &#39;s/^FM_HERDR_CI_VERSION=\([0-9][0-9.]*\)$/\1/p&#39;, and hard-fails the suite when that pattern does not match. That couples a required-lane suite to the byte shape of an implementation file, which the repo's own rule forbids (firstmate-coding-guidelines: "Tests must exercise behavior through an executable or public interface and must never assert implementation-source bytes, including through parsers, regexes, snapshots, or indirect wrappers"). Failure: a behavior-preserving edit to the install script, quoting the value as FM_HERDR_CI_VERSION="0.7.4" or appending a trailing comment, makes the sed return empty and this suite exits 1 with "could not read the exact Herdr pin", blocking the required Herdr lane on every PR while the install script itself still works. Keeping bin/fm-install-herdr.sh as the single pin owner is compatible with an interface: a --print-pin mode, or a tiny pin file both the installer and the suite read. Raising rather than changing it because the intent specifies reading the pin from that file.

🔧 Fix: expose Herdr pin via flag and cover spelling reconciliation
2 infos still open:

  • ℹ️ bin/fm-teardown.sh:1127 - A spelling-independent failure is now reported twice. When the first spelling fails for a reason that is neither an unmanaged-path refusal nor an index.lock signature, the loop continues to the next spelling, which then fails with the identical text; that text lands in first_out (printed at :1126) and again in other_out (printed at :1127). Concretely, the worktree return at bin/fm-teardown.sh:2502 passes cd_dir=$PROJ with no [ -d &#34;$PROJ&#34; ] or command -v treehouse guard, so on a symlinked home a missing project dir prints cd: /.../project: No such file or directory twice, and a missing treehouse binary prints treehouse: command not found twice, before teardown aborted. The same shape exists at bin/fm-treehouse-lib.sh:135. The intent behind keeping other_out was that a later spelling's genuinely different failure is the real cause; text identical to first_out is not a different failure. Suggest skipping an other_out entry equal to first_out.
  • ℹ️ bin/fm-treehouse-lib.sh:127 - The library copy of the later-attempt real-failure reporting added this round has no test. tests/fm-secondmate-safety.test.sh:327 covers only fm_treehouse_return_force succeeding on the alternative spelling, and the pre-existing test_home_seed_warns_when_acquired_home_return_fails drives an FM_FAKE_TREEHOUSE_RETURN_FAIL fake that exits 17 with no output, so other_out stays empty on both paths. Deleting lines 127-128 and 135 leaves every portable suite green while the seed-rollback warning again claims only that the lease may still be held, which is exactly the misleading message the round-1 fix existed to remove. The teardown twin is covered by test_managed_spelling_real_failure_is_not_hidden_by_the_recorded_refusal; the equivalent for the lib is one FM_FAKE_TREEHOUSE_MANAGED plus FM_FAKE_TREEHOUSE_MANAGED_FAIL-style case asserting the managed spelling's own error text reaches the seed's stderr. Separately, the third candidate at line 87 (the fully physical reverse direction) is live, since WT at bin/fm-teardown.sh:453 is the raw meta value and a meta hand-edited to the $HOME spelling reaches it, but no test exercises it either.
⚠️ **Test** - 2 warnings
  • ⚠️ tests/fm-backend-herdr-presentation-e2e.test.sh:22 - The presentation suite's substantive claim (the abort is gone and the suite now reaches and passes five checks, including the previously unreachable opted-out spawn and teardown) is not verifiable on this host: herdr is 0.8.2 while the suite is bound to the 0.7.4 pin, and downgrading was explicitly ruled out to protect the live session. Locally I could only prove the skip path plus the gate's loud failure modes. Only the required Herdr CI lane, which asserts the exact 0.7.4 pin before running any suite, can confirm the five checks actually pass. You may want to decide whether CI confirmation is enough or whether you want that lane's output attached before merge.
  • ⚠️ tests/fm-backend-herdr-workspace-per-home-e2e.test.sh:74 - Three real-herdr-gated suites this change touches (fm-backend-autodetect-smoke, fm-backend-herdr-launcher-workspace-e2e, fm-backend-herdr-workspace-per-home-e2e) received the new fm_treehouse_return_force cleanup call but have no pin gate, so on this host they would run against the captain's live herdr 0.8.2 server and the real $HOME treehouse pool. I deliberately did not run them: on 0.8.2 a projected cleanup orphans a workspace (the defect this change itself documents as herdr-082-projection-orphan-o4), and they would acquire and reset real pool worktrees while parked validation runs hold slots. Their changed line is the shared cleanup call, which I verified directly against the real treehouse binary, so the residual gap is their own suite bodies at the pin, which the Herdr CI lane owns.
  • bash tests/fm-teardown.test.sh
  • bash tests/fm-secondmate-safety.test.sh
  • bash tests/fm-gotmp.test.sh
  • bash tests/fm-test-run.test.sh
  • bash tests/fm-backend-herdr-presentation-e2e.test.sh
  • bash tests/fm-backlog-handoff.test.sh
  • Real-tooling e2e: treehouse get --lease under a symlinked HOME, treehouse return --force &lt;physical&gt; (refused, lease held), then real bin/fm-teardown.sh task-e2e (returns; pool leased -> available)
  • Same e2e against a pre-fix 1cb900c tree (teardown aborted, exit 1, lease still held)
  • Boundary probe of fm_treehouse_path_spellings / fm_treehouse_return_force against the real treehouse binary
  • Pre-fix baseline runs of both colocated regressions (both fail without the fix)
  • Mutation check: removing dir=$spelling from teardown_treehouse_return fails the case I added
  • bin/fm-install-herdr.sh --required-version plus the three pin-gate paths (unreadable pin, unreadable version, version equals pin)
  • bin/fm-test-run.sh --list --changed --base 1cb900c and an isolated single-path selection for bin/fm-treehouse-lib.sh
🔧 **Document** - 2 issues found → auto-fixed ✅
  • ℹ️ docs/herdr-backend.md:318 - Unblocking the presentation suite exposed a real Herdr 0.8.2 defect (closing a tab's last pane respawns a pane, so projected cleanup never removes the projected workspace and orphans one per cleanup), and docs/herdr-backend.md's "Presentation spaces" and "Active limits" sections state no such limit. I did not add it: this change does not alter product behavior on 0.8.2, that doc scopes its claims to the verified 0.7.1-0.8.0 set so nothing there is currently contradicted, and the intent files the defect separately as herdr-082-projection-orphan-o4 at priority 1. Judgment call for you: if that task will not land soon, the captain-visible limit is worth one line in docs/herdr-backend.md's "Active limits" (owned by that follow-up task, not this change), since this host itself runs 0.8.2.
  • ℹ️ .github/workflows/ci.yml:236 - Out-of-scope follow-up: .github/workflows/ci.yml's "Assert Herdr pin and protocol floor" step hand-copies the pin value 0.7.4 twice, while this change made bin/fm-install-herdr.sh the queryable single owner via --required-version. The copies agree today so nothing is stale, and a docs phase cannot change workflow behavior, but reading the pin from the interface would remove the hand-copied duplicate the new flag exists to serve.

🔧 Fix: note Herdr 0.8.2 projected-cleanup workspace orphan limit
✅ Re-checked - no issues remain.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 127)
✅ **Push** - passed

✅ No issues found.

… home

treehouse matches its managed inventory by string and builds pool paths from
its configured root exactly as written, defaulting to a literal $HOME.
fm-spawn.sh records the fully resolved physical worktree path instead, because
the liveness and worktree-isolation checks must prove one exact directory. On a
host whose home is a symlink the two records name a single directory yet differ
as strings, so `treehouse return` refused the recorded path as "not managed by
treehouse" and teardown aborted after it had already reaped the worktree
processes. Every cleanup on such a host needed the recorded path rewritten by
hand first, and the refusal text read like unlanded-work protection.

Add bin/fm-treehouse-lib.sh as the single owner of the spellings that provably
name one directory, and reconcile where the paths are used rather than making
either side adopt the other's spelling, so neither the isolation checks nor
treehouse's inventory change. A candidate is offered only when it resolves to
the identical physical directory, so reconciliation can never act on a
different worktree, and a total failure still reports the recorded path's own
refusal rather than an alternative spelling's.

fm-teardown.sh iterates the spellings itself so its index.lock patience window
still applies to whichever spelling treehouse recognized. fm-home-seed.sh's
seed rollback used the same raw return and warned that a lease had leaked when
it had not. Four real-Herdr suites returned recorded paths that this host
silently refused, orphaning a pool slot per spawn, and now release them.
Unblocking this suite's pool-path abort immediately exposed a real Herdr
0.8.2 defect that CI's exact 0.7.4 pin hides: closing a tab's last pane
respawns a replacement pane, so a projected workspace never becomes empty,
the emptying-close plan in bin/backends/herdr.sh never matches, and a
projected workspace is never removed by a pane close. Every projected
crewmate cleanup on 0.8.2 therefore orphans a workspace, and this suite's
ordering assertions see the leftover fixture workspace.

Skip on any release other than the exact pin rather than adapting the
fixtures to whatever the local release does. Adapting them would report a
green tick over a live defect, so that option was rejected; the 0.8.2
cleanup gap is filed separately as herdr-082-projection-orphan-o4.

bin/fm-install-herdr.sh stays the single owner of the pin and is read rather
than duplicated. An unreadable pin or an unreadable installed version
reports the exact missing requirement and exits non-zero instead of
skipping, because a skip nobody can trust would quietly disable this whole
lane. The required Herdr CI lane also asserts the pin before it runs any
suite, so a skip here cannot hide a mispinned runner.

Verified: all four gate branches, meaning pin match, pin mismatch,
unreadable installed version, and unreadable pin; the suite skips in 8ms on
this 0.8.2 host while creating no fixtures and leaving the Herdr session
list untouched; and bin/fm-test-run.sh records gate_skip=true with failed=0
without tripping the required lane's fail-on-gate-skip token.
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: I reviewed the full diff (treehouse path-spelling reconciliation, teardown/seed callers, colocated tests) and approved fork CI. Waiting on green checks including no-mistakes.

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.

2 participants