Skip to content

feat(bin): support per-project worktree acquisition commands - #2411

Open
Parsunic wants to merge 7 commits into
kunchenguid:mainfrom
Parsunic:fm/fm-mathpath-worktree-command
Open

feat(bin): support per-project worktree acquisition commands#2411
Parsunic wants to merge 7 commits into
kunchenguid:mainfrom
Parsunic:fm/fm-mathpath-worktree-command

Conversation

@Parsunic

Copy link
Copy Markdown

Intent

Teach Firstmate to support an optional local, per-project worktree acquisition command so Mathpath can use its own prepared-worktree script instead of Treehouse. The local configuration must follow existing conventions and be documented with a task-slug placeholder; the accepted implementation uses gitignored config/worktree-acquire/, containing exactly one non-empty shell command line with literal . Mathpath's effective command must be representable exactly as scripts/worktree-new.sh && cd ../mathpath-worktrees/, because its script creates but does not enter the directory. Treat this command as trusted local operator configuration, but substitute every occurrence of only the already-validated task slug using shell-safe quoting so task input cannot become shell syntax. Do not put the captain's private effective configuration or private absolute paths into tracked examples; report after merge that config/worktree-acquire/mathpath should contain scripts/worktree-new.sh && cd ../mathpath-worktrees/.

When this config is absent, preserve the existing Treehouse acquisition path with byte-for-byte-equivalent outcomes. Preserve the isolation assertion, two consecutive identical working-directory reads, 60 one-second polls, base freshness, failure preservation, and cleanup safety; do not weaken or simplify them. A configured command that fails, including a retry where Mathpath's target directory already exists, must surface its actionable status promptly rather than hanging until the poll timeout, publish no successful task record, preserve every existing or partially created target and all unlanded work, and never fall back to Treehouse. A successful custom acquisition must retain provider identity so guarded cleanup performs all ordinary dirty, landed-work, process, endpoint, and path-identity checks, verifies that the exact recorded worktree remains registered to the exact recorded project, then uses native Git worktree removal rather than asking Treehouse to manage a worktree it did not create.

Freshen every fresh ship/scout worktree by fetching origin and resetting content to the fetched remote default tip while preserving the acquired task branch's exact attached/detached identity: it must not check out the default branch, detach an attached task branch, or erase task-branch identity. Control-plane relaunch must reuse the recorded worktree and provider, must not rerun fresh acquisition, and must preserve the worktree on failure. Apply the shared acquisition path to every currently participating session-provider runtime (tmux, Herdr, Zellij, and cmux), while keeping Orca excluded because it owns acquisition and cleanup and keeping secondmate launch excluded because it uses its seeded home; do not redesign Treehouse globally.

Add executable-interface regression coverage using existing test patterns for safe placeholder substitution, successful creator-plus-cd acquisition, attached task-branch preservation, two-read settling, prompt existing-target refusal and preservation, malformed configuration refusal, relaunch reuse without reacquisition, and guarded provider-aware cleanup. Update docs/configuration.md as schema owner, owning script headers and help as exact mechanics owners, architecture/backend/operator docs, README, and relevant recovery/runtime guidance without bloating AGENTS.md or creating static source-grep proxy tests. Run the relevant tests, bin/fm-doc-audience-check.sh, bin/fm-lint.sh with pinned ShellCheck 0.11.0, repository-required checks, and fm-ensure-agents-md.sh.

Prove behavior end to end with one real throwaway Mathpath task using /home/parsu/firstmate/projects/mathpath/scripts/worktree-new.sh from a disposable test home. Confirm the resulting /home/parsu/firstmate/projects/mathpath-worktrees/ location, attached task branch at the refreshed origin/main tip, .env.local presence without reading or printing its contents, and node_modules as the expected link. Clean up only the artifacts created by this proof through guarded paths; leave Mathpath's primary copy clean on main. Do not edit Mathpath. Mathpath's current full test command has an independently recorded baseline failure involving environment loading and one curriculum audit record; do not attribute it to this Firstmate change.

Three unrelated Firstmate broad-suite failures were also observed and must not be attributed to or changed by this task. A clean detached origin/main control at commit 6789876 reproduced all three with exit 1 and the same exact assertions: tests/fm-calm-pi-extension.test.sh reported “Pi follow-up absent case rendered a duplicate captain answer”; tests/fm-backend-tmux-smoke.test.sh reported “the tmux task shell did not become ready”; tests/fm-tmux-agent-liveness.test.sh reported “a running harness-named foreground process must classify alive”. The branch broad run showed those same baseline failures; do not change unrelated Calm or tmux behavior to address them. Exact clean-control logs are in /tmp/fm-fm-mathpath-worktree-command/control-logs for this validation session.

Later accepted corrections remain part of this task. Forced secondmate child cleanup must read worktree_provider from each child record and route project-command children through the existing Git-aware registered-worktree removal path, while preserving the Treehouse and Orca paths, adding focused executable regression coverage, and not broadening the feature. For project-command acquisition on Herdr, use a positive marker-based completion and path probe that does not depend on foreground_cwd after a top-level shell cd; preserve Treehouse, Zellij, cmux, Orca, secondmate exclusions, the 60-second timeout, two stable path reads, isolation proof, and existing failure cleanup, with focused executable regression tests. Do not drive live Herdr lifecycle from this task because its brief was not scaffolded as a guarded Herdr lab.

What Changed

  • bin/fm-spawn.sh reads an optional local config/worktree-acquire/<project-name> file (exactly one non-empty command line containing a literal <slug>) and runs it in the task pane instead of treehouse get for fresh ship/scout spawns on tmux, herdr, zellij, and cmux; every <slug> is replaced with a shell-quoted form of the validated task id, the command's exit status is published atomically so a failure (including an already-existing target) is reported without waiting out the 60s poll, and the existing isolation assertion, two-identical-cwd-read settling, and failure preservation are unchanged. Relaunch, secondmate, and Orca spawns keep their existing paths, and base freshening now asserts the worktree's attached/detached branch identity is unchanged by the reset --hard to the fetched remote default tip.
  • Successful custom acquisition records worktree_provider=project-command in task metadata, which bin/fm-teardown.sh uses to run the full dirty/landed/process/endpoint checks, verify the recorded worktree is still registered to the recorded project, and then remove it via git worktree remove --force instead of treehouse return; forced secondmate child cleanup reads the same field per child, preflights it non-destructively, and refuses unknown or misplaced provider values while records stay intact. bin/backends/herdr.sh gains a marker-based active pwd probe for this provider, since foreground_cwd is stale once the command's cd runs in the pane's own top-level shell.
  • Adds executable regression coverage in tests/fm-spawn-worktree-settle.test.sh, tests/fm-teardown.test.sh, tests/fm-backend-herdr.test.sh, tests/fm-control-relaunch.test.sh, and tests/fm-spawn-pool-base-freshen.test.sh; documents the new config file in docs/configuration.md (schema owner), AGENTS.md, README.md, and the architecture/backend/skill docs.

Risk Assessment

⚠️ Medium: The change is opt-in behind a gitignored per-project config and leaves the Treehouse default path control-flow-equivalent, with fail-closed refusals on every unknown provider value and thorough executable-interface coverage, but it touches spawn and teardown lifecycle for all four session backends and adds a new active herdr pane probe that intent explicitly barred from live verification, so it is safe to merge with the two informational notes as follow-ups.

Testing

Ran the five suites the change touches — all pass, including every newly added executable-interface case (safe slug substitution, creator-plus-cd acquisition, two-read settling, no cwd probe before the acquisition publishes completion, malformed-config and existing-target refusals, relaunch reuse without reacquisition, Herdr marker-probe reads, and provider-aware guarded teardown incl. the forced secondmate child path and unknown-provider preflight refusal). Beyond unit coverage I drove the real product surface: real fm-spawn.sh and fm-teardown.sh against a real tmux server with a disposable FM_HOME and Mathpath's own unmodified worktree-new.sh, capturing the actual task-window transcript. The surface here is a terminal, so the reviewer-visible artifact is a real tmux pane capture rather than a screenshot — no GUI is involved. It shows the shell-quoted slug at each placeholder, Mathpath's script creating and entering ../mathpath-worktrees/<slug>, the attached task branch sitting at the freshened origin/main tip, .env.local present without its contents being read, node_modules symlinked to the primary, a prompt 1-second actionable refusal (vs the 60s timeout) on an already-existing target that preserved unlanded work and published nothing, and guarded native-Git teardown with Treehouse never invoked. Lint, doc-audience, and AGENTS.md checks were left to their owning phases per this phase's rules, and the three recorded baseline suite failures were neither rerun nor touched. The /tmp sandbox and tmux server were deleted afterwards; the repo worktree is clean and Mathpath's real checkout was never written to.

Evidence: End-to-end proof: config, task-window transcript, resulting state, prompt refusal, guarded cleanup

Source: End-to-end proof: config, task-window transcript, resulting state, prompt refusal, guarded cleanup

# Project worktree acquisition — end-to-end proof

Real `bin/fm-spawn.sh` and `bin/fm-teardown.sh`, a real tmux server on a private
socket, a disposable `FM_HOME`, and Mathpath's own unmodified
`scripts/worktree-new.sh` + `scripts/sync-main.sh` (copied read-only out of
/home/parsu/firstmate/projects/mathpath) driving a disposable clone named `mathpath`
under /tmp. Mathpath's real checkout was never written to.
`origin/main` was advanced after the primary clone, so the primary starts stale and
the task worktree can only reach the current tip by being freshened.

## The local config — gitignored, one line, literal `<slug>`

`` `
$ cat "$FM_HOME/config/worktree-acquire/mathpath"
scripts/worktree-new.sh <slug> && cd ../mathpath-worktrees/<slug>
`` `

## 1. Fresh ship spawn

`` `
$ fm-spawn.sh fm-worktree-proof-c3 <project> --mode local-only --yolo off
spawned fm-worktree-proof-c3 harness=codex kind=ship mode=local-only yolo=off window=firstmate:fm-fm-worktree-proof-c3 worktree=/tmp/fm-e2e-mathpath-worktree/mathpath-worktrees/fm-worktree-proof-c3
`` `

The task window itself (real tmux `capture-pane`): the already-validated slug is
single-quoted at every placeholder, and Mathpath's own script runs and reports.

`` `
__fm_worktree_acquire() { scripts/worktree-new.sh 'fm-worktree-proof-c3' && cd ../mathpath-worktrees/'fm-worktree-proof-c3'; }; if __fm_worktree_acquire; then __fm_worktree_acquire_rc=0; else __fm_worktree_acquire_rc=$?; fi; unset -f __fm_worktree_acquire; printf '%s\n' "$__fm_worktree_acquire_rc" > '/tmp/fm-e2e-mathpath-worktree/fm-home/state/.worktree-acquire-fm-worktree-proof-c3.1350875.status.partial' && mv -f -- '/tmp/fm-e2e-mathpath-worktree/fm-home/state/.worktree-acquire-fm-worktree-proof-c3.1350875.status.partial' '/tmp/fm-e2e-mathpath-worktree/fm-home/state/.worktree-acquire-fm-worktree-proof-c3.1350875.status'; unset __fm_worktree_acquire_rc
parsu@LAPTOP-67VJT5A2:/tmp/fm-e2e-mathpath-worktree/mathpath$ __fm_worktree_acquire() { scripts/worktree-new.sh 'fm-worktree-proof-c3' && cd ../mathpath-worktrees/'fm-worktree-proof-c3'; }; if __fm_worktree_acquire; then __fm_worktree_acquire_rc=0; else __fm_worktree_acquire_rc=$?; fi; unset -f __fm_worktree_acquire; printf '%s\n' "$__fm_worktree_acquire_rc" > '/tmp/fm-e2e-mathpath-worktree/fm-home/state/.worktree-acquire-fm-worktree-proof-c3.1350875.status.partial' && mv -f -- '/tmp/fm-e2e-mathpath-worktree/fm-home/state/.worktree-acquire-fm-worktree-proof-c3.1350875.status.partial' '/tmp/fm-e2e-mathpath-worktree/fm-home/state/.worktree-acquire-fm-worktree-proof-c3.1350875.status'; unset __fm_worktree_acquire_rc
  local main fast-forwarded to 9738c17
Preparing worktree (new branch 'fm-worktree-proof-c3')
branch 'fm-worktree-proof-c3' set up to track 'origin/main'.
HEAD is now at 9738c17 advance origin/main after the primary clone
  copied .env.local
  linked node_modules -> primary checkout
✓ worktree ready: /tmp/fm-e2e-mathpath-worktree/mathpath-worktrees/fm-worktree-proof-c3
  branch 'fm-worktree-proof-c3' based on origin/main
  cd "/tmp/fm-e2e-mathpath-worktree/mathpath-worktrees/fm-worktree-proof-c3"
  npm run dev -- -p 3000      # run this branch alongside your other instance(s)
  # or open a new Claude session in this directory
  note: shares the primary's node_modules + .env (same DATABASE_URL/DB).
        if this branch changes deps: rm node_modules && npm install
parsu@LAPTOP-67VJT5A2:/tmp/fm-e2e-mathpath-worktree/mathpath-worktrees/fm-worktree-proof-c3$ export GOTMPDIR=/tmp/fm-fm-worktree-proof-c3/gotmp
parsu@LAPTOP-67VJT5A2:/tmp/fm-e2e-mathpath-worktree/mathpath-worktrees/fm-worktree-proof-c3$ env -u CURSOR_AGENT -u CURSOR_INVOKED_AS codex --dangerously-bypass-approvals-and-sandbox -c "notify=[\"bash\",\"-c\",\"touch '/tmp/fm-e2e-mathpath-worktree/fm-home/state/fm-worktree-proof-c3.turn-ended'\"]" "$('/home/parsu/.no-mistakes/worktrees/3ec255a4fab4/01M01HXJXW8V3T2RJCV8FVZ3N0/bin/fm-operational-input.sh' encode launch-brief < '/tmp/fm-e2e-mathpath-worktree/fm-home/data/fm-worktree-proof-c3/brief.md')"
`` `

## 2. Resulting state

`` `
=== state/fm-worktree-proof-c3.meta (task record) ===
window=firstmate:fm-fm-worktree-proof-c3
worktree=/tmp/fm-e2e-mathpath-worktree/mathpath-worktrees/fm-worktree-proof-c3
project=/tmp/fm-e2e-mathpath-worktree/mathpath
worktree_provider=project-command
harness=codex
kind=ship
mode=local-only

=== git registration in the primary checkout ===
/tmp/fm-e2e-mathpath-worktree/mathpath                                9738c17 [main]
/tmp/fm-e2e-mathpath-worktree/mathpath-worktrees/fm-worktree-proof-b2 9738c17 [fm-worktree-proof-b2]
/tmp/fm-e2e-mathpath-worktree/mathpath-worktrees/fm-worktree-proof-c3 9738c17 [fm-worktree-proof-c3]

=== task branch identity and freshness ===
attached branch:       fm-worktree-proof-c3
worktree HEAD:         9738c1711f90255e7b8896104a4fe68ac87fece4
fetched origin/main:   9738c1711f90255e7b8896104a4fe68ac87fece4
detached:              no
working tree changes:  0

=== gitignored local material carried in by Mathpath's script ===
.env.local present:    yes   (contents never read or printed)
node_modules:          symlink -> /tmp/fm-e2e-mathpath-worktree/mathpath/node_modules

=== Treehouse never involved ===
treehouse invocations: 0
=== private acquisition-status artifacts left behind ===
leftovers:             0
`` `

## 3. A retry whose target already exists

A separate task whose target directory was pre-created with unlanded work.

`` `
=== fm-spawn output on a retry whose target already exists (exit 1 after 1s, poll timeout is 60s) ===
error: project worktree acquisition for '/tmp/fm-e2e-mathpath-worktree/mathpath' exited with status 1 before entering an isolated worktree; any existing or partly-created target is preserved. Inspect window firstmate:fm-fm-worktree-proof-b2, land or deliberately remove that target, then retry

=== nothing was published, nothing was destroyed ===
task record published:  no (none)
existing target kept:   yes
unlanded work intact:   unlanded work that must survive the refusal
treehouse fallback:     0 invocations
status artifacts left:  0
`` `

## 4. Guarded, provider-aware cleanup

`` `
$ fm-teardown.sh fm-worktree-proof-c3
teardown: reaping leaked worktree process(es) for fm-worktree-proof-c3: 1350971 1351919
teardown: force-killing leaked worktree process(es) for fm-worktree-proof-c3: 1350971
teardown fm-worktree-proof-c3 complete (window firstmate:fm-fm-worktree-proof-c3, worktree /tmp/fm-e2e-mathpath-worktree/mathpath-worktrees/fm-worktree-proof-c3)

=== after guarded teardown of the project-command worktree ===
worktree directory:    removed
task record:           removed
treehouse invocations: 0   (native Git removal; Treehouse never asked)
remaining registrations in the primary checkout:
/tmp/fm-e2e-mathpath-worktree/mathpath                                9738c17 [main]
/tmp/fm-e2e-mathpath-worktree/mathpath-worktrees/fm-worktree-proof-b2 9738c17 [fm-worktree-proof-b2]
primary checkout:      ## main...origin/main
`` `

_(`fm-worktree-proof-b2` is the deliberately-preserved refusal fixture from step 3;
the entire /tmp sandbox was deleted after this evidence was captured.)_
Evidence: Raw tmux capture-pane transcript of the crewmate task window during acquisition

Source: Raw tmux capture-pane transcript of the crewmate task window during acquisition

__fm_worktree_acquire() { scripts/worktree-new.sh 'fm-worktree-proof-c3' && cd ../mathpath-worktrees/'fm-worktree-proof-c3'; }; if __fm_worktree_acquire; then __fm_worktree_acquire_rc=0; else __fm_worktree_acquire_rc=$?; fi; unset -f __fm_worktree_acquire; printf '%s\n' "$__fm_worktree_acquire_rc" > '/tmp/fm-e2e-mathpath-worktree/fm-home/state/.worktree-acquire-fm-worktree-proof-c3.1350875.status.partial' && mv -f -- '/tmp/fm-e2e-mathpath-worktree/fm-home/state/.worktree-acquire-fm-worktree-proof-c3.1350875.status.partial' '/tmp/fm-e2e-mathpath-worktree/fm-home/state/.worktree-acquire-fm-worktree-proof-c3.1350875.status'; unset __fm_worktree_acquire_rc
parsu@LAPTOP-67VJT5A2:/tmp/fm-e2e-mathpath-worktree/mathpath$ __fm_worktree_acquire() { scripts/worktree-new.sh 'fm-worktree-proof-c3' && cd ../mathpath-worktrees/'fm-worktree-proof-c3'; }; if __fm_worktree_acquire; then __fm_worktree_acquire_rc=0; else __fm_worktree_acquire_rc=$?; fi; unset -f __fm_worktree_acquire; printf '%s\n' "$__fm_worktree_acquire_rc" > '/tmp/fm-e2e-mathpath-worktree/fm-home/state/.worktree-acquire-fm-worktree-proof-c3.1350875.status.partial' && mv -f -- '/tmp/fm-e2e-mathpath-worktree/fm-home/state/.worktree-acquire-fm-worktree-proof-c3.1350875.status.partial' '/tmp/fm-e2e-mathpath-worktree/fm-home/state/.worktree-acquire-fm-worktree-proof-c3.1350875.status'; unset __fm_worktree_acquire_rc
  local main fast-forwarded to 9738c17
Preparing worktree (new branch 'fm-worktree-proof-c3')
branch 'fm-worktree-proof-c3' set up to track 'origin/main'.
HEAD is now at 9738c17 advance origin/main after the primary clone
  copied .env.local
  linked node_modules -> primary checkout
✓ worktree ready: /tmp/fm-e2e-mathpath-worktree/mathpath-worktrees/fm-worktree-proof-c3
  branch 'fm-worktree-proof-c3' based on origin/main
  cd "/tmp/fm-e2e-mathpath-worktree/mathpath-worktrees/fm-worktree-proof-c3"
  npm run dev -- -p 3000      # run this branch alongside your other instance(s)
  # or open a new Claude session in this directory
  note: shares the primary's node_modules + .env (same DATABASE_URL/DB).
        if this branch changes deps: rm node_modules && npm install
parsu@LAPTOP-67VJT5A2:/tmp/fm-e2e-mathpath-worktree/mathpath-worktrees/fm-worktree-proof-c3$ export GOTMPDIR=/tmp/fm-fm-worktree-proof-c3/gotmp
parsu@LAPTOP-67VJT5A2:/tmp/fm-e2e-mathpath-worktree/mathpath-worktrees/fm-worktree-proof-c3$ env -u CURSOR_AGENT -u CURSOR_INVOKED_AS codex --dangerously-bypass-approvals-and-sandbox -c "notify=[\"bash\",\"-c\",\"touch '/tmp/fm-e2e-mathpath-worktree/fm-home/state/fm-worktree-proof-c3.turn-ended'\"]" "$('/home/parsu/.no-mistakes/worktrees/3ec255a4fab4/01M01HXJXW8V3T2RJCV8FVZ3N0/bin/fm-operational-input.sh' encode launch-brief < '/tmp/fm-e2e-mathpath-worktree/fm-home/data/fm-worktree-proof-c3/brief.md')"
Evidence: Task window: the effective Mathpath command as it actually ran
$ cat "$FM_HOME/config/worktree-acquire/mathpath"
scripts/worktree-new.sh <slug> && cd ../mathpath-worktrees/<slug>

--- tmux capture-pane of firstmate:fm-fm-worktree-proof-c3 ---
__fm_worktree_acquire() { scripts/worktree-new.sh 'fm-worktree-proof-c3' && cd ../mathpath-worktrees/'fm-worktree-proof-c3'; }; ...
local main fast-forwarded to 9738c17
Preparing worktree (new branch 'fm-worktree-proof-c3')
branch 'fm-worktree-proof-c3' set up to track 'origin/main'.
HEAD is now at 9738c17 advance origin/main after the primary clone
copied .env.local
linked node_modules -> primary checkout
✓ worktree ready: .../mathpath-worktrees/fm-worktree-proof-c3
branch 'fm-worktree-proof-c3' based on origin/main
Evidence: Resulting task record and worktree identity
worktree=.../mathpath-worktrees/fm-worktree-proof-c3
worktree_provider=project-command

attached branch: fm-worktree-proof-c3
worktree HEAD: 9738c1711f90255e7b8896104a4fe68ac87fece4
fetched origin/main: 9738c1711f90255e7b8896104a4fe68ac87fece4
detached: no
working tree changes: 0
.env.local present: yes (contents never read or printed)
node_modules: symlink -> .../mathpath/node_modules
treehouse invocations: 0
Evidence: Retry onto an existing target: prompt actionable refusal, nothing destroyed
=== exit 1 after 1s (poll timeout is 60s) ===
error: project worktree acquisition for '.../mathpath' exited with status 1 before entering an isolated worktree; any existing or partly-created target is preserved. Inspect window firstmate:fm-fm-worktree-proof-b2, land or deliberately remove that target, then retry

task record published: no (none)
existing target kept: yes
unlanded work intact: unlanded work that must survive the refusal
treehouse fallback: 0 invocations
Evidence: Guarded provider-aware teardown
$ fm-teardown.sh fm-worktree-proof-c3
teardown fm-worktree-proof-c3 complete (window firstmate:fm-fm-worktree-proof-c3, worktree .../mathpath-worktrees/fm-worktree-proof-c3)

worktree directory: removed
task record: removed
treehouse invocations: 0 (native Git removal; Treehouse never asked)
primary checkout: ## main...origin/main
- Outcome: ⚠️ 1 warning across 1 run (8m20s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ⚠️ bin/fm-spawn.sh:2371 - The worktree-discovery poll reads the pane path on every iteration even when the project command has not reported completion, then throws the value away (line 2372 resets candidate unconditionally while acquire_complete != 1). For herdr, zellij, and cmux that read is an ACTIVE probe: fm_backend_herdr_probe_current_path / fm_backend_zellij_current_path call send_text_line, which for herdr is pane run (types the text AND submits it). So while the operator's trusted acquisition command is still running in the pane's foreground, up to ~60 lines of printf &#39;%s\n&#39; &#39;__FM_..._CWD_BEGIN__&#39;; pwd; printf ... are typed into that running process's stdin, plus 60 extra backend CLI round-trips and 0.3s sleeps, purely to compute a discarded value. Unlike treehouse get (firstmate-owned, non-interactive), a project-local creator script is arbitrary operator code that may read stdin. Fix: when WORKTREE_PROVIDER = project-command and acquire_complete -ne 1, reset candidate and sleep 1; continue before calling spawn_current_path, so no probe is issued until the status file proves the command finished.
  • ⚠️ bin/fm-teardown.sh:2233 - The new worktree_provider validation for secondmate children (unsupported value, or project-command on an orca/secondmate child) runs only inside cleanup_firstmate_home_children, which is the destructive loop. The file's established invariant is that every refusal is discovered before anything is destroyed: validate_firstmate_home_children_removal (fm-teardown.sh:2054, called at 2355/2357) and preflight_firstmate_home_herdr_children (2361) both enumerate the same children for exactly that reason, and the preflight's validate_child_worktree_for_removal already covers the missing-project and unregistered-worktree cases the new code re-checks at 2298/2302. The provider-value check is the one that is not mirrored, so a home whose second child records an unknown provider (a forward-version or hand-edited record) has its first child's pane killed, worktree returned, and records removed before teardown refuses and stops. Fix: read worktree_provider in validate_firstmate_home_children_removal and apply the same case refusal there.
  • ℹ️ bin/fm-spawn.sh:2390 - On the 60s acquisition timeout, fm-spawn exits and spawn_abort_cleanup (813-816) deletes the status file, but the project command is still running in the pane and will later recreate state/.worktree-acquire-&lt;id&gt;.&lt;pid&gt;.status itself. Nothing ever removes that late-written file; it is a dotfile so it does not match the state/*.meta globs other tooling uses, making this harmless clutter rather than a correctness problem. If you want it bounded, sweep &#34;$STATE&#34;/.worktree-acquire-$ID.*.status* at line 2320 alongside the existing rm -f of the current pair (safe: concurrent same-id spawns are already excluded by SPAWN_TASK_LOCK).

🔧 Fix: gate acquisition cwd probe on published status; preflight child provider
2 infos still open:

  • ℹ️ bin/fm-teardown.sh:2319 - Forced secondmate cleanup of a project-command child calls teardown_project_worktree_remove without first detaching and deleting the child's task branch, unlike the top-level path (fm-teardown.sh:2513-2518) which does git checkout --detach + git branch -D under the stated goal "drop the local task branch so the shared repo does not accumulate refs". Native Git removal deletes the child worktree outright, so the &lt;slug&gt; ref survives pointing at a worktree that no longer exists. This is not a regression - the sibling Treehouse child branch at 2325 never dropped the ref either, and treehouse return's "clean, reset" leaves the pooled worktree in place - so project-command children inherit the child path's pre-existing shape rather than introducing new leakage. Noting it because the top-level and child paths now state opposite policies for the same provider.
  • ℹ️ bin/fm-teardown.sh:2071 - The worktree_provider schema case now exists three times with divergent wording: the top-level task at 457 ("refusing cleanup"), the non-destructive preflight at 2071 ("forced teardown changed nothing"), and the destructive child loop at 2251 ("retaining that child's durable identity records"). The duplication at 2071/2251 is deliberate defence-in-depth per the accepted correction, and the messages are intentionally distinct, so this is not a demand for consolidation - only a note that adding a future provider value requires editing all three sites, and missing one makes preflight and cleanup disagree about what is acceptable. A shared helper taking a message prefix would preserve both the defensive re-check and the distinct wording.
⚠️ **Test** - 1 warning
  • ⚠️ The end-to-end proof was run against a disposable /tmp clone named mathpath driven by Mathpath's own unmodified scripts/worktree-new.sh + scripts/sync-main.sh (copied read-only), not against /home/parsu/firstmate/projects/mathpath itself. This test phase is bound to a worktree-only write boundary, and a live Mathpath run would create /home/parsu/firstmate/projects/mathpath-worktrees/<slug> and fast-forward Mathpath's primary main — writes outside the assigned worktree. Everything the intent asks to confirm (target path shape ../mathpath-worktrees/<slug>, attached task branch at the refreshed origin/main tip, .env.local present without reading it, node_modules symlink to the primary) was confirmed on the sandbox with the real scripts, real tmux, real fm-spawn.sh and real fm-teardown.sh. The captain should decide whether they additionally want the literal throwaway task run inside their real Mathpath checkout.
  • bin/fm-test-run.sh tests/fm-spawn-worktree-settle.test.sh (6 cases, incl. safe <slug> substitution, creator+cd acquisition, two-read settling, no cwd read before completion is published, malformed-config refusal, existing-target refusal)
  • bin/fm-test-run.sh tests/fm-spawn-pool-base-freshen.test.sh tests/fm-control-relaunch.test.sh (attached-task-branch freshening; relaunch reuses the recorded worktree/provider without rerunning acquisition)
  • bin/fm-test-run.sh tests/fm-teardown.test.sh tests/fm-backend-herdr.test.sh (project-command guarded native-Git cleanup, forced secondmate project-command child routing, unknown-child-provider preflight refusal, Herdr marked-probe cwd read and unrendered-probe empty read)
  • Manual E2E: real bin/fm-spawn.sh &lt;id&gt; &lt;project&gt; --mode local-only --yolo off on a real tmux server (private socket), disposable FM_HOME with config/worktree-acquire/mathpath = scripts/worktree-new.sh &lt;slug&gt; &amp;&amp; cd ../mathpath-worktrees/&lt;slug&gt;, running Mathpath's unmodified scripts/worktree-new.sh against a disposable clone with origin/main advanced after the primary clone
  • Manual E2E evidence capture: tmux capture-pane of the task window, state/&lt;id&gt;.meta, git worktree list, git symbolic-ref --short HEAD vs git rev-parse origin/main, test -f .env.local (contents never read), readlink node_modules, treehouse tripwire log
  • Manual E2E failure path: second spawn onto a pre-created target holding unlanded work — timed refusal (1s vs the 60s poll timeout), no meta published, target and work intact, zero Treehouse calls
  • Manual E2E cleanup: real bin/fm-teardown.sh &lt;id&gt; — worktree removed and unregistered via native Git, task record retired, Treehouse never invoked, primary checkout left clean on main
⚠️ **Document** - 1 info
  • ℹ️ docs/configuration.md:108 - Operator action that deliberately cannot live in tracked documentation: per the change's own intent, the captain's effective local config file config/worktree-acquire/mathpath must be created after merge containing scripts/worktree-new.sh &lt;slug&gt; &amp;&amp; cd ../mathpath-worktrees/&lt;slug&gt;. Tracked docs intentionally carry only the generic path/to/create-worktree &lt;slug&gt; &amp;&amp; cd path/to/worktrees/&lt;slug&gt; placeholder shape, so this remains an out-of-repo step.
⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 127)
✅ **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