Skip to content

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
kunchenguid:mainfrom
cedmos:fm/overlay-hiding-redesign
Open

fix(bin): keep firstmate-repo crews out of the firstmate role with a visible instruction overlay#2889
cedmos wants to merge 12 commits into
kunchenguid:mainfrom
cedmos:fm/overlay-hiding-redesign

Conversation

@cedmos

@cedmos cedmos commented Aug 23, 2026

Copy link
Copy Markdown

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 --porcelain is empty, yet the operation fails with "Your local changes to the following files would be overwritten". git stash saves 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-edit sidecar 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-unchanged was 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):

  • Stop concealing the overlay from git at all; install it as an ordinary VISIBLE modification to AGENTS.md and CLAUDE.md. Concealment was never buying what it appeared to: a branch move onto a commit carrying a different AGENTS.md refuses either way, and the bit only changed whether the worker could act on the refusal. Visible, the same refusal comes with a modification the worker can see, a git stash that actually works, and bin/fm-crew-instructions.sh remove to clear it in one step.
  • --assume-unchanged was evaluated and REJECTED rather than adopted: it produces the identical refusal, and it additionally lets a merge destroy the overlay while still reporting failure.
  • In-progress instruction edits now go to git's own stash instead of a private 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 the commit, and refuses only a staged file carrying the overlay marker without being the overlay byte for byte.
  • Concealment had 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 launch scaffolding reads as clean while a real edit still refuses teardown.
  • Removal heals a worktree left behind by the superseded mechanism, folding any leftover sidecar into the stash rather than deleting what may be the only copy.
  • A new entry point bin/fm-crew-instructions.sh gives the worker the reachable escape the old design lacked.

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

  • Added bin/fm-crew-worktree-instructions-lib.sh, which overlays crewmate instructions onto the auto-loaded AGENTS.md and CLAUDE.md of 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.sh installs the overlay after the pooled-worktree refresh and refuses the spawn when it cannot, and the install adds a worktree-local core.hooksPath pre-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.
  • The overlay is an ordinary visible working-tree modification rather than being hidden behind skip-worktree or assume-unchanged, so git stash and removal work; bin/fm-crew-instructions.sh is a new worker-runnable entry point with status, remove, saved, and recover. In-progress instruction edits are saved as commits under per-task refs/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.sh filters the overlay by exact content so a genuine instruction edit still refuses teardown, and bin/fm-session-start.sh now exits 2 in a linked firstmate task worktree.
  • Both bin/fm-brief.sh brief variants and the overlay body carry the anti-inversion fence with an "except through this repository's own test suite" carve-out and the firstmate-coding-guidelines load line. Docs updated in AGENTS.md, CONTRIBUTING.md, docs/architecture.md, and docs/scripts.md, with a new docs/verification/crew-instruction-overlay.md record (registered in docs/documentation-audiences.json) covering the git behavior behind the non-concealment choice. Tests: new tests/fm-crew-worktree-instructions.test.sh plus additions to tests/fm-brief.test.sh and tests/fm-teardown.test.sh.

Risk Assessment

⚠️ Medium: The change is a broad mechanism redesign touching spawn abort paths, teardown's unlanded-work data-loss guard, and a new worker-facing CLI, and although all four prior rounds' fixes verify correct against the current code and are covered by genuinely behavioral regressions, the only remaining issue is a non-blocking reporting gap that is fine to address as a follow-up.

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.sh against real git 2.50.1, and captured a CLI transcript as the primary artifact: the overlay is now a visible M AGENTS.md with 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 by git status, git stash saves a real entry, and remove clears it in one step so the rebase then succeeds; two relaunches keep both saved edits as distinct per-task refs, saved/recover restore them, and the slot's next occupant is refused loudly rather than handed them. The full tests/fm-crew-worktree-instructions.test.sh suite (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-unchanged a 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)

git 2.50.1

=== SPAWN: the overlay is installed as a VISIBLE modification

$ head -8 AGENTS.md    # what the harness auto-loads as my role
  <!-- firstmate-crew-worktree-instructions -->
  
  # Role
  
  You are a crewmate: an autonomous worker agent managed by firstmate.
  You are not the first mate.
  Spawn replaced this worktree's harness-loaded project instructions before launch so you cannot adopt the firstmate role from them.
  

$ grep -n "except through this repository" AGENTS.md   # fleet fence carve-out
  9:Do not run `bin/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.

$ 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

$ git log -1 --format=%H:%s HEAD:AGENTS.md is untouched -- committed job description:
  You are the first mate.

=== 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(+)

$ git status --porcelain    # ...and is still installed and still visible
   M AGENTS.md

=== THE OLD WEDGE: main moves AGENTS.md, I rebase, git refuses

$ git rebase main
  error: cannot rebase: You have unstaged changes.
  error: Please commit or stash them.

$ git checkout 700f0337c5560033fa84b18cb98c7019de6205ab    # the same branch move, phrased the way the old bug report quoted it
  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

$ git stash list
  stash@{0}: 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 status
  crew instruction overlay: installed in /private/var/folders/sr/jsglj1fs5dngwtr7d4jry7c40000gn/T/crew-overlay-evidence.sku6Ps/task-worktree
  run 'bin/fm-crew-instructions.sh remove' to restore the committed files.

$ bin/fm-crew-instructions.sh remove
  removed the crew instruction overlay from /private/var/folders/sr/jsglj1fs5dngwtr7d4jry7c40000gn/T/crew-overlay-evidence.sku6Ps/task-worktree; the committed AGENTS.md and CLAUDE.md are restored.
  you are still a crewmate: your launch brief governs, and you have no fleet.

$ git status --porcelain    # clean
  []

$ git rebase main
  Rebasing (1/1)
Successfully rebased and updated refs/heads/fm/task-alpha.

$ git log --oneline -2   # my commit rebased onto the moved main
  1225fcc feat: crew work
  700f033 docs: upstream AGENTS.md change

$ bin/fm-crew-instructions.sh status    # honest after removal
  crew instruction overlay: not installed in /private/var/folders/sr/jsglj1fs5dngwtr7d4jry7c40000gn/T/crew-overlay-evidence.sku6Ps/task-worktree

=== RELAUNCH x2: neither in-progress instruction edit is destroyed

$ relaunch #1 (spawn reinstalls the overlay over my uncommitted AGENTS.md edit)
  warning: saved in-progress AGENTS.md to refs/fm-crew/task-alpha-a72eff0d82c2/instruction-wip/1; restore it with: git checkout refs/fm-crew/task-alpha-a72eff0d82c2/instruction-wip/1 -- AGENTS.md && git reset -q HEAD -- AGENTS.md

$ relaunch #2
  warning: saved in-progress AGENTS.md to refs/fm-crew/task-alpha-a72eff0d82c2/instruction-wip/2; restore it with: git checkout refs/fm-crew/task-alpha-a72eff0d82c2/instruction-wip/2 -- AGENTS.md && git reset -q HEAD -- AGENTS.md

$ bin/fm-crew-instructions.sh saved   # BOTH relaunches are still here
  saved instruction edits for task 'task-alpha' in /private/var/folders/sr/jsglj1fs5dngwtr7d4jry7c40000gn/T/crew-overlay-evidence.sku6Ps/task-worktree:
    refs/fm-crew/task-alpha-a72eff0d82c2/instruction-wip/1
      working-tree instruction edits saved for task-alpha before the crew overlay
      AGENTS.md: git checkout refs/fm-crew/task-alpha-a72eff0d82c2/instruction-wip/1 -- AGENTS.md && git reset -q HEAD -- AGENTS.md
    refs/fm-crew/task-alpha-a72eff0d82c2/instruction-wip/2
      working-tree instruction edits saved for task-alpha before the crew overlay
      AGENTS.md: git checkout refs/fm-crew/task-alpha-a72eff0d82c2/instruction-wip/2 -- AGENTS.md && git reset -q HEAD -- AGENTS.md

$ bin/fm-crew-instructions.sh recover
  restored AGENTS.md from refs/fm-crew/task-alpha-a72eff0d82c2/instruction-wip/2 as an uncommitted modification in /private/var/folders/sr/jsglj1fs5dngwtr7d4jry7c40000gn/T/crew-overlay-evidence.sku6Ps/task-worktree.
  the entry is kept, not consumed; restore it again with:
    git checkout refs/fm-crew/task-alpha-a72eff0d82c2/instruction-wip/2 -- AGENTS.md && git reset -q HEAD -- AGENTS.md
  run 'bin/fm-crew-instructions.sh saved' to see this task's other entries.

$ tail -1 AGENTS.md   # the newest saved edit is back on disk
  SECOND IN-PROGRESS EDIT

$ git show refs/fm-crew/task-alpha-a72eff0d82c2/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 in /private/var/folders/sr/jsglj1fs5dngwtr7d4jry7c40000gn/T/crew-overlay-evidence.sku6Ps/task-worktree.
  looked for refs under refs/fm-crew/task-beta-b3794eb2fa60/instruction-wip/.
  entries another task saved are deliberately not offered here.
  (exit 1)

=== TEARDOWN: scaffolding reads clean, a real edit still refuses

$ git status --porcelain | fm_crew_filter_overlay_status   # what fm-teardown.sh reads, only the overlay present
  []

$ git status --porcelain | fm_crew_filter_overlay_status   # with a genuine worker edit on top
   M AGENTS.md
Evidence: Reproduction script that generated the transcript (runs against any firstmate checkout)

Source: Reproduction script that generated the transcript (runs against any firstmate checkout)

#!/usr/bin/env bash
# Reproduction script for the crew-instruction-overlay redesign.
#
# Drives the SHIPPED library and the SHIPPED worker CLI against real git in a
# disposable scratch repository, and prints exactly what a crew worker sitting
# in a firstmate-repo task worktree would see on their terminal.
#
# usage: reproduce-worker-experience.sh <firstmate-repo-root>
set -u

ROOT=${1:?usage: reproduce-worker-experience.sh <firstmate-repo-root>}
ROOT=$(cd "$ROOT" && pwd -P)
# shellcheck source=/dev/null
. "$ROOT/bin/fm-crew-worktree-instructions-lib.sh"
CREW_CLI="$ROOT/bin/fm-crew-instructions.sh"

PROBE=$(mktemp -d "${TMPDIR:-/tmp}/crew-overlay-evidence.XXXXXX")
trap 'rm -rf "$PROBE"' EXIT

say() { printf '\n=== %s\n' "$1"; }
worker() { printf '\n$ %s\n' "$*"; }

# ---------------------------------------------------------------------------
# A firstmate-shaped upstream, its real AGENTS.md / CLAUDE.md / coding
# guidelines copied out of the checkout under test, plus a linked task worktree
# of the kind fm-spawn.sh hands a crew.
# ---------------------------------------------------------------------------
REPO="$PROBE/firstmate"
WT="$PROBE/task-worktree"
mkdir -p "$REPO/bin" "$REPO/.agents/skills/firstmate-coding-guidelines"
git init -q -b main "$REPO"
git -C "$REPO" config user.email crew@example.invalid
git -C "$REPO" config user.name 'crew worker'
cp "$ROOT/AGENTS.md" "$REPO/AGENTS.md"
cp "$ROOT/CLAUDE.md" "$REPO/CLAUDE.md"
cp "$ROOT/.agents/skills/firstmate-coding-guidelines/SKILL.md" \
  "$REPO/.agents/skills/firstmate-coding-guidelines/SKILL.md"
cp "$ROOT/bin/fm-session-start.sh" "$ROOT/bin/fm-spawn.sh" "$REPO/bin/"
git -C "$REPO" add -A
git -C "$REPO" commit -qm 'firstmate checkout'
git -C "$REPO" worktree add --quiet --detach "$WT"
git -C "$WT" checkout -q -b fm/task-alpha

printf '%s\n' "git $(git --version | awk '{print $3}')"

# ===========================================================================
say 'SPAWN: the overlay is installed as a VISIBLE modification'
# ===========================================================================
fm_install_crew_worktree_instructions "$WT" 'task-alpha' >/dev/null || exit 1

worker 'head -8 AGENTS.md    # what the harness auto-loads as my role'
head -8 "$WT/AGENTS.md" | sed 's/^/  /'

worker 'grep -n "except through this repository" AGENTS.md   # fleet fence carve-out'
grep -n 'except through this repository' "$WT/AGENTS.md" | sed 's/^/  /'

worker 'git status --porcelain      # the overlay is not hidden from me'
git -C "$WT" status --porcelain | sed 's/^/  /'

worker 'git ls-files -v AGENTS.md CLAUDE.md   # no skip-worktree / assume-unchanged bit'
git -C "$WT" ls-files -v -- AGENTS.md CLAUDE.md | sed 's/^/  /'

worker 'git log -1 --format=%H:%s HEAD:AGENTS.md is untouched -- committed job description:'
git -C "$WT" show HEAD:AGENTS.md | grep -m1 -F 'You are the first mate.' | sed 's/^/  /'

# ===========================================================================
say 'DAY OF WORK: git add -A + commit lands my work, not the overlay'
# ===========================================================================
printf 'crew work\n' > "$WT/feature.txt"
worker 'git add -A && git commit -m "feat: crew work"'
git -C "$WT" add -A
git -C "$WT" commit -qm 'feat: crew work' 2>&1 | sed 's/^/  /'
worker 'git show --stat --format= HEAD    # the overlay stayed out of the commit'
git -C "$WT" show --stat --format= HEAD | sed 's/^/  /'
worker 'git status --porcelain    # ...and is still installed and still visible'
git -C "$WT" status --porcelain | sed 's/^/  /'

# ===========================================================================
say 'THE OLD WEDGE: main moves AGENTS.md, I rebase, git refuses'
# ===========================================================================
git -C "$REPO" checkout -q main
printf '\n<!-- upstream edit -->\n' >> "$REPO/AGENTS.md"
git -C "$REPO" commit -qam 'docs: upstream AGENTS.md change'
git -C "$WT" fetch -q "$REPO" main 2>/dev/null || true

worker 'git rebase main'
git -C "$WT" rebase main 2>&1 | head -6 | sed 's/^/  /'

MOVED=$(git -C "$REPO" rev-parse main)
worker "git checkout $MOVED    # the same branch move, phrased the way the old bug report quoted it"
git -C "$WT" checkout "$MOVED" 2>&1 | head -6 | sed 's/^/  /'

worker 'git status --porcelain    # the refusal is EXPLAINED: I can see the modification'
git -C "$WT" status --porcelain | sed 's/^/  /'

worker 'git stash push -- AGENTS.md CLAUDE.md   # the remedy git names actually saves something'
git -C "$WT" stash push -- AGENTS.md CLAUDE.md 2>&1 | sed 's/^/  /'
worker 'git stash list'
git -C "$WT" stash list | sed 's/^/  /'
git -C "$WT" stash pop -q 2>/dev/null || true

# ===========================================================================
say 'THE ESCAPE: the worker clears it in one step and the rebase goes through'
# ===========================================================================
worker 'bin/fm-crew-instructions.sh status'
"$CREW_CLI" status "$WT" 2>&1 | sed 's/^/  /'

worker 'bin/fm-crew-instructions.sh remove'
"$CREW_CLI" remove "$WT" 2>&1 | sed 's/^/  /'

worker 'git status --porcelain    # clean'
printf '  [%s]\n' "$(git -C "$WT" status --porcelain | tr -d '\n')"

worker 'git rebase main'
git -C "$WT" rebase main 2>&1 | head -3 | sed 's/^/  /'
worker 'git log --oneline -2   # my commit rebased onto the moved main'
git -C "$WT" log --oneline -2 | sed 's/^/  /'

worker 'bin/fm-crew-instructions.sh status    # honest after removal'
"$CREW_CLI" status "$WT" 2>&1 | sed 's/^/  /'

# ===========================================================================
say 'RELAUNCH x2: neither in-progress instruction edit is destroyed'
# ===========================================================================
printf 'FIRST IN-PROGRESS EDIT\n' >> "$WT/AGENTS.md"
worker 'relaunch #1 (spawn reinstalls the overlay over my uncommitted AGENTS.md edit)'
fm_install_crew_worktree_instructions "$WT" 'task-alpha' 2>&1 | sed 's/^/  /'

git -C "$WT" checkout -q HEAD -- AGENTS.md
printf 'SECOND IN-PROGRESS EDIT\n' >> "$WT/AGENTS.md"
worker 'relaunch #2'
fm_install_crew_worktree_instructions "$WT" 'task-alpha' 2>&1 | sed 's/^/  /'

worker 'bin/fm-crew-instructions.sh saved   # BOTH relaunches are still here'
"$CREW_CLI" saved "$WT" 2>&1 | sed 's/^/  /'

worker 'bin/fm-crew-instructions.sh recover'
"$CREW_CLI" recover "$WT" 2>&1 | sed 's/^/  /'
worker 'tail -1 AGENTS.md   # the newest saved edit is back on disk'
tail -1 "$WT/AGENTS.md" | sed 's/^/  /'

FIRST_REF=$(fm_crew_list_wip_refs "$WT" 'task-alpha' | head -1)
worker "git show $FIRST_REF:AGENTS.md | tail -1   # relaunch #1's edit, not overwritten"
git -C "$WT" show "$FIRST_REF:AGENTS.md" | tail -1 | sed 's/^/  /'

# ===========================================================================
say "OWNERSHIP: the pool's next occupant is refused, not handed my edits"
# ===========================================================================
git -C "$WT" checkout -q HEAD -- AGENTS.md CLAUDE.md
fm_install_crew_worktree_instructions "$WT" 'task-beta' >/dev/null || true
worker 'bin/fm-crew-instructions.sh recover   # run as task-beta, the slot reused'
beta_out=$("$CREW_CLI" recover "$WT" 2>&1)
beta_rc=$?
printf '%s\n' "$beta_out" | sed 's/^/  /'
printf '  (exit %s)\n' "$beta_rc"

# ===========================================================================
say 'TEARDOWN: scaffolding reads clean, a real edit still refuses'
# ===========================================================================
git -C "$WT" checkout -q HEAD -- AGENTS.md CLAUDE.md
fm_install_crew_worktree_instructions "$WT" 'task-beta' >/dev/null || true
worker 'git status --porcelain | fm_crew_filter_overlay_status   # what fm-teardown.sh reads, only the overlay present'
printf '  [%s]\n' "$(git -C "$WT" status --porcelain | fm_crew_filter_overlay_status "$WT" | tr -d '\n')"
printf 'A REAL UNCOMMITTED EDIT\n' >> "$WT/AGENTS.md"
worker 'git status --porcelain | fm_crew_filter_overlay_status   # with a genuine worker edit on top'
git -C "$WT" status --porcelain | fm_crew_filter_overlay_status "$WT" | sed 's/^/  /'
printf '\n'
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 survived

git version 2.50.1 (Apple Git-155)

fatal: No current branch.
One  invocation per (mechanism, shape). Does the overlay survive, and is
the outcome consistent with what the command reported?

### skip-worktree    / ff-agents-md-differs
  merge rc:   1
  merge says: error: Your local changes to the following files would be overwritten by merge:
  AGENTS.md:  OVERLAY      -> overlay survived

### 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 / ff-agents-md-differs
  merge rc:   1
  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             / ff-agents-md-differs
  merge rc:   1
  merge says: error: Your local changes to the following files would be overwritten by merge:
  AGENTS.md:  OVERLAY      -> overlay survived

### 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 survived
Evidence: 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)

git version 2.50.1 (Apple Git-155)

### 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: aaa328c 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 run bin/fm-session-start.sh, bin/fm-spawn.sh, bin/fm-brief.sh, tasks-axi add, or any other fleet-management command.

### 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 run `bin/fm-session-start.sh`, `bin/fm-spawn.sh`, `bin/fm-brief.sh`, `tasks-axi add`, or any other fleet-management command.

### restored; suite green again
ok - fm-brief.sh: custom pause verb renders in every scaffold
ok - fm-brief.sh: investigation and visual-review completions load the shared decision policy
ok - fm-brief: scout and secondmate code paths still scaffold well-formed briefs
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 run bin/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 run bin/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.

Briefs generated by the real bin/fm-brief.sh in a scratch FM_HOME, showing the
emitted fleet-command fence line that reaches the crew agent.
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 run `bin/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 run `bin/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.
- Outcome: ⚠️ 2 issues (1 warning, 1 info) across 1 run (9m20s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • 🚨 bin/fm-crew-worktree-instructions-lib.sh:474 - fm_crew_rescue_legacy_sidecars does cp &#34;$wt/$dest&#34; &#34;$wt/$rel&#34; 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 remove hits 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 - The installed flag 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 status in a worktree with no overlay (including immediately after a successful remove, and on the primary checkout) prints "crew instruction overlay: installed in $WT" and advises running remove; remove then 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: "If git stash list shows an fm-crew: entry, in-progress instruction edits were saved there before this overlay was installed. Recover them with git 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 an fm-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 the fm-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 with git stash list/git stash show before popping, rather than an unconditional git 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 wedges git 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 -- &#34;$wt/$rel&#34; at line 433) and then restores with git checkout HEAD -- &#34;$rel&#34;, which overwrites the INDEX as well as the worktree. Concrete sequence: a crew worker restores AGENTS.md, edits it to S, runs git 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, then git checkout HEAD -- AGENTS.md resets 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.md records the index tree as stash^2, recoverable with git stash pop --index / git stash show &#39;stash@{0}^2&#39;. 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 when git ls-files -s -- $rel differs 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 to unowned-&lt;git-dir-basename&gt; 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 running bin/fm-crew-instructions.sh saved gets "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 exact git checkout &lt;ref&gt; -- &lt;rel&gt; 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 like v1..v2 or probe.lock passes 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 .lock in 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 - saved prints git checkout &lt;ref&gt; -- &lt;rel&gt; as the restore command, and recover repeats it at line 149 as "restore it again with ...". git checkout &lt;commit&gt; -- &lt;path&gt; 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 -- &#34;$rel&#34; 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 plain git commit quietly lands the recovered WIP. Print git checkout &lt;ref&gt; -- &lt;rel&gt; &amp;&amp; git reset -q HEAD -- &lt;rel&gt; so the advertised command matches what recover actually produces.
  • ℹ️ The authoritative intent states "In-progress instruction edits now go to git's own stash instead of a private 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." The shipped code no longer does this: FM_CREW_STASH_LABEL is gone and edits are saved under refs/fm-crew/<task-id>/instruction-wip/<n> (bin/fm-crew-worktree-instructions-lib.sh:88-89, 369-455). This is not a defect - the round-1 fix instructions explicitly ruled the stash carrier out and prescribed the per-task ref namespace, and the code, docs/architecture.md, and docs/verification/crew-instruction-overlay.md were all re-derived to match (the verification doc's new probe output genuinely demonstrates the pooled slot popping the primary checkout's unrelated stash entry). Flagging only so the stale bullet is refreshed before the PR body is written from it, since it currently describes a carrier the change deliberately rejects.

🔧 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 does git ls-files -s -- &#34;$rel&#34; | awk &#39;{print $2}&#39; 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 becomes AGENTS.md=&lt;sha1&gt;\n&lt;sha2&gt;\n&lt;sha3&gt;, 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. Every update-index --add --cacheinfo succeeds, so nothing errors: fm_crew_report_saved_entry prints "saved staged AGENTS.md to <ref>" and bin/fm-crew-instructions.sh saved advertises 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 runs recover - both call fm_crew_save_instruction_wip. The subsequent git checkout HEAD -- AGENTS.md for the disk path also silently collapses the conflict for that path. Fix: select stage 0 only (awk &#39;$3 == &#34;0&#34; {print $2}&#39;), 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 runs git add -A while 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 between add and commit), 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 saved lists 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 .lock to 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: owner probe..v2.lock folds to probe.v2, and owner probe.v2 is already probe.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 and recover restores 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 does mktemp &#34;$wt/.fm-crew-wip-index.XXXXXX&#34;, creating the temporary index inside the worktree. Every normal exit removes it, but a kill between mktemp and the final rm -f leaves an untracked .fm-crew-wip-index.* in the slot. freshen_spawn_worktree_base (bin/fm-spawn.sh:1775-1782) refuses on ANY git status --porcelain output 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 a M &lt;file&gt; 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.md pointer (this fleet's own house convention) and does not commit. git status --porcelain is exactly M CLAUDE.md. bin/fm-teardown.sh:1158 now pipes that through the filter, dirty becomes 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: a M line 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 own refs/fm-crew/&lt;task-id&gt;/instruction-wip/ refs", and the constant block comment at lines 94-97 says the same. The shipped namespace is refs/fm-crew/&lt;folded-slug&gt;-&lt;12-char-digest&gt;/instruction-wip/&lt;n&gt; (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 runs git for-each-ref refs/fm-crew/&lt;their-task-id&gt;/ 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 &lt;task-id&gt;-&lt;digest&gt;, or have the overlay body point only at bin/fm-crew-instructions.sh saved, which resolves the base itself.
  • ℹ️ bin/fm-crew-instructions.sh:132 - recover calls 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 second recover (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. saved then 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_paths is appended unconditionally when fm_crew_instruction_has_wip is true (line 563), but disk_pairs/disk_paths are 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 runs git 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 command saved printed 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 takes tail -n 1 of the sorted refs (bin/fm-crew-instructions.sh:129). Nothing is lost - A is still in entry 1 and saved lists 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.
⚠️ **Test** - 2 issues (1 warning, 1 info)
  • ⚠️ tests/fm-teardown.test.sh:2724 - tests/fm-teardown.test.sh aborts on the first failure, and the pre-existing herdr-preflight-missing-adapter failure is registered at position 13 while this change's four new overlay cases are at positions 22-25. A plain bash tests/fm-teardown.test.sh therefore never reaches 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, or test_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 with git archive and ran the teardown suite there, and herdr-preflight-missing-adapter fails 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 cases
  • bash tests/fm-brief.test.sh — full file green, including the newly added test_fleet_command_fence_carves_out_the_test_suite
  • New 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 real bin/fm-brief.sh and requires every emitted fleet-command fence line to carry the test-suite carve-out
  • Fail-before/pass-after proof for that test: replaced bin/fm-brief.sh with git show 8e3a68e:bin/fm-brief.sh, re-ran the suite (not ok - ship brief fences the fleet commands with no test-suite carve-out), then git checkout -- bin/fm-brief.sh and re-ran green
  • bash tests/fm-teardown.test.sh — reaches the pre-existing herdr-preflight-missing-adapter failure and aborts
  • Ran 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 -x into a scratch dir, then bash 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 &amp;&amp; git commit, git rebase main, git checkout &lt;moved-main-sha&gt;, git stash push, fm-crew-instructions.sh status|remove|saved|recover, two relaunches, reuse of the slot under a second task id, and fm_crew_filter_overlay_status fed real git status --porcelain output
  • Independently re-ran the concealment probe recorded in docs/verification/crew-instruction-overlay.md and diffed it against the committed output (commit sha normalized) — byte-identical match
  • Probed the doc's unreproduced side observation across skip-worktree/assume-unchanged/none x fast-forward/non-fast-forward merge, one git merge invocation per cell
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

cedmos added 10 commits August 23, 2026 17:16
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.
@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Reviews (3): Last reviewed commit: "no-mistakes: apply CI fixes" | Re-trigger Greptile

Comment thread bin/fm-crew-worktree-instructions-lib.sh
Comment thread bin/fm-crew-worktree-instructions-lib.sh
Comment thread bin/fm-crew-worktree-instructions-lib.sh Outdated
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