Skip to content

fix(bin): harden pooled worktree ownership checks - #4468

Open
jericho1050 wants to merge 11 commits into
kunchenguid:mainfrom
jericho1050:fm/fm-pr3519-recreate
Open

jericho1050 wants to merge 11 commits into
kunchenguid:mainfrom
jericho1050:fm/fm-pr3519-recreate

Conversation

@jericho1050

Copy link
Copy Markdown

Intent

Recreate and resolve merge conflict for #3519 from current upstream main, preserving worktree ownership protections and positive controls. Validate and submit a replacement through no-mistakes on a new branch; do not merge or close the original PR. Current-main implementation refuses fresh pooled worktree allocation when any reachable local task record still names the selected copy, refuses foreign or unreadable slot-owner claims instead of overwriting them, and refuses teardown when a recorded copy is checked out on another fm/ branch while preserving safe cleanup for the task's own branch and existing reassigned-slot handling. Regression coverage stays in existing spawn-pool and teardown-endpoint suites, with maintainer architecture wording updated to match.

What Changed

  • Refuse fresh pooled-slot allocation when reachable task records still name the worktree, ownership state is unreadable, or an existing claim belongs to another task or physical Firstmate home.
  • Make teardown fail closed for unreadable ownership evidence, same-ID cross-home claims, and worktrees checked out on another fm/<task> branch, while preserving reassigned-slot and own-branch cleanup.
  • Expand spawn and teardown regression coverage and update the architecture documentation for the strengthened ownership rules.

Risk Assessment

✅ Low: Captain, the ownership checks satisfy the required allocation, claim, branch, reassignment, descendant-cleanup, and positive-control invariants without a substantiated remaining defect.

Testing

Inspected the clean target diff, ran both focused executable-interface suites, and manually drove the changed spawn and teardown paths against isolated Git worktrees. All ownership guards and positive controls passed; four CLI/state transcripts were captured. This is a CLI-only change, so no screenshot was applicable.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Fresh pooled allocation refuses a slot retained by any reachable local task record without changing its HEAD, claim, or metadata ✅ pass live spawn-ownership-cli-evidence.log and spawn-pool-live-transcript.log
Fresh allocation refuses foreign, same-ID/different-home, and retained post-publication owner claims instead of overwriting them ✅ pass live spawn-ownership-cli-evidence.log and spawn-pool-live-transcript.log
Unreadable registries, state directories, task records, and owner claims fail closed before allocation or teardown mutation ✅ pass live Both focused suite transcripts and teardown-ownership-cli-evidence.log
Forced teardown refuses a recorded copy on another fm/<task> branch, including pooled, non-pooled, Orca, descendant, and branch-inspection-error paths ✅ pass live teardown-safety-live-transcript.log and teardown-ownership-cli-evidence.log
Teardown still cleans the task's own branch and physical-home aliases while preserving reassigned slots and their new owner claims ✅ pass live teardown-ownership-cli-evidence.log and teardown-safety-live-transcript.log
Evidence: Spawn ownership CLI evidence

Source: Spawn ownership CLI evidence

SCENARIO: retained reachable task record blocks fresh allocation
command: FM_HOME=<isolated-home> bin/fm-spawn.sh evidence-new-task <project> --scout
exit=1
error: spawn refused: worktree '/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-ownership-evidence.vmUpLy/retained-record/slots/1/project' is already held by task 'older-task'
Resolve task 'older-task' and retry after its work is safely landed and its record is cleaned up; do not bypass this refusal.
slot_head_unchanged=yes
new_metadata_absent=yes
slot_claim_absent=yes

SCENARIO: well-formed foreign owner claim blocks overwrite
command: FM_HOME=<isolated-home> bin/fm-spawn.sh shared-name <project> --scout
exit=1
error: could not claim Treehouse pool slot /private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-ownership-evidence.vmUpLy/foreign-claim/slots/1/project for task shared-name; refusing to launch a worker whose slot cannot later be proved to be its own; inspect window firstmate:fm-shared-name
claim_after:
task=shared-name
home=<foreign-home>
claim_unchanged=yes
new_metadata_absent=yes
Evidence: Teardown ownership CLI evidence

Source: Teardown ownership CLI evidence

SCENARIO: another task branch refuses teardown before mutation
command: FM_HOME=<isolated-home> bin/fm-teardown.sh stale-task --force
exit=1
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  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.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
REFUSED: task stale-task's recorded worktree <fixture>/worktree is checked out on branch fm/other-task for task other-task; refusing teardown to avoid changing another task's copy.
Resolve task other-task and retry teardown after its work is safely landed; do not use --force to bypass this refusal.
branch_after=fm/other-task
metadata_preserved=yes
sentinel_preserved=yes
runtime_calls=none

SCENARIO: own task branch and claim still tear down safely
command: FM_HOME=<isolated-home> bin/fm-teardown.sh own-task --force
exit=0
teardown own-task complete (window firstmate:fm-own-task, worktree <fixture>/worktree)
Backlog: own-task just finished (this home keeps no markdown backlog at <fixture>~/backlog.md). Update <fixture>~/backlog.md - move own-task 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.
metadata_removed=yes
claim_removed=yes
runtime_calls:
treehouse <return> <--force> <<fixture>/worktree>
tmux <kill-window> <-t> <=firstmate:=fm-own-task>

SCENARIO: reassigned slot skips slot mutation but finishes stale-record cleanup
command: FM_HOME=<isolated-home> bin/fm-teardown.sh stale-task --force
exit=0
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  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.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
warning: task stale-task's recorded worktree <fixture>/pool/1/project was reassigned to task new-owner (home <fixture>/other-home), which claimed that pool slot after this record was written; that slot is no longer stale-task's, so its processes, copy, and claim are left untouched and only stale-task's own cleanup runs.
stale_metadata_removed=yes
slot_sentinel_preserved=yes
new_owner_claim_unchanged=yes
treehouse_return_called=no

SCENARIO: unreadable reachable task record refuses teardown before mutation
command: FM_HOME=<isolated-home> bin/fm-teardown.sh stale-task --force
exit=1
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  2 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.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
~/.no-mistakes/worktrees/a884983a9df2/01M2GMD4G34KABRAMVWM4H4DJE/bin/fm-wake-lib.sh: line 1350: <fixture>~/hidden-task.meta: Permission denied
REFUSED: cannot read local task record <fixture>~/hidden-task.meta; refusing teardown because slot ownership is unproven.
both_metadata_records_preserved=yes
sentinel_preserved=yes
runtime_calls=none
Evidence: Spawn-pool focused suite transcript

Source: Spawn-pool focused suite transcript

# remote-seeded Treehouse spawn command
$ FM_HOME=/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/remote-seeded/home bin/fm-spawn.sh pool-remote-seeded-r13 /private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/remote-seeded/project --scout
spawned pool-remote-seeded-r13 harness=codex kind=scout window=firstmate:fm-pool-remote-seeded-r13 worktree=/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/remote-seeded/pool
exit=0
published worktree=/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/remote-seeded/pool
resolved project lock=/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/remote-seeded/home/state/.treehouse-project-44fc1131a97bef9ecbe922910d4b41f0b1131387.lock
ok - a remote-seeded secondmate home allocates and launches from its Treehouse pool
ok - a Treehouse slot claim names the launched task, refuses when unclaimable, and is dropped by a locked abort
ok - an unreadable nested Firstmate registry refuses pooled allocation
ok - an unreadable nested Firstmate state refuses pooled allocation
ok - an unreadable alternate state refuses pooled allocation
ok - an unreadable task record refuses pooled allocation
ok - fresh Treehouse allocation refuses retained task records and foreign owner claims
# evidence begin: linked-home spawn, returned=primary
$ bin/fm-spawn.sh pool-linked-primary-r12 /private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/linked-primary/secondmate --scout
error: treehouse get did not enter an isolated worktree within 60s (last seen '/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/linked-primary/project': it is the repository's primary checkout (its git dir is the spawning project's common git dir); spawning project '/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/linked-primary/secondmate'); inspect window firstmate:fm-pool-linked-primary-r12
exit=1
primary HEAD before=717f9fadfb1de83b9bed9ac9be087cbed29f7da2 after=717f9fadfb1de83b9bed9ac9be087cbed29f7da2
primary reflog before:
717f9fa HEAD@{0}: commit (initial): initial
primary reflog after:
717f9fa HEAD@{0}: commit (initial): initial
FETCH_HEAD absent
task metadata absent
# evidence end
ok - linked spawning home: primary preserves the primary before any refresh
# evidence begin: linked-home spawn, returned=primary-alias
$ bin/fm-spawn.sh pool-linked-primary-alias-r12 /private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/linked-primary-alias/secondmate --scout
error: treehouse get did not enter an isolated worktree within 60s (last seen '/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/linked-primary-alias/primary-alias': it is the repository's primary checkout (its git dir is the spawning project's common git dir); spawning project '/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/linked-primary-alias/secondmate'); inspect window firstmate:fm-pool-linked-primary-alias-r12
exit=1
primary HEAD before=baf7625d46d5f9d82405b4fead052713947e282d after=baf7625d46d5f9d82405b4fead052713947e282d
primary reflog before:
baf7625 HEAD@{0}: commit (initial): initial
primary reflog after:
baf7625 HEAD@{0}: commit (initial): initial
FETCH_HEAD absent
task metadata absent
# evidence end
ok - linked spawning home: primary-alias preserves the primary before any refresh
# evidence begin: linked-home spawn, returned=spawning
$ bin/fm-spawn.sh pool-linked-spawning-r12 /private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/linked-spawning/secondmate --scout
error: treehouse get did not enter an isolated worktree within 60s (last seen '/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/linked-spawning/secondmate': it is the spawning project itself; spawning project '/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/linked-spawning/secondmate'); inspect window firstmate:fm-pool-linked-spawning-r12
exit=1
primary HEAD before=aa4ef3a0837bbd15841e4acb8e7f370a299165ac after=aa4ef3a0837bbd15841e4acb8e7f370a299165ac
primary reflog before:
aa4ef3a HEAD@{0}: commit (initial): initial
primary reflog after:
aa4ef3a HEAD@{0}: commit (initial): initial
FETCH_HEAD absent
task metadata absent
# evidence end
ok - linked spawning home: spawning preserves the primary before any refresh
# evidence begin: linked-home spawn, returned=scout
$ bin/fm-spawn.sh pool-linked-scout-r12 /private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/linked-scout/secondmate --scout
spawned pool-linked-scout-r12 harness=codex kind=scout window=firstmate:fm-pool-linked-scout-r12 worktree=/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/linked-scout/pool
exit=0
primary HEAD before=301f7d49083432f0902a9e9b35bf28ab58841b17 after=301f7d49083432f0902a9e9b35bf28ab58841b17
primary reflog before:
301f7d4 HEAD@{0}: commit (initial): initial
primary reflog after:
301f7d4 HEAD@{0}: commit (initial): initial
FETCH_HEAD absent
saved task metadata:
window=firstmate:fm-pool-linked-scout-r12
endpoint_task_id=pool-linked-scout-r12
worktree=/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/linked-scout/pool
project=/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/linked-scout/secondmate
harness=codex
kind=scout
tasktmp=/tmp/fm-pool-linked-scout-r12
model=default
effort=default
spawn_gen=s1789419254.97093.21484
worker HEAD=49e907ca349589036811ed847bf1177179bcc7ac origin/main=49e907ca349589036811ed847bf1177179bcc7ac
# evidence end
ok - linked spawning home: scout preserves the primary before any refresh
# observed spawn: spawned pool-current-base-r1 harness=codex kind=ship mode=no-mistakes yolo=off window=firstmate:fm-pool-current-base-r1 worktree=/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/current-base/pool
# observed base: HEAD=b108ff2f5be2417a70210ac48c61592465af5bb5 origin/main=b108ff2f5be2417a70210ac48c61592465af5bb5 advanced-main=must survive a newly spawned branch
ok - a stale pooled worktree refreshes to current origin/main before a crew branch is created
ok - a stale pooled worktree resolves and refreshes a non-main default branch
# observed direct-pr spawn: spawned pool-direct-pr-r3 harness=codex kind=ship mode=direct-PR yolo=off window=firstmate:fm-pool-direct-pr-r3 worktree=/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/direct-pr/pool
# observed scout spawn: spawned pool-scout-r3 harness=codex kind=scout window=firstmate:fm-pool-scout-r3 worktree=/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/scout/pool
ok - direct-PR ships and scouts both refresh stale pooled worktrees before launch
# observed dirty refusal: error: pooled worktree '/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/dirty-refusal/pool' is not clean; refusing to discard uncommitted work while refreshing its base; preserved=keep this local work
ok - a dirty pooled worktree is refused without discarding its local work
# observed unresolved-default refusal: error: could not resolve origin's current default branch for pooled worktree '/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/unresolved-default/pool'; refusing to launch from a potentially stale base
ok - an unresolved remote default branch refuses the pooled worktree
# observed unreachable-origin refusal: error: could not fetch origin for pooled worktree '/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/unreachable-origin/pool'; refusing to launch from a potentially stale base
ok - an unreachable origin refuses a potentially stale pooled worktree
# observed origin-less launch: spawned pool-originless-r6 harness=codex kind=ship mode=no-mistakes yolo=off window=firstmate:fm-pool-originless-r6 worktree=/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-spawn-pool-base-freshen.Vmhr38/originless/pool
ok - an origin-less pooled worktree launches as-is, skipping the freshness gate
ok - a dirty origin-less pooled worktree is refused without discarding its local work
ok - an origin configuration without a URL refuses the pooled worktree
ok - an empty origin configuration section refuses the pooled worktree
ok - an empty-only included origin section documents the accepted detection boundary
ok - an inactive conditional origin include leaves the pooled worktree origin-less
# observed stale-pin refusal: error: submodule 'ui' is checked out at 118f3a1f6bdfbd3ed7b2697ca094ab5b1c8e32d5, but this base records 65b6b03cde40d31e5ff3c168aaa0d49577ddbf33
ok - an origin-less pool with a stale submodule pin refuses while naming both pins and no remedy
ok - an unpushed submodule commit keeps the uncommitted-work refusal and survives it
ok - work inside a submodule is still refused as uncommitted work, not called stale
ok - a stale pin carrying real work is refused conservatively, never called stale
ok - a stale pin beside other dirt yields the conservative refusal alone, with no stale-pin line
# all fm-spawn-pool-base-freshen tests passed
Evidence: Teardown-safety focused suite transcript

Source: Teardown-safety focused suite transcript

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 - 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 - 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 refuses when a reachable state directory cannot be enumerated
ok - fm-teardown refuses when its alternate state directory cannot be enumerated
ok - fm-teardown refuses when a reachable task record cannot be read
ok - fm-teardown: a task that solely holds its slot still returns it
ok - fm-teardown refuses a copy checked out on another task's branch before cleanup
ok - fm-teardown refuses before cleanup when branch ownership inspection fails
ok - fm-teardown still cleans up a copy checked out on its own task branch
ok - forced secondmate teardown refuses a descendant checked out on another task's branch
ok - forced secondmate teardown refuses a non-pool descendant on another task's branch
ok - forced secondmate teardown refuses an Orca descendant on another task's branch
ok - fm-teardown: a pool slot claimed by another task is left alone while the task's own cleanup finishes
ok - fm-teardown refuses same-ID slot ownership from another physical home
ok - fm-teardown: physical own-home identity 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
# remote-seeded Treehouse teardown command
$ FM_HOME=/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-teardown-endpoint-safety.8XDkAb/remote-home-teardown/home bin/fm-teardown.sh remote-task --force
stdout:
teardown remote-task complete (window firstmate:fm-remote-task, worktree /private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-teardown-endpoint-safety.8XDkAb/remote-home-teardown/worktree)
Backlog: remote-task just finished (this home keeps no markdown backlog at /private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-teardown-endpoint-safety.8XDkAb/remote-home-teardown/home/data/backlog.md). Update /private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-teardown-endpoint-safety.8XDkAb/remote-home-teardown/home/data/backlog.md - move remote-task 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.
stderr:
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  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.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
exit=0
runtime calls:
treehouse <return> <--force> </private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-teardown-endpoint-safety.8XDkAb/remote-home-teardown/worktree>
tmux <kill-window> <-t> <=firstmate:=fm-remote-task>
task metadata=removed
slot sentinel=present
ok - fm-teardown: a remote-seeded secondmate home returns its own uncontested pool slot
# remote-seeded cross-home collision command
$ FM_HOME=/private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-teardown-endpoint-safety.8XDkAb/remote-home-collision/home bin/fm-teardown.sh remote-stale --force
stderr:
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  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.
●  repair missing watcher supervision according to the session-start block for this harness; do not use shell &.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
REFUSED: task remote-stale's recorded worktree /private/var/folders/j3/dxjkmnxn11vc2r7dj5zdbk480000gn/T/fm-teardown-endpoint-safety.8XDkAb/remote-home-collision/pool/1/project is also task child-task's recorded worktree.
Returning that pool slot would kill child-task's processes and reset its copy, so nothing was changed - not even with --force.
Reconcile whichever record is wrong (bin/fm-crew-state.sh remote-stale; bin/fm-crew-state.sh child-task), then re-run teardown.
exit=1
runtime calls=none
remote metadata=preserved
child metadata=preserved
slot sentinel=preserved
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

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (9) ✅
  • 🚨 bin/fm-wake-lib.sh:1316 - Required intent says fresh allocation “refuses ... when any reachable local task record still names the selected copy” and refuses foreign owner claims. This exemption treats task IDs as globally unique, although different Firstmate homes can use the same ID. If a child home retains same.meta and a task=same, home=&lt;child&gt; claim, a parent-home spawn named same skips the record here and accepts the claim as its own at line 1380 because ownership comparison ignores home; it can then refresh and overwrite the held copy. Bind ownership to both home and task ID, and do not exempt same-named records from other homes.
  • 🚨 bin/fm-teardown.sh:3049 - Required intent says cleanup “refuses teardown when a recorded copy is checked out on another fm/<task> branch,” including with --force. The new refusal is called only for direct task cleanup. Forced secondmate retirement instead preflights descendants through preflight_descendant_treehouse_slots, where an absent/own claim passes, and cleanup_firstmate_home_children then returns the pooled copy without checking its branch. A legacy child record naming a copy now on fm/other-task can therefore still reset another task’s work. Apply the same branch-owner check during descendant preflight, while preserving the existing reassigned-claim bypass and own-branch control.

🔧 Fix applied.
1 error still open:

  • 🚨 bin/fm-wake-lib.sh:1424 - Required intent preserves safe cleanup for a task’s own branch, but ownership compares the recorded home= with the caller’s raw path spelling. Absolute symlink-spelled FM_HOME values are explicitly supported and preserved, so a task spawned through /home-link and torn down through the equivalent real path is classified as reassigned: teardown skips the own-branch guard and worktree cleanup, removes the task record, and strands the copy and claim. Compare canonical existing-home identities while continuing to distinguish genuinely different homes.

🔧 Fix applied.
2 errors still open:

  • 🚨 bin/fm-wake-lib.sh:1274 - The required positive control preserves safe cleanup for a task’s own branch. With supported symlink-spelled FM_HOME=/h-link, the collector seeds /h-link/state and then adds the canonical /h/state because it compares raw strings. Teardown consequently scans the same &lt;id&gt;.meta twice, treats its second spelling as another record at fm-teardown.sh:2122, and refuses an uncontested fm/&lt;id&gt; worktree. Canonicalize state or metadata identities before deduplication/comparison; the added alias test misses this because run_case invokes teardown through the physical home.
  • 🚨 bin/fm-teardown.sh:2734 - The required behavior says teardown refuses when a recorded copy is checked out on another fm/&lt;task&gt; branch, but the descendant branch guard is reached only for recognized pooled, non-Orca worktrees. During forced secondmate retirement, an Orca child on fm/other-task is skipped here and later has its terminal and worktree removed at lines 2958-2964; an unrecognized legacy Treehouse copy is similarly skipped. Apply the branch-owner refusal at the common descendant preflight boundary for every non-secondmate child, preserving only the verified reassigned-pool-claim bypass.

🔧 Fix applied.
1 error still open:

  • 🚨 bin/fm-teardown.sh:3040 - git symbolic-ref ... || true treats both a detached HEAD (exit 1) and an inspection failure (exit 128) as an empty branch. If a copy on fm/other-task has a transient HEAD read error during forced cleanup, the guard succeeds and later cleanup can reset/return it once access recovers. Preserve detached-HEAD handling, but refuse other Git errors; preferably inspect the full refs/heads/... value. Git documents the distinct exit statuses in git-symbolic-ref.

🔧 Fix applied.
1 error still open:

  • 🚨 bin/fm-teardown.sh:2170 - A fix round added successful teardown for a claim with the same task ID but a different physical home, yet the remaining cleanup is not home-scoped. If home A retains x.meta and home B now owns the slot as task x, teardown in A classifies the claim as reassigned here, then can reap B's shared /tmp/fm-x processes (line 3296), kill its reused firstmate:fm-x endpoint (line 3398), and delete /tmp/fm-x (line 3455). This acceptance path exceeds the prior allocation/alias fixes and is not required by intent; remove it by refusing same-ID/different-home claims at this shared teardown boundary, while retaining physical-home aliases and different-ID reassignment handling.

🔧 Fix applied.
3 issues (1 error, 2 warnings) still open:

  • 🚨 bin/fm-wake-lib.sh:1301 - If one nested secondmates.md cannot be read, this inner loop fails but the outer traversal continues; a later successfully processed home can overwrite the failure status. A root with children A/B can therefore miss A's registered grandchild C, return success after B, and allocate/reset a pre-claim slot still named by C's task record. Detect registry read failure here and return immediately from the shared collector.
  • ⚠️ bin/fm-wake-lib.sh:1400 - The new foreign-claim refusal makes the abort warning false. After metadata publication, a delivery failure can roll back the record while leaving its claim; a different-ID retry is now refused here rather than replacing that claim. Update the abort comment and warning to direct the operator to reconcile the retained claim.
  • ⚠️ tests/fm-spawn-pool-base-freshen.test.sh:835 - This format writes literal \n text, producing one malformed task= line with no home=. The test therefore exercises the unreadable-claim path, not the required well-formed foreign-owner path. Use actual newline escapes so the existing suite pins foreign-claim refusal.

🔧 Fix applied.
2 errors still open:

  • 🚨 bin/fm-wake-lib.sh:1336 - The shared ownership scan treats a collected state directory that cannot be enumerated as empty. For example, if a registered child’s state/ is searchable but unreadable and contains an older pre-claim record naming the selected slot, the glob remains unmatched and allocation proceeds to claim and freshen that slot. Detect directory-enumeration failures at the shared collector boundary and refuse allocation/cleanup.
  • 🚨 bin/fm-teardown.sh:3045 - The .git existence check converts an inspection failure into permission to continue. If a copy on fm/other-task has a transient .git stat failure, forced cleanup skips the branch guard; if access recovers afterward, later cleanup can reset and return the copy. Remove this precheck and let git symbolic-ref distinguish detached HEAD from all inspection errors.

🔧 Fix applied.
1 error still open:

  • 🚨 bin/fm-teardown.sh:2125 - A reachable metadata file can be unreadable while its state directory remains enumerable. fm_meta_get deliberately converts read failures to empty values, so teardown misses both worktree= and home= here; with an absent legacy claim and detached HEAD, --force then resets and returns a slot still owned by that record. Read candidate records through an error-reporting path and refuse when any cannot be read.

🔧 Fix applied.
1 error still open:

  • 🚨 bin/fm-wake-lib.sh:1274 - record_state is pre-seeded but never receives the enumeration check applied to each discovered &lt;home&gt;/state. With FM_STATE_OVERRIDE=/alt/state on a write/search-only directory, known task paths and new writes remain usable while *.meta cannot expand; an existing record naming the selected slot is therefore missed, allowing spawn to claim/freshen it or teardown to return it. Apply the same enumeration refusal to the explicit record_state before adding it to the shared scan.

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

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Fresh pooled allocation refuses a slot retained by any reachable local task record without changing its HEAD, claim, or metadata ✅ pass live spawn-ownership-cli-evidence.log and spawn-pool-live-transcript.log
Fresh allocation refuses foreign, same-ID/different-home, and retained post-publication owner claims instead of overwriting them ✅ pass live spawn-ownership-cli-evidence.log and spawn-pool-live-transcript.log
Unreadable registries, state directories, task records, and owner claims fail closed before allocation or teardown mutation ✅ pass live Both focused suite transcripts and teardown-ownership-cli-evidence.log
Forced teardown refuses a recorded copy on another fm/<task> branch, including pooled, non-pooled, Orca, descendant, and branch-inspection-error paths ✅ pass live teardown-safety-live-transcript.log and teardown-ownership-cli-evidence.log
Teardown still cleans the task's own branch and physical-home aliases while preserving reassigned slots and their new owner claims ✅ pass live teardown-ownership-cli-evidence.log and teardown-safety-live-transcript.log
  • git status --short
  • git diff --stat 18fe6e90ce71fda14d1a7f4782a66efe0b7b07ca..8420b35aaf7ba6aae2b80b9857fcc652cc067e9d
  • FM_TEST_EVIDENCE=1 bash tests/fm-spawn-pool-base-freshen.test.sh
  • FM_TEST_EVIDENCE=1 bash tests/fm-teardown-endpoint-safety.test.sh
  • Manual isolated bin/fm-spawn.sh checks for retained-record and well-formed foreign-owner collisions
  • Manual isolated bin/fm-teardown.sh --force checks for foreign branch refusal, own-branch cleanup, reassigned-slot preservation, and unreadable-record refusal
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

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