Skip to content

fix(bin): resolve backend adapter sibling libs when sourced under zsh - #6279

Closed
tuprogramadorfav wants to merge 3 commits into
kunchenguid:mainfrom
tuprogramadorfav:fm/fm-backend-zsh-split
Closed

tuprogramadorfav wants to merge 3 commits into
kunchenguid:mainfrom
tuprogramadorfav:fm/fm-backend-zsh-split

Conversation

@tuprogramadorfav

@tuprogramadorfav tuprogramadorfav commented Oct 1, 2026 •

Copy link
Copy Markdown

Intent

"When doing bug fixes ... Apply that same high standard to engineering excellence: lint, test failures, and test flakiness. If you see one, even if it is not caused by what you are working on right now, still get it fixed." (standing instruction)

Context: the fm-captain-hold-cross-home worker found that tests/fm-backend.test.sh fails its zsh case on main. In bin/fm-backend.sh, fm_backend_source loops over an unquoted $siblings list, which zsh does not word-split, so sourcing the backend adapters (herdr) fails under zsh.

What Changed

  • The backend adapters (bin/backends/{cmux,herdr,orca,zellij}.sh) and the shared libs (bin/fm-tmux-lib.sh, bin/fm-agent-process-lib.sh, bin/fm-session-lock-lib.sh) now find their own directory with ${BASH_SOURCE[0]:-$0}. zsh does not set BASH_SOURCE, so before this change, sourcing bin/fm-backend.sh from zsh printed errors and could not load the sibling libraries. The adapters compute their root without a case inside command substitution, and the libs pick the directory with a case on whether the path contains /.
  • test_backend_source_shell_portable in tests/fm-backend.test.sh now runs under both zsh and bash (FM_TEST_BASH can override the bash binary). For each backend (tmux, herdr, zellij, orca, cmux), it checks that fm_backend_source loads the adapter and its sibling-lib functions without printing anything, and that an unknown backend is rejected. The existing zsh check that PATH stays intact is kept.
  • The macOS CI job now runs that test with stock /bin/bash (3.2) and /bin/zsh, and fails unless all three ok - lines appear (zsh, bash 3.2, zsh PATH).

🤖 Generated with Claude Code

Risk Assessment

✅ Low: The change is limited to seven own-directory lookups plus one test and one CI step. Probes showed all five adapters load with their sibling libraries under zsh and stock /bin/bash 3.2, and the expected count of 3 matches the three pass sites when /bin/zsh is present.

Testing

I compared sourcing results across shells at HEAD and at base, driven from a different working directory, for all 5 backends under zsh, /bin/bash 3.2 and bash 5. I ran 48 edge cases per shell: sourcing inside a function, relative paths, cd before loading, a repo path containing spaces, and sourcing herdr.sh, orca.sh, fm-tmux-lib.sh and fm-agent-process-lib.sh directly. All were clean. In a guarded fm-lab-* Herdr session I drove the herdr adapter's send and capture against a real pane from all three shells, and showed base under zsh half-loading. I ran the real fm-peek.sh entrypoint against a disposable lab FM_HOME under /bin/bash, bash 5 and zsh, and tore the lab down. I ran the new regression test against the base tree (exit 1) and at HEAD (pass), and replayed the macos-stock-bash CI step exactly (count 3, exit 0). tests/fm-backend.test.sh passed 31/31 under both bashes. Under /bin/bash 3.2, the cmux (61/61) and herdr (227/227) adapter tests pass. Orca and zellij each have one failure, both the already-ignored host issue where tasks-axi is missing. A real tmux pane could not be driven because tmux is not installed. This is a shell-library change with no UI, so the evidence is CLI transcripts, not screenshots.

  • Live validation: ✅ go - 6 of 10 scenarios driven live against the product
Scenario Result Live Evidence
zsh user sources bin/fm-backend.sh and runs fm_backend_source for each of tmux/herdr/zellij/orca/cmux: rc=0, empty stderr, sibling-lib functions defined, PATH still resolves dirname ✅ pass live head-4d49cedb-sourcing-matrix.txt (/bin/zsh block: all 5 backends rc=0 missing=[] stderr_lines=0 dirname=/usr/bin/dirname)
Bug reproduction: on base f593060 the same zsh session half-loads every adapter with 'no such file or directory' sibling errors and missing fm_composer/fm_transition/fm_agent_process functions while… ✅ pass live base-f5930603-sourcing-matrix.txt (zsh: tmux 5, herdr 3, zellij 2, orca 1, cmux 2 sourcing errors; bash rows clean)
Stock macOS /bin/bash 3.2 and bash 5 still load every adapter cleanly (no herdr root regression from case-in-$(...)) ✅ pass live head-4d49cedb-sourcing-matrix.txt (/bin/bash 3.2.57 and bash 5.3.9 blocks all rc=0, stderr_lines=0, missing=[])
Adversarial sourcing shapes: inside a function, relative ./fm-backend.sh then cd / before load, herdr.sh sourced directly and from its own dir (no slash), nested libs sourced directly, repo path conta… ✅ pass live head-4d49cedb-sourcing-edge-cases.txt (48/48 rc=0, herdr_root_ok=yes, sibling functions defined, PATH ok under zsh, bash 3.2, bash 5)
Live Herdr lab: from zsh, /bin/bash 3.2 and bash 5 a user loads the herdr adapter, sends a command into a real Herdr pane and captures its output ✅ pass live head-4d49cedb-live-herdr-lab.txt (herdr 0.9.1, fm-lab-zshlive-38232-4609:w1:p1; LIVE_..._42 echoed and captured for all three shells; base under zsh prints the three sibling-load errors; teardown ok)
Real product entrypoint: bin/fm-peek.sh peeks a herdr-backed task via a disposable lab FM_HOME when run by /bin/bash 3.2, bash 5, and from a zsh shell ✅ pass live head-4d49cedb-live-herdr-lab.txt (fm-peek.sh zshprobe 4 returned PEEK_MARKER_42, rc=0, in all three invocations)
Strengthened regression test test_backend_source_shell_portable fails on the pre-fix tree and passes at HEAD ⏸️ untested no The prior payload recorded this only as an automated test run (live=false), not a drive of the live product, so it did not establish a live result. The live behavior this test guards is covered by the…
New macos-stock-bash CI step passes under the job's PATH with /bin/bash 3.2 and /bin/zsh present (expects exactly 3 ok lines) ⏸️ untested no The prior payload recorded only a local replay of the CI step body (live=false), not a run on real GitHub CI, so it did not establish a live result. The actual GitHub CI run belongs to the CI phase.
No bash regression in the adapter unit suites that source the changed files directly ⏸️ untested no The prior payload recorded these only as automated unit-suite runs (live=false), not drives of the live product, so it did not establish a live result. The live bash 3.2 and bash 5 adapter loading and…
tmux backend: drive a real tmux pane through fm_backend_tmux_send/capture after loading the adapter from zsh ⏸️ untested no The tmux binary is not on PATH on this host, and the workspace boundary forbids installing system packages (brew install tmux). Building tmux in the workspace would also need libevent and ncurses tool…
Evidence: Sourcing matrix at HEAD (zsh / bash 3.2 / bash 5 x 5 backends)

Source: Sourcing matrix at HEAD (zsh / bash 3.2 / bash 5 x 5 backends)

=== HEAD 4d49cedb | /bin/zsh (zsh 5.9)
tmux   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
herdr  rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
zellij rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
orca   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
cmux   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
=== HEAD 4d49cedb | /bin/bash (3.2.57(1)-release)
tmux   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
herdr  rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
zellij rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
orca   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
cmux   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
=== HEAD 4d49cedb | /run/current-system/sw/bin/bash (5.3.9(1)-release)
tmux   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
herdr  rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
zellij rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
orca   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
cmux   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
Evidence: Sourcing matrix at pre-fix base f593060 (zsh half-load reproduced)

Source: Sourcing matrix at pre-fix base f5930603 (zsh half-load reproduced)

=== BASE f5930603 (pre-fix) | /bin/zsh (zsh 5.9)
tmux   rc=0 missing=[fm_composer_strip_ghost fm_cursor_canonical_path fm_gemini_path_is_gemini] dirname=/usr/bin/dirname stderr_lines=5
         stderr: /var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/fm-tmux-lib.sh:.:49: no such file or directory: ./fm-composer-lib.sh
         stderr: /var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/fm-tmux-lib.sh:.:51: no such file or directory: ./fm-cursor-lib.sh
         stderr: /var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/fm-session-lock-lib.sh:.:24: no such file or directory: ./fm-cursor-lib.sh
         stderr: /var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/fm-agent-process-lib.sh:.:19: no such file or directory: ./fm-session-lock-lib.sh
         stderr: /var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/fm-agent-process-lib.sh:.:21: no such file or directory: ./fm-gemini-lib.sh
herdr  rc=0 missing=[fm_composer_strip_ghost fm_transition_policy fm_agent_process_classify_name fm_cursor_canonical_path fm_gemini_path_is_gemini] dirname=/usr/bin/dirname stderr_lines=3
         stderr: /var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/backends/herdr.sh:.:79: no such file or directory: //bin/fm-composer-lib.sh
         stderr: /var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/backends/herdr.sh:.:87: no such file or directory: //bin/fm-transition-lib.sh
         stderr: /var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/backends/herdr.sh:.:94: no such file or directory: //bin/fm-agent-process-lib.sh
zellij rc=0 missing=[fm_backend_hometag fm_composer_strip_ghost] dirname=/usr/bin/dirname stderr_lines=2
         stderr: /var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/backends/zellij.sh:.:120: no such file or directory: //bin/fm-backend-hometag-lib.sh
         stderr: /var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/backends/zellij.sh:.:125: no such file or directory: //bin/fm-composer-lib.sh
orca   rc=0 missing=[fm_composer_strip_ghost] dirname=/usr/bin/dirname stderr_lines=1
         stderr: /var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/backends/orca.sh:.:13: no such file or directory: ./../fm-composer-lib.sh
cmux   rc=0 missing=[fm_backend_hometag fm_composer_strip_ghost] dirname=/usr/bin/dirname stderr_lines=2
         stderr: /var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/backends/cmux.sh:.:115: no such file or directory: //bin/fm-backend-hometag-lib.sh
         stderr: /var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/backends/cmux.sh:.:121: no such file or directory: //bin/fm-composer-lib.sh
=== BASE f5930603 (pre-fix) | /bin/bash (3.2.57(1)-release)
tmux   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
herdr  rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
zellij rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
orca   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
cmux   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
=== BASE f5930603 (pre-fix) | /run/current-system/sw/bin/bash (5.3.9(1)-release)
tmux   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
herdr  rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
zellij rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
orca   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
cmux   rc=0 missing=[] dirname=/usr/bin/dirname stderr_lines=0
Evidence: Adversarial sourcing edge cases at HEAD (48 cases, all clean)

Source: Adversarial sourcing edge cases at HEAD (48 cases, all clean)

=== /bin/zsh
[worktree] abs path, cwd=/, caller args after load=[keep me]: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] source+load inside a function: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] relative ./fm-backend.sh from bin/, then cd / before load: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] herdr.sh sourced directly, relative path: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] herdr.sh sourced from its own dir: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] fm-tmux-lib.sh sourced directly: rc=0 sibling_funcs_defined=yes
[worktree] fm-agent-process-lib.sh sourced directly: rc=0 sibling_funcs_defined=yes
[worktree] orca.sh sourced directly: rc=0 sibling_funcs_defined=yes
[path-with-spaces] abs path, cwd=/, caller args after load=[keep me]: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] source+load inside a function: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] relative ./fm-backend.sh from bin/, then cd / before load: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] herdr.sh sourced directly, relative path: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] herdr.sh sourced from its own dir: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] fm-tmux-lib.sh sourced directly: rc=0 sibling_funcs_defined=yes
[path-with-spaces] fm-agent-process-lib.sh sourced directly: rc=0 sibling_funcs_defined=yes
[path-with-spaces] orca.sh sourced directly: rc=0 sibling_funcs_defined=yes
=== /bin/bash
[worktree] abs path, cwd=/, caller args after load=[keep me]: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] source+load inside a function: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] relative ./fm-backend.sh from bin/, then cd / before load: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] herdr.sh sourced directly, relative path: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] herdr.sh sourced from its own dir: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] fm-tmux-lib.sh sourced directly: rc=0 sibling_funcs_defined=yes
[worktree] fm-agent-process-lib.sh sourced directly: rc=0 sibling_funcs_defined=yes
[worktree] orca.sh sourced directly: rc=0 sibling_funcs_defined=yes
[path-with-spaces] abs path, cwd=/, caller args after load=[keep me]: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] source+load inside a function: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] relative ./fm-backend.sh from bin/, then cd / before load: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] herdr.sh sourced directly, relative path: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] herdr.sh sourced from its own dir: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] fm-tmux-lib.sh sourced directly: rc=0 sibling_funcs_defined=yes
[path-with-spaces] fm-agent-process-lib.sh sourced directly: rc=0 sibling_funcs_defined=yes
[path-with-spaces] orca.sh sourced directly: rc=0 sibling_funcs_defined=yes
=== /run/current-system/sw/bin/bash
[worktree] abs path, cwd=/, caller args after load=[keep me]: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] source+load inside a function: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] relative ./fm-backend.sh from bin/, then cd / before load: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] herdr.sh sourced directly, relative path: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] herdr.sh sourced from its own dir: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[worktree] fm-tmux-lib.sh sourced directly: rc=0 sibling_funcs_defined=yes
[worktree] fm-agent-process-lib.sh sourced directly: rc=0 sibling_funcs_defined=yes
[worktree] orca.sh sourced directly: rc=0 sibling_funcs_defined=yes
[path-with-spaces] abs path, cwd=/, caller args after load=[keep me]: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] source+load inside a function: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] relative ./fm-backend.sh from bin/, then cd / before load: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] herdr.sh sourced directly, relative path: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] herdr.sh sourced from its own dir: rc=0 herdr_root_ok=yes transition=y agentproc=y composer=y PATH_ok=yes
[path-with-spaces] fm-tmux-lib.sh sourced directly: rc=0 sibling_funcs_defined=yes
[path-with-spaces] fm-agent-process-lib.sh sourced directly: rc=0 sibling_funcs_defined=yes
[path-with-spaces] orca.sh sourced directly: rc=0 sibling_funcs_defined=yes
Evidence: Live Herdr lab: adapter send/capture from zsh/bash3.2/bash5, base zsh failure, fm-peek.sh via three shells

Source: Live Herdr lab: adapter send/capture from zsh/bash3.2/bash5, base zsh failure, fm-peek.sh via three shells

herdr: herdr 0.9.1  session: fm-lab-zshlive-38232-4609
lab pane target: fm-lab-zshlive-38232-4609:w1:p1
================ HEAD under /bin/zsh -f (interactive zsh user)
fm_backend_source herdr rc=0
  sibling fn fm_transition_policy: defined
  sibling fn fm_agent_process_classify_name: defined
  sibling fn fm_composer_classify_screen: defined
send_text_line rc=0
--- fm_backend_herdr_capture fm-lab-zshlive-38232-4609:w1:p1 4:
❯ echo LIVE_HEAD_under__bin_zsh__f__interactive_zsh_user__$((6*7))
LIVE_HEAD_under__bin_zsh__f__interactive_zsh_user__42
/private/tmp
❯capture rc=0
================ HEAD under /bin/bash 3.2
fm_backend_source herdr rc=0
  sibling fn fm_transition_policy: defined
  sibling fn fm_agent_process_classify_name: defined
  sibling fn fm_composer_classify_screen: defined
send_text_line rc=0
--- fm_backend_herdr_capture fm-lab-zshlive-38232-4609:w1:p1 4:
❯ echo LIVE_HEAD_under__bin_bash_3_2_$((6*7))
LIVE_HEAD_under__bin_bash_3_2_42
/private/tmp
❯capture rc=0
================ HEAD under bash 5
fm_backend_source herdr rc=0
  sibling fn fm_transition_policy: defined
  sibling fn fm_agent_process_classify_name: defined
  sibling fn fm_composer_classify_screen: defined
send_text_line rc=0
--- fm_backend_herdr_capture fm-lab-zshlive-38232-4609:w1:p1 4:
❯ echo LIVE_HEAD_under_bash_5_$((6*7))
LIVE_HEAD_under_bash_5_42
/private/tmp
❯capture rc=0
================ BASE f5930603 (pre-fix) under /bin/zsh -f
<base-tree>/bin/backends/herdr.sh:.:79: no such file or directory: //bin/fm-composer-lib.sh
<base-tree>/bin/backends/herdr.sh:.:87: no such file or directory: //bin/fm-transition-lib.sh
<base-tree>/bin/backends/herdr.sh:.:94: no such file or directory: //bin/fm-agent-process-lib.sh
fm_backend_source herdr rc=0
  sibling fn fm_transition_policy: MISSING
  sibling fn fm_agent_process_classify_name: MISSING
  sibling fn fm_composer_classify_screen: MISSING
send_text_line rc=0
--- fm_backend_herdr_capture fm-lab-zshlive-38232-4609:w1:p1 4:
❯ echo LIVE_BASE_f5930603__pre_fix__under__bin_zsh__f_$((6*7))
LIVE_BASE_f5930603__pre_fix__under__bin_zsh__f_42
/private/tmp
❯capture rc=0
================ fm-peek.sh zshprobe 4  via: /bin/bash
❯ echo PEEK_MARKER_$((40+2))
PEEK_MARKER_42
/private/tmp
❯
rc=0
================ fm-peek.sh zshprobe 4  via: /run/current-system/sw/bin/bash
WARNING: watcher still down (same stale episode; last beat: never, grace 300s) - full banner already printed this episode.
❯ echo PEEK_MARKER_$((40+2))
PEEK_MARKER_42
/private/tmp
❯
rc=0
================ fm-peek.sh zshprobe 4  via: zsh-runs-shebang
WARNING: watcher still down (same stale episode; last beat: never, grace 300s) - full banner already printed this episode.
❯ echo PEEK_MARKER_$((40+2))
PEEK_MARKER_42
/private/tmp
❯
rc=0
teardown fm-lab-zshlive-38232-4609: ok
Evidence: Regression test fails on base, CI macos-stock-bash step replay passes (count 3)

Source: Regression test fails on base, CI macos-stock-bash step replay passes (count 3)

\### HEAD regression test run against BASE f5930603 bin/ (pre-fix) - must FAIL
$ FM_TEST_ONLY=test_backend_source_shell_portable FM_TEST_BASH=/bin/bash /bin/bash tests/fm-backend.test.sh
not ok - zsh: fm_backend_source tmux should load the adapter and its sibling libs
/var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/fm-tmux-lib.sh:.:49: no such file or directory: ./fm-composer-lib.sh
/var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/fm-tmux-lib.sh:.:51: no such file or directory: ./fm-cursor-lib.sh
/var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/fm-session-lock-lib.sh:.:24: no such file or directory: ./fm-cursor-lib.sh
/var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/fm-agent-process-lib.sh:.:19: no such file or directory: ./fm-session-lock-lib.sh
/var/folders/wj/1hdfpv_57fl8y5y14nkv27gw0000gn/T/fmz/base-f5930603/bin/fm-agent-process-lib.sh:.:21: no such file or directory: ./fm-gemini-lib.sh
exit=

\### CI step replay at HEAD 4d49cedb (macos-stock-bash job, PATH=/bin:/usr/bin:/usr/sbin:/sbin, set -e)
ok - zsh: fm_backend_source loads every adapter with its sibling libs and rejects unknown backends
ok - /bin/bash: fm_backend_source loads every adapter with its sibling libs and rejects unknown backends
ok - zsh: fm_backend_source keeps PATH intact while loading a backend adapter
CI step: portable_count=3 -> PASS
exit=0
base-tree regression test exit code: 1 (non-zero = test fails pre-fix, as required)
Evidence: tests/fm-backend.test.sh under /bin/bash 3.2

Source: tests/fm-backend.test.sh under /bin/bash 3.2

ok - fm_backend_name: FM_BACKEND env > config/backend > default tmux
ok - fm_backend_detect: no markers -> undetected, HERDR_ENV=1 -> herdr, $TMUX -> tmux, CMUX_WORKSPACE_ID -> cmux, nested combinations resolve innermost-first
ok - fm_backend_detect: falls back to __CFBundleIdentifier=com.cmuxterm.app when CMUX_WORKSPACE_ID is absent (signal bundle-id; foreign bundle ids rejected)
ok - fm_backend_detect: the cmux fallback signals are macOS-only (inert on a non-Darwin uname)
ok - fm_backend_detect: an inherited cmux bundle id never outranks $TMUX or HERDR_ENV (tmux/herdr-inside-cmux false positive absorbed)
ok - fm_backend_detect: ancestry fallback matches the lsappinfo-resolved (bundle-id) cmux app pid in the parent chain
ok - fm_backend_detect: ancestry fallback matches a bundle-shaped cmux comm path at any install location when lsappinfo cannot resolve a pid
ok - fm_backend_detect: ancestry fallback stops undetected at launchd (a reparented tmux server never reaches cmux)
ok - fm_backend_name: a fallback-detected cmux prints a NOTICE naming the fallback signal; the primary-marker notice is unchanged
ok - fm_backend_name: verified Herdr and tmux stay silent while experimental cmux remains loud
ok - fm_backend_name: an explicit FM_BACKEND or config/backend setting always wins over runtime auto-detection, including an ambient cmux marker
ok - fm_backend_validate: implemented adapters accepted, unknown and blocked codex-app backends refused loudly
ok - zsh: fm_backend_source loads every adapter with its sibling libs and rejects unknown backends
ok - /bin/bash: fm_backend_source loads every adapter with its sibling libs and rejects unknown backends
ok - zsh: fm_backend_source keeps PATH intact while loading a backend adapter
ok - fm_backend_source: missing adapter fails before lifecycle continuation
ok - fm_backend_source: unreadable adapter fails before lifecycle continuation
ok - fm_backend_validate_spawn: all implemented lifecycle backends are spawn-supported
ok - fm_meta_get / fm_backend_of_meta: read last key=value and default backend to tmux
ok - fm_backend_resolve_selector: session:window literal, exact task id first, legacy fm-<id> label fallback, ad hoc bare name via tmux list-windows
ok - fm_backend_of_selector: exact task ids, legacy fm-<id> labels, and matching explicit targets inherit metadata backend
ok - fm-send.sh: explicit tmux targets are verified; text types once and submits with Enter
ok - fm-peek.sh: capture-pane invocation and output are byte-identical old vs new
ok - fm-spawn.sh: a project reached through a symlinked prefix (e.g. macOS /tmp -> /private/tmp) does not trip the isolation guard's false refusal
ok - fm-teardown.sh: treehouse return remains compatible while tmux cleanup uses exact selectors
ok - fm-spawn.sh --backend bogus is refused loudly
ok - fm-spawn.sh --backend codex-app is refused
ok - fm-spawn.sh honors FM_BACKEND and refuses an unimplemented value loudly
ok - fm-spawn.sh: an explicit --backend tmux resolves silently and writes no backend= (missing means tmux)
ok - fm-spawn.sh: explicit --backend tmux wins over an ambient HERDR_ENV=1 auto-detect marker
ok - fm-spawn.sh: auto-detect resolves nested tmux-in-herdr to tmux and stays silent end to end
/bin/bash tests/fm-backend.test.sh  8.80s user 11.10s system 39% cpu 50.083 total
Evidence: tests/fm-backend.test.sh under bash 5

Source: tests/fm-backend.test.sh under bash 5

ok - fm_backend_name: FM_BACKEND env > config/backend > default tmux
ok - fm_backend_detect: no markers -> undetected, HERDR_ENV=1 -> herdr, $TMUX -> tmux, CMUX_WORKSPACE_ID -> cmux, nested combinations resolve innermost-first
ok - fm_backend_detect: falls back to __CFBundleIdentifier=com.cmuxterm.app when CMUX_WORKSPACE_ID is absent (signal bundle-id; foreign bundle ids rejected)
ok - fm_backend_detect: the cmux fallback signals are macOS-only (inert on a non-Darwin uname)
ok - fm_backend_detect: an inherited cmux bundle id never outranks $TMUX or HERDR_ENV (tmux/herdr-inside-cmux false positive absorbed)
ok - fm_backend_detect: ancestry fallback matches the lsappinfo-resolved (bundle-id) cmux app pid in the parent chain
ok - fm_backend_detect: ancestry fallback matches a bundle-shaped cmux comm path at any install location when lsappinfo cannot resolve a pid
ok - fm_backend_detect: ancestry fallback stops undetected at launchd (a reparented tmux server never reaches cmux)
ok - fm_backend_name: a fallback-detected cmux prints a NOTICE naming the fallback signal; the primary-marker notice is unchanged
ok - fm_backend_name: verified Herdr and tmux stay silent while experimental cmux remains loud
ok - fm_backend_name: an explicit FM_BACKEND or config/backend setting always wins over runtime auto-detection, including an ambient cmux marker
ok - fm_backend_validate: implemented adapters accepted, unknown and blocked codex-app backends refused loudly
ok - zsh: fm_backend_source loads every adapter with its sibling libs and rejects unknown backends
ok - /run/current-system/sw/bin/bash: fm_backend_source loads every adapter with its sibling libs and rejects unknown backends
ok - zsh: fm_backend_source keeps PATH intact while loading a backend adapter
ok - fm_backend_source: missing adapter fails before lifecycle continuation
ok - fm_backend_source: unreadable adapter fails before lifecycle continuation
ok - fm_backend_validate_spawn: all implemented lifecycle backends are spawn-supported
ok - fm_meta_get / fm_backend_of_meta: read last key=value and default backend to tmux
ok - fm_backend_resolve_selector: session:window literal, exact task id first, legacy fm-<id> label fallback, ad hoc bare name via tmux list-windows
ok - fm_backend_of_selector: exact task ids, legacy fm-<id> labels, and matching explicit targets inherit metadata backend
ok - fm-send.sh: explicit tmux targets are verified; text types once and submits with Enter
ok - fm-peek.sh: capture-pane invocation and output are byte-identical old vs new
ok - fm-spawn.sh: a project reached through a symlinked prefix (e.g. macOS /tmp -> /private/tmp) does not trip the isolation guard's false refusal
ok - fm-teardown.sh: treehouse return remains compatible while tmux cleanup uses exact selectors
ok - fm-spawn.sh --backend bogus is refused loudly
ok - fm-spawn.sh --backend codex-app is refused
ok - fm-spawn.sh honors FM_BACKEND and refuses an unimplemented value loudly
ok - fm-spawn.sh: an explicit --backend tmux resolves silently and writes no backend= (missing means tmux)
ok - fm-spawn.sh: explicit --backend tmux wins over an ambient HERDR_ENV=1 auto-detect marker
ok - fm-spawn.sh: auto-detect resolves nested tmux-in-herdr to tmux and stays silent end to end
bash tests/fm-backend.test.sh  8.69s user 10.84s system 38% cpu 50.296 total
Evidence: tests/fm-backend-cmux.test.sh under /bin/bash 3.2 (61/61)

Source: tests/fm-backend-cmux.test.sh under /bin/bash 3.2 (61/61)

ok - fm_backend_cmux_version_check: accepts the verified minimum (0.64.17)
ok - fm_backend_cmux_version_check: accepts a newer version (0.70.0)
ok - fm_backend_cmux_version_check: refuses an old version loudly
ok - fm_backend_cmux_version_check: refuses loudly when cmux is not found on PATH or at the bundle path
ok - fm_backend_cmux_password: reads the first non-empty line of config/cmux-socket-password
ok - fm_backend_cmux_password: preserves spaces and tabs in config/cmux-socket-password
ok - fm_backend_cmux_password: respects FM_CONFIG_OVERRIDE
ok - fm_backend_cmux_password: empty when config/cmux-socket-password is absent
ok - fm_backend_cmux_cli: exports CMUX_SOCKET_PASSWORD when config/cmux-socket-password is set
ok - fm_backend_cmux_parse_target: splits '<workspace_uuid>:<surface_uuid>' on the first colon
ok - fm_backend_cmux_normalize_key: Enter/Escape/C-c map to cmux's verified enter/escape/ctrl-c
ok - fm_backend_cmux_scoped_title: scopes a primary task title with firstmate plus root hash
ok - fm_backend_cmux_scoped_title: scopes a secondmate task title with the home marker plus root hash
ok - fm_backend_cmux_scoped_title: includes the resolved FM_ROOT hash in the home label
ok - fm_backend_validate: cmux is a known backend
ok - fm_backend_busy_state: cmux (no native primitive) always reports unknown, same as tmux/zellij/orca
ok - fm_backend_composer_state: routes cmux to the cmux composer classifier
ok - fm_backend_cmux_ping_state: reports 'ok' on PONG
ok - fm_backend_cmux_ping_state: reports 'denied' when socketControlMode=cmuxOnly rejects the connection
ok - fm_backend_cmux_ping_state: reports 'unauth' when password mode rejects a missing/wrong password
ok - fm_backend_cmux_ping_state: reports 'unauth' when password mode rejects a wrong password (Invalid password)
ok - fm_backend_cmux_ping_state: reports 'down' when the app is not running yet
ok - fm_backend_cmux_ensure_running: returns immediately when cmux is already reachable
ok - fm_backend_cmux_ensure_running: fails fast on a denied socket without attempting to launch, naming every viable mode
ok - fm_backend_cmux_ensure_running: fails fast on an unauthenticated socket, naming the password config and the Automation mode alternative
ok - fm_backend_cmux_create_task: refuses a duplicate workspace title (cmux's own new-workspace has no uniqueness check)
ok - fm_backend_cmux_create_task: creates a workspace and parses workspace_id/surface_id from list responses
ok - fm_backend_cmux_target_ready: fails when the workspace/surface is not found (list-panes structural check)
ok - fm_backend_cmux_target_ready: verifies the workspace title against the expected label first
ok - fm_backend_cmux_target_ready: rejects a workspace id reused under a different title
ok - fm_backend_cmux_capture: fetches generously and trims to N lines locally
ok - fm_backend_cmux_capture: propagates a read-screen failure even when stdout is empty
ok - fm_backend_cmux_capture: fails when the target surface is absent
ok - fm_backend_cmux_send_key: normalizes the key (Escape -> escape) and targets the explicit workspace/surface
ok - fm_backend_cmux_send_key: recovers stale workspace/surface ids by expected label
ok - fm_backend_cmux_send_literal: calls send with an explicit workspace/surface and a -- separator
ok - fm_backend_cmux_send_text_line: clears partial input when Enter fails
ok - fm_backend_cmux_send_text_line: reports unsafe input when cleanup also fails
ok - fm_backend_cmux_current_path: actively probes with marked begin/end lines (zellij-shape frozen cwd)
ok - fm_backend_cmux_composer_state: a bare '❯' composer row reads empty
ok - fm_backend_cmux_composer_state: a borderless Claude '❯' composer row reads empty
ok - fm_backend_cmux_composer_state: a borderless Claude row outranks stale bordered scrollback
ok - fm_backend_cmux_composer_state: a borderless Claude '❯'+NBSP composer row reads empty under LC_ALL=C
ok - fm_backend_cmux_composer_state: plain-capture text after a bare glyph degrades to unknown (never false pending)
ok - fm_backend_cmux_composer_state: the ghost placeholder text reads empty, not pending
ok - fm_backend_cmux_composer_state: real composer text reads pending
ok - fm_backend_cmux_composer_state: a slash-command popup's argument-hint placeholder still reads pending (the incident fix)
ok - fm_backend_cmux_composer_state: reports unknown when the surface cannot be captured
ok - fm_backend_cmux_composer_state: reports unknown when no border-delimited composer row is found
ok - fm_backend_cmux_send_text_submit: reports 'empty' once the composer row reads empty after one Enter
ok - fm_backend_cmux_send_text_submit: reports 'pending' when the composer never clears after retried Enters (swallowed)
ok - fm_backend_cmux_send_text_submit: retries past a popup-placeholder-fill Enter and lands the real second Enter (the incident fix)
ok - fm_backend_cmux_send_text_submit: reports 'send-failed' when the target workspace/surface is absent
ok - fm_backend_cmux_window_of_workspace: walks windows and counts the membership-confirming workspace list
ok - fm_backend_cmux_window_of_workspace: echoes nothing when no window holds the workspace
ok - fm_backend_cmux_kill: closes the task workspace directly when it is not the last in its window
ok - fm_backend_cmux_kill: adds a throwaway sibling then closes the target when it is the last workspace in its window
ok - fm_backend_cmux_kill: never fails even when close-workspace fails
ok - fm_backend_cmux_kill: recovers stale workspace/surface ids by expected label
ok - fm_backend_cmux_list_live: lists only this home's scoped task workspaces using plain fm-<id> labels
ok - fm-spawn.sh: refuses backend=cmux for --secondmate spawns (mirrors Orca's refusal; no secondmate launch design exists yet)
EXIT=0
Evidence: tests/fm-backend-orca.test.sh under /bin/bash 3.2 (38 ok, 1 known tasks-axi host failure)

Source: tests/fm-backend-orca.test.sh under /bin/bash 3.2 (38 ok, 1 known tasks-axi host failure)

ok - fm_backend_orca_capture: parses result.terminal.tail and calls terminal read
ok - fm_backend_orca_capture: falls back to result text fields
ok - fm_backend_orca_capture: fails closed on Orca read error JSON
ok - fm_backend_orca_runtime_check: accepts reachable ready runtime
ok - fm_backend_orca_runtime_check: fails closed when runtime is not ready
ok - fm_backend_orca_send_text_submit: verifies empty composer after Enter with one bounded read
ok - fm_backend_orca_send_text_submit: a borderless claude composer confirms delivery (the missing #2029 shape)
ok - fm_backend_orca_composer_state: a stale startup banner cannot outrank the live composer row
ok - fm_backend_orca_send_text_submit: retries Enter while composer remains pending
ok - fm_backend_orca_composer_state: a slash-command popup's argument-hint placeholder still reads pending
ok - fm_backend_orca_composer_state: a bare dead-shell prompt reads unknown (unsafe-for-injection), never empty
ok - fm_backend_orca_send_text_submit: a slash-command popup's placeholder fill on Enter #1 does not short-circuit as submitted; Enter #2 is retried and lands it
ok - fm_backend_orca_send_literal: sends text without submitting
ok - fm_backend_orca_send_text_submit: reports send-failed when Orca send fails
ok - Orca send helpers: fail closed on ok:false JSON
ok - fm_backend_orca_send_key: Enter maps to empty enter, C-c maps to interrupt
ok - fm_backend_orca_send_key: refuses unsupported keys loudly
ok - fm_backend_orca_send_key: refuses Escape instead of mapping it to interrupt
ok - fm_backend_orca_kill: calls terminal close and stays best-effort
ok - fm_backend_orca_kill: a close its missing CLI never attempted reports the failure instead of a success
ok - fm_backend_orca_remove_worktree: refuses empty worktree ids
ok - fm_backend_orca_remove_worktree: fails closed on ok:false JSON
ok - fm_backend_orca_worktree_path: resolves an Orca worktree id to its path
ok - fm-backend dispatcher: accepts orca and routes capture through bin/backends/orca.sh
ok - fm_backend_orca_json_get: ignores undocumented terminal id shapes
ok - Orca lifecycle helpers: register repo, create worktree, create terminal, parse stable ids
ok - fm_backend_orca_worktree_create: removes created worktree when path is missing
ok - fm-spawn.sh --backend orca: preserves metadata when pathless cleanup fails
ok - fm-spawn.sh --backend orca: reuses implicit terminal, records metadata, launches harness
ok - fm-spawn.sh --backend orca --secondmate: refuses before secondmate-home mutation
ok - fm-spawn.sh --backend orca: refuses before mutation when Orca runtime is not ready
ok - fm-spawn.sh --backend orca: refuses non-isolated worktrees and closes implicit terminals
ok - fm-spawn.sh --backend orca: removes worktree when terminal creation fails
ok - fm-spawn.sh --backend orca: preserves metadata when abort cleanup fails
ok - fm-spawn.sh --backend orca: releases terminal and worktree on later aborts
ok - fm-peek/fm-send/fm-crew-state route through backend=orca metadata and its durable inbox
ok - fm-peek/fm-crew-state: Orca read error JSON fails closed
ok - fm_backend_target_exists: Orca ok:false read JSON is not live
not ok - Orca scout teardown should succeed once report exists
fm-captain-hold: compatible tasks-axi is required
REFUSED: scout task orcateardownz3 has not passed the captain-call completion gate.
Inventory its report and any visual review through bin/fm-captain-hold.sh before teardown.: expected exit 0, got 1
EXIT=1

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 0 issues

✅ No issues found.

  • 🚨 .github/workflows/ci.yml:511 - The step that round 2's fix added to the macos-stock-bash job will always fail. It requires exactly 2 ok - lines from FM_TEST_ONLY=test_backend_source_shell_portable FM_TEST_BASH=/bin/bash /bin/bash tests/fm-backend.test.sh, but on macos-latest that test prints 3. The job's PATH (/bin:/usr/bin:...) contains /bin/zsh, which macOS always ships, so all three pass sites run: the zsh leg of the shell loop (tests/fm-backend.test.sh:531), the /bin/bash leg of the same loop (tests/fm-backend.test.sh:531), and the zsh PATH-intact block (tests/fm-backend.test.sh:546). grep -c &#39;^ok - &#39; returns 3, [ 3 -eq 2 ] is false, and the step exits 1. If any assertion failed, fail exits 1 and the step's set -e aborts it anyway, so no outcome passes. Round 2's fix introduced this; it is not a sibling left behind. Remedy: expect 3 in ci.yml:511 and fix the message at ci.yml:512 to match. Add a short comment that macOS ships /bin/zsh, so this step also runs the zsh legs. That is useful, because it is the only CI leg where zsh is known to exist.

🔧 Fix applied.
✅ Re-checked - no issues remain.

🔧 **Test** - 2 issues found ✅
  • ⚠️ tests/fm-backend-orca.test.sh - Local setup issue, not caused by this change. 'Orca scout teardown should succeed once report exists' (tests/fm-backend-orca.test.sh) and 'fm-teardown should succeed for a zellij scout whose worktree is already gone' (tests/fm-backend-zellij.test.sh) fail identically under /bin/bash 3.2 and Bash 5. The refusal is 'fm-captain-hold: compatible tasks-axi is required ... REFUSED: scout task ... has not passed the captain-call completion gate', and tasks-axi isn't on PATH on this host. Neither the failing code path (fm-captain-hold) nor the tests are part of this change, and installing the tool system-wide is outside this phase's boundary. Put a compatible tasks-axi on PATH to run these two tests locally; CI should cover them.
  • ℹ️ bin/fm-composer-lib.sh:214 - Outside this change's scope, for awareness. The zsh contract (from fix: make backend matching shell-portable #230 and this fix) covers recognizing and loading backends. Under zsh, loading and the send/capture functions now work against a real Herdr pane. But some runtime functions still use bash-only code that predates this change, so they fail when called from zsh. fm_backend_herdr_composer_state fails with 'bad substitution' in fm_composer_normalize_spaces_var (${!1} and printf -v at bin/fm-composer-lib.sh:214). fm_backend_herdr_version_check fails with 'read-only variable: status' at bin/backends/herdr.sh:528. Every product entrypoint is a #!/usr/bin/env bash script and works from zsh, as the fm-peek scenario shows, so only direct interactive zsh calls are affected.
  • Live validation: ✅ go - 8 of 9 scenarios driven live against the product
Scenario Result Live Evidence
A zsh user sources bin/fm-backend.sh and runs fm_backend_source herdr (and tmux, zellij, orca, cmux): rc=0, no stderr, sibling-lib functions (fm_composer_, fm_transition_policy, fm_agent_process_, f… ✅ pass live sourcing-matrix-head.txt (zsh block); sourcing-before-fix.txt shows base aedb7bb returning rc=1 with fm_transition_policy missing
Stock macOS /bin/bash 3.2 still loads every adapter cleanly (guard against the round-1 case-inside-$(...) regression in herdr.sh) ✅ pass live sourcing-matrix-head.txt (/bin/bash 3.2.57 block, clean); sourcing-before-fix.txt shows 4ebc5b4b printing the 'syntax error near unexpected token newline' cascade
Bash 5 behaviour is unchanged: every adapter loads with rc=0, no stderr, all sibling functions present ✅ pass live sourcing-matrix-head.txt (bash 5.3.9 block)
Adversarial: fm_backend_source bogus is still rejected with "unknown backend 'bogus'" and rc=1 under zsh, bash 3.2 and bash 5 ✅ pass live sourcing-matrix-head.txt (bogus rows)
Adversarial: zsh's PATH-tied path array is not clobbered and the caller's positional args survive set -- inside fm_backend_source; loading works from another cwd, inside a function, via relative p… ✅ pass live sourcing-edge-cases.txt (rc=0 root_ok=yes path_ok=yes args=[keep me] in all 18 cases)
The strengthened test_backend_source_shell_portable fails before the fix and passes after, including the new CI macos-stock-bash step (FM_TEST_BASH=/bin/bash, expects 2 ok lines) ✅ pass live regression-test-before-after.txt: HEAD exit=0 with ok-count=2; base fails on the zsh tmux leg; 4ebc5b4b fails on the /bin/bash herdr leg
Live Herdr lab: a zsh-loaded herdr adapter sends a command to a real fm-lab-* pane and reads the output back (fm_backend_herdr_send_text_line + fm_backend_herdr_capture shows LIVE_HEAD_ZSH_42) ✅ pass live live-herdr-lab-zsh-adapter-head.txt; contrast in live-herdr-lab-zsh-adapter.txt (base zsh: adapter functions 'command not found'; round-1 bash 3.2: fm_composer_classify_screen missing so composer_stat…
Live Herdr lab: the real bin/fm-peek.sh entrypoint resolves a herdr task from a disposable FM_HOME and prints the pane tail under /bin/bash 3.2, Bash 5, and when launched from zsh ✅ pass live live-fm-peek-herdr-lab.txt (PEEK_MARKER_42 shown, rc=0 for all three)
Full per-adapter test files (tests/fm-backend, -herdr, -orca, -cmux, -zellij) pass under both /bin/bash 3.2 and Bash 5 ⏸️ untested no Not a live-surface limit. The orca and zellij failures are scout-teardown tests refused by fm-captain-hold: compatible tasks-axi is required. tasks-axi isn't on PATH on this host, it isn't reposit…
  • /bin/zsh, /bin/bash (3.2.57) and Bash 5.3: cd $ROOT &amp;&amp; source bin/fm-backend.sh &amp;&amp; fm_backend_source &lt;tmux|herdr|zellij|orca|cmux|bogus&gt;, capturing rc, stderr, sibling-lib functions defined and whether command -v dirname still works
  • Same fm_backend_source herdr check on base aedb7bbf (zsh) and round-1 4ebc5b4b (/bin/bash), extracted with git archive into a temp dir
  • FM_TEST_ONLY=test_backend_source_shell_portable bash tests/fm-backend.test.sh at HEAD
  • FM_TEST_ONLY=test_backend_source_shell_portable FM_TEST_BASH=/bin/bash /bin/bash tests/fm-backend.test.sh (the new CI macos-stock-bash step; ok-count must be 2) at HEAD
  • HEAD's strengthened test file run against aedb7bbf bin/ and 4ebc5b4b bin/ (must fail)
  • Edge cases in all three shells: absolute path from cwd=/, sourcing inside a function, ./fm-backend.sh from bin/, cd away before fm_backend_source, sourcing bin/backends/herdr.sh directly, comparing PATH before and after, keeping caller $@, and checking FM_BACKEND_HERDR_ROOT equals the repo root
  • bin/fm-herdr-lab.sh name zshsrc + provision + run &lt;s&gt; workspace create (isolated Herdr session fm-lab-zshsrc-58392-31225)
  • zsh -f: fm_backend_source herdr, fm_backend_herdr_send_text_line, fm_backend_herdr_capture, fm_backend_herdr_composer_state against the real lab pane; same under /bin/bash and on the base/round-1 trees for contrast
  • bin/fm-lab-home.sh create $LAB + state/zshprobe.meta (backend=herdr), then FM_HOME=$LAB bin/fm-peek.sh zshprobe 4 via /bin/bash, Bash 5, and launched from zsh -f
  • bin/fm-herdr-lab.sh teardown (tripwire check passed; default session still running), rm -rf of the lab home and temp trees
  • FM_TEST_BASH=&lt;sh&gt; &lt;sh&gt; tests/fm-backend{,-orca,-cmux,-zellij,-herdr}.test.sh for /bin/bash 3.2 and Bash 5 (cmux passed in both, fm-backend passed under Bash 5, orca and zellij failed one tasks-axi-gated teardown test each in both, herdr unfinished)
  • Confirmation reruns, unfinished at handoff: FM_TEST_BASH=/bin/bash /bin/bash tests/fm-backend.test.sh alone, and tests/fm-backend-orca.test.sh on the base commit

✅ No issues found.

  • Live validation: ✅ go - 6 of 10 scenarios driven live against the product
Scenario Result Live Evidence
zsh user sources bin/fm-backend.sh and runs fm_backend_source for each of tmux/herdr/zellij/orca/cmux: rc=0, empty stderr, sibling-lib functions defined, PATH still resolves dirname ✅ pass live head-4d49cedb-sourcing-matrix.txt (/bin/zsh block: all 5 backends rc=0 missing=[] stderr_lines=0 dirname=/usr/bin/dirname)
Bug reproduction: on base f593060 the same zsh session half-loads every adapter with 'no such file or directory' sibling errors and missing fm_composer/fm_transition/fm_agent_process functions while… ✅ pass live base-f5930603-sourcing-matrix.txt (zsh: tmux 5, herdr 3, zellij 2, orca 1, cmux 2 sourcing errors; bash rows clean)
Stock macOS /bin/bash 3.2 and bash 5 still load every adapter cleanly (no herdr root regression from case-in-$(...)) ✅ pass live head-4d49cedb-sourcing-matrix.txt (/bin/bash 3.2.57 and bash 5.3.9 blocks all rc=0, stderr_lines=0, missing=[])
Adversarial sourcing shapes: inside a function, relative ./fm-backend.sh then cd / before load, herdr.sh sourced directly and from its own dir (no slash), nested libs sourced directly, repo path conta… ✅ pass live head-4d49cedb-sourcing-edge-cases.txt (48/48 rc=0, herdr_root_ok=yes, sibling functions defined, PATH ok under zsh, bash 3.2, bash 5)
Live Herdr lab: from zsh, /bin/bash 3.2 and bash 5 a user loads the herdr adapter, sends a command into a real Herdr pane and captures its output ✅ pass live head-4d49cedb-live-herdr-lab.txt (herdr 0.9.1, fm-lab-zshlive-38232-4609:w1:p1; LIVE_..._42 echoed and captured for all three shells; base under zsh prints the three sibling-load errors; teardown ok)
Real product entrypoint: bin/fm-peek.sh peeks a herdr-backed task via a disposable lab FM_HOME when run by /bin/bash 3.2, bash 5, and from a zsh shell ✅ pass live head-4d49cedb-live-herdr-lab.txt (fm-peek.sh zshprobe 4 returned PEEK_MARKER_42, rc=0, in all three invocations)
Strengthened regression test test_backend_source_shell_portable fails on the pre-fix tree and passes at HEAD ⏸️ untested no The prior payload recorded this only as an automated test run (live=false), not a drive of the live product, so it did not establish a live result. The live behavior this test guards is covered by the…
New macos-stock-bash CI step passes under the job's PATH with /bin/bash 3.2 and /bin/zsh present (expects exactly 3 ok lines) ⏸️ untested no The prior payload recorded only a local replay of the CI step body (live=false), not a run on real GitHub CI, so it did not establish a live result. The actual GitHub CI run belongs to the CI phase.
No bash regression in the adapter unit suites that source the changed files directly ⏸️ untested no The prior payload recorded these only as automated unit-suite runs (live=false), not drives of the live product, so it did not establish a live result. The live bash 3.2 and bash 5 adapter loading and…
tmux backend: drive a real tmux pane through fm_backend_tmux_send/capture after loading the adapter from zsh ⏸️ untested no The tmux binary is not on PATH on this host, and the workspace boundary forbids installing system packages (brew install tmux). Building tmux in the workspace would also need libevent and ncurses tool…
  • matrix.sh &lt;worktree&gt; HEAD: fm_backend_source {tmux,herdr,zellij,orca,cmux} under /bin/zsh, /bin/bash 3.2, bash 5.3 from cwd=/, checking rc, stderr, sibling-lib functions and PATH
  • matrix.sh &lt;git-archive f5930603&gt; BASE: same matrix on the pre-fix tree, reproducing the zsh half-load
  • Edge cases under each shell: source+load inside a function, relative ./fm-backend.sh then cd /, herdr.sh sourced directly and from its own dir, fm-tmux-lib.sh, fm-agent-process-lib.sh and orca.sh sourced directly, worktree and path-with-spaces copies (48 cases)
  • bin/fm-herdr-lab.sh name/provision/run workspace create/teardown fm-lab-zshlive-* session: fm_backend_herdr_send_text_line plus fm_backend_herdr_capture against a real pane from /bin/zsh -f, /bin/bash 3.2, bash 5, plus base under zsh
  • bin/fm-lab-home.sh create $LAB + state/zshprobe.meta (backend=herdr): bin/fm-peek.sh zshprobe 4 via /bin/bash, bash 5, and a zsh shell running the shebang
  • FM_TEST_ONLY=test_backend_source_shell_portable FM_TEST_BASH=/bin/bash /bin/bash tests/fm-backend.test.sh against the base tree (expect fail, exit 1)
  • Replay of the ci.yml macos-stock-bash step under env -i PATH=/bin:/usr/bin:/usr/sbin:/sbin /bin/bash -e (portable_count=3)
  • /bin/bash tests/fm-backend.test.sh and bash tests/fm-backend.test.sh (31/31 each)
  • /bin/bash tests/fm-backend-{orca,cmux,zellij,herdr}.test.sh
✅ **Document** - passed

✅ No issues found.

✅ No issues found.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 1)

  • ⚠️ linter found issues (exit code 1)

✅ **Push** - passed

✅ No issues found.

✅ No issues found.

@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Fixes shell script sourcing to work under zsh.

The PR appears safe to merge based on the reviewed changes.

Reviews (2) · Last reviewed commit: "no-mistakes(review): Expect three shell-..."

@tuprogramadorfav
tuprogramadorfav force-pushed the fm/fm-backend-zsh-split branch from a1d859d to 4d49ced Compare October 2, 2026 04:46
@tuprogramadorfav tuprogramadorfav changed the title fix(bin): make backend adapter sourcing work under zsh fix(bin): resolve backend adapter sibling libs when sourced under zsh Oct 2, 2026
@tuprogramadorfav

Copy link
Copy Markdown
Author

@kunchenguid this PR comes from a fork, so its workflow runs are waiting on maintainer approval and no CI job has run yet. Could you approve the runs when you have a moment, and merge if they come back green?

The branch has no conflicts with main and the Greptile review passes. Happy to address anything the checks surface.

@tuprogramadorfav

Copy link
Copy Markdown
Author

Closing, I will not be following up on this one.

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