Skip to content

fix(bin): stop firstmate-repo crews from adopting the firstmate role - #2880

Open
cedmos wants to merge 5 commits into
kunchenguid:mainfrom
cedmos:fm/crew-role-identity-inversion
Open

fix(bin): stop firstmate-repo crews from adopting the firstmate role#2880
cedmos wants to merge 5 commits into
kunchenguid:mainfrom
cedmos:fm/crew-role-identity-inversion

Conversation

@cedmos

@cedmos cedmos commented Aug 23, 2026

Copy link
Copy Markdown

What Changed

  • New bin/fm-crew-worktree-instructions-lib.sh overlays crewmate project instructions onto the auto-loaded AGENTS.md and CLAUDE.md of a firstmate-repo ship or scout worktree (CLAUDE.md keeps the canonical @AGENTS.md pointer), hides them with skip-worktree, installs a worktree-local core.hooksPath pre-commit guard so an overlaid file cannot be committed with overlay content or silently dropped from a commit, and refuses the spawn when the overlay cannot be hidden or still presents firstmate identity; secondmate homes, non-firstmate projects, and primary checkouts are left alone.
  • bin/fm-spawn.sh installs the overlay after the pooled-worktree refresh and removes any stale overlay (plus its saved .fm-agents-md-edit/.fm-claude-md-edit sidecars) before that refresh, so a returned slot self-heals instead of wedging git reset --hard on an invisible skip-worktree entry; bin/fm-session-start.sh now exits 2 with no digest in a linked firstmate task worktree.
  • bin/fm-brief.sh scaffolds a crew role fence into ship and scout briefs ("You have no fleet", no fleet-management commands, no interactive dialog, conditional firstmate-coding-guidelines load) but not into secondmate charters; tests/fm-crew-worktree-instructions.test.sh covers the library and tests/fm-brief.test.sh asserts the fence, with AGENTS.md, CONTRIBUTING.md, docs/architecture.md, docs/scripts.md, the shard-timing table, and the guidelines skill updated to match.

Risk Assessment

⚠️ Medium: The core fix is sound, thoroughly behavior-tested through real git and real consumers, and correctly scoped to firstmate-shaped linked worktrees, but the skip-worktree mechanism it introduces opens a silent work-loss path at fm-teardown.sh's dirty check and can wedge the rebase the crew brief mandates - both addressable as follow-ups without reverting the change.

Testing

Ran the new crew-worktree-instructions suite plus fm-brief, and the three touched consumers (fm-session-start, fm-spawn-pool-base-freshen, fm-ensure-agents-md) — all green. For product-level proof I drove the real fm-brief/fm-spawn/fm-session-start scripts against a real clone of this repo in a throwaway sandbox fleet and captured two CLI transcripts: at the base commit a spawned firstmate-repo crew worktree still auto-loads "You are the first mate." and session start runs there, while at the target commit the same spawn leaves the crewmate role file as the auto-load corpus with the committed job untouched and git clean, session start refuses with exit 2, a reused pooled slot self-heals and refreshes onto a moved default branch, and the pre-commit guard blocks a commit that would silently drop an AGENTS.md edit until the documented restore is done. I used the repo's own documented FM_GATE_REFUSE_BYPASS test-harness hatch (the same one tests/lib.sh sets) so spawn would drive the temp sandbox fleet, never a real one. No screenshot artifact applies: this change has no rendered UI surface — the end-user surface is the agent-facing instruction files and CLI refusals, which the transcripts show verbatim. Worktree left clean and the per-task /tmp/fm-* roots the demo created were removed.

Evidence: End-to-end transcript at target commit: spawn → crew overlay, session-start refusal, commit guard, pooled-slot self-heal

Source: End-to-end transcript at target commit: spawn → crew overlay, session-start refusal, commit guard, pooled-slot self-heal


=== 1. The brief firstmate hands a crewmate now fences the role itself ===

$ sed -n '1,7p' '<TMP>/home/data/demo-crew-role-a1/brief.md'
You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human.
You are not the first mate, even if this worktree's project files say you are.
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.
You have no fleet.
Do not open an interactive question dialog; the only channel that reaches anyone is the status file named below.
If this task edits firstmate's shared tracked material, load `firstmate-coding-guidelines` from `.agents/skills/firstmate-coding-guidelines/SKILL.md` before editing.


=== 2. BEFORE the fix's overlay: what a harness auto-loads in the crew worktree ===

$ sed -n '1,8p' '<TMP>/pool/AGENTS.md'
# Firstmate

You are the first mate.
The user is the captain.
This file is your entire job description.

Address the user as "captain" at least once in every response.
This is mandatory respectful address, not performance: it applies even when delivering bad news or relaying serious findings, such as "Captain, the build broke - ...".

$ cat '<TMP>/pool/CLAUDE.md'
<!-- Points Claude at AGENTS.md via import; edit AGENTS.md, not this file. -->
@AGENTS.md

-> the worker is told it IS the first mate; that outranks its launch brief.

=== 3. Spawn the scout into that pooled firstmate worktree ===

$ fm_spawn demo-crew-role-a1 | tail -3
spawned demo-crew-role-a1 harness=codex kind=scout window=firstmate:fm-demo-crew-role-a1 worktree=<TMP>/pool

=== 4. AFTER: the same auto-load corpus a harness would read ===

$ cat '<TMP>/pool/AGENTS.md'
<!-- 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.

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.
You have no fleet.
Do not address anyone as captain.
Do not open an interactive question dialog.
The only channel that reaches the supervisor is the status file named in your launch brief.

# Editing this repository

The committed `AGENTS.md` is firstmate's own operating contract.
It is source you may edit when the task requires it, not your job description.
Load `.agents/skills/firstmate-coding-guidelines/SKILL.md` before editing firstmate's shared tracked material.

Git still has the committed files.
This working copy is a spawn-time overlay.

If this task requires editing `AGENTS.md` itself, restore the committed file first with `git update-index --no-skip-worktree AGENTS.md` and then `git checkout HEAD -- AGENTS.md`.
You remain a crewmate after that restore.
Editing an overlaid file without that restore does not fail silently: a pre-commit guard refuses the commit and names the same restore commands.
Committing this overlay over the committed file is refused for the same reason.

If `.fm-agents-md-edit` exists, in-progress `AGENTS.md` edits were saved there before this overlay was installed.
If `.fm-claude-md-edit` exists, the same is true for `CLAUDE.md`.
The pre-commit guard refuses every commit while either sidecar exists, so restore the committed file, re-apply the saved edits, and delete the sidecar.

## Maintaining this file

Keep this file for knowledge useful to almost every future agent session in this project.
Do not repeat what the codebase already shows; point to the authoritative file or command instead.
Prefer rewriting or pruning existing entries over appending new ones.
When updating this file, preserve this bar for all agents and keep entries concise.

$ cat '<TMP>/pool/CLAUDE.md'
<!-- Points Claude at AGENTS.md via import; edit AGENTS.md, not this file. -->
@AGENTS.md

=== 5. The committed job description is untouched, and git sees no dirt ===

$ git -C '<TMP>/pool' show HEAD:AGENTS.md | sed -n '1,2p'
# Firstmate


$ git -C '<TMP>/pool' status --porcelain --untracked-files=no; echo '(clean)'
(clean)

=== 6. A crewmate that still reaches for session start is refused ===

$ cd '<TMP>/pool' && FM_HOME='<TMP>/home' FM_ROOT_OVERRIDE='<TMP>/pool' FM_STATE_OVERRIDE='<TMP>/home/state' FM_DATA_OVERRIDE='<TMP>/home/data' FM_CONFIG_OVERRIDE='<TMP>/home/config' '<TMP>/firstmate/bin/fm-session-start.sh'; echo "exit=$?"
error: this checkout is a crewmate worktree of firstmate, not a firstmate home; do not run session start. You are a crewmate. Follow the launch brief and the crew worktree instructions in AGENTS.md.
exit=2

=== 7. The next task reuses the same overlaid slot: self-heal, refresh, re-overlay ===

$ fm_spawn demo-crew-role-b2 | tail -2
spawned demo-crew-role-b2 harness=codex kind=scout window=firstmate:fm-demo-crew-role-b2 worktree=<TMP>/pool

$ git -C '<TMP>/pool' log --oneline -1 origin/main
90dcc34 main moved on while the slot was overlaid

$ git -C '<TMP>/pool' log --oneline -1 HEAD
90dcc34 main moved on while the slot was overlaid

$ sed -n '1,6p' '<TMP>/pool/AGENTS.md'
<!-- firstmate-crew-worktree-instructions -->

# Role

You are a crewmate: an autonomous worker agent managed by firstmate.
You are not the first mate.

$ git -C '<TMP>/pool' status --porcelain --untracked-files=no; echo '(clean)'
(clean)

=== 8. Editing the overlaid AGENTS.md cannot vanish from a commit ===

$ git -C '<TMP>/pool' add -A && git -C '<TMP>/pool' commit -m 'crew edit' 2>&1; echo "exit=$?"
error: AGENTS.md is the crew overlay, hidden from git with skip-worktree, and it has been edited.
error: this commit would silently omit every change to AGENTS.md.
error: restore the committed file, then re-apply the intended source edits:
error:   git update-index --no-skip-worktree -- AGENTS.md && git checkout HEAD -- AGENTS.md
exit=1

=== 9. The restore the overlay documents makes the same edit land ===

$ git -C '<TMP>/pool' update-index --no-skip-worktree -- AGENTS.md

$ git -C '<TMP>/pool' checkout HEAD -- AGENTS.md

$ git -C '<TMP>/pool' commit -qam 'crew edit to firstmate AGENTS.md' 2>&1; echo "exit=$?"
exit=0

$ git -C '<TMP>/pool' show --stat --oneline HEAD | head -3
0d4ec8d crew edit to firstmate AGENTS.md
 AGENTS.md | 1 +
 1 file changed, 1 insertion(+)
Evidence: Baseline transcript at base commit f170ced: the role inversion this change removes

Source: Baseline transcript at base commit f170ced: the role inversion this change removes

BASE COMMIT (f170ced) - before the fix

$ sed -n '1,3p' '<TMP>/home/data/base-crew-role-z9/brief.md'
You are a crewmate: an autonomous worker agent managed by firstmate. Work on your own; do not wait for a human.

# Task

$ fm_spawn base-crew-role-z9 | tail -1
spawned base-crew-role-z9 harness=codex kind=scout window=firstmate:fm-base-crew-role-z9 worktree=<TMP>/pool

-- what the harness auto-loads in that crew worktree after spawn --

$ sed -n '1,6p' '<TMP>/pool/AGENTS.md'
# Firstmate

You are the first mate.
The user is the captain.
This file is your entire job description.


-- and session start still runs there, so a crewmate can take the helm --

$ cd '<TMP>/pool' && FM_HOME='<TMP>/home' FM_ROOT_OVERRIDE='<TMP>/pool' FM_STATE_OVERRIDE='<TMP>/home/state' FM_DATA_OVERRIDE='<TMP>/home/data' FM_CONFIG_OVERRIDE='<TMP>/home/config' timeout 120 '<TMP>/firstmate/bin/fm-session-start.sh' 2>&1 | head -5; echo '...'

================================================================================
SESSION START - <TMP>/home
================================================================================

...
Evidence: Demo driver used for the target-commit transcript (reproducible)

Source: Demo driver used for the target-commit transcript (reproducible)

#!/usr/bin/env bash
# End-to-end demo of the crew role-identity fix, driven through the real
# bin/fm-spawn.sh, bin/fm-brief.sh and bin/fm-session-start.sh against a real
# clone of the firstmate repo. Only tmux/treehouse are faked, because this
# machine has no live pane to launch into.
set -u

ROOT=${1:?worktree root}
SHA=${2:?target sha}
DEMO=$(mktemp -d "${TMPDIR:-/tmp}/fm-crew-demo.XXXXXX")
PROJECT="$DEMO/firstmate"
ORIGIN="$DEMO/origin.git"
POOL="$DEMO/pool"
FMHOME="$DEMO/home"
FAKEBIN="$DEMO/fakebin"

step() { printf '\n\033[1m=== %s ===\033[0m\n' "$*"; }
run()  { printf '\n$ %s\n' "$*"; eval "$@"; }

# --- fixture: a real firstmate checkout, its origin, and a pooled worktree ----
git clone --quiet --no-hardlinks --branch fm/crew-role-identity-inversion "$ROOT" "$PROJECT"
git -C "$PROJECT" checkout -q -B main "$SHA"
git -C "$PROJECT" config user.name 'fm demo'
git -C "$PROJECT" config user.email 'fm-demo@example.invalid'
git clone --quiet --bare "$PROJECT" "$ORIGIN"
git -C "$PROJECT" remote set-url origin "file://$ORIGIN"
git -C "$PROJECT" worktree add --quiet --detach "$POOL" HEAD

mkdir -p "$FAKEBIN" "$FMHOME/data" "$FMHOME/projects" "$FMHOME/state" "$FMHOME/config"
printf 'codex\n' > "$FMHOME/config/crew-harness"
touch "$FMHOME/state/.last-watcher-beat"
cat > "$FAKEBIN/tmux" <<'SH'
#!/usr/bin/env bash
set -u
case "$*" in *"#{pane_current_path}"*) printf '%s\n' "$FM_FAKE_PANE_PATH"; exit 0 ;; esac
case "${1:-}" in display-message) printf 'firstmate\n'; exit 0 ;; esac
exit 0
SH
printf '#!/bin/sh\nexit 0\n' > "$FAKEBIN/treehouse"
chmod +x "$FAKEBIN/tmux" "$FAKEBIN/treehouse"

fm_spawn() {  # <id>
  FM_ROOT_OVERRIDE='' FM_HOME="$FMHOME" \
    FM_STATE_OVERRIDE="$FMHOME/state" FM_DATA_OVERRIDE="$FMHOME/data" \
    FM_PROJECTS_OVERRIDE="$FMHOME/projects" FM_CONFIG_OVERRIDE="$FMHOME/config" \
    FM_SPAWN_NO_GUARD=1 TMUX="fake,1,0" FM_FAKE_PANE_PATH="$POOL" \
    FM_GATE_REFUSE_BYPASS=1 \
    PATH="$FAKEBIN:$PATH" "$PROJECT/bin/fm-spawn.sh" "$1" "$PROJECT" --scout 2>&1
}
# Same documented test-harness escape hatch tests/lib.sh uses: this demo drives
# only its own temp-sandbox fleet, never the real one.

step "1. The brief firstmate hands a crewmate now fences the role itself"
FM_HOME="$FMHOME" FM_DATA_OVERRIDE="$FMHOME/data" \
  "$PROJECT/bin/fm-brief.sh" demo-crew-role-a1 firstmate --scout >/dev/null 2>&1
run "sed -n '1,7p' '$FMHOME/data/demo-crew-role-a1/brief.md'"

step "2. BEFORE the fix's overlay: what a harness auto-loads in the crew worktree"
run "sed -n '1,8p' '$POOL/AGENTS.md'"
run "cat '$POOL/CLAUDE.md'"
printf '\n-> the worker is told it IS the first mate; that outranks its launch brief.\n'

step "3. Spawn the scout into that pooled firstmate worktree"
run "fm_spawn demo-crew-role-a1 | tail -3"

step "4. AFTER: the same auto-load corpus a harness would read"
run "cat '$POOL/AGENTS.md'"
run "cat '$POOL/CLAUDE.md'"

step "5. The committed job description is untouched, and git sees no dirt"
run "git -C '$POOL' show HEAD:AGENTS.md | sed -n '1,2p'"
run "git -C '$POOL' status --porcelain --untracked-files=no; echo '(clean)'"

step "6. A crewmate that still reaches for session start is refused"
run "cd '$POOL' && FM_HOME='$FMHOME' FM_ROOT_OVERRIDE='$POOL' FM_STATE_OVERRIDE='$FMHOME/state' FM_DATA_OVERRIDE='$FMHOME/data' FM_CONFIG_OVERRIDE='$FMHOME/config' '$PROJECT/bin/fm-session-start.sh'; echo \"exit=\$?\""

step "7. The next task reuses the same overlaid slot: self-heal, refresh, re-overlay"
git -C "$PROJECT" commit -q --allow-empty -m 'main moved on while the slot was overlaid'
git -C "$PROJECT" push -q origin main
FM_HOME="$FMHOME" FM_DATA_OVERRIDE="$FMHOME/data" \
  "$PROJECT/bin/fm-brief.sh" demo-crew-role-b2 firstmate --scout >/dev/null 2>&1
run "fm_spawn demo-crew-role-b2 | tail -2"
run "git -C '$POOL' log --oneline -1 origin/main"
run "git -C '$POOL' log --oneline -1 HEAD"
run "sed -n '1,6p' '$POOL/AGENTS.md'"
run "git -C '$POOL' status --porcelain --untracked-files=no; echo '(clean)'"

step "8. Editing the overlaid AGENTS.md cannot vanish from a commit"
printf 'A crewmate edit to the real job description.\n' >> "$POOL/AGENTS.md"
run "git -C '$POOL' add -A && git -C '$POOL' commit -m 'crew edit' 2>&1; echo \"exit=\$?\""

step "9. The restore the overlay documents makes the same edit land"
run "git -C '$POOL' update-index --no-skip-worktree -- AGENTS.md"
run "git -C '$POOL' checkout HEAD -- AGENTS.md"
printf 'A crewmate edit to the real job description.\n' >> "$POOL/AGENTS.md"
run "git -C '$POOL' commit -qam 'crew edit to firstmate AGENTS.md' 2>&1; echo \"exit=\$?\""
run "git -C '$POOL' show --stat --oneline HEAD | head -3"

printf '\n'
git -C "$PROJECT" worktree remove --force "$POOL" >/dev/null 2>&1
rm -rf "$DEMO"
Evidence: Demo driver used for the base-commit baseline (reproducible)

Source: Demo driver used for the base-commit baseline (reproducible)

#!/usr/bin/env bash
# Same spawn, run against the BASE commit (before the fix), to show the
# role inversion this change removes.
set -u

ROOT=${1:?worktree root}
SHA=${2:?base sha}
DEMO=$(mktemp -d "${TMPDIR:-/tmp}/fm-crew-base.XXXXXX")
PROJECT="$DEMO/firstmate"; ORIGIN="$DEMO/origin.git"; POOL="$DEMO/pool"
FMHOME="$DEMO/home"; FAKEBIN="$DEMO/fakebin"

run() { printf '\n$ %s\n' "$*"; eval "$@"; }

git clone --quiet --no-hardlinks --branch fm/crew-role-identity-inversion "$ROOT" "$PROJECT"
git -C "$PROJECT" checkout -q -B main "$SHA"
git -C "$PROJECT" config user.name 'fm demo'
git -C "$PROJECT" config user.email 'fm-demo@example.invalid'
git clone --quiet --bare "$PROJECT" "$ORIGIN"
git -C "$PROJECT" remote set-url origin "file://$ORIGIN"
git -C "$PROJECT" worktree add --quiet --detach "$POOL" HEAD

mkdir -p "$FAKEBIN" "$FMHOME/data/base-crew-role-z9" "$FMHOME/projects" "$FMHOME/state" "$FMHOME/config"
printf 'codex\n' > "$FMHOME/config/crew-harness"
touch "$FMHOME/state/.last-watcher-beat"
cat > "$FAKEBIN/tmux" <<'SH'
#!/usr/bin/env bash
set -u
case "$*" in *"#{pane_current_path}"*) printf '%s\n' "$FM_FAKE_PANE_PATH"; exit 0 ;; esac
case "${1:-}" in display-message) printf 'firstmate\n'; exit 0 ;; esac
exit 0
SH
printf '#!/bin/sh\nexit 0\n' > "$FAKEBIN/treehouse"
chmod +x "$FAKEBIN/tmux" "$FAKEBIN/treehouse"

printf '%s\n' 'BASE COMMIT (f170ced) - before the fix'
FM_HOME="$FMHOME" FM_DATA_OVERRIDE="$FMHOME/data" \
  "$PROJECT/bin/fm-brief.sh" base-crew-role-z9 firstmate --scout >/dev/null 2>&1
run "sed -n '1,3p' '$FMHOME/data/base-crew-role-z9/brief.md'"

fm_spawn() {
  FM_ROOT_OVERRIDE='' FM_HOME="$FMHOME" \
    FM_STATE_OVERRIDE="$FMHOME/state" FM_DATA_OVERRIDE="$FMHOME/data" \
    FM_PROJECTS_OVERRIDE="$FMHOME/projects" FM_CONFIG_OVERRIDE="$FMHOME/config" \
    FM_SPAWN_NO_GUARD=1 TMUX="fake,1,0" FM_FAKE_PANE_PATH="$POOL" \
    FM_GATE_REFUSE_BYPASS=1 PATH="$FAKEBIN:$PATH" \
    "$PROJECT/bin/fm-spawn.sh" "$1" "$PROJECT" --scout 2>&1
}
run "fm_spawn base-crew-role-z9 | tail -1"

printf '\n-- what the harness auto-loads in that crew worktree after spawn --\n'
run "sed -n '1,6p' '$POOL/AGENTS.md'"

printf '\n-- and session start still runs there, so a crewmate can take the helm --\n'
run "cd '$POOL' && FM_HOME='$FMHOME' FM_ROOT_OVERRIDE='$POOL' FM_STATE_OVERRIDE='$FMHOME/state' FM_DATA_OVERRIDE='$FMHOME/data' FM_CONFIG_OVERRIDE='$FMHOME/config' timeout 120 '$PROJECT/bin/fm-session-start.sh' 2>&1 | head -5; echo '...'"

git -C "$PROJECT" worktree remove --force "$POOL" >/dev/null 2>&1
rm -rf "$DEMO"
Evidence: What a crewmate now reads before vs after (excerpt from the transcripts)
BEFORE (base f170ced), after a real spawn:
# Firstmate
You are the first mate.
The user is the captain.
This file is your entire job description.
$ fm-session-start.sh -> SESSION START - <home> (digest prints, exit 0)

AFTER (8e3a68e), same spawn:
<!-- firstmate-crew-worktree-instructions -->
# Role
You are a crewmate: an autonomous worker agent managed by firstmate.
You are not the first mate.
$ fm-session-start.sh
error: this checkout is a crewmate worktree of firstmate, not a firstmate home; do not run session start.
exit=2
$ git commit -m 'crew edit' (AGENTS.md edited under the overlay)
error: AGENTS.md is the crew overlay, hidden from git with skip-worktree, and it has been edited.
error: this commit would silently omit every change to AGENTS.md.
exit=1
committed AGENTS.md in HEAD still starts "# Firstmate"; git status clean
Evidence: CONTRIBUTING claim check: guidelines load line is scaffolded into every crewmate brief
no-mistakes ship brief: 1
direct-PR ship brief: 1
local-only ship brief: 1
--scout scout brief: 1

line 6: If this task edits firstmate's shared tracked material, load `firstmate-coding-guidelines` from `.agents/skills/firstmate-coding-guidelines/SKILL.md` before editing.

Pipeline

Updates from git push no-mistakes

⏭️ **intent** - skipped

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 4 issues (3 warnings, 1 info)
  • ⚠️ bin/fm-crew-worktree-instructions-lib.sh:202 - The overlay makes crewmate work on the instruction files invisible to fm-teardown.sh's uncommitted-work guard, so it can be discarded without a refusal. Concrete path: a crew in a firstmate-repo worktree edits the overlaid AGENTS.md in place (the case the pre-commit guard exists precisely because it is expected). skip-worktree (fm_crew_skip_worktree, line 212) makes git status --porcelain report nothing. fm-teardown.sh:1145 computes dirty from exactly that command and its filter at :1153 only whitelists .claude/ and .fm-(grok|kimi)-turnend, so dirty is empty and validate_worktree_teardown_safety returns 0 instead of REFUSED at :1183. The edit is destroyed. The same hole covers the saved sidecar: fm_crew_save_instruction_wip appends .fm-agents-md-edit to info/exclude (line 202), so a sidecar holding a relaunch-sidelined AGENTS.md edit is not even an untracked ?? entry that would have blocked teardown. The change closes this silent-loss class at the commit boundary but not at the teardown boundary. Earliest supported shared boundary: export a predicate from this library (e.g. fm_crew_worktree_has_unsaved_instruction_work, reusing fm_crew_file_is_installed_overlay plus a sidecar existence check) and have validate_worktree_teardown_safety consult it alongside git status, rather than adding a third symptom-specific guard.
  • ⚠️ bin/fm-crew-worktree-instructions-lib.sh:212 - The skip-worktree bits wedge the branch-freshening step the crew brief mandates, with no in-task recovery documented. bin/fm-brief.sh:381 instructs every crewmate: "if main has advanced, rebase onto it so the eventual merge stays a fast-forward", and bin/fm-merge-local.sh:61 makes firstmate issue the same instruction. Concrete path: a firstmate-repo crew is spawned, the overlay sets skip-worktree on AGENTS.md and CLAUDE.md, main then advances with a commit touching AGENTS.md (routine in this repo - this very branch changes it), and the crew runs git rebase origin/main. git's unpack-trees refuses with "Your local changes to the following files would be overwritten by checkout: AGENTS.md". This branch's own test test_removal_unwedges_a_pooled_reset (tests/fm-crew-worktree-instructions.test.sh:442) asserts that exact refusal for git reset --hard origin/main, proving the git behavior; rebase/merge/checkout go through the same machinery. fm_remove_crew_worktree_instructions only runs at the NEXT spawn (bin/fm-spawn.sh:1745), so the running crew has no supported exit: the overlay body (lines 83-86) documents restoring AGENTS.md only for the case where the task edits it, never mentions CLAUDE.md's restore, and never connects a failing rebase to the overlay. Options: have the overlay body name this failure and its recipe for both files, or clear the bits around a base refresh instead of leaving them set for the task's whole lifetime.
  • ⚠️ bin/fm-brief.sh:307 - The new absolute claim in the scout brief contradicts a capability the same generated brief grants 40 lines later. Line 307 emits "the only channel that reaches anyone is the status file named below", but line 347 of the same heredoc tells the scout "If your deliverable is a visual artifact the captain will review and iterate on, you may host the Lavish review loop yourself (poll, revise, re-serve, staying alive)", and line 349 routes it through the captain-hold-lifecycle completion gate. A scout that honors line 307 will not host the Lavish loop, silently dropping a supported scout deliverable path; a scout that honors line 347 has to decide line 307 is wrong. The intended fence appears to be the harness question dialog specifically, not every channel. Narrowing the clause (e.g. "the only channel that reaches firstmate is the status file named below") would fence the dialog without contradicting the Lavish loop.
  • ℹ️ bin/fm-crew-worktree-instructions-lib.sh:117 - fm_crew_write_overlay_body has no callers anywhere in the repository - fm_crew_install_overlay_file writes through fm_crew_overlay_content instead, which is the variant that handles the CLAUDE.md pointer case. Leaving it in invites a future caller to use it for CLAUDE.md and write the crew body over the canonical @AGENTS.md pointer, which would make the brief-mandated fm-ensure-agents-md.sh report a conflict. Delete it.
✅ **Test** - passed

✅ No issues found.

  • bin/fm-test-run.sh tests/fm-crew-worktree-instructions.test.sh — all 22 cases pass (overlay, refusals, commit guard, sidecars, pooled reuse, real spawn)
  • bin/fm-test-run.sh tests/fm-brief.test.sh — includes the new crew-role-fence assertions for ship, scout, and the secondmate negative case
  • bin/fm-test-run.sh tests/fm-session-start.test.sh tests/fm-spawn-pool-base-freshen.test.sh tests/fm-ensure-agents-md.test.sh — regression check on the three consumers this change touches
  • Manual end-to-end at target commit: bash crew-role-demo.sh &lt;worktree&gt; 8e3a68e — real fm-brief.sh + fm-spawn.sh --scout into a pooled worktree of a real firstmate clone, then fm-session-start.sh in that worktree, git commit over the overlay, the documented git update-index --no-skip-worktree -- AGENTS.md &amp;&amp; git checkout HEAD -- AGENTS.md restore, and a second spawn into the same slot after origin/main moved
  • Manual baseline at base commit: bash crew-role-baseline.sh &lt;worktree&gt; f170ced — same spawn shows the pre-fix role inversion (auto-loaded AGENTS.md still says "You are the first mate.", session start prints the digest)
  • Doc-claim check: generated briefs with bin/fm-brief.sh &lt;id&gt; firstmate --mode {no-mistakes,direct-PR,local-only} and --scout, confirming each scaffolds the firstmate-coding-guidelines load line CONTRIBUTING.md now claims
⚠️ **Document** - 2 infos
  • ℹ️ docs/fm-test-portable-shards.md:72 - docs/fm-test-portable-shards.md's own rule is to refresh the per-script weight hints in bin/fm-test-run.sh whenever the serial lane gains scripts. The new tests/fm-crew-worktree-instructions.test.sh has no measured hint and falls back to PORTABLE_SERIAL_DEFAULT_WEIGHT_MS (20000), and the recomputed table shows the lane was already carrying one other unhinted script before this change. I updated the derived table to current truth, but refreshing the hints themselves needs per-shard timing artifacts from a green CI run and a code edit to bin/fm-test-run.sh, both outside this documentation phase.
  • ℹ️ docs/architecture.md:170 - docs/architecture.md:170 asserted that firstmate's operating checkout (FM_ROOT) is itself a linked git worktree. That contradicts bin/fm-tangle-lib.sh's header ("the PRIMARY checkout ... is a normal checkout on a real branch"), bin/fm-primary-scope-lib.sh ("only a plain checkout is primary, never a linked task worktree"), docs/turnend-guard.md's guard-predicate section, and this change's new linked-worktree session-start refusal, which would refuse in a linked FM_ROOT. I resolved the conflict in favor of the three code owners and corrected the prose. Flagging it because it is a pre-existing architectural premise, not a fact this change introduced.
⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 1)
✅ **Push** - passed

✅ No issues found.

cedmos added 4 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.
@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge because no unacknowledged blocking failure remains.

No blocking failure remains; the two previously reported silent-loss behaviors are explicitly deferred to the separate hiding-mechanism redesign described by the author.

Reviews (2): Last reviewed commit: "no-mistakes(document): fix stale brief-s..." | Re-trigger Greptile

Comment on lines +381 to +384
if ! rm -f "$wt/$FM_CREW_AGENTS_WIP" "$wt/$FM_CREW_CLAUDE_WIP"; then
echo "error: could not remove the saved instruction sidecars in $wt" >&2
failed=1
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Cleanup deletes saved instruction work

When a task is interrupted after modified AGENTS.md or CLAUDE.md content is saved to an excluded sidecar, the next pooled-slot refresh removes that sidecar before checking cleanliness, permanently deleting the only durable copy of the worker's uncommitted edits.

Context Used: If there is a VISION.md file at the root of the re... (source)

Comment on lines +187 to +197
head_hash=$(git -C "$wt" rev-parse --verify --quiet "HEAD:$rel" 2>/dev/null) || return 1
disk_hash=$(git -C "$wt" hash-object --no-filters -- "$wt/$rel" 2>/dev/null) || return 1
[ -n "$head_hash" ] && [ -n "$disk_hash" ] || return 1
[ "$head_hash" != "$disk_hash" ]
}

fm_crew_save_instruction_wip() { # <worktree> <rel> <dest>
local wt=$1 rel=$2 dest=$3
[ -f "$wt/$rel" ] || return 0
fm_crew_file_is_installed_overlay "$wt" "$rel" && return 0
fm_crew_file_differs_from_head "$wt" "$rel" || return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing HEAD blob bypasses WIP save

When a relaunch worktree contains an untracked instruction file with no corresponding HEAD blob, fm_crew_file_differs_from_head returns false and the save helper reports success without creating a sidecar; overlay installation then replaces the file and silently destroys its in-progress content.

@cedmos cedmos changed the title fix(spawn): stop firstmate-repo crews from adopting the firstmate role fix(bin): stop firstmate-repo crews from adopting the firstmate role Aug 23, 2026
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