Skip to content

fix(spawn): prove the shell is ready before typing a launch command - #1

Merged
stoneevenson-biz merged 1 commit into
mainfrom
fix/spawn-shell-readiness
Aug 27, 2026
Merged

stoneevenson-biz merged 1 commit into
mainfrom
fix/spawn-shell-readiness

Conversation

@stoneevenson-biz

Copy link
Copy Markdown
Owner

The incident

Two registered secondmates — cellarsky-sm and hermes-jarvis-sm — were found on 2026-08-26 as bare zsh prompts, not running agents. Their launch line had been typed into a shell that was not at a prompt:

(base) stoneevenson@Air firstmate % claude --dangerously-skip-permissions You are a secondmate…
zsh: parse error near `do'

They had never started. data/secondmates.md listed three live supervisors; one was real. firstmate had already recorded their metas. Nothing noticed for weeks.

Root cause

fm-spawn.sh inferred shell readiness from pane_current_path changing after treehouse get. That proves a chdir happened — it says nothing about whether the shell returned to a prompt, and tmux send-keys has no acknowledgment channel to tell the difference:

tmux send-keys -t "$T" -l "$LAUNCH"   # blind keystroke injection
sleep 0.3                              # a timing guess, no backing signal
tmux send-keys -t "$T" Enter

The fix

Two guards in bin/fm-tmux-lib.sh:

fm_tmux_wait_shell_ready — a bounded echo round-trip. Send a unique marker through the shell; wait to see it rendered back on its own line. Seeing it is positive proof the shell read a command line, ran it, and printed a result. A probe that lands in a busy shell is a harmless short printf, unlike a multi-KB launch string. Only the output line matches (grep -qx), so the echo of the probe's own keystrokes is not accepted as proof.

fm_tmux_launch_failed — reads the pane after launch and fails loudly on a shell error, instead of recording a meta for a pane that holds nothing.

Both are bypassable with FM_SKIP_SHELL_READY=1, which the two suites that exercise spawn's worktree logic over a fake tmux now set — the same way they already set FM_INTAKE_OVERRIDE=1.

Drive-by

tests/lib.sh — fm_test_cleanup ended on a falsy [ -n "$d" ] and returned 1. Its own header tells suites to call it from a custom EXIT trap, so any suite following that advice failed with every test passing. Now returns 0.

Verification

Falsifiable — red before green. With the readiness guard stubbed to return 0 (the pre-fix behavior):

ok     - ready shell: probe round-trips, gate opens
not ok - deaf shell: gate opened on a shell that never ran the probe
exit=1

Restored, all six pass. New suite tests/fm-spawn-shell-ready.test.sh pins both guards, including the exact zsh: parse error near \do'` text from the incident, and asserts a healthy running-agent pane is not flagged.

full suite:  38 pass, 1 fail
shellcheck:  clean

The one failure, fm-loop-l2, fails identically at 24f6891 — before any commit in this branch. Verified in a detached worktree, not assumed.

Related

herdr v0.8.2 ships "agent start now properly waits for pane/agent readiness instead of racing" — the same defect class, solved upstream at the multiplexer. This fix makes the tmux driver safe today; docs/plans/cmux-herdr-surface-split.md remains the longer-term answer.

Two registered secondmates, cellarsky-sm and hermes-jarvis-sm, were found on
2026-08-26 as bare zsh prompts rather than running agents. Their launch line
had been typed into a shell that was not at a prompt, so the whole
`claude --dangerously-skip-permissions "<charter>"` string was consumed as
raw text and died on a zsh parse error. firstmate had already recorded a meta
and registered them as live supervisors. Nothing noticed for weeks.

Root cause: fm-spawn inferred shell readiness from pane_current_path changing
after `treehouse get`. That proves a chdir happened; it does not prove the
shell returned to a prompt, and tmux send-keys has no acknowledgment channel.
The 'sleep 0.3' between the literal text and Enter was a timing guess with no
backing signal.

Adds two guards in fm-tmux-lib.sh:

  fm_tmux_wait_shell_ready - a bounded echo round-trip. Send a unique marker
  through the shell, wait to see it rendered back on its own line. Seeing it
  is positive proof the shell read a command line, ran it and printed a
  result. A probe landing in a busy shell is a harmless short printf, unlike
  a multi-KB launch string.

  fm_tmux_launch_failed - reads the pane after launch and fails loudly on a
  shell error rather than recording a meta for a pane holding nothing.

Also fixes tests/lib.sh: fm_test_cleanup ended on a falsy test and returned 1,
so any suite following the header's advice to call it from its own EXIT trap
failed with every test passing.

Verified red before green: with the readiness guard stubbed to return 0 (the
pre-fix behavior) the deaf-shell case fails; restored, all six pass. Full
suite 38/39 - fm-loop-l2 fails identically at 24f6891, before any of this.
@stoneevenson-biz
stoneevenson-biz merged commit 57bc27e into main Aug 27, 2026
@stoneevenson-biz
stoneevenson-biz deleted the fix/spawn-shell-readiness branch August 27, 2026 18:40
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