Repository navigation
fix(bin): harden pooled worktree ownership checks - #4468
Open
jericho1050 wants to merge 11 commits into
Open
jericho1050 wants to merge 11 commits into
jericho1050 wants to merge 11 commits into
Conversation
This was referenced Sep 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
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
fm/<task>branch, while preserving reassigned-slot and own-branch cleanup.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.
Evidence: Spawn ownership CLI evidence
Source: Spawn ownership CLI evidence
Evidence: Teardown ownership CLI evidence
Source: Teardown ownership CLI evidence
Evidence: Spawn-pool focused suite transcript
Source: Spawn-pool focused suite transcript
Evidence: Teardown-safety focused suite transcript
Source: Teardown-safety focused suite transcript
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 retainssame.metaand atask=same, home=<child>claim, a parent-home spawn namedsameskips the record here and accepts the claim as its own at line 1380 because ownership comparison ignoreshome; 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 throughpreflight_descendant_treehouse_slots, where an absent/own claim passes, andcleanup_firstmate_home_childrenthen returns the pooled copy without checking its branch. A legacy child record naming a copy now onfm/other-taskcan 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 recordedhome=with the caller’s raw path spelling. Absolute symlink-spelledFM_HOMEvalues are explicitly supported and preserved, so a task spawned through/home-linkand 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-spelledFM_HOME=/h-link, the collector seeds/h-link/stateand then adds the canonical/h/statebecause it compares raw strings. Teardown consequently scans the same<id>.metatwice, treats its second spelling as another record atfm-teardown.sh:2122, and refuses an uncontestedfm/<id>worktree. Canonicalize state or metadata identities before deduplication/comparison; the added alias test misses this becauserun_caseinvokes teardown through the physical home.bin/fm-teardown.sh:2734- The required behavior says teardown refuses when a recorded copy is checked out on anotherfm/<task>branch, but the descendant branch guard is reached only for recognized pooled, non-Orca worktrees. During forced secondmate retirement, an Orca child onfm/other-taskis 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 ... || truetreats both a detached HEAD (exit 1) and an inspection failure (exit 128) as an empty branch. If a copy onfm/other-taskhas 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 fullrefs/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 retainsx.metaand home B now owns the slot as taskx, teardown in A classifies the claim as reassigned here, then can reap B's shared/tmp/fm-xprocesses (line 3296), kill its reusedfirstmate:fm-xendpoint (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 nestedsecondmates.mdcannot 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\ntext, producing one malformedtask=line with nohome=. 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’sstate/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.gitexistence check converts an inspection failure into permission to continue. If a copy onfm/other-taskhas a transient.gitstat failure, forced cleanup skips the branch guard; if access recovers afterward, later cleanup can reset and return the copy. Remove this precheck and letgit symbolic-refdistinguish 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_getdeliberately converts read failures to empty values, so teardown misses bothworktree=andhome=here; with an absent legacy claim and detached HEAD,--forcethen 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_stateis pre-seeded but never receives the enumeration check applied to each discovered<home>/state. WithFM_STATE_OVERRIDE=/alt/stateon a write/search-only directory, known task paths and new writes remain usable while*.metacannot 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 explicitrecord_statebefore adding it to the shared scan.🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
git status --shortgit diff --stat 18fe6e90ce71fda14d1a7f4782a66efe0b7b07ca..8420b35aaf7ba6aae2b80b9857fcc652cc067e9dFM_TEST_EVIDENCE=1 bash tests/fm-spawn-pool-base-freshen.test.shFM_TEST_EVIDENCE=1 bash tests/fm-teardown-endpoint-safety.test.shManual isolatedbin/fm-spawn.shchecks for retained-record and well-formed foreign-owner collisionsManual isolatedbin/fm-teardown.sh --forcechecks 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.