Repository navigation
fix(bin): let teardown retire task records whose pooled worktree was already returned and reassigned - #13
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
The captain's standing preference: fix failing or broken behaviour noticed along the way. Observed 2026-10-02 in this home: teardown of task distill-scale returned its brain-ingest pool slot 1 (worktree ~/.treehouse/brain-ingest-7252df/1/brain-ingest) to the treehouse pool, then refused to finish because the Herdr pane close was refused (the pane was the captain's active tab), so it kept the task record. The pool then handed slot 1 to a new task, wispr-register-fix. Now both records name the same worktree and every teardown of either refuses ('also task X's recorded worktree ... nothing was changed - not even with --force'); deleting worktree= from the stale record makes it refuse as 'missing, empty, or ambiguous worktree identity'. Both tasks' PRs are merged, so no work is at risk, but the two records cannot be cleaned up and their idle panes keep raising stale wakes.
What Changed
bin/fm-teardown.shnow writes atreehouse_returned=<worktree>receipt into the task metadata after a successful Treehouse pool return. It does this before dropping the slot's owner claim, for both the task's own slot and forced secondmate child slots. A rerun that finds the receipt skips the slot and only cleans up the task's own endpoint and records. It finishes with a new "already returned and left untouched" message. Empty, duplicated, or path-mismatched receipts refuse before anything is changed. A failed return writes no receipt.Teardown now checks ownership before the exclusivity check. Another task's ordinary
worktree=record no longer blocks the current owner's cleanup when that record has a matching receipt or when the slot's.fm-slot-ownerclaim names a different holder. Two records for the same slot still refuse if neither is resolved and the claim is absent. The same order now applies to forced secondmate descendant preflight. The script header documents how to recover stale records: keepworktree=, then rerun teardown without--force.bin/fm-spawn.shclears any oldtreehouse_returned=receipt when a task is relaunched into its worktree. New tests cover:The Herdr descendant fixtures now use
gnusleepwhen it is available, because a multicallsleepcannot run through thepialias. The verification notes are indocs/verification/runtime-backends.md, anddocs/configuration.mdpoints to the new header section.🤖 Generated with Claude Code
Risk Assessment
✅ Low: The change keeps every unproven case closed: with an absent or unreadable claim, or an empty, duplicate, or mismatched receipt, teardown still refuses. Exclusivity is skipped only when a valid receipt or a claim naming another task proves this record no longer owns the slot. Relaunch clears the receipt, and the reported stuck-record sequence (legacy record, successor claim) now retires both records without --force.
Testing
I ran three suites that launch the real fm-teardown.sh, fm-spawn.sh and fm-control.sh as processes against throwaway homes: endpoint-safety, control-relaunch and secondmate-safety. All passed with no skips. I also ran a red/green check: the new retry case fails against the base teardown and passes on the target. Finally, I drove the exact incident shape by hand (two receipt-less records on one pool slot, with the claim naming the successor). On the base commit both teardowns refuse; on the target both records retire in either order and the successor's copy is protected. The real Herdr active-tab close refusal and the operator's real home were not driven: the tests stand in a tmux close-failure shim for the refused Herdr close, and the operator's real home is off-limits. This is a CLI-only change with no UI surface.
Evidence: Incident reproduction: base refuses both records, target retires the stale one
Source: Incident reproduction: base refuses both records, target retires the stale one
Evidence: Target: both stale and current records retire in either teardown order
Source: Target: both stale and current records retire in either teardown order
Evidence: fm-teardown-endpoint-safety suite transcript
Source: fm-teardown-endpoint-safety suite transcript
Evidence: fm-control-relaunch suite transcript
Source: fm-control-relaunch suite transcript
Evidence: fm-secondmate-safety suite transcript
Source: fm-secondmate-safety suite transcript
Evidence: Base refusal vs target retirement (excerpt)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ **Review** - passed
✅ No issues found.
bin/fm-teardown.sh- Order matters for receipt-less records. If the current owner is torn down first, its claim is dropped, so a remaining stale record (for example distill-scale) runs as the slot's owner and returns the now-free slot a second time. The manual drive saw this as 1 treehouse return on the distill-scale run, using the fake treehouse. Tests did not cover how the real treehouse responds to returning an already-free slot. The documented recovery (stale records first, while the claim names the successor) avoids this path.bin/fm-test-run.sh tests/fm-teardown-endpoint-safety.test.sh(exit=0, gate_skip=false, no skip lines)bin/fm-test-run.sh tests/fm-control-relaunch.test.sh(exit=0, gate_skip=false)bin/fm-test-run.sh tests/fm-secondmate-safety.test.sh(exit=0, gate_skip=false; descendant/child slot paths changed by this diff)Red check: ran test_returned_slot_retry_survives_reallocation against base 8ef80a3 bin/fm-teardown.sh in a throwaway tree -> fails ('retained record cannot retire safely ... uncommitted changes'); passes on targetManual incident drive: real bin/fm-teardown.sh against a throwaway FM_HOME + git/treehouse-pool fixture with records distill-scale and wispr-register-fix both naming one pool slot, no receipts, slot claim naming wispr-register-fix; run on base (both REFUSED 'also task X's recorded worktree') and on target, in both teardown orders (all exit 0)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.