Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
3903d7e
Refuse stale worktree ownership claims
jleemcf Aug 27, 2026
c27e4e2
no-mistakes(review): prove worktree ownership without deadlocking rec…
jleemcf Aug 27, 2026
f112e56
no-mistakes(review): make ownership proof jq-optional, marker-backed,…
jleemcf Aug 27, 2026
e506655
no-mistakes(review): lease crewmate slots at acquisition; narrow bran…
jleemcf Aug 27, 2026
48ea86c
Remove out-of-scope crewmate leases
jleemcf Aug 27, 2026
6f46d7a
no-mistakes(review): publish orca path match separately; fix owner-ma…
jleemcf Aug 28, 2026
8021909
no-mistakes(review): keep claim retirement recoverable until the reco…
jleemcf Aug 28, 2026
6c14e30
no-mistakes(review): retire released worktrees by receipt, never by r…
jleemcf Aug 28, 2026
50174c9
no-mistakes(review): bind retirement receipts to record identity and …
jleemcf Aug 28, 2026
950cbf3
no-mistakes(review): keep retired secondmate reruns off the reissued …
jleemcf Aug 28, 2026
b1c2983
no-mistakes(review): keep child-home traversal off returned secondmat…
jleemcf Aug 28, 2026
bceb1d2
no-mistakes(document): document worktree ownership proof and owner-ma…
jleemcf Aug 28, 2026
bfc604d
no-mistakes(lint): replace ls|head claim-backup lookup with glob array
jleemcf Aug 28, 2026
3a27be9
no-mistakes(review): quarantine unrecorded-release evidence out of re…
jleemcf Aug 28, 2026
a50f468
no-mistakes(review): drop the exact task branch on receipt-backed tea…
jleemcf Aug 28, 2026
1f56e6e
no-mistakes(review): gate task-branch drop on release; record ownersh…
jleemcf Aug 28, 2026
e076f2a
no-mistakes(review): key pending ownership by generation; report reta…
jleemcf Aug 28, 2026
a73b281
no-mistakes(review): prove interrupted marker restamps by exact gener…
jleemcf Aug 28, 2026
ae480a6
no-mistakes(document): correct worktree ownership record docs for res…
jleemcf Aug 28, 2026
8b5aa86
fix(spawn): refuse same-id markers on fresh handout
jleemcf Aug 31, 2026
2048018
no-mistakes(review): refuse absent marker-era owner markers; keep rec…
jleemcf Aug 31, 2026
a309fd4
no-mistakes(review): prove task-branch containment before dropping; f…
jleemcf Aug 31, 2026
d177707
no-mistakes(review): refuse ambiguous spawn generations instead of pr…
jleemcf Aug 31, 2026
94bf05b
no-mistakes(review): restore quarantine symlink test byte-for-byte wi…
jleemcf Aug 31, 2026
4bc363a
no-mistakes(review): sweep stale marker backups; dedup child rc4 rele…
jleemcf Aug 31, 2026
80a1af7
fix(worktree): distinguish marker-aware task records
jleemcf Sep 1, 2026
711eb4f
no-mistakes(review): keep and name both halves of retirement recovery…
jleemcf Sep 1, 2026
44afe7f
no-mistakes(review): retire reissued-slot claims as non-restorable ev…
jleemcf Sep 1, 2026
af7cc26
no-mistakes(review): apply moved-on retirement in abandon; fix retire…
jleemcf Sep 1, 2026
5faa283
no-mistakes(review): never leave a half-restored claim; map ownership…
jleemcf Sep 1, 2026
de3b456
no-mistakes(review): cover pre-marker record rollback restoring its c…
jleemcf Sep 1, 2026
4b9ca94
no-mistakes(document): correct spawn, teardown, and retirement-restor…
jleemcf Sep 1, 2026
c64682c
fix(teardown): park interrupted worktree retirement
jleemcf Sep 1, 2026
8c22132
no-mistakes(review): refuse destructive child-return retry; fix aware…
jleemcf Sep 1, 2026
12d3e2b
no-mistakes(review): correct stale retirement-contract docs and child…
jleemcf Sep 1, 2026
882365f
no-mistakes(document): correct parked-retirement wording in teardown …
jleemcf Sep 1, 2026
0db0647
fix(lint): remove retired marker scratch state
jleemcf Sep 2, 2026
78b59f4
no-mistakes(review): retire only owner markers bound to the record
jleemcf Sep 2, 2026
003929d
no-mistakes(review): warn that a left foreign marker refuses the next…
jleemcf Sep 2, 2026
fe00678
no-mistakes(review): refuse retirement over a foreign owner marker
jleemcf Sep 2, 2026
78f93cd
no-mistakes(review): refuse every unprovable owner marker entry
jleemcf Sep 2, 2026
c40785c
no-mistakes(review): name unattributable marker entries in the recove…
jleemcf Sep 2, 2026
1c88653
no-mistakes(review): check marker admissibility before stripping the …
jleemcf Sep 2, 2026
84b04f9
no-mistakes(review): correct skill doc on preflight marker refusal
jleemcf Sep 2, 2026
e2bd383
no-mistakes(review): report marker refusal state per caller
jleemcf Sep 2, 2026
1313f53
no-mistakes(test): fix test fixtures for ownership proof and host ign…
jleemcf Sep 2, 2026
dd87a73
no-mistakes(document): correct retirement preflight contract in teard…
jleemcf Sep 2, 2026
be8036f
test(spawn): use distinct slots and proved relaunches
jleemcf Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/secondmate-provisioning/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ SSH exit 255 preserves the route and local records because remote completion is
When safe, teardown kills the direct endpoint, removes the `data/secondmates.md` route, clears the main home metadata, and removes the retired secondmate home.
Removing a leased home releases its durable treehouse lease via `treehouse return`, so the pool slot is freed for reuse rather than left leased forever.
A plain-clone home with no pool slot is simply removed.
If `treehouse return` fails for a leased home, teardown stops with state intact rather than raw-removing the directory and hiding a held lease.
If `treehouse return` fails for a leased home, teardown stops rather than raw-removing the directory and hiding a held lease: the home and its records stay, but that home's worktree claim retirement is parked, so a rerun refuses until the manual-recovery drill that refusal prints has been worked through.
Before either return or direct removal, teardown asks the target home's process-event runner to retire its registrations and physically owned machine-wide claims through the safe generation-bound path.
It refuses retirement while that cleanup is uncertain or unavailable, preserving the home and retirement records for a later retry.
Raw deletion is unsupported because a blocking process-event child can outlive its home.
Expand Down
13 changes: 13 additions & 0 deletions .agents/skills/stuck-crewmate-recovery/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ Use `treehouse status` for treehouse-backed tmux, herdr, zellij, or cmux tasks,
Do not sweep another home's endpoints or infer ownership from a matching window label.

Before relaunch, prove that no live agent still owns the recorded task and that the existing worktree remains available.
`bin/fm-control.sh relaunch` and its `bin/fm-spawn.sh --relaunch` delegate enforce the shared ownership proof in `bin/fm-worktree-ownership-lib.sh`; a worktree whose `.fm-task-owner` marker names another task or another spawn generation, a conflicting task claim, a contradicting provider binding, a foreign task branch, or a mismatched secondmate home marker each refuse before either path acts on the recorded worktree.
A record with no `worktree=` line has already had that claim retired, so relaunching it refuses rather than adopting a path the provider may have taken back.
Only an interrupted retirement can leave a recoverable copy of the claim, and the refusal prints one manual-recovery drill naming that copy together with the slot's retired `.fm-task-owner` copy whenever that half was stashed too; put both halves back together or neither, and only after confirming with the provider that the path was never released, because a released slot may already belong to another task and a claim restored without its marker can never prove ownership again.
A slot whose `.fm-task-owner` the record cannot prove is its own - one naming another task or another spawn generation, an unreadable or incomplete one, or an entry that is not a regular file - refuses before any retirement begins: the record keeps its `worktree=` line, no claim or marker copy is written, no drill is printed, and the entry is left byte-for-byte as it is.
Nothing is parked there, so attribute that entry by the rules below and rerun once it is resolved, instead of looking for a preserved copy that was never made.
Every retirement that has already begun and then stops short of a confirmed provider release parks that way instead: nothing is recorded as final on its own, no runtime path restores either half, and the drill stops rather than creating or overwriting another owner's marker.
Until an operator works through that drill, the parked record keeps refusing its own teardown and relaunch, so reconcile it deliberately instead of expecting a rerun to finish the remaining cleanup.
A pool slot refused because its `.fm-task-owner` marker names a task with no record was taken by a spawn that was killed before it published one; the refusal reports whether that spawn's ownership record still stands beside the task records in `state/` and names both files, and clearing them by hand after confirming the slot is idle is the only recovery, because no teardown exists for a task that was never recorded.
A fresh spawn of that same task id refuses while any such record still names a slot that carries that marker, or that the spawn cannot read well enough to tell, naming each unresolved spawn generation and the worktree it took, so resolve the stranded slot first and then spawn the id again.
A record whose recorded worktree no longer carries that marker strands nothing: the spawn reports it as leftover paperwork that is safe to delete and proceeds, so it never wedges the id for good.
The same removal is the recovery when the refusal reports that the marked task's own record has moved on to another generation and another worktree, because that task's teardown retires its marker elsewhere and will never clear this slot.
When the refusal instead reports that the marked task's record does not say clearly enough who owns the slot, ownership is unknown: remove nothing, repair that record or establish with the crew which task is working there first.
A relaunch interrupted between restamping the worktree's marker and advancing its record leaves the marker one generation ahead; that is recorded as a generation handoff and needs no repair, so relaunch or tear the task down normally rather than editing either half by hand.
Preserve its uncommitted changes and commits, keep the same task identity, and resume or relaunch the recorded harness in that existing worktree with the same brief plus a concise progress note.
Do not use a fresh generic spawn while the recorded worktree is unaccounted for, because allocating another worktree can split one task across two copies.
If the worktree or ownership cannot be reconciled safely, leave all state intact and report the task failed or blocked with the conflicting evidence.
Expand Down
52 changes: 48 additions & 4 deletions bin/fm-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,43 @@ fm_backend_endpoint_atom_valid() { # <value>
esac
}

fm_backend_validate_task_endpoint() { # <meta-file> <task-id>
local meta=$1 id=$2 backend_count backend window worktree project binding_count binding
# A worktree-claim retirement (bin/fm-worktree-ownership-lib.sh) is the one way
# a live record loses its worktree identity, and its surviving copy is the only
# remaining record of the path, so name it wherever that loss surfaces. Whether
# the retirement is RECORDED decides the story, not which namespace the copy
# happens to sit in: a recorded retirement - by receipt or by quarantined
# released evidence - means this record holds no path at all and every surviving
# copy is evidence only. An unrecorded retirement parks instead: no runtime path
# restores it, and the same manual drill printed by ownership proof names the
# preserved state and deliberate reconciliation steps.
fm_backend_report_worktree_claim_backup() { # <meta-file>
local meta=$1 backup evidence retired=1
if declare -F fm_worktree_retirement_receipt_present >/dev/null 2>&1 \
&& fm_worktree_retirement_receipt_present "$meta" >/dev/null 2>&1; then
retired=0
fi
if declare -F fm_worktree_released_evidence_hint >/dev/null 2>&1 \
&& evidence=$(fm_worktree_released_evidence_hint "$meta" 2>/dev/null) \
&& [ -n "$evidence" ]; then
echo "This record's worktree was already retired and that path may already belong to another task, so a copy of the record it held was quarantined at $evidence. It is evidence of that retirement, never authority over the path, and must never be restored over the record." >&2
return 0
fi
declare -F fm_worktree_claim_backup_hint >/dev/null 2>&1 || return 0
backup=$(fm_worktree_claim_backup_hint "$meta" 2>/dev/null) || return 0
[ -n "$backup" ] || return 0
if [ "$retired" -eq 0 ]; then
echo "This record's retirement is recorded, so it holds no worktree and the superseded copy at $backup names a path it no longer owns; it is evidence only and must never be restored over the record." >&2
return 0
fi
if declare -F fm_worktree_interrupted_retirement_manual_drill >/dev/null 2>&1; then
echo "An interrupted worktree retirement is parked.$(fm_worktree_interrupted_retirement_manual_drill "$meta")" >&2
else
echo "An interrupted worktree retirement is parked at $backup; automatic restoration is disabled, so preserve the copy and reconcile the provider outcome manually before any lifecycle action." >&2
fi
}

fm_backend_validate_task_endpoint() { # <meta-file> <task-id> [allow-retired]
local meta=$1 id=$2 allow_retired=${3:-} backend_count backend window worktree project binding_count binding
local session pane recorded_session workspace tab terminal worktree_id surface
FM_BACKEND_VALIDATED_BACKEND=
FM_BACKEND_VALIDATED_TARGET=
Expand All @@ -401,8 +436,17 @@ fm_backend_validate_task_endpoint() { # <meta-file> <task-id>
return 1
}
worktree=$(fm_backend_meta_exact_value "$meta" worktree) || {
echo "REFUSED: task $id has a missing, empty, or ambiguous worktree identity; preserving task state." >&2
return 1
# A retired record legitimately identifies no worktree: its provider step
# already released the path. Only cleanup asks to accept that, and it gets
# no path to act on either way.
if [ "$allow_retired" != allow-retired ] \
|| ! declare -F fm_worktree_retirement_receipt_present >/dev/null 2>&1 \
|| ! fm_worktree_retirement_receipt_present "$meta" >/dev/null 2>&1; then
echo "REFUSED: task $id has a missing, empty, or ambiguous worktree identity; preserving task state." >&2
fm_backend_report_worktree_claim_backup "$meta"
return 1
fi
worktree=
}
project=$(fm_backend_meta_exact_value "$meta" project) || {
echo "REFUSED: task $id has a missing, empty, or ambiguous project identity; preserving task state." >&2
Expand Down
6 changes: 6 additions & 0 deletions bin/fm-control.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
# inherits the local copy but none of the conversation; a
# secondmate reconciles its own home's records at startup, so its
# standing charter is never rewritten.
# Before any checkpoint, stop, or worktree mutation, the shared
# ownership resolver proves that no other task claims the path and
# that its provider binding plus task branch or home marker agree.
# Records a durable checkpoint and that note, exits the old agent,
# then delegates the launch to its single owner,
# bin/fm-spawn.sh --relaunch. A failure before publication keeps
Expand Down Expand Up @@ -126,6 +129,8 @@ DATA="${FM_DATA_OVERRIDE:-$FM_HOME/data}"

# shellcheck source=bin/fm-backend.sh
. "$SCRIPT_DIR/fm-backend.sh"
# shellcheck source=bin/fm-worktree-ownership-lib.sh
. "$SCRIPT_DIR/fm-worktree-ownership-lib.sh"
# shellcheck source=bin/fm-busy-lib.sh
. "$SCRIPT_DIR/fm-busy-lib.sh"
# shellcheck source=bin/fm-control-lib.sh
Expand Down Expand Up @@ -785,6 +790,7 @@ do_relaunch() {
local exit_result state note_line
local -a spawn_args

fm_worktree_ownership_prove "$STATE" "$ID" "$META" || return 1
require_state_verified_backend relaunch
resolve_relaunch_profile

Expand Down
Loading