fix(bin): keep firstmate-repo crews out of the firstmate role with a visible instruction overlay - #2889
Open
cedmos wants to merge 12 commits into
Open
fix(bin): keep firstmate-repo crews out of the firstmate role with a visible instruction overlay#2889cedmos wants to merge 12 commits into
cedmos wants to merge 12 commits into
Conversation
Harnesses auto-load AGENTS.md and CLAUDE.md from the worktree as project instructions that outrank the launch brief. When the project is firstmate itself, those files are the firstmate job description, so a crew or scout runs session start, scaffolds a brief, and tries to spawn a worker for its own task. After refreshing a ship or scout worktree, spawn now overlays those auto-loaded files with crewmate instructions and hides the overlay from git status. The committed job stays in HEAD for editing, and the overlay keeps firstmate-coding-guidelines reachable. Session start refuses in the same linked firstmate worktree so a worker that still reaches for it cannot build a ghost home. The regression asserts the auto-load corpus no longer contains firstmate identity, not merely that a warning string is present.
The crew instruction overlay was concealed with `git update-index --skip-worktree`. That concealment, not the overlay itself, produced the findings: it wedged every branch move with no working escape, it hid a worker's own edits so they needed a private sidecar, and that sidecar was overwritten without an error on a second relaunch. Install the overlay as an ordinary visible modification instead. Concealment was never buying what it appeared to. A branch move onto a commit carrying a different AGENTS.md refuses either way; what the bit changed was whether the worker could act on the refusal. Concealed, `git status` explains nothing and `git stash` saves nothing, so git's advice is a dead end. Visible, the same refusal comes with a modification the worker can see and a stash that works, and `bin/fm-crew-instructions.sh remove` clears it in one step. `--assume-unchanged` is not a substitute: it produces the identical refusal, and it additionally let a merge destroy the overlay while still reporting failure. In-progress instruction edits now go to git's own stash rather than a sidecar. The stash is a stack, so a second relaunch adds an entry instead of destroying the first, and refs/stash is shared ref space that outlives the disposable worktree. Because `git add -A` now stages the overlay, the pre-commit guard keeps it out of the commit instead of refusing one, and refuses only a staged file carrying the overlay marker without being the overlay byte for byte. Concealment also blinded fm-teardown.sh's unlanded-work test to a worker's genuine uncommitted instruction edit. Teardown now filters the overlay by exact content, so scaffolding reads as clean and a real edit still refuses. Also carve this repository's own test suite out of the anti-inversion fleet command fence, in the overlay body and both brief variants, since those scripts are what the suite invokes. Removal heals a worktree left by the superseded mechanism, folding any sidecar into the stash rather than deleting what may be the only copy.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Reviews (3): Last reviewed commit: "no-mistakes: apply CI fixes" | Re-trigger Greptile |
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
Redesign how crew instructions are hidden in a firstmate-repo worktree. This is a MECHANISM change, not another guard.
WHY THIS EXISTS. The earlier crew-role-identity-inversion work fixed a real bug: when a worker's project is the firstmate repo, the worktree's AGENTS.md is firstmate's own job description, it outranks the launch brief, and workers obey it - one ran session start, scaffolded a brief for itself, and tried to spawn a worker for its own task id. That fix overlays those instruction files with crew instructions and hides the overlay from git with
git update-index --skip-worktree. It works and it is shipping. But that one mechanism produced SIX findings which are not independent defects - they are the same abstraction failing in six places. Four were patched with guards, and a standing decision says a fifth means redesign rather than a fifth guard. These are the fifth and sixth:(1) It wedges the worker, with every escape a dead end. skip-worktree blocks merge, rebase and checkout whenever the target commit carries a different AGENTS.md blob. Reproduced in a scratch repo on git 2.50.1:
git status --porcelainis empty, yet the operation fails with "Your local changes to the following files would be overwritten".git stashsaves nothing because skip-worktree hides the divergence. "Commit your changes" records nothing, because the file on disk IS the overlay. Git's error never names the overlay. The removal function existed but was only called by the NEXT spawn's freshen - unreachable by the worker who is stuck. Live route: any firstmate-repo crew that rebases onto origin/main after an AGENTS.md change, or runs the mandated sync step when a fix round touched it.(2) A second relaunch destroyed the first relaunch's saved edits, silently. fm_crew_save_instruction_wip copied the edit to a
.fm-agents-md-editsidecar with no check that an unresolved earlier save was already there. Two relaunches, and the first edit was gone with no error and the same "saved in-progress" message either way. Relaunch is a live path.WHAT THE REDESIGN HAD TO ACHIEVE. Keep the property that actually matters: a crew or scout in a firstmate-repo worktree must not adopt the firstmate role, and the brief must outrank the repo's own instructions. Lose the properties that keep biting: a mechanism whose failure mode is a worker stuck with no reachable escape; a mechanism where an edit can disappear with no error; a mechanism that needs a new guard each time someone finds another path through it. Candidates were to be judged by their FAILURE mode, not their happy path - what happens when the worker rebases, when the pool reuses the worktree, when a relaunch lands on uncommitted edits, and when someone commits - preferring a design where the bad case is loud and recoverable by the worker over one that is invisible and recoverable only by firstmate.
--assume-unchangedwas named as a lead, explicitly NOT as an answer, with a requirement to consider genuinely different shapes and say why rejected ones were rejected.DECISIONS MADE AND ACCEPTED (these are deliberate, not oversights):
git stashthat actually works, andbin/fm-crew-instructions.sh removeto clear it in one step.--assume-unchangedwas evaluated and REJECTED rather than adopted: it produces the identical refusal, and it additionally lets a merge destroy the overlay while still reporting failure.git add -Anow stages the overlay, the pre-commit guard keeps it OUT of the commit instead of refusing the commit, and refuses only a staged file carrying the overlay marker without being the overlay byte for byte.ALSO INCLUDED, a small ruled fix that missed the previous round: the anti-inversion fence in the overlay body and in BOTH brief variants forbade running fm-spawn.sh, fm-brief.sh and other fleet commands with no carve-out - but those are exactly what this repository's own tests invoke. Added an "except through this repository's own test suite" carve-out so a crew is not told to skip the test suite.
EVIDENCE STANDARD REQUIRED. The old mechanism's wedge was proven in a scratch repo, and this redesign was held to the same standard: demonstrate the new design's behaviour on the branch-move case and the relaunch case with real git rather than arguing it, and state plainly which properties were verified and which are asserted. That verification record is committed as docs/verification/crew-instruction-overlay.md, and it is an intentional evidence artifact, not stray documentation.
CONSTRAINTS THAT APPLIED. This is firstmate's own shared tracked material, so the firstmate-coding-guidelines conventions govern (one sentence per line, plain dash, shellcheck-clean bin scripts, colocated tests, no agent name as commit co-author). /Users/cedmo/firstmate/bin must be on PATH so bin/fm-lint.sh runs BOTH linters (ShellCheck 0.11.0 and actionlint 1.7.12); both are green locally. Build ON the crew-role-identity-inversion work, do not revert it and do not race it. Do not merge - merge authority is the captain's.
STACKING CONTEXT FOR REVIEW. This branch is stacked on fm/crew-role-identity-inversion (PR #2880), which is not yet merged, so a diff against the default branch also contains that PR's commits. Only the final commit 13e168c belongs to this task; the earlier commits are that PR's already-reviewed work.
KNOWN PRE-EXISTING FAILURE, NOT INTRODUCED HERE. tests/fm-teardown.test.sh case "herdr-preflight-missing-adapter" already fails on origin/main (f170ced) and on the parent commit 8e3a68e, verified by extracting each tree and running the suite. It is unrelated to this change.
What Changed
bin/fm-crew-worktree-instructions-lib.sh, which overlays crewmate instructions onto the auto-loadedAGENTS.mdandCLAUDE.mdof a firstmate-repo ship or scout worktree so the harness cannot load firstmate's own job description as the worker's role.bin/fm-spawn.shinstalls the overlay after the pooled-worktree refresh and refuses the spawn when it cannot, and the install adds a worktree-localcore.hooksPathpre-commit guard that unstages a file staged with exactly the overlay content and refuses only a staged file carrying the overlay marker without being the overlay byte for byte.skip-worktreeorassume-unchanged, sogit stashand removal work;bin/fm-crew-instructions.shis a new worker-runnable entry point withstatus,remove,saved, andrecover. In-progress instruction edits are saved as commits under per-taskrefs/fm-crew/<task-id>-<digest>/instruction-wip/<n>refs (staged and on-disk versions as separate entries, unmerged index stages refused), removal folds any leftover sidecar from the superseded mechanism into those refs,bin/fm-teardown.shfilters the overlay by exact content so a genuine instruction edit still refuses teardown, andbin/fm-session-start.shnow exits 2 in a linked firstmate task worktree.bin/fm-brief.shbrief variants and the overlay body carry the anti-inversion fence with an "except through this repository's own test suite" carve-out and thefirstmate-coding-guidelinesload line. Docs updated inAGENTS.md,CONTRIBUTING.md,docs/architecture.md, anddocs/scripts.md, with a newdocs/verification/crew-instruction-overlay.mdrecord (registered indocs/documentation-audiences.json) covering the git behavior behind the non-concealment choice. Tests: newtests/fm-crew-worktree-instructions.test.shplus additions totests/fm-brief.test.shandtests/fm-teardown.test.sh.Risk Assessment
Testing
I exercised the redesign the way a crew worker in a firstmate-repo worktree would experience it, using the shipped library and the new
bin/fm-crew-instructions.shagainst real git 2.50.1, and captured a CLI transcript as the primary artifact: the overlay is now a visibleM AGENTS.mdwith no skip-worktree or assume-unchanged bit, a branch move onto a commit carrying a different AGENTS.md still refuses but the refusal is now explained bygit status,git stashsaves a real entry, andremoveclears it in one step so the rebase then succeeds; two relaunches keep both saved edits as distinct per-task refs,saved/recoverrestore them, and the slot's next occupant is refused loudly rather than handed them. The fulltests/fm-crew-worktree-instructions.test.shsuite (41 cases) passes, and this change's four new teardown cases pass when run past the suite's pre-existing abort point. I independently reproduced the committed verification record's concealment probe byte-for-byte, and additionally reproduced the one claim that record marked as unreproduced — under--assume-unchangeda non-fast-forward merge reports failure yet silently destroys the overlay, while the shipped visible design preserves it. I found and closed one coverage gap: the fleet-fence carve-out had no test on the two generated brief variants, so I added a focused test that drives the real generator and verified it fails at the parent commit and passes at HEAD. No screenshot or rendered-HTML artifact applies here — the change is entirely shell scripts, git state, and agent-facing instruction text, so the terminal transcript and the generated brief output are the actual end-user surface. Overall result: the user intent is satisfied; the only open item is that the author's acknowledged pre-existing teardown failure currently prevents this change's own new teardown cases from running in a normal suite invocation.Evidence: Crew worker end-to-end CLI transcript (wedge, escape, relaunch x2, ownership, teardown filter)
Source: Crew worker end-to-end CLI transcript (wedge, escape, relaunch x2, ownership, teardown filter)
=== SPAWN: the overlay is installed as a VISIBLE modification $ git status --porcelain # the overlay is not hidden from me M AGENTS.md $ git ls-files -v AGENTS.md CLAUDE.md # no skip-worktree / assume-unchanged bit H AGENTS.md H CLAUDE.md === DAY OF WORK: git add -A + commit lands my work, not the overlay $ git add -A && git commit -m "feat: crew work" note: kept the crew overlay out of this commit (AGENTS.md is launch scaffolding, not source). $ git show --stat --format= HEAD # the overlay stayed out of the commit feature.txt | 1 + 1 file changed, 1 insertion(+) === THE OLD WEDGE: main moves AGENTS.md, I rebase, git refuses $ git checkout 700f0337c5560033fa84b18cb98c7019de6205ab error: Your local changes to the following files would be overwritten by checkout: AGENTS.md Please commit your changes or stash them before you switch branches. Aborting $ git status --porcelain # the refusal is EXPLAINED: I can see the modification M AGENTS.md $ git stash push -- AGENTS.md CLAUDE.md # the remedy git names actually saves something Saved working directory and index state WIP on fm/task-alpha: 925ff8f feat: crew work === THE ESCAPE: the worker clears it in one step and the rebase goes through $ bin/fm-crew-instructions.sh remove removed the crew instruction overlay; the committed AGENTS.md and CLAUDE.md are restored. you are still a crewmate: your launch brief governs, and you have no fleet. $ git rebase main Rebasing (1/1)Successfully rebased and updated refs/heads/fm/task-alpha. === RELAUNCH x2: neither in-progress instruction edit is destroyed $ relaunch #1 warning: saved in-progress AGENTS.md to refs/fm-crew/task-alpha-a72eff0d82c2/instruction-wip/1 $ relaunch #2 warning: saved in-progress AGENTS.md to refs/fm-crew/task-alpha-a72eff0d82c2/instruction-wip/2 $ bin/fm-crew-instructions.sh recover restored AGENTS.md from refs/fm-crew/task-alpha-a72eff0d82c2/instruction-wip/2 $ git show .../instruction-wip/1:AGENTS.md | tail -1 # relaunch #1's edit, not overwritten FIRST IN-PROGRESS EDIT === OWNERSHIP: the pool's next occupant is refused, not handed my edits $ bin/fm-crew-instructions.sh recover # run as task-beta, the slot reused error: task 'task-beta' has no saved instruction edits. looked for refs under refs/fm-crew/task-beta-b3794eb2fa60/instruction-wip/. entries another task saved are deliberately not offered here. (exit 1)Evidence: Reproduction script that generated the transcript (runs against any firstmate checkout)
Source: Reproduction script that generated the transcript (runs against any firstmate checkout)
Evidence: assume-unchanged destroys the overlay while reporting failure; the shipped visible design does not
Source: assume-unchanged destroys the overlay while reporting failure; the shipped visible design does not
### skip-worktree / nonff-agents-md-differs merge rc: 2 merge says: error: Your local changes to the following files would be overwritten by merge: AGENTS.md: OVERLAY -> overlay survived ### assume-unchanged / nonff-agents-md-differs merge rc: 2 merge says: error: Your local changes to the following files would be overwritten by merge: AGENTS.md: committed -> overlay DESTROYED ### none / nonff-agents-md-differs merge rc: 2 merge says: error: Your local changes to the following files would be overwritten by merge: AGENTS.md: OVERLAY -> overlay survivedEvidence: Independent re-run of the committed verification record's concealment probe (byte-identical)
Source: Independent re-run of the committed verification record's concealment probe (byte-identical)
### skip-worktree status: [] merge: error: Your local changes to the following files would be overwritten by merge: stash: No local changes to save stashed: 0 entry(ies) ### assume-unchanged status: [] merge: error: Your local changes to the following files would be overwritten by merge: stash: No local changes to save stashed: 0 entry(ies) ### none status: [ M AGENTS.md] merge: error: Your local changes to the following files would be overwritten by merge: stash: Saved working directory and index state WIP on target: <sha> base stashed: 1 entry(ies)Evidence: New brief-fence test: fails at the parent commit, passes at HEAD
Source: New brief-fence test: fails at the parent commit, passes at HEAD
### AFTER the fix (bin/fm-brief.sh at HEAD) ok - fm-brief.sh: the ship and scout fleet-command fences carve out this repository's own test suite ### BEFORE the fix (bin/fm-brief.sh reverted to the parent commit 8e3a68e) not ok - ship brief fences the fleet commands with no test-suite carve-out: Do not runbin/fm-session-start.sh,bin/fm-spawn.sh,bin/fm-brief.sh,tasks-axi add, or any other fleet-management command.Evidence: Fleet-fence line as actually emitted into both generated briefs
Source: Fleet-fence line as actually emitted into both generated briefs
scaffolded: <FM_HOME>/data/probe-ship-a1/brief.md (ship, mode=no-mistakes; replace {TASK}) scaffolded: <FM_HOME>/data/probe-scout-a1/brief.md (scout; replace {TASK}) ### ship brief 3:Do not runbin/fm-session-start.sh,bin/fm-spawn.sh,bin/fm-brief.sh,tasks-axi add, or any other fleet-management command, except through this repository's own test suite. ### scout brief 3:Do not runbin/fm-session-start.sh,bin/fm-spawn.sh,bin/fm-brief.sh,tasks-axi add, or any other fleet-management command, except through this repository's own test suite.Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-crew-worktree-instructions-lib.sh:474- fm_crew_rescue_legacy_sidecars doescp "$wt/$dest" "$wt/$rel"with no check on what $rel currently holds, and fm_install_crew_worktree_instructions (line 546) calls it BEFORE fm_crew_stash_instruction_wip. Concrete sequence: a worktree left by the superseded mechanism holds.fm-agents-md-edit(edit E1) while the worker has restored AGENTS.md and edited it (edit E2, uncommitted). Relaunch runs fm_crew_unhide_instruction_files, then rescue cp's E1 over E2 on disk and stashes E1; fm_crew_stash_instruction_wip then sees AGENTS.md == HEAD and stashes nothing. E2 is unrecoverable and the function returns 0 with only a "recovered ... into the git stash" warning.bin/fm-crew-instructions.sh removehits the same path: the removal loop deliberately skips a non-overlay file to preserve the worker's edit, and rescue then overwrites it two lines later. This is the exact failure the intent requires the redesign to eliminate ("a mechanism where an edit can disappear with no error") and it contradicts the function's own comment about not destroying the only copy. Fix: stash whatever $rel already holds before folding the sidecar in (or run fm_crew_stash_instruction_wip first), so both edits land as separate stash entries.bin/fm-crew-instructions.sh:69- Theinstalledflag is set when EITHER instruction file matches its overlay content, but the CLAUDE.md overlay (fm_crew_claude_pointer_body) is byte-identical to the repo's committed CLAUDE.md (blob a9d4d2694af261cf23ed331d4b446bdec194c47b in this checkout). So fm_crew_file_is_installed_overlay "$WT" CLAUDE.md is true in every firstmate worktree, overlay or not.fm-crew-instructions.sh statusin a worktree with no overlay (including immediately after a successfulremove, and on the primary checkout) prints "crew instruction overlay: installed in $WT" and advises runningremove;removethen prints "removed the crew instruction overlay ... the committed AGENTS.md and CLAUDE.md are restored" having restored nothing. Wrong state reported to the worker with no error, and the only CLI test (test_crew_instructions_cli_clears_a_branch_move) asserts the installed case, so it passes either way. Fix: require the file to also differ from HEAD (fm_crew_file_differs_from_head) before counting it as an installed overlay, or key the CLI's determination on AGENTS.md only.bin/fm-crew-worktree-instructions-lib.sh:112- The overlay body tells the worker: "Ifgit stash listshows anfm-crew:entry, in-progress instruction edits were saved there before this overlay was installed. Recover them withgit stash pop." refs/stash is shared ref space (the verification doc proves entries pushed from a linked worktree are visible from the primary), and nothing ever prunes the entries. Concrete path: crew A relaunches with an uncommitted AGENTS.md edit, so anfm-crew:entry is pushed; task A ends and the pooled slot is returned; crew B spawns into the same slot with a clean AGENTS.md, so no new entry is pushed; crew B reads the overlay, sees thefm-crew:entry, pops it, and applies task A's AGENTS.md edits onto task B's branch. stash@{0} may also belong to the captain's primary checkout. The superseded mechanism deleted sidecars on removal for exactly this reason ("a sidecar that outlived its task would otherwise be handed to the next worker in the same pooled slot"), so this property regressed while the stash decision itself stayed correct. Consider labelling the entry with the task id and telling the worker to identify their own entry withgit stash list/git stash showbefore popping, rather than an unconditionalgit stash pop.bin/fm-spawn.sh:1744- freshen_spawn_worktree_base's comment still says "skip-worktree hides it from the clean check below and then wedges the reset". After this change the overlay is visible, so the clean check DOES see it (that is now the reason removal must run first) and an installed overlay no longer wedgesgit reset --hard(tests/fm-crew-worktree-instructions.test.sh:552 asserts exactly that). The comment now states the opposite of the shipped behavior at the one call site that depends on it.🔧 Fix: replace crew instruction stash carrier with per-task refs
5 issues (2 warnings, 3 infos) still open:
bin/fm-crew-worktree-instructions-lib.sh:449- fm_crew_save_instruction_wip records only the WORKING-TREE blob (git hash-object -w --no-filters -- "$wt/$rel"at line 433) and then restores withgit checkout HEAD -- "$rel", which overwrites the INDEX as well as the worktree. Concrete sequence: a crew worker restores AGENTS.md, edits it to S, runsgit add AGENTS.md, then edits further to W. Relaunch calls fm_install_crew_worktree_instructions -> fm_crew_save_instruction_wip. fm_crew_instruction_has_wip is true (disk W != HEAD), so W is stored under refs/fm-crew/<task>/instruction-wip/1, thengit checkout HEAD -- AGENTS.mdresets index and worktree to HEAD. Blob S is now unreferenced and unnamed; nothing reports it, and the function prints the success line "saved in-progress AGENTS.md to <ref>". The superseded carrier did not lose it:git stash push -- AGENTS.mdrecords the index tree as stash^2, recoverable withgit stash pop --index/git stash show 'stash@{0}^2'. This is a regression on the redesign's stated property that an edit is either recorded or loudly refused, never dropped, and it makes bin/fm-crew-instructions.sh's header claim "It never discards a worker's own edits" untrue. Fix: capture the staged blob too whengit ls-files -s -- $reldiffers from both HEAD and the worktree blob - either as its own entry beside the worktree one, or by recording it in the same commit as a second parent the way stash does.bin/fm-crew-worktree-instructions-lib.sh:673- On a fresh (non-relaunch) spawn into a pooled slot left by the superseded mechanism, bin/fm-spawn.sh:1751 runs fm_remove_crew_worktree_instructions BEFORE fm_install_crew_worktree_instructions. Removal calls fm_crew_rescue_legacy_sidecars, which resolves the owner with no explicit task id (line 666); the legacy slot carries no owner marker (the superseded mechanism never wrote one), so fm_crew_resolve_owner falls back tounowned-<git-dir-basename>and RECORDS that marker. The sidecar lands at refs/fm-crew/unowned-<slot>/instruction-wip/1 and line 673 prints "recovered .fm-agents-md-edit into <ref>; list it with 'bin/fm-crew-instructions.sh saved'". fm_install_crew_worktree_instructions then overwrites the marker with the real task id (line 744), so the worker runningbin/fm-crew-instructions.sh savedgets "error: task '<ID>' has no saved instruction edits ... looked for refs under refs/fm-crew/<ID>/instruction-wip/". The rescued edit is preserved but the recovery instruction printed for it is false, and it is reachable only by someone who scrolled the spawn stderr for the ref name. Withholding the previous occupant's work from the new task is correct; the message is not. Fix: have the rescue print the exactgit checkout <ref> -- <rel>that works regardless of owner, or thread the task id into fm_remove_crew_worktree_instructions from freshen_spawn_worktree_base so the rescue and the CLI agree on one owner. tests/fm-crew-worktree-instructions.test.sh:626 does not cover this because it never re-installs under a second task id after healing.bin/fm-crew-worktree-instructions-lib.sh:327- fm_crew_wip_ref_base's slug (${owner//[^A-Za-z0-9._-]/-}) only maps out-of-charset bytes, but git's ref grammar additionally rejects a component containing..or ending in.lock. fm_task_id_creation_valid (bin/fm-pr-lib.sh:106) accepts both: a task id likev1..v2orprobe.lockpasses spawn validation. With an uncommitted AGENTS.md edit present, fm_crew_next_wip_ref then fails check-ref-format, fm_crew_store_wip_entry errors "task '<id>' does not yield a legal ref name for saved instruction edits", fm_crew_save_instruction_wip returns 1, and fm_install_crew_worktree_instructions -> bin/fm-spawn.sh:2290 aborts the spawn. The failure is loud, so nothing is silently wrong, but a task id firstmate itself accepts can now block a launch. Collapsing..and stripping a trailing.lockin the slug (or falling back to a short hash of the owner when check-ref-format refuses) removes the case.bin/fm-crew-instructions.sh:106-savedprintsgit checkout <ref> -- <rel>as the restore command, andrecoverrepeats it at line 149 as "restore it again with ...".git checkout <commit> -- <path>updates the index as well as the worktree, so a worker who follows that advice ends up with the recovered instruction edit STAGED - which is exactly what recover itself takes care to avoid two lines earlier (git reset -q HEAD -- "$rel"at line 138, asserted by the ' M AGENTS.md' check in tests/fm-crew-worktree-instructions.test.sh). A staged AGENTS.md carrying no overlay marker passes the pre-commit guard, so the next plaingit commitquietly lands the recovered WIP. Printgit checkout <ref> -- <rel> && git reset -q HEAD -- <rel>so the advertised command matches what recover actually produces.🔧 Fix: capture staged instruction edits and fix recovery advice
4 issues (1 warning, 3 infos) still open:
bin/fm-crew-worktree-instructions-lib.sh:470- fm_crew_staged_blob doesgit ls-files -s -- "$rel" | awk '{print $2}'with no stage filter. During an unresolved merge/rebase conflict git emits three lines (stages 1/2/3), so staged_blob becomes three newline-separated hashes. At line 501 that value is unequal to both head_blob and disk_blob, so it is accepted as a distinct staged version, and staged_pairs becomesAGENTS.md=<sha1>\n<sha2>\n<sha3>, passed unquoted (SC2086 disabled) into fm_crew_store_wip_entry, where bash splits it into three args. I reproduced this with real git 2.50.1 under bash: pair 1 records the MERGE BASE blob (stage 1) as AGENTS.md, and pairs 2 and 3 add junk paths literally named after the stage-2/stage-3 hashes. Everyupdate-index --add --cacheinfosucceeds, so nothing errors: fm_crew_report_saved_entry prints "saved staged AGENTS.md to <ref>" andbin/fm-crew-instructions.sh savedadvertises a restore command that gives the worker the common-ancestor content instead of what they staged. Reachable path: the worker follows the shipped advice (bin/fm-crew-instructions.sh remove, then rebase/merge onto origin/main after an AGENTS.md change), hits a genuine conflict, and either firstmate relaunches or the worker runsrecover- both call fm_crew_save_instruction_wip. The subsequentgit checkout HEAD -- AGENTS.mdfor the disk path also silently collapses the conflict for that path. Fix: select stage 0 only (awk '$3 == "0" {print $2}'), and refuse loudly rather than saving when the index carries unmerged entries for an instruction file.bin/fm-crew-worktree-instructions-lib.sh:501- The staged-version test only excludes staged_blob equal to head_blob or disk_blob; it does not exclude the overlay's own blob. Concrete sequence: a crew runsgit add -Awhile the overlay is installed (index now holds the overlay blob for AGENTS.md - the pre-commit guard only unstages at commit time, so this state persists betweenaddandcommit), then restores and edits AGENTS.md on disk. A relaunch then sees staged_blob = overlay hash, which differs from HEAD and from the disk edit, so an entry is created and reported as "saved staged AGENTS.md".bin/fm-crew-instructions.sh savedlists launch scaffolding as one of the worker's recoverable staged edits, and its advertised command reinstalls the overlay as an unstaged modification. No data is lost, but the tool reports a version of the worker's own work that is not one. Skip the staged capture when staged_blob equals fm_crew_overlay_blob_hash "$wt" "$rel".bin/fm-crew-worktree-instructions-lib.sh:349- fm_crew_wip_ref_base folds..to.and strips a trailing.lockto make the slug a legal ref component, but the fold is not injective, so two distinct task ids can land on one namespace. Both are accepted by fm_task_id_creation_valid: ownerprobe..v2.lockfolds toprobe.v2, and ownerprobe.v2is alreadyprobe.v2, so both resolve to refs/fm-crew/probe.v2/instruction-wip/. If a pooled slot is used by one and later by the other, require_owned_entries under the second id lists andrecoverrestores entries the first task saved - the exact "recovery must never restore anything it did not itself create" property this carrier was redesigned to guarantee, and the failure is silent because the ref exists and resolves. The digest fallback at line 362 only fires when git refuses the slug outright, so a collision never reaches it. Append a short digest of the ORIGINAL owner whenever the folded slug differs from it (or unconditionally), so the namespace stays injective.bin/fm-crew-worktree-instructions-lib.sh:427- fm_crew_store_wip_entry doesmktemp "$wt/.fm-crew-wip-index.XXXXXX", creating the temporary index inside the worktree. Every normal exit removes it, but a kill between mktemp and the finalrm -fleaves an untracked.fm-crew-wip-index.*in the slot. freshen_spawn_worktree_base (bin/fm-spawn.sh:1775-1782) refuses on ANYgit status --porcelainoutput and its exclusion grep covers only.claude/and.fm-(grok|kimi)-turnend, so the leftover blocks every subsequent spawn into that pooled slot with "is not clean; refusing to discard uncommitted work", and teardown's unlanded-work check refuses too. Put the temp index in the worktree's own git dir alongside the owner marker and the hooks dir (fm_crew_owner_marker_path already resolves it), where it cannot appear in status.🔧 Fix: refuse conflicted saves and make wip namespace injective
3 issues (1 warning, 2 infos) still open:
bin/fm-crew-worktree-instructions-lib.sh:611- fm_crew_filter_overlay_status drops aM <file>line whenever the file is one of AGENTS.md/CLAUDE.md and fm_crew_file_is_installed_overlay is true — with no gate on the worktree actually being a firstmate-shaped crew worktree, and no gate on the overlay actually being installed. For CLAUDE.md the "overlay content" is fm_crew_claude_pointer_body (line 154-157), which is byte-identical to the canonical pointer bin/fm-ensure-agents-md.sh:105-110 writes into ARBITRARY projects, so the predicate is a pure content test that any repository can satisfy. Concrete path: a ship task in a non-firstmate project foo whose committed CLAUDE.md is real content; the worker normalizes it to the two-line@AGENTS.mdpointer (this fleet's own house convention) and does not commit.git status --porcelainis exactlyM CLAUDE.md. bin/fm-teardown.sh:1158 now pipes that through the filter,dirtybecomes empty, validate_worktree_teardown_safety returns 0, and the worktree is torn down with the worker's uncommitted change silently discarded. Before this change teardown refused. Note the previous round's fix (fm_crew_file_is_overlay_modification) does not help here: aMline already implies the file differs from HEAD, so adding the differs-from-HEAD test leaves this case unchanged. Fix at the filter: require fm_checkout_is_firstmate_shaped "$wt" and, for CLAUDE.md specifically, require that AGENTS.md is itself carrying the overlay, before dropping the line. tests/fm-teardown.test.sh:720 does not catch this because its fixture worktree is also not firstmate-shaped and it only exercises the AGENTS.md body.bin/fm-crew-worktree-instructions-lib.sh:138- The worker-facing overlay body says edits "were saved as commits under this task's ownrefs/fm-crew/<task-id>/instruction-wip/refs", and the constant block comment at lines 94-97 says the same. The shipped namespace isrefs/fm-crew/<folded-slug>-<12-char-digest>/instruction-wip/<n>(fm_crew_wip_ref_base, line 379), because the round-3 injectivity fix appends a digest of the exact owner unconditionally. A worker who follows the text and runsgit for-each-ref refs/fm-crew/<their-task-id>/finds nothing, and is left believing the save did not happen. docs/verification/crew-instruction-overlay.md:96 carries the same pre-digest shape in its prose, while the library head comment (line 39) and docs/architecture.md:201 already have it right. Correct the three stale statements to<task-id>-<digest>, or have the overlay body point only atbin/fm-crew-instructions.sh saved, which resolves the base itself.bin/fm-crew-instructions.sh:132-recovercalls fm_crew_save_instruction_wip before restoring, which is right the first time (the working tree still holds the overlay, so nothing is saved). But after a successful recover, AGENTS.md holds the recovered edit, which is neither HEAD nor the overlay, so fm_crew_instruction_has_wip is true. A secondrecover(or the next relaunch) therefore stores a byte-identical copy of the entry it is about to restore as a new ref, and repeats on every subsequent invocation.savedthen lists N entries whose subjects and diffs are indistinguishable, so the worker cannot tell which one is the version they were looking for — and the listing is the only interface the overlay body points them at. Nothing is lost, and the refs are cheap, but the reported set is wrong. Skip the save when the working-tree blob already equals the blob an existing entry for this owner holds for that path.🔧 Fix: gate overlay status filter on an installed crew overlay
1 info still open:
bin/fm-crew-worktree-instructions-lib.sh:564- The round-4 dedup made a save that finds an existing entry holding the same blob revert the worker's file with no output at all. In fm_crew_save_instruction_wip,restore_pathsis appended unconditionally when fm_crew_instruction_has_wip is true (line 563), butdisk_pairs/disk_pathsare only appended when fm_crew_wip_entry_holds_blob is false (line 564). When dedup hits, disk_pairs stays empty, so fm_crew_report_saved_entry is never called, and the restore loop at line 598 still runsgit checkout HEAD -- AGENTS.md. The function returns 0 having printed nothing. Concrete sequence: task T saves edit A as entry 1; the worker later produces B, saved as entry 2; the worker then decides A was right and restores it with the commandsavedprinted for entry 1, so AGENTS.md holds A again. The next relaunch dedups (entry 1 holds A), reverts AGENTS.md to HEAD and installs the overlay with zero stderr output - where every prior save printed 'warning: saved ... to <ref>; restore it with: ...'. The worker then follows the overlay body's advice (line 145, 'Run bin/fm-crew-instructions.sh recover to restore the newest one') and gets B, not the A they were working on, because recover takestail -n 1of the sorted refs (bin/fm-crew-instructions.sh:129). Nothing is lost - A is still in entry 1 andsavedlists it - but the save is silent in a case where it used to be loud, and the advertised one-command recovery hands back a different version than the one it just reverted. Fix: when the dedup branch skips, still announce it, printing the existing entry's fm_crew_restore_command for that path, so the revert is never silent and the worker can tell which entry holds what they had.tests/fm-teardown.test.sh:2724- tests/fm-teardown.test.sh aborts on the first failure, and the pre-existingherdr-preflight-missing-adapterfailure is registered at position 13 while this change's four new overlay cases are at positions 22-25. A plainbash tests/fm-teardown.test.shtherefore never reachestest_installed_crew_overlay_is_not_unlanded_work,test_a_canonical_claude_pointer_in_another_project_still_refuses,test_a_claude_pointer_without_the_overlay_still_refuses, ortest_real_edit_over_the_crew_overlay_still_refuses. I ran them in isolation and all four pass, but until the pre-existing failure is resolved this change's teardown coverage will not execute in CI. Deciding whether to fix, quarantine, or reorder that case is the author's call.tests/fm-teardown.test.sh- Confirmed the author's pre-existing-failure claim rather than taking it on trust: I extracted the base tree at f170ced withgit archiveand ran the teardown suite there, andherdr-preflight-missing-adapterfails identically. Not introduced by this change.bash tests/fm-crew-worktree-instructions.test.sh— all 41 cases pass, including the branch-move-escapable, two-relaunch, per-task-ownership, commit-guard and legacy-sidecar-healing casesbash tests/fm-brief.test.sh— full file green, including the newly addedtest_fleet_command_fence_carves_out_the_test_suiteNew regression test added:tests/fm-brief.test.sh::test_fleet_command_fence_carves_out_the_test_suite— generates the ship and scout briefs through the realbin/fm-brief.shand requires every emitted fleet-command fence line to carry the test-suite carve-outFail-before/pass-after proof for that test: replacedbin/fm-brief.shwithgit show 8e3a68e:bin/fm-brief.sh, re-ran the suite (not ok - ship brief fences the fleet commands with no test-suite carve-out), thengit checkout -- bin/fm-brief.shand re-ran greenbash tests/fm-teardown.test.sh— reaches the pre-existingherdr-preflight-missing-adapterfailure and abortsRan this change's four new teardown cases past that abort point via a temporary head-truncated copy of the suite:test_installed_crew_overlay_is_not_unlanded_work,test_a_canonical_claude_pointer_in_another_project_still_refuses,test_a_claude_pointer_without_the_overlay_still_refuses,test_real_edit_over_the_crew_overlay_still_refuses— all pass (temp copy deleted afterwards)Verified the pre-existing failure at the base commit:git archive f170ced | tar -xinto a scratch dir, thenbash tests/fm-teardown.test.sh— same case fails (scratch dir deleted afterwards)Manual end-to-end worker transcript against real git 2.50.1 driving the shipped library and CLI: overlay install,git status --porcelain,git ls-files -v,git add -A && git commit,git rebase main,git checkout <moved-main-sha>,git stash push,fm-crew-instructions.sh status|remove|saved|recover, two relaunches, reuse of the slot under a second task id, andfm_crew_filter_overlay_statusfed realgit status --porcelainoutputIndependently re-ran the concealment probe recorded indocs/verification/crew-instruction-overlay.mdand diffed it against the committed output (commit sha normalized) — byte-identical matchProbed the doc's unreproduced side observation acrossskip-worktree/assume-unchanged/none x fast-forward/non-fast-forward merge, onegit mergeinvocation per cell✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.