Skip to content

fix(spawn): give every launched worker its own FM_HOME - #81

Merged
Freudator86 merged 2 commits into
mainfrom
fm/firstmate-crew-inherits-wrong-bridge-seat
Aug 8, 2026
Merged

fix(spawn): give every launched worker its own FM_HOME#81
Freudator86 merged 2 commits into
mainfrom
fm/firstmate-crew-inherits-wrong-bridge-seat

Conversation

@Freudator86

Copy link
Copy Markdown
Owner

Intent

Make a firstmate-launched worker run as its own vessel, so its Bridge mail is not composed and published from another seat's clone.

Measured defect: two firstmate homes share one OS account on this machine (/home/captain/sc1-firstmate, vessel sc1; /home/captain/firstmate-upstream, vessel tugboat). The operator's ~/.bashrc derives the Bridge seat from FM_HOME and deliberately defaults to sc1 when FM_HOME is unset. That file is the operator's own and is NOT to be changed - explicitly out of scope. The bug is on firstmate's side: bin/fm-spawn.sh exported FM_HOME into the launched pane only for KIND=secondmate, so an ordinary crewmate or scout started with FM_HOME unset, the profile resolved the seat to sc1, and every bridge-*.sh command that worker ran read and wrote the staff captain's clone. Evidence: an envelope whose own from field said tugboat was carried by a commit authored 'sc1 sc1@fleet.local' out of sc1's clone; our own clone's git identity was already correct (tugboat), which is why a git-config change would fix nothing. This matters because the fleet's approval chain authenticates an envelope by its commit author.

Decisions made while doing the work, which a reviewer reading only the diff would not know:

  1. The originally-specified fix (widen the secondmate-only FM_HOME= launch-command prefix to every kind) was implemented AND then measured to be necessary but NOT sufficient. ~/.bashrc returns early for non-interactive shells, and the seat variables are exported once by the pane's interactive shell - which fm-spawn starts BEFORE it sends the launch command. Every later non-interactive shell inherits the stale value rather than recomputing it. Measured: 'FM_HOME= bash -c echo $BRIDGE_VESSEL' still printed sc1, and with BRIDGE_VESSEL scrubbed it printed nothing at all, proving the derivation never re-runs. An earlier proof of mine that used 'bash -ic' was invalid because it forced interactivity real tool shells never have.

  2. Therefore the fix is deliberately in two places, and the second is not a redundant copy of the first: (a) the launch-command FM_HOME= prefix for every kind, which reaches the agent process on every backend and is what makes fm-send, the findings surface and other FM_HOME-resolving scripts answer for this home; (b) tmux new-window -e FM_HOME=, which tmux applies before the window's shell starts, so the existing profile derivation comes out right. Verified on real tmux 3.6: a window created with -e printed seat=tugboat.

  3. Layering was chosen deliberately. Firstmate names ONLY its own variable FM_HOME and never BRIDGE_VESSEL/BRIDGE_ROOT or any other profile-derived name; re-deriving another tool's variables was considered and rejected as a wrong-layer fix.

  4. Known and deliberately-accepted limit, recorded rather than hidden: only tmux has a pre-shell environment seam. herdr, zellij, Orca and cmux create a task through CLIs taking a label and cwd and no environment, so on those a worker learns its home from the launch command only and a profile-derived value is still fixed before firstmate can influence it. docs/tmux-backend.md records this, the measurement, the negative control, and the tmux 3.0 floor for -e. This also only affects newly spawned workers; nothing re-derives for one already running.

  5. AXI_LAUNCH_HOME was renamed to LAUNCH_HOME and hoisted to before task-surface creation, because it now serves both the AXI bin path and the pane environment and must exist earlier than it used to. Its per-kind value is unchanged (this process's FM_HOME for crew/scout, PROJ_ABS for --secondmate). The secondmate branch's clearing of FM_ROOT/STATE/DATA/PROJECTS/CONFIG_OVERRIDE is deliberately left exactly as it was; ordinary crew must NOT clear those.

  6. The five exact-equality launch-line assertions in tests/fm-spawn-dispatch-profile.test.sh were updated to carry the new FM_HOME=' ' prefix; that prefix appearing on the raw launch-command escape hatch is intended, not an accident. Two new tests pin the change: an ordinary crew launch names its own home both in the created window's environment and on the launch command, and a secondmate window is created in the SECONDMATE's home rather than the primary's.

  7. Checked and confirmed safe rather than assumed: no path assumes FM_HOME is unset inside a task worktree. bin/fm-test-run.sh already unsets FM_HOME and the FM_*_OVERRIDE vars per test script; every FM_HOME-sensitive tracked hook is gated by fm_primary_scope_matches, which refuses a linked task worktree before FM_HOME is consulted, and fm-spawn asserts the worktree is linked; bin/fm-send.sh fails closed WITHOUT FM_HOME, so inheriting it only removes a failure mode.

Repo constraints: this is firstmate's own shared tracked material, so firstmate-coding-guidelines applies - one sentence per line in tracked Markdown, plain dash never an em dash, shellcheck-clean bin scripts, tests colocated in tests/ extending the existing suite rather than a new runner, and backend-verification docs recording exact commands, versions and output. No agent co-author on commits.

Pre-existing and unrelated: tests/fm-backend-tmux-smoke.test.sh fails on this machine with 'the tmux task shell did not become ready'. It was verified to fail identically against a pristine extract of the base commit cb55c08, so it is not caused by this change and was deliberately not chased.

What Changed

  • bin/fm-spawn.sh now prefixes FM_HOME=<home> onto the launch command for every spawn kind, not just --secondmate, so an ordinary crewmate or scout resolves this home's state, findings surface and fm-send targets instead of falling back to another home's clone. AXI_LAUNCH_HOME was renamed to LAUNCH_HOME and hoisted above task-surface creation so both the AXI bin path and the pane environment can use it; its per-kind value (this process's FM_HOME, or PROJ_ABS for a secondmate) and the secondmate-only clearing of FM_ROOT/STATE/DATA/PROJECTS/CONFIG_OVERRIDE are unchanged.
  • fm_backend_tmux_create_task takes an optional <fm-home> and passes it as new-window -e FM_HOME=..., which tmux applies before the window's shell starts - the launch-command prefix alone arrives after a shell profile has already derived its per-vessel values. The flag is omitted entirely when no home is given.
  • Tests: the five exact-equality launch-line assertions in tests/fm-spawn-dispatch-profile.test.sh now carry the FM_HOME= prefix, plus two new cases pinning that an ordinary crew launch names its own home in both the window environment and the launch command, and that a secondmate window is created in the secondmate's home. docs/tmux-backend.md records the measurement, the negative control, the tmux 3.0 floor for -e, and the accepted limit that herdr, zellij, Orca and cmux have no pre-shell environment seam; docs/configuration.md notes the per-worker inheritance.

Risk Assessment

✅ Low: The change is tightly bounded to two well-understood seams (an env prefix on the launch command and an optional tmux new-window -e), preserves the secondmate path byte-for-byte, is covered by two new tests plus five updated exact-equality assertions, and the surrounding invariants (hook scope gate, test-runner env scrubbing, fm-send fail-closed, other tmux argv assertions) were each checked and hold.

Testing

Ran the targeted spawn/backend suites (fm-spawn-dispatch-profile with its two new tests, fm-backend, fm-spawn-batch, fm-spawn-worktree-settle, fm-secondmate-harness, fm-backend-orca, fm-backend-herdr) - all pass - and then proved the intent end-to-end on a real tmux 3.6 server: with a sandbox profile mirroring the operator's FM_HOME-derived Bridge seat, a crewmate belonging to the tugboat home publishes as vessel=sc1 from sc1's clone on base cb55c08, still publishes as sc1 when only the launch-command FM_HOME prefix is added, and publishes as vessel=tugboat from tugboat's clone on the target commit; a second transcript shows the real fm-spawn issuing both new-window -e FM_HOME=&lt;home&gt; and the FM_HOME=&#39;&lt;home&gt;&#39; launch prefix, with the secondmate case naming the secondmate's home in both places. This is a shell/tmux change with no rendered UI surface, so the reviewer-visible artifacts are CLI transcripts rather than screenshots. tests/fm-backend-tmux-smoke.test.sh fails, but reproduces identically against a pristine extract of base cb55c08, so it is pre-existing and unrelated; the working tree is clean and transient sandbox directories were removed.

Evidence: Real tmux: worker seat before vs after (base / prefix-only / target)

# tmux 3.6 on Linux 6.18.33.2-microsoft-standard-WSL2 # one OS account, two firstmate homes: .../sc1-firstmate -> vessel sc1 (profile default when FM_HOME is unset), .../firstmate-upstream -> vessel tugboat # in every case below, firstmate is spawning a crewmate that belongs to the TUGBOAT home. === A-base-cb55c08 === agent process FM_HOME : <unset> bridge-.sh publishes: vessel=sc1 clone=.../sc1-firstmate/projects/coditan-bridge === B-base-plus-launch-prefix-only === agent process FM_HOME : .../firstmate-upstream bridge-.sh publishes: vessel=sc1 clone=.../sc1-firstmate/projects/coditan-bridge === C-target-25e06b5 === agent process FM_HOME : .../firstmate-upstream bridge-*.sh publishes: vessel=tugboat clone=.../firstmate-upstream/projects/coditan-bridge

# tmux 3.6 on Linux 6.18.33.2-microsoft-standard-WSL2
# one OS account, two firstmate homes:
#   /tmp/no-mistakes-evidence/01KZHMCYP25C7JMDJEAAX8D5ST/sandbox/home/sc1-firstmate       -> vessel sc1 (profile default when FM_HOME is unset)
#   /tmp/no-mistakes-evidence/01KZHMCYP25C7JMDJEAAX8D5ST/sandbox/home/firstmate-upstream -> vessel tugboat
# in every case below, firstmate is spawning a crewmate that belongs to the TUGBOAT home.

=== A-base-cb55c08 ===
  agent process FM_HOME : <unset>
  bridge-*.sh publishes: vessel=sc1 clone=/tmp/no-mistakes-evidence/01KZHMCYP25C7JMDJEAAX8D5ST/sandbox/home/sc1-firstmate/projects/coditan-bridge

=== B-base-plus-launch-prefix-only ===
  agent process FM_HOME : /tmp/no-mistakes-evidence/01KZHMCYP25C7JMDJEAAX8D5ST/sandbox/home/firstmate-upstream
  bridge-*.sh publishes: vessel=sc1 clone=/tmp/no-mistakes-evidence/01KZHMCYP25C7JMDJEAAX8D5ST/sandbox/home/sc1-firstmate/projects/coditan-bridge

=== C-target-25e06b5 ===
  agent process FM_HOME : /tmp/no-mistakes-evidence/01KZHMCYP25C7JMDJEAAX8D5ST/sandbox/home/firstmate-upstream
  bridge-*.sh publishes: vessel=tugboat clone=/tmp/no-mistakes-evidence/01KZHMCYP25C7JMDJEAAX8D5ST/sandbox/home/firstmate-upstream/projects/coditan-bridge
Evidence: Real fm-spawn.sh: the tmux commands issued for a crewmate and a secondmate

=== ordinary-crewmate === this firstmate home : /tmp/.../ordinary-crewmate/home window created with : tmux new-window -dP -F #{window_id} -t firstmate: -n fm-crew-t1 -c /tmp/.../project -e FM_HOME=/tmp/.../ordinary-crewmate/home launch command sent : FM_HOME='/tmp/.../ordinary-crewmate/home' CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions --settings '...' === secondmate === this firstmate home : /tmp/.../secondmate/home secondmate home : /tmp/.../secondmate/secondmate-home window created with : tmux new-window -dP -F #{window_id} -t firstmate: -n fm-second-t2 -c /tmp/.../secondmate-home -e FM_HOME=/tmp/.../secondmate/secondmate-home launch command sent : FM_ROOT_OVERRIDE= FM_STATE_OVERRIDE= FM_DATA_OVERRIDE= FM_PROJECTS_OVERRIDE= FM_CONFIG_OVERRIDE= FM_HOME='/tmp/.../secondmate/secondmate-home' ... claude --dangerously-skip-permissions ...

=== ordinary-crewmate ===
this firstmate home : /tmp/fm-spawn-transcript.2vFKPm/ordinary-crewmate/home
window created with : tmux new-window -dP -F #{window_id} -t firstmate: -n fm-crew-t1 -c /tmp/fm-spawn-transcript.2vFKPm/ordinary-crewmate/project -e FM_HOME=/tmp/fm-spawn-transcript.2vFKPm/ordinary-crewmate/home
launch command sent : FM_HOME='/tmp/fm-spawn-transcript.2vFKPm/ordinary-crewmate/home' CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions --settings '/tmp/fm-spawn-transcript.2vFKPm/ordinary-crewmate/wt/.claude/settings.fm-task.json' "$('/home/captain/.no-mistakes/worktrees/d6232fda45ac/01KZHMCYP25C7JMDJEAAX8D5ST/bin/fm-operational-input.sh' encode launch-brief < '/tmp/fm-spawn-transcript.2vFKPm/ordinary-crewmate/home/data/crew-t1/brief.md')"

=== secondmate ===
this firstmate home : /tmp/fm-spawn-transcript.2vFKPm/secondmate/home
secondmate home     : /tmp/fm-spawn-transcript.2vFKPm/secondmate/secondmate-home
window created with : tmux new-window -dP -F #{window_id} -t firstmate: -n fm-second-t2 -c /tmp/fm-spawn-transcript.2vFKPm/secondmate/secondmate-home -e FM_HOME=/tmp/fm-spawn-transcript.2vFKPm/secondmate/secondmate-home
launch command sent : FM_ROOT_OVERRIDE= FM_STATE_OVERRIDE= FM_DATA_OVERRIDE= FM_PROJECTS_OVERRIDE= FM_CONFIG_OVERRIDE= FM_HOME='/tmp/fm-spawn-transcript.2vFKPm/secondmate/secondmate-home' CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions "$('/home/captain/.no-mistakes/worktrees/d6232fda45ac/01KZHMCYP25C7JMDJEAAX8D5ST/bin/fm-operational-input.sh' encode launch-brief < '/tmp/fm-spawn-transcript.2vFKPm/secondmate/secondmate-home/data/charter.md')"
Evidence: Reproducer: real-tmux seat E2E (base vs prefix-only vs target)
#!/usr/bin/env bash
# End-to-end reproduction of the measured defect on a real tmux server:
# two firstmate homes under one OS account, and a shell profile that derives the
# Bridge seat from FM_HOME, defaulting to sc1 when FM_HOME is unset.
# The operator's own ~/.bashrc is NOT touched; its derivation is mirrored in a
# sandbox HOME so the run is self-contained.
set -u
EV=/tmp/no-mistakes-evidence/01KZHMCYP25C7JMDJEAAX8D5ST
REPO=$1
SB=$EV/sandbox
rm -rf "$SB"; mkdir -p "$SB/proj"
FAKE=$SB/home
mkdir -p "$FAKE/sc1-firstmate/config" "$FAKE/firstmate-upstream/config"
echo sc1     > "$FAKE/sc1-firstmate/config/bridge-vessel"
echo tugboat > "$FAKE/firstmate-upstream/config/bridge-vessel"

# Mirror of the operator's ~/.bashrc lines 132-134 (seat from FM_HOME, sc1 default).
cat > "$FAKE/.bashrc" <<EOF
case \$- in *i*) ;; *) return ;; esac
_fm_seat="\${FM_HOME:-$FAKE/sc1-firstmate}"
export BRIDGE_VESSEL="\$(awk 'NR==1{print \$1}' "\$_fm_seat/config/bridge-vessel" 2>/dev/null || echo sc1)"
export BRIDGE_ROOT="\$_fm_seat/projects/coditan-bridge"
EOF
# tmux panes start a LOGIN shell; make it read the profile the same way.
printf '. "$HOME/.bashrc"\n' > "$FAKE/.bash_profile"

# Stand-in for the launched agent. It reports its own FM_HOME, then runs a
# bridge-*.sh command in a NON-interactive shell, exactly like every tool call a
# worker makes.
cat > "$SB/agent.sh" <<'EOF'
#!/usr/bin/env bash
printf '  agent process FM_HOME : %s\n' "${FM_HOME:-<unset>}"
bash -c 'printf "  bridge-*.sh publishes: vessel=%s clone=%s\n" "${BRIDGE_VESSEL:-<unset>}" "${BRIDGE_ROOT:-<unset>}"'
EOF
chmod +x "$SB/agent.sh"

TUG=$FAKE/firstmate-upstream

run_case() {  # <label> <backend-file> <socket> <launch-prefix> [<create-task-home>]
  local label=$1 backend=$2 sock=$3 prefix=$4; shift 4
  mkdir -p "$SB/bin-$sock"
  # The backend calls plain `tmux`; pin its calls to this case's private server.
  printf '#!/usr/bin/env bash\nexec /usr/bin/tmux -L %s "$@"\n' "$sock" > "$SB/bin-$sock/tmux"
  chmod +x "$SB/bin-$sock/tmux"
  printf '=== %s ===\n' "$label"
  ( set -e
    export HOME=$FAKE FM_BACKEND_LIB_DIR="$REPO/bin" PATH="$SB/bin-$sock:$PATH"
    # shellcheck disable=SC1090
    . "$backend"
    tmux new-session -d -s firstmate -c "$SB/proj"
    wid=$(fm_backend_tmux_create_task firstmate fm-task-1 "$SB/proj" "$@")
    sleep 1
    tmux send-keys -t "$wid" "${prefix}$SB/agent.sh > $SB/$label.out 2>&1" Enter
    sleep 2
    cat "$SB/$label.out"
    tmux kill-server 2>/dev/null
  )
  echo
}

git -C "$REPO" show cb55c08:bin/backends/tmux.sh > "$SB/tmux-base.sh"
for s in a b c; do /usr/bin/tmux -L "fmev-$s" kill-server 2>/dev/null; done

echo "# tmux $(tmux -V | awk '{print $2}') on $(uname -sr)"
echo "# one OS account, two firstmate homes:"
echo "#   $FAKE/sc1-firstmate       -> vessel sc1 (profile default when FM_HOME is unset)"
echo "#   $TUG -> vessel tugboat"
echo "# in every case below, firstmate is spawning a crewmate that belongs to the TUGBOAT home."
echo
run_case "A-base-cb55c08" "$SB/tmux-base.sh" fmev-a ""
run_case "B-base-plus-launch-prefix-only" "$SB/tmux-base.sh" fmev-b "FM_HOME='$TUG' "
run_case "C-target-25e06b5" "$REPO/bin/backends/tmux.sh" fmev-c "FM_HOME='$TUG' " "$TUG"
Evidence: Reproducer: fm-spawn tmux command transcript
#!/usr/bin/env bash
# Drive the REAL bin/fm-spawn.sh for an ordinary crewmate (and for a secondmate)
# behind a recording fake tmux, and print the literal tmux commands firstmate
# issues. Uses the repo's own test library, no test assertions - this is a
# transcript, not a pass/fail.
set -u
ROOT_REPO=$1
. "$ROOT_REPO/tests/lib.sh"
fm_test_tmproot TMP fm-spawn-transcript

mk_fakebin() {
  local dir=$1 fb
  fb=$(fm_fakebin "$dir")
  cat > "$fb/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
  list-panes) printf '%%1 1\n'; exit 0 ;;
  display-message)
    for a in "$@"; do case "$a" in *pane_id*) printf '%%1\n'; exit 0 ;; esac; done
    printf 'firstmate\n'; exit 0 ;;
  list-windows) exit 0 ;;
  new-window) printf 'tmux %s\n' "$*" >> "$FM_FAKE_LAUNCH_LOG.newwindow"; exit 0 ;;
  send-keys)
    prev=
    for a in "$@"; do
      [ "$prev" = "-l" ] && printf '%s\n' "$a" >> "$FM_FAKE_LAUNCH_LOG"
      prev=$a
    done
    exit 0 ;;
esac
exit 0
SH
  chmod +x "$fb/tmux"
  fm_fake_exit0 "$fb" treehouse
  printf '%s\n' "$fb"
}

one_case() {  # <label> <id> <target> [--secondmate]
  local label=$1 id=$2 target=$3; shift 3
  local dir=$TMP/$label home=$TMP/$label/home proj=$TMP/$label/project wt=$TMP/$label/wt
  local log=$TMP/$label/launch.log fb
  fb=$(mk_fakebin "$dir/fake")
  mkdir -p "$home/data/$id" "$home/projects" "$home/state" "$home/config"
  printf 'claude\n' > "$home/config/crew-harness"
  printf 'brief for %s\n' "$id" > "$home/data/$id/brief.md"
  fm_git_worktree "$proj" "$wt" "wt-$label" >/dev/null
  touch "$home/state/.last-watcher-beat"
  : > "$log"
  local resolved=$target
  case "$target" in PROJ) resolved=$proj ;; SECONDMATE)
      resolved=$dir/secondmate-home
      mkdir -p "$resolved/bin" "$resolved/data"
      printf '# Firstmate\n' > "$resolved/AGENTS.md"
      printf '%s\n' "$id" > "$resolved/.fm-secondmate-home"
      printf 'charter\n' > "$resolved/data/charter.md" ;;
  esac
  FM_ROOT_OVERRIDE='' FM_HOME="$home" \
    FM_STATE_OVERRIDE="$home/state" FM_DATA_OVERRIDE="$home/data" \
    FM_PROJECTS_OVERRIDE="$home/projects" FM_CONFIG_OVERRIDE="$home/config" \
    FM_SPAWN_NO_GUARD=1 FM_FAKE_PANE_PATH="$wt" TMUX="fake,1,0" \
    FM_FAKE_LAUNCH_LOG="$log" PATH="$fb:$PATH" \
    "$ROOT_REPO/bin/fm-spawn.sh" "$id" "$resolved" "$@" >/dev/null 2>&1
  printf '=== %s ===\n' "$label"
  printf 'this firstmate home : %s\n' "$home"
  [ "$target" = SECONDMATE ] && printf 'secondmate home     : %s\n' "$resolved"
  printf 'window created with : %s\n' "$(sed -n 1p "$log.newwindow")"
  printf 'launch command sent : %s\n' "$(sed -n 1p "$log")"
  echo
}

one_case ordinary-crewmate crew-t1 PROJ
one_case secondmate second-t2 SECONDMATE --secondmate
- Outcome: ⚠️ 1 info across 1 run (8m48s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 3 infos
  • ℹ️ bin/fm-spawn.sh:927 - HERDR_LABEL_HOME (lines 927-930) now computes exactly what LAUNCH_HOME already holds from line 863: this process's FM_HOME, or PROJ_ABS when KIND=secondmate. Two independent copies of the same per-kind rule can drift apart on the next change to it. Collapse to HERDR_LABEL_HOME=$LAUNCH_HOME, or use LAUNCH_HOME directly at lines 940/949/977/988 and drop the variable.
  • ℹ️ bin/backends/tmux.sh:126 - Every tmux spawn now passes -e, which requires tmux >= 3.0 (recorded in docs/tmux-backend.md:209 but not gated in code). On an older tmux new-window aborts with its own usage error, fm_backend_tmux_create_task returns 1 and fm-spawn exits 1 with no firstmate-authored message pointing at the cause - unlike the herdr adapter, which refuses loudly through fm_backend_herdr_version_check. Failing closed is defensible (silently dropping -e would restore the wrong-seat bug), so this is noted rather than recommended for change; a one-line message naming the tmux 3.0 requirement on the failure path would make the diagnosis obvious.
  • ℹ️ docs/tmux-backend.md:210 - The accepted limitation for herdr, zellij, Orca and cmux - a worker there learns its home only from the launch command, so a profile-derived seat in its shell is still fixed before firstmate can influence it - is recorded only in the tmux backend document. docs/herdr-backend.md ("Known gaps and follow-up notes") and docs/zellij-backend.md ("Known gaps left for a follow-up") already exist as the per-backend homes for exactly this kind of fact, and an operator reading those will not find it. A one-line pointer back to this section in each would make the recorded limit discoverable where it applies.
⚠️ **Test** - 1 info
  • ℹ️ tests/fm-backend-tmux-smoke.test.sh - tests/fm-backend-tmux-smoke.test.sh fails on this machine, but at a different point than the intent records: it fails at "could not make agent-fallback the session's current window" (tmux reports "can't find window: agent-fallback"), not "the tmux task shell did not become ready". I confirmed it is pre-existing by running the same script against a pristine git archive cb55c08 extract, where it fails identically at the same assertion; the change's own create_task assertions in that file still pass. No action needed for this change.
  • ./bin/fm-test-run.sh tests/fm-spawn-dispatch-profile.test.sh - full file including the two new tests (test_ordinary_crew_launch_names_its_own_home, test_secondmate_window_is_created_in_its_own_home) and the five updated exact-equality launch-line assertions
  • ./bin/fm-test-run.sh tests/fm-backend.test.sh tests/fm-spawn-batch.test.sh tests/fm-spawn-worktree-settle.test.sh - backend dispatch and spawn paths around the edited code
  • ./bin/fm-test-run.sh tests/fm-secondmate-harness.test.sh tests/fm-backend-orca.test.sh tests/fm-backend-herdr.test.sh - the other suites that assert launch commands or non-tmux backends
  • Manual real-tmux E2E (/tmp/no-mistakes-evidence/01KZHMCYP25C7JMDJEAAX8D5ST/seat-e2e.sh): sourced the real fm_backend_tmux_create_task from base cb55c08 and from the target commit against private tmux servers (tmux 3.6), each spawning a window for the tugboat home under a sandbox HOME whose profile derives the seat from FM_HOME and defaults to sc1, then ran a bridge-style command in a non-interactive shell inside the pane
  • Manual negative control in the same run: base create_task plus the FM_HOME=&#39;&lt;home&gt;&#39; launch-command prefix alone, showing the profile-derived seat stays sc1
  • Manual spawn transcript (/tmp/no-mistakes-evidence/01KZHMCYP25C7JMDJEAAX8D5ST/spawn-transcript.sh): drove the real bin/fm-spawn.sh behind a recording fake tmux for an ordinary crewmate and for a --secondmate spawn, printing the literal new-window and launch commands issued
  • git archive cb55c08 | tar -x -C /tmp/fm-base-check &amp;&amp; ./bin/fm-test-run.sh tests/fm-backend-tmux-smoke.test.sh - confirmed the tmux smoke failure is pre-existing and identical at base
⚠️ **Document** - 1 info
  • ℹ️ bin/fm-spawn.sh:927 - bin/fm-spawn.sh now derives the same per-kind home twice: LAUNCH_HOME (line 863) and the herdr arm's HERDR_LABEL_HOME (line 927), with identical rules. docs/herdr-backend.md:105 documents HERDR_LABEL_HOME as its own computation. Collapsing the herdr arm onto LAUNCH_HOME and reducing that doc line to name the shared variable would remove the duplicate, but that is a code change outside this documentation phase.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

fm-spawn told only a --secondmate launch which firstmate home it belonged
to. An ordinary crewmate or scout started with FM_HOME unset, so an operator
shell profile that derives per-vessel values from it resolved them from its
own default. On a machine carrying two firstmate homes under one OS account
that default is another vessel entirely, and workers of this home published
Bridge mail out of the other home's clone: the envelopes' own from field said
this vessel while the commits carrying them were authored by the other.

Two places now carry the home, and the second is not a copy of the first.

The launch command gets an FM_HOME= prefix for every kind, not just
secondmate. That reaches the agent process on every backend, which is what
makes fm-send, the findings surface, and every other FM_HOME-resolving script
answer for this home.

It is not enough on its own, and this was measured rather than assumed. A
profile runs once, when the task's shell starts, and nothing re-derives it
afterwards; firstmate sends the launch command after that shell already
exists. So the tmux adapter now also seeds FM_HOME with `new-window -e`,
which tmux applies before the shell starts, and the existing derivation comes
out right with firstmate naming no variable but its own. herdr, zellij, Orca
and cmux create a task through CLIs with no environment seam, so there a
worker learns its home from the launch command only; docs/tmux-backend.md
records that limit, the measurement, and the negative control that shows why
the prefix alone cannot do it.

The overrides stay cleared for a secondmate only, unchanged.
@Freudator86
Freudator86 merged commit e6762c5 into main Aug 8, 2026
10 checks passed
@Freudator86
Freudator86 deleted the fm/firstmate-crew-inherits-wrong-bridge-seat branch August 8, 2026 23:45
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.

2 participants