Skip to content

fix(bin): keep a leased secondmate home's reused pool slot from being returned by teardown - #6273

Open
LuisGzz13 wants to merge 3 commits into
kunchenguid:mainfrom
LuisGzz13:fm/fm-secondmate-slot-upstream-c1
Open

LuisGzz13 wants to merge 3 commits into
kunchenguid:mainfrom
LuisGzz13:fm/fm-secondmate-slot-upstream-c1

Conversation

@LuisGzz13

@LuisGzz13 LuisGzz13 commented Oct 1, 2026 •

Copy link
Copy Markdown

Summary

Problem. A persistent secondmate home seeded into a reused Treehouse pool slot could still be named by an older finished task's record, or carry an old task's .fm-slot-owner claim. Ordinary or forced teardown of that stale task could then return, reset, or reap the live secondmate home. #6213 lets a stale record retire records-only only when a different owner claim is already present, so a seed that never published its own claim, or a home seeded before claims existed, stayed exposed.

Fix. A fresh leased-home seed publishes the secondmate's slot-owner claim under the Treehouse project lock, and seed rollback drops only its own claim. A new fm-home-seed.sh claim-slot <id> recovers already-seeded homes with an atomic, recoverable claim transfer, gated on positive pool, lease, identity, parent and registry proof of persistent-home ownership. Ordinary teardown refuses to return a durable secondmate home whose claim is its own or absent; the duplicate-record allowance is narrowed to a completed scout with a proved persistent owner and a dead or missing endpoint, still refusing contradictory secondmate records; and forced parent teardown never returns a child slot that is a persistent home. A slot the pool records as unleased stays ordinary, whatever markers a retired secondmate left behind.

Tests. Behavioural regressions in tests/fm-secondmate-safety.test.sh and tests/fm-teardown-endpoint-safety.test.sh, a new real-Herdr test tests/fm-teardown-persistent-slot-herdr-e2e.test.sh, and a live lab run with real Treehouse and Herdr (results below).

What Changed

  • Seeding: when a - seed leases a Treehouse pool slot, bin/fm-home-seed.sh now holds the firstmate repo's Treehouse project lock from before the lease until the slot-owner claim naming the secondmate and its home is published. A contended lock refuses before anything is leased. Rollback re-takes the lock and drops the claim only while it still names this secondmate. A new fm-home-seed.sh claim-slot <id> command re-publishes the claim for homes that were seeded before seeding wrote one. It takes the registry lock and then the project lock, and writes only after the new fm_treehouse_secondmate_slot_proof in bin/fm-wake-lib.sh proves five things: the slot is in this repo's pool, the pool state records a durable lease to that id, .fm-secondmate-home names the holder, .fm-secondmate-parent is a valid local binding, and the registering home's registry routes the id to this slot. The new fm_treehouse_slot_owner_transfer handles the swap: it keeps the replaced claim's bytes at .fm-slot-owner.prior and converges when retried.
  • Teardown: bin/fm-teardown.sh treats a slot as a persistent secondmate home when it has a durable lease, or when its pool state can't be read and it carries a secondmate marker or parent binding. If the claim on such a slot is the task's own or missing, teardown refuses, even with --force, and points to claim-slot. A forced parent teardown refuses a child slot that is a persistent home in the same way. Once the claim names the proved owner, a completed scout (no --force) can finish its own records-only cleanup while other stale records still name the slot, but only if the backend reads its exact recorded endpoint as dead or missing. A live or unreadable endpoint, or a contradictory secondmate record, still refuses. A slot the pool state records as unleased stays an ordinary slot, whatever markers a retired secondmate left behind.
  • Docs and tests: documented the claim and claim-slot in the secondmate-provisioning skill, docs/architecture.md, docs/configuration.md, docs/herdr-backend.md and docs/verification/runtime-backends.md (measured Herdr 0.9.1 endpoint states). Added regressions to tests/fm-secondmate-safety.test.sh and tests/fm-teardown-endpoint-safety.test.sh. Added a new real-Herdr test, tests/fm-teardown-persistent-slot-herdr-e2e.test.sh, which is registered in the Herdr family in bin/fm-test-run.sh and excluded in bin/fm-test-isolation-proof.sh.

🤖 Generated with Claude Code

Risk Assessment

✅ Low: The fix round applies the user-chosen option (b) narrowly. Every refusal path still fails closed, the round-1 retire-then-reuse regression is fixed and covered by behavioural tests in both directions, and the full change meets every required item of the stated intent with no remaining defect found.

Testing

I ran the change's own real-Herdr e2e test, then a new live driver: the target tree's real scripts in a disposable marked lab home, with a real Treehouse pool kept inside the lab and a named fm-lab-* Herdr session. The driver covered fresh seed, rollback, stale-scout cleanup, legacy-claim refusal (with and without --force, and with no claim), claim-slot refusal, reconcile and repeat, contradictory secondmate record, forced parent teardown, and real retirement followed by crewmate reuse. It also swapped in the pre-fix wake library to show the review-round-1 regression reproduces before the fix and is gone after it. All 78 live checks passed, and both targeted suites passed. Scout and secondmate records were written in fm-spawn's format rather than created by spawning agents (no harness was launched or spent tokens). The pre-change seed state (claim still naming the old scout) was created with the product's own claim writer. Lab session, lab root and temp files were all removed, and the worktree is unchanged. One process note: my first probe found that Treehouse 2.0.1 ignores TREEHOUSE_ROOT, so it created one throwaway pool (repo-9965f2) in ~/.treehouse. I returned and destroyed that pool, removed its directory, and confirmed ~/.treehouse matches its original listing. After that, every run used a repo-local treehouse.toml root. This is a CLI and shell change, so the evidence is command transcripts, not screenshots.

  • Live validation: ✅ go - 10 of 10 scenarios driven live against the product
Scenario Result Live Evidence
Operator seeds a leased secondmate home (fm-home-seed.sh mate - --no-projects) into a slot two scouts used: the slot's .fm-slot-owner then names task=mate and the registering home, and the pool reco… ✅ pass live live-run.log, section 'scenario: a fresh leased seed publishes the secondmate's slot-owner claim'
A seed that fails after leasing (placeholder charter) rolls back: real treehouse return leaves the slot unleased, its own claim is dropped, and the replaced claim of old-scout-b is kept at .fm-slot-ow… ✅ pass live live-run.log, section 'scenario: seed rollback returns its leased slot and drops only its own claim'
Stale completed scouts whose records name the freshly seeded home tear down records-only: each dead pane is closed, no treehouse command runs, and the home, lease and claim stay byte-identical ✅ pass live live-run.log, section 'scenario: stale completed scouts on the freshly seeded home finish records-only'
Adversarial: on a leased home whose claim still names the old scout, or has no claim, ordinary teardown refuses with and without --force, names claim-slot mate, and changes nothing ✅ pass live live-run.log, section 'phase 4: legacy shape'. The legacy claim was written with the product's own fm_treehouse_slot_owner_claim
Adversarial: claim-slot refuses without writing when ownership can't be proved (identity marker names 'impostor', or the id is unregistered) ✅ pass live live-run.log, section 'scenario: claim-slot refuses when ownership proof fails'
Operator runs the claim-slot command the refusal printed: the claim is replaced with task=mate, the old claim is kept at .prior, a repeat reports unchanged, and the stale scout then finishes records-o… ✅ pass live live-run.log, sections 'claim-slot reconciles the legacy claim' and 'after reconciliation the stale dead scout finishes'
Adversarial: another secondmate record naming the same home keeps the duplicate-record refusal (and names other-mate) until that record is gone ✅ pass live live-run.log, section 'scenario (adversarial): a contradictory secondmate record naming the home still refuses'
Adversarial: a completed scout whose old endpoint is live (registered claude) or unreadable keeps the refusal on a reconciled home, while dead or missing endpoints finish without touching it ✅ pass live herdr-e2e.log (tests/fm-teardown-persistent-slot-herdr-e2e.test.sh against real Herdr 0.9.1)
Forced retirement of a parent secondmate whose stale child record names another secondmate's leased home refuses before closing any pane or returning anything; after claim-slot it finishes, and that h… ✅ pass live live-run.log, section 'phase 5: forced retirement of another secondmate'
Retire-then-reuse: real secondmate retirement returns its slot unleased with markers left behind. A crewmate that takes the slot through real treehouse get then tears down normally (real treehouse ret… ✅ pass live live-run.log, sections 'phase 6', 'phase 7', 'regression check', and 'at the target commit the crewmate in the retired slot tears down normally'
Evidence: Real-Herdr persistent-slot e2e output

Source: Real-Herdr persistent-slot e2e output

evidence: herdr 0.9.1 endpoint states: shell-only=dead closed=missing registered-claude=alive unknown-status-claude=unreadable ok - real Herdr: a live or unreadable old endpoint keeps the duplicate-record refusal on a reconciled persistent home ok - real Herdr: completed scouts whose endpoints are dead or missing finish without touching the reconciled persistent home

evidence: herdr 0.9.1 endpoint states: shell-only=dead closed=missing registered-claude=alive unknown-status-claude=unreadable
ok - real Herdr: a live or unreadable old endpoint keeps the duplicate-record refusal on a reconciled persistent home
ok - real Herdr: completed scouts whose endpoints are dead or missing finish without touching the reconciled persistent home
rc=0
Evidence: Retire-then-reuse: pre-fix refusal vs target teardown (excerpt)
[727f692 wake lib] $ fm-teardown.sh next-crew (rc=1)
REFUSED: task next-crew's recorded worktree .../1/home shows a persistent secondmate home ..., but its ownership cannot be proved: no readable durable Treehouse lease is recorded ...
[1e71c99] $ fm-teardown.sh next-crew (rc=0)
🌳 Worktree returned to pool.
teardown next-crew complete (...)
pool-op| treehouse <return> <--force> <.../1/home>
Evidence: Legacy-claim refusal naming the recovery command (excerpt)
$ fm-teardown.sh old-scout-c --force (rc=1)
REFUSED: task old-scout-c's recorded worktree .../1/home is secondmate mate's leased persistent home, but that slot's owner claim still names old-scout-c; returning or resetting it would discard that home, so nothing was changed - not even with --force.
Reconcile the claim from its registering home (FM_HOME=.../home bin/fm-home-seed.sh claim-slot mate), then re-run teardown.
$ fm-home-seed.sh claim-slot mate (rc=0)
slot-owner claim replaced: .../1/.fm-slot-owner names task=mate home=.../home
Evidence: Targeted suite: fm-teardown-endpoint-safety

Source: Targeted suite: fm-teardown-endpoint-safety

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
skip - tmux not installed
skip - tmux not installed
skip - tmux not installed
skip - tmux not installed
skip - tmux not installed
ok - fm-teardown: an Orca close its missing CLI never attempted refuses even under --force, keeping the record naming the terminal
skip - tmux not installed
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 stale record on a claimed slot retires, then the claimant tears down
ok - fm-teardown: a task's own slot claim, and an unclaimed slot, both still tear down
ok - fm-teardown: a seeded persistent home named by stale scout records and an old claim is never returned, reset, or reaped
ok - fm-teardown: after a proved claim reconciliation, completed scouts with dead endpoints finish without touching the seeded home
ok - fm-teardown: a mismatched, missing, unreadable, or contradictory persistent-home proof refuses reconciliation and cleanup without mutation
ok - fm-teardown: reconciliation keeps the report, decision-inventory, and live or unreadable endpoint refusals, and a retry after endpoint death finishes
ok - fm-home-seed claim-slot: a repeated reconciliation is a no-op and an interrupted one converges without losing prior-claim evidence
ok - fm-teardown: a retired secondmate's leftover markers do not hold a reused unleased slot, while a leased or unreadable pool still refuses
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
rc=0

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ bin/fm-wake-lib.sh:1635 - fm_treehouse_slot_persistent_evidence treats a .fm-secondmate-home or .fm-secondmate-parent marker as proof of a persistent home "whatever its lease reads". Nothing removes those markers when a secondmate retires. remove_firstmate_home (bin/fm-teardown.sh ~2749) only runs treehouse return --force on a leased home. Both markers are gitignored, and Treehouse keeps gitignored files across a return: its README says so, and returned pool slots still hold gitignored state. fm-spawn does not reject such a slot either.

Failing sequence:

  1. Secondmate mate retires. Its slot S goes back to the pool unleased, but both markers stay in S.
  2. A crewmate task T for the firstmate project gets S from treehouse get, and spawn writes the claim task=T.
  3. fm-teardown.sh T runs teardown_persistent_slot_gate. The evidence check is true from the markers alone, the claim reads mine, and the proof fails with "no readable durable Treehouse lease". Teardown exits at bin/fm-teardown.sh:2488 with "REFUSED ... not even with --force".

The forced-parent preflight (bin/fm-teardown.sh ~3088) refuses the same way. Before this change, T would have been torn down and its slot returned normally. Now every later firstmate-project crewmate in that slot is stuck. claim-slot cannot help because the home is no longer registered, and the only advice printed is to "reconcile the home's lease, identity marker, parent binding, and registry route". The new seed claim (task=<secondmate>) is also never released on retirement, so it stays on the returned slot as well.

The defect is a correctness regression, but choosing the fix touches deliberate design, so it needs your decision. Option (a): have secondmate retirement drop its markers and its own slot claim once treehouse return succeeds. Option (b): count marker-only evidence only when the pool state cannot answer (exit 2), not when it positively records no lease (exit 1). Either way, add a regression for retire-then-reuse.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 10 of 10 scenarios driven live against the product
Scenario Result Live Evidence
Operator seeds a leased secondmate home (fm-home-seed.sh mate - --no-projects) into a slot two scouts used: the slot's .fm-slot-owner then names task=mate and the registering home, and the pool reco… ✅ pass live live-run.log, section 'scenario: a fresh leased seed publishes the secondmate's slot-owner claim'
A seed that fails after leasing (placeholder charter) rolls back: real treehouse return leaves the slot unleased, its own claim is dropped, and the replaced claim of old-scout-b is kept at .fm-slot-ow… ✅ pass live live-run.log, section 'scenario: seed rollback returns its leased slot and drops only its own claim'
Stale completed scouts whose records name the freshly seeded home tear down records-only: each dead pane is closed, no treehouse command runs, and the home, lease and claim stay byte-identical ✅ pass live live-run.log, section 'scenario: stale completed scouts on the freshly seeded home finish records-only'
Adversarial: on a leased home whose claim still names the old scout, or has no claim, ordinary teardown refuses with and without --force, names claim-slot mate, and changes nothing ✅ pass live live-run.log, section 'phase 4: legacy shape'. The legacy claim was written with the product's own fm_treehouse_slot_owner_claim
Adversarial: claim-slot refuses without writing when ownership can't be proved (identity marker names 'impostor', or the id is unregistered) ✅ pass live live-run.log, section 'scenario: claim-slot refuses when ownership proof fails'
Operator runs the claim-slot command the refusal printed: the claim is replaced with task=mate, the old claim is kept at .prior, a repeat reports unchanged, and the stale scout then finishes records-o… ✅ pass live live-run.log, sections 'claim-slot reconciles the legacy claim' and 'after reconciliation the stale dead scout finishes'
Adversarial: another secondmate record naming the same home keeps the duplicate-record refusal (and names other-mate) until that record is gone ✅ pass live live-run.log, section 'scenario (adversarial): a contradictory secondmate record naming the home still refuses'
Adversarial: a completed scout whose old endpoint is live (registered claude) or unreadable keeps the refusal on a reconciled home, while dead or missing endpoints finish without touching it ✅ pass live herdr-e2e.log (tests/fm-teardown-persistent-slot-herdr-e2e.test.sh against real Herdr 0.9.1)
Forced retirement of a parent secondmate whose stale child record names another secondmate's leased home refuses before closing any pane or returning anything; after claim-slot it finishes, and that h… ✅ pass live live-run.log, section 'phase 5: forced retirement of another secondmate'
Retire-then-reuse: real secondmate retirement returns its slot unleased with markers left behind. A crewmate that takes the slot through real treehouse get then tears down normally (real treehouse ret… ✅ pass live live-run.log, sections 'phase 6', 'phase 7', 'regression check', and 'at the target commit the crewmate in the retired slot tears down normally'
  • tests/fm-teardown-persistent-slot-herdr-e2e.test.sh (real Herdr, isolated fm-lab-* session via bin/fm-herdr-lab.sh)
  • ~/.no-mistakes/evidence/01M3TD5RAXF1M89YXYS6R1JSE3/live-driver.sh: a marked lab home made by bin/fm-lab-home.sh create (a primary checkout of the 1e71c99 tree); a real Treehouse pool rooted inside the lab through a repo-local treehouse.toml; a real Herdr lab session; real fm-brief.sh, fm-home-seed.sh and fm-teardown.sh
  • Live: scouts take pool slot 1 through interactive treehouse get in their own Herdr panes, then fm-home-seed.sh mate - --no-projects with a placeholder charter (rollback), then a valid seed (claim published, durable lease)
  • Live: fm-teardown.sh old-scout-a and old-scout-b on the freshly seeded home (records-only; no treehouse call; pool fingerprint byte-identical)
  • Live: legacy claim shape, then fm-teardown.sh old-scout-c, --force, and --force with no claim (all refused; the refusal names claim-slot mate)
  • Live: fm-home-seed.sh claim-slot mate with a mismatched identity marker, and claim-slot ghost (both refused, claim unchanged), then claim-slot mate (replaced, .prior kept) and a repeat (unchanged), then fm-teardown.sh old-scout-c finishes
  • Live: a contradictory other-mate secondmate record makes fm-teardown.sh old-scout-d refuse; once that record is removed, old-scout-d finishes
  • Live: fm-home-seed.sh domain &lt;path&gt; --no-projects, then fm-teardown.sh domain --force with a stale child record naming mate's leased home (refused, no pane closed, no pool op); after claim-slot mate it finishes, and mate's home is never returned
  • Live: fm-teardown.sh mate retirement (real treehouse return, slot unleased, markers left behind), then next-crew takes the slot through real treehouse get. Teardown is refused with 727f692's bin/fm-wake-lib.sh and finishes at the target tree (real treehouse return, claim released)
  • tests/fm-teardown-endpoint-safety.test.sh (targeted; includes the new persistent-home and retired-marker cases)
  • tests/fm-secondmate-safety.test.sh (targeted; includes the seed-claim, rollback, and forced-parent persistent-home cases)
✅ **Document** - passed

✅ No issues found.

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

✅ No issues found.

Luis Gonzalez added 3 commits September 30, 2026 18:17
…rned

A "-" home seed durably leases a Treehouse pool slot but never published
Firstmate's slot-owner claim, so completed tasks that used the slot before
still name what is now the secondmate's home. Once only one stale record
remained and its old claim read as its own, or the claim was absent, its
teardown returned the seeded home to the pool. The reassigned-slot shortcut
also let any different claim skip the duplicate-record scan on such a home,
even when its ownership could not be proved or the old endpoint was live.

- fm-wake-lib: read a slot's durable lease from the pool state, recognize
  persistent-home evidence, positively prove a committed local secondmate home
  (pool identity, lease holder, identity marker, local parent binding, exact
  registry route), and transfer a claim atomically while keeping the replaced
  one at .fm-slot-owner.prior.
- fm-home-seed: publish the claim under the Treehouse project lock when a seed
  leases its home, drop only that claim on rollback, and add claim-slot to
  re-publish it for an already-seeded home after re-proving ownership.
- fm-teardown: never return, reset, or reap a slot showing a persistent home
  while the claim still makes the ordinary task its owner, even with --force;
  once the claim names the proved owner, a completed scout whose exact
  endpoint is dead or missing may take the existing no-slot-touch cleanup while
  other stale records or the owner's own record name the home, and any other
  secondmate record still refuses. Forced parent teardown refuses a child slot
  that is a persistent home.
- Tests cover the diagnostic cases, proof mismatches, live and unreadable
  endpoints, completion gates, interrupted reconciliation, seed claim and
  rollback, and real Herdr endpoint classification in an isolated lab.
The real-Herdr persistent-slot cleanup test relied on Herdr's own screen
detection leaving an unreported claude-named pane at the undetermined
`unknown` status. Herdr settles that pane on `idle` about 4 seconds after
detection, so a slow run classified it `alive` instead of `unreadable` and
failed; a 6-second delay before classification reproduced it every time.

Report that pane's status as `unknown` explicitly, which makes the reporter
the status authority, and wait for that status before classifying. The
classifier path under test and every assertion are unchanged; the fixed test
passes with 6- and 15-second delays and in 10 of 10 sequential runs.
@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High risk] Adds pool slot ownership reconciliation for persistent secondmate homes.

The PR appears safe to merge; no actionable regression was established.

Reviews (1) · Last reviewed commit: "no-mistakes(review): Ignore retired seco..."

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