Skip to content

fix(bin): let teardown retire task records whose pooled worktree was already returned and reassigned - #13

Merged
Mauryanx merged 3 commits into
mainfrom
fm/teardown-slot-reuse
Oct 3, 2026
Merged

Mauryanx merged 3 commits into
mainfrom
fm/teardown-slot-reuse

Conversation

@Mauryanx

@Mauryanx Mauryanx commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

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.sh now writes a treehouse_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-owner claim 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: keep worktree=, then rerun teardown without --force.

  • bin/fm-spawn.sh clears any old treehouse_returned= receipt when a task is relaunched into its worktree. New tests cover:

    • retiring stale records before and after the slot is reassigned
    • malformed receipts
    • relaunch clearing the receipt

    The Herdr descendant fixtures now use gnusleep when it is available, because a multicall sleep cannot run through the pi alias. The verification notes are in docs/verification/runtime-backends.md, and docs/configuration.md points 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.

  • Live validation: ✅ go - 7 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Incident regression: with two receipt-less records on one pool slot and the claim naming the successor, tearing down the stale record finishes and leaves the successor's copy, claim and record untouch… ✅ pass live incident-repro.txt: base fm-teardown.sh refuses both with 'is also task X's recorded worktree'. Target: distill-scale exits 0 with 'pool slot ... left to task wispr-register-fix', sentinel kept, 0 tre…
After the stale record is gone, or even while it is still present, the current owner's teardown returns the slot and retires its record ✅ pass live incident-both-orders.txt: wispr-register-fix exits 0 'teardown wispr-register-fix complete (... worktree ...)' in both orders. Also teardown-endpoint-safety.log 'ok - fm-teardown: a successful return…
Retry after a successful return with a failed endpoint close retires the kept record without a second return, before and after reallocation, and even if the pool registration has changed ✅ pass live teardown-endpoint-safety.log: 'ok - fm-teardown: a successful return followed by a failed close can retire before or after slot reallocation, including legacy records' (drives real fm-teardown.sh thro…
Adversarial: empty, duplicated or drifted treehouse_returned receipts refuse with no change, and a failed pool return publishes no receipt and keeps its claim ✅ pass live teardown-endpoint-safety.log: 'ok - fm-teardown: malformed return receipts refuse before mutation and failed returns retain their claim without a receipt'
Adversarial: two live records on one slot with no receipt and no claim proving reassignment are still refused, even with --force (the guard is not loosened) ✅ pass live teardown-endpoint-safety.log: 'ok - fm-teardown: a pool slot named by a second task record is never returned, killed, or reset' and 'ok - fm-teardown: a pool slot held by another firstmate home is nev…
Relaunching a record that carries a return receipt drops the receipt, so its replacement is treated as the slot owner again (the review-1 concern) ✅ pass live control-relaunch.log: 'ok - fm-control relaunch: durable task metadata survives while the prior return receipt is cleared' (run_control launches the real fm-control.sh relaunch)
Secondmate descendant-slot preflight and child cleanup still behave after the receipt-aware ordering change ✅ pass live secondmate-safety.log: 'ok - forced secondmate teardown refuses duplicated descendant pool slots', 'ok - secondmate force teardown discards child work'; FM_TEST_END exit=0 gate_skip=false
The original trigger: a real Herdr pane close refused because the pane is the captain's active tab, followed by a retry after selecting another tab ⏸️ untested no This needs a real Herdr server with an active tab under the operator's control. fm-teardown.sh refuses gate worktrees, and the runbook forbids pointing at the real home. An operator can supply this by…
Evidence: Incident reproduction: base refuses both records, target retires the stale one

Source: Incident reproduction: base refuses both records, target retires the stale one

===== BASE 8ef80a3 bin/fm-teardown.sh =====
--- STATE: two records name /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/worktree; claim: task=wispr-register-fix home=/tmp/fm-teardown-endpoint-safety.Rdp51z/incident/home 
--- $ FM_HOME=<home> bin/fm-teardown.sh distill-scale  -> exit 1
    /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/distill-scale.err:●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/distill-scale.err:●  WATCHER DOWN - SUPERVISION IS OFF
    /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/distill-scale.err:●  2 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
    /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/distill-scale.err:●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
    /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/distill-scale.err:●  This is a supervision warning only; the guarded operation WILL still run.
    /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/distill-scale.err:●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
    /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/distill-scale.err:●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/distill-scale.err:REFUSED: task distill-scale's recorded worktree /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/pool/1/project is also task wispr-register-fix's recorded worktree.
    /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/distill-scale.err:Returning that pool slot would kill wispr-register-fix's processes and reset its copy, so nothing was changed - not even with --force.
    /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/distill-scale.err:Reconcile whichever record is wrong (bin/fm-crew-state.sh distill-scale; bin/fm-crew-state.sh wispr-register-fix), then re-run teardown.
    record present: yes; sentinel present: yes; treehouse return calls: 0
    claim now: task=wispr-register-fix home=/tmp/fm-teardown-endpoint-safety.Rdp51z/incident/home 
--- $ FM_HOME=<home> bin/fm-teardown.sh wispr-register-fix  -> exit 1
    /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/wispr-register-fix.err:REFUSED: task wispr-register-fix's recorded worktree /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/pool/1/project is also task distill-scale's recorded worktree.
    /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/wispr-register-fix.err:Returning that pool slot would kill distill-scale's processes and reset its copy, so nothing was changed - not even with --force.
    /tmp/fm-teardown-endpoint-safety.Rdp51z/incident/wispr-register-fix.err:Reconcile whichever record is wrong (bin/fm-crew-state.sh wispr-register-fix; bin/fm-crew-state.sh distill-scale), then re-run teardown.
    record present: yes; sentinel present: yes; treehouse return calls: 0
    claim now: task=wispr-register-fix home=/tmp/fm-teardown-endpoint-safety.Rdp51z/incident/home 
===== TARGET 7fe5863 bin/fm-teardown.sh =====
--- STATE: two records name /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/worktree; claim: task=wispr-register-fix home=/tmp/fm-teardown-endpoint-safety.kBDkxj/incident/home 
--- $ FM_HOME=<home> bin/fm-teardown.sh distill-scale  -> exit 0
    /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/distill-scale.out:teardown distill-scale complete (window firstmate:fm-distill-scale; pool slot /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/worktree left to task wispr-register-fix (home /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/home), which it was reassigned to)
    /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/distill-scale.out:Backlog: distill-scale just finished (this home keeps no markdown backlog at /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/home/data/backlog.md). Update /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/home/data/backlog.md - move distill-scale 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.
    /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/distill-scale.err:●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/distill-scale.err:●  WATCHER DOWN - SUPERVISION IS OFF
    /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/distill-scale.err:●  2 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
    /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/distill-scale.err:●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
    /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/distill-scale.err:●  This is a supervision warning only; the guarded operation WILL still run.
    /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/distill-scale.err:●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
    /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/distill-scale.err:●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/distill-scale.err:warning: task distill-scale's recorded worktree /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/pool/1/project was reassigned to task wispr-register-fix (home /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/home), which claimed that pool slot after this record was written; that slot is no longer distill-scale's, so its processes, copy, and claim are left untouched and only distill-scale's own cleanup runs.
    record present: no; sentinel present: yes; treehouse return calls: 0
    claim now: task=wispr-register-fix home=/tmp/fm-teardown-endpoint-safety.kBDkxj/incident/home 
--- $ FM_HOME=<home> bin/fm-teardown.sh wispr-register-fix  -> exit 1
    /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/wispr-register-fix.err:REFUSED: worktree /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/worktree has uncommitted changes.
    /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/wispr-register-fix.err:uncommitted changes present
    /tmp/fm-teardown-endpoint-safety.kBDkxj/incident/wispr-register-fix.err:Commit them (or get the captain's explicit OK to discard, then --force).
    record present: yes; sentinel present: yes; treehouse return calls: 0
    claim now: task=wispr-register-fix home=/tmp/fm-teardown-endpoint-safety.kBDkxj/incident/home 
Evidence: Target: both stale and current records retire in either teardown order

Source: Target: both stale and current records retire in either teardown order

===== TARGET 7fe5863, clean owner copy, both teardown orders =====
--- STATE: two records name <case-root>/incident-distill-scale/worktree; claim: task=wispr-register-fix home=<case-root>/incident-distill-scale/home 
--- $ FM_HOME=<home> bin/fm-teardown.sh distill-scale  -> exit 0
    <case-root>/incident-distill-scale/distill-scale.out:teardown distill-scale complete (window firstmate:fm-distill-scale; pool slot <case-root>/incident-distill-scale/worktree left to task wispr-register-fix (home <case-root>/incident-distill-scale/home), which it was reassigned to)
    <case-root>/incident-distill-scale/distill-scale.err:warning: task distill-scale's recorded worktree <case-root>/incident-distill-scale/pool/1/project was reassigned to task wispr-register-fix (home <case-root>/incident-distill-scale/home), which claimed that pool slot after this record was written; that slot is no longer distill-scale's, so its processes, copy, and claim are left untouched and only distill-scale's own cleanup runs.
    record present: no; sentinel present: yes; treehouse return calls: 0
    claim now: task=wispr-register-fix home=<case-root>/incident-distill-scale/home 
--- $ FM_HOME=<home> bin/fm-teardown.sh wispr-register-fix  -> exit 0
    <case-root>/incident-distill-scale/wispr-register-fix.out:teardown wispr-register-fix complete (window firstmate:fm-wispr-register-fix, worktree <case-root>/incident-distill-scale/worktree)
    record present: no; sentinel present: no; treehouse return calls: 1
    claim now: 
--- STATE: two records name <case-root>/incident-wispr-register-fix/worktree; claim: task=wispr-register-fix home=<case-root>/incident-wispr-register-fix/home 
--- $ FM_HOME=<home> bin/fm-teardown.sh wispr-register-fix  -> exit 0
    <case-root>/incident-wispr-register-fix/wispr-register-fix.out:teardown wispr-register-fix complete (window firstmate:fm-wispr-register-fix, worktree <case-root>/incident-wispr-register-fix/worktree)
    record present: no; sentinel present: no; treehouse return calls: 1
    claim now: 
--- $ FM_HOME=<home> bin/fm-teardown.sh distill-scale  -> exit 0
    <case-root>/incident-wispr-register-fix/distill-scale.out:teardown distill-scale complete (window firstmate:fm-distill-scale, worktree <case-root>/incident-wispr-register-fix/worktree)
    record present: no; sentinel present: no; treehouse return calls: 1
    claim now: 
Evidence: fm-teardown-endpoint-safety suite transcript

Source: fm-teardown-endpoint-safety suite transcript

FM_TEST_BEGIN 2026-10-03T18:40:12Z tests/fm-teardown-endpoint-safety.test.sh family=backend-dispatch expected_gate_skip=none
ok - fm-teardown: missing, empty, malformed, ambiguous, and task-mismatched endpoints refuse before every mutation or runtime call
ok - fm-teardown: a concurrent lifecycle action refuses before mutation
ok - fm-teardown: non-pool cleanup ignores unrelated task publication locks
ok - fm-teardown: destructive cleanup serializes with metadata writers
ok - cleanup identity: valid tmux, Herdr, Zellij, Orca, and cmux records validate while every empty backend target refuses
ok - cleanup identity: an Orca record's real composite worktree id validates while a separatorless or newline-carrying id refuses
ok - tmux backend: direct empty target returns nonzero without invoking tmux
ok - process cleanup: creation-time PID identity removes only the exact child and preserves the control child
ok - fm-teardown: exact tmux cleanup preserves invalid and prefix-matched neighbors while removing only the recorded target
ok - fm-teardown: a close that genuinely failed refuses and keeps the record naming the surviving endpoint, and the same teardown finishes once the close works
ok - fm-teardown: a successful return followed by a failed close can retire before or after slot reallocation, including legacy records
ok - fm-teardown: malformed return receipts refuse before mutation and failed returns retain their claim without a receipt
ok - fm-teardown: --force continues past a close it could not make while still reporting it, and the same case refuses without --force
ok - fm-teardown: a close re-read that could not run refuses, while a definitively absent session or server still completes silently
ok - fm-teardown: forced secondmate cleanup still refuses on a child endpoint close that failed
ok - fm-teardown: an Orca close its missing CLI never attempted refuses even under --force, keeping the record naming the terminal
ok - fm-teardown: an already-exited endpoint, and a server that is already gone, still complete cleanup silently
ok - Treehouse locking resolves a bare local origin against its source project, matching the provisioned clone
ok - fm-teardown: a pool slot named by a second task record is never returned, killed, or reset
ok - fm-teardown: a pool slot held by another firstmate home is never returned
ok - fm-teardown: a task that solely holds its slot still returns it
ok - fm-teardown: a pool slot claimed by another task is left alone while the task's own cleanup finishes
ok - fm-teardown: a task's own slot claim, and an unclaimed slot, both still tear down
ok - fm-teardown: an exact recorded endpoint still tears down after changing cwd outside its worktree
ok - Treehouse project locking anchors at the local root for main-home, local-secondmate, and remote-seeded layouts
ok - fm-teardown: a remote-seeded secondmate home returns its own uncontested pool slot
ok - fm-teardown: slot ownership across a remote-seeded home and its local child still refuses
ok - Treehouse project locking still serializes two homes across the remote-seeded boundary
FM_TEST_END 2026-10-03T18:43:13Z tests/fm-teardown-endpoint-safety.test.sh exit=0 duration_ms=181192 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=181365
FM_TEST_SUMMARY_FAMILY family=backend-dispatch count=1 duration_ms=181192 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-teardown-endpoint-safety.test.sh duration_ms=181192
exit=0
Evidence: fm-control-relaunch suite transcript

Source: fm-control-relaunch suite transcript

FM_TEST_BEGIN 2026-10-03T18:40:12Z tests/fm-control-relaunch.test.sh family=backend-dispatch expected_gate_skip=none
ok - fm-control relaunch: a same-harness relaunch replaces the agent in the same endpoint and worktree
ok - fm-control relaunch: a same-harness codex relaunch carries codex_home forward unchanged
ok - fm-control relaunch: an explicit --codex-home replaces the recorded account
ok - fm-control relaunch: --codex-home ~/ expands against the launching user's HOME
ok - fm-control relaunch: a harness switch away from codex drops the recorded account
ok - fm-control relaunch: --codex-home on a non-codex target refuses before the agent is touched
ok - fm-control relaunch: a carried account with no auth.json refuses before the agent is touched
ok - fm-spawn --relaunch: with no --codex-home it reuses the task's recorded Codex account
ok - fm-spawn --relaunch: a recorded account with no auth.json refuses instead of falling back
ok - fm-control relaunch: pending composer text refuses before the exit command is typed
ok - fm-control relaunch: an unreadable composer fails safe before the exit command is typed
ok - fm-control relaunch: a linked spawning home preserves committed and unfinished work in the recorded copy
ok - fm-control relaunch: durable task metadata survives while the prior return receipt is cleared
ok - fm-control relaunch: delivery and concurrent task metadata publication serialize
ok - fm-control relaunch: disabling tracing clears metadata and pane context
ok - fm-control relaunch: progress and the Firstmate-worktree worker identity reach the replacement
ok - fm-control relaunch: a ship task refuses without the progress note its replacement needs
ok - fm-control relaunch: switching harness is one ordinary relaunch, and the old wiring goes with the old agent
ok - fm-control relaunch: a harness switch resets model and effort unless they are named too
ok - fm-control relaunch: a prefixed recorded harness can switch adapters transactionally
ok - fm-control relaunch: a prefixed command requires an explicit replacement harness
ok - fm-control relaunch: a same-harness relaunch keeps the profile axes it was running with
ok - native Ultra relaunch preserves its profile and rejects an unsupported model before stopping
ok - fm-control relaunch: explicit model and effort win over the recorded ones
ok - fm-control relaunch: refuses to relaunch onto an adapter with no verified mechanics
ok - fm-control relaunch: the retired incarnation's global turn-end token is revoked
ok - fm-control relaunch: wiring cleanup failure refuses replacement arming
ok - fm-control-lib: one owner resolves each harness's turn-end registry entry, and refuses a malformed token
ok - fm-control relaunch: a secondmate relaunch re-resolves its durable configured harness pin
ok - fm-control relaunch: invalid configured effort is ignored before stop
ok - fm-control relaunch: an adapter unverified for this task kind refuses before the agent is stopped
ok - fm-control relaunch: --codex-home on a secondmate refuses before the agent is touched
ok - fm-control relaunch: explicit secondmate harness resets unnamed profile axes
ok - fm-control relaunch: a ship task keeps its recorded harness instead of re-reading crew config
ok - fm-spawn --relaunch: with no explicit harness it reuses the task's recorded one, never the crew default
ok - fm-promote/fm-spawn --relaunch: the current ship contract supersedes stale scout delivery text
ok - fm-spawn --relaunch: wiring armed under a prefixed harness name is still retired
ok - fm-spawn --relaunch: switching away from muse retires its session binding
ok - fm-spawn --relaunch: switching away from cursor retires its session binding
ok - fm-control relaunch: an unaccountable local copy refuses before the agent is touched
ok - fm-control relaunch: a worker with nothing to work from is never launched
ok - fm-control relaunch: a refusal before the agent is stopped leaves the durable record untouched
ok - fm-control relaunch: checkpoint inspection failures refuse before stopping
ok - fm-control relaunch: a launch failure after the stop keeps the prior record and reports the real state
ok - fm-control relaunch: unpublished rollback keeps concurrent durable metadata
ok - fm-control relaunch: post-publication failure keeps the new durable record
ok - fm-control relaunch: partial stop reconciles actual agent state
ok - fm-control relaunch: failed journal replacement preserves durable phase
ok - fm-spawn relaunch: prepublication abort removes replacement state
ok - fm-control relaunch: the checkpoint records the exact unlanded work it preserved
ok - fm-control relaunch: a secondmate's child work is accounted for and its charter is left alone
ok - fm-control relaunch: a secondmate home that is not this secondmate's is refused
ok - fm-control relaunch: unreadable and untraversable child state fails checkpoint
ok - fm-control relaunch: two control actions on one task serialize instead of interleaving
ok - fm-spawn relaunch: direct entry participates in lifecycle serialization
ok - fm-promote: promotion participates in lifecycle serialization
ok - fm-spawn --relaunch: refuses to launch a second agent into a live endpoint
ok - fm-spawn --relaunch: symlinked records refuse before inspection
ok - fm-spawn --relaunch: keeps its early meta lock continuous
ok - fm-spawn --relaunch: pending closes refuse before replacement begins
ok - fm-spawn --relaunch: every identity axis comes from the record, and a contradicting flag refuses
ok - fm-spawn --relaunch: an unrecorded task is refused
ok - fm-spawn --relaunch: refuses to start a replacement outside the copy holding its work
ok - relaunch re-reads the backlog item instead of blindly re-running the transition
ok - relaunch heals an item that drifted out of In flight while the task stayed live
FM_TEST_END 2026-10-03T18:44:06Z tests/fm-control-relaunch.test.sh exit=0 duration_ms=233825 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=234011
FM_TEST_SUMMARY_FAMILY family=backend-dispatch count=1 duration_ms=233825 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-control-relaunch.test.sh duration_ms=233825
exit=0
Evidence: fm-secondmate-safety suite transcript

Source: fm-secondmate-safety suite transcript

FM_TEST_BEGIN 2026-10-03T18:40:12Z tests/fm-secondmate-safety.test.sh family=secondmate expected_gate_skip=none
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 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 - forced secondmate teardown refuses duplicated descendant pool slots
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
FM_TEST_END 2026-10-03T18:43:54Z tests/fm-secondmate-safety.test.sh exit=0 duration_ms=222301 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=222525
FM_TEST_SUMMARY_FAMILY family=secondmate count=1 duration_ms=222301 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-secondmate-safety.test.sh duration_ms=222301
exit=0
Evidence: Base refusal vs target retirement (excerpt)
BASE: bin/fm-teardown.sh distill-scale -> exit 1: REFUSED: task distill-scale's recorded worktree .../pool/1/project is also task wispr-register-fix's recorded worktree.
BASE: bin/fm-teardown.sh wispr-register-fix -> exit 1: REFUSED: ... is also task distill-scale's recorded worktree.
TARGET: bin/fm-teardown.sh distill-scale -> exit 0: teardown distill-scale complete (window firstmate:fm-distill-scale; pool slot .../worktree left to task wispr-register-fix ..., which it was reassigned to); sentinel present: yes; treehouse return calls: 0
TARGET: bin/fm-teardown.sh wispr-register-fix -> exit 0: teardown wispr-register-fix complete
- Outcome: ⚠️ 1 info across 1 run (6m36s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

⚠️ **Test** - 1 info
  • ℹ️ 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.
  • Live validation: ✅ go - 7 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Incident regression: with two receipt-less records on one pool slot and the claim naming the successor, tearing down the stale record finishes and leaves the successor's copy, claim and record untouch… ✅ pass live incident-repro.txt: base fm-teardown.sh refuses both with 'is also task X's recorded worktree'. Target: distill-scale exits 0 with 'pool slot ... left to task wispr-register-fix', sentinel kept, 0 tre…
After the stale record is gone, or even while it is still present, the current owner's teardown returns the slot and retires its record ✅ pass live incident-both-orders.txt: wispr-register-fix exits 0 'teardown wispr-register-fix complete (... worktree ...)' in both orders. Also teardown-endpoint-safety.log 'ok - fm-teardown: a successful return…
Retry after a successful return with a failed endpoint close retires the kept record without a second return, before and after reallocation, and even if the pool registration has changed ✅ pass live teardown-endpoint-safety.log: 'ok - fm-teardown: a successful return followed by a failed close can retire before or after slot reallocation, including legacy records' (drives real fm-teardown.sh thro…
Adversarial: empty, duplicated or drifted treehouse_returned receipts refuse with no change, and a failed pool return publishes no receipt and keeps its claim ✅ pass live teardown-endpoint-safety.log: 'ok - fm-teardown: malformed return receipts refuse before mutation and failed returns retain their claim without a receipt'
Adversarial: two live records on one slot with no receipt and no claim proving reassignment are still refused, even with --force (the guard is not loosened) ✅ pass live teardown-endpoint-safety.log: 'ok - fm-teardown: a pool slot named by a second task record is never returned, killed, or reset' and 'ok - fm-teardown: a pool slot held by another firstmate home is nev…
Relaunching a record that carries a return receipt drops the receipt, so its replacement is treated as the slot owner again (the review-1 concern) ✅ pass live control-relaunch.log: 'ok - fm-control relaunch: durable task metadata survives while the prior return receipt is cleared' (run_control launches the real fm-control.sh relaunch)
Secondmate descendant-slot preflight and child cleanup still behave after the receipt-aware ordering change ✅ pass live secondmate-safety.log: 'ok - forced secondmate teardown refuses duplicated descendant pool slots', 'ok - secondmate force teardown discards child work'; FM_TEST_END exit=0 gate_skip=false
The original trigger: a real Herdr pane close refused because the pane is the captain's active tab, followed by a retry after selecting another tab ⏸️ untested no This needs a real Herdr server with an active tab under the operator's control. fm-teardown.sh refuses gate worktrees, and the runbook forbids pointing at the real home. An operator can supply this by…
  • 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 target
  • Manual 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.

@Mauryanx
Mauryanx merged commit 09a7158 into main Oct 3, 2026
19 checks passed
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