Skip to content

fix(bin): load backend sibling libraries when fm-backend.sh is sourced from zsh - #17

Merged
cloud-practitioner merged 4 commits into
mainfrom
fm/fm-backend-source-zsh-split
Oct 1, 2026
Merged

cloud-practitioner merged 4 commits into
mainfrom
fm/fm-backend-source-zsh-split

Conversation

@cloud-practitioner

@cloud-practitioner cloud-practitioner commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Intent

Fix test failures found along the way: the tests/fm-backend.test.sh zsh case fails because fm_backend_source in bin/fm-backend.sh loops for rel in $siblings, which zsh does not word-split, so sibling libraries do not load under zsh. The earlier fix makes fm_backend_source load sibling libraries under zsh, already open as #17.

What Changed

  • fm_backend_source in bin/fm-backend.sh now keeps each backend's sibling library list in positional parameters (set -- ...) and loops over "$@", not an unquoted string. zsh doesn't word-split an unquoted string, so the old loop never sourced the sibling libraries there. The local variable path is also renamed to lib_path because path is a special array in zsh.
  • Sibling-directory lookups now use ${BASH_SOURCE[0]:-$0} so they still resolve under zsh, where BASH_SOURCE is unset. This covers the backend adapters (bin/backends/{cmux,herdr,orca,zellij}.sh) and the libraries that source other libraries (fm-tmux-lib.sh, fm-session-lock-lib.sh, fm-agent-process-lib.sh).
  • The zsh case in tests/fm-backend.test.sh now sources every backend (tmux, herdr, zellij, orca, cmux) under zsh. For each one it checks that the adapter and the functions from all of its sibling libraries are defined and that nothing is written to stderr. It still checks that unknown backends are rejected.

🤖 Generated with Claude Code

Risk Assessment

✅ Low: The change is small and bounded. It swaps the word-split sibling list for positional parameters, and adds a ${BASH_SOURCE[0]:-$0} fallback that already exists at bin/fm-backend.sh:50, so bash resolution is unchanged. Under zsh, every backend now loads its sibling libraries from any working directory with no stderr, and the tightened test fails on any missing sibling function.

Testing

I drove the change through real interactive and non-interactive zsh 5.9.1 and bash 5.2 shells. Under zsh, every backend now loads its adapter and all its sibling libraries: return code 0, empty stderr, and sibling functions both defined and working (agent-process classify, harness name, gemini/cursor detection, transition record, hometag). It also holds from an unrelated working directory, under a path with spaces, on repeat calls, and when a library is sourced on its own. A missing sibling still fails closed, an unknown backend is still rejected, and the caller's positional parameters are untouched. Bash results are identical to zsh. The tightened zsh test fails on base sources and passes at HEAD. The rest of tests/fm-backend.test.sh, plus the session-lock and composer-lib suites, also pass. In the live Herdr lab smoke test, the first five bash adapter checks pass, then one assertion fails exactly the same way at the base commit, so this change did not cause it. The tmux liveness suite skipped itself because tmux is not installed. Separately, some sibling functions (for example fm_composer_classify_content) still stop with "bad substitution" when actually run under zsh. That is outside this change, which is only about loading.

  • Live validation: ✅ go - 8 of 9 scenarios driven live against the product
Scenario Result Live Evidence
In an interactive zsh session (outside the repo), sourcing bin/fm-backend.sh and running fm_backend_source <backend> loads each of tmux/herdr/zellij/orca/cmux with its sibling libraries: rc=0, no stde… ✅ pass live zsh-interactive-all-backends.clean.txt
Regression: the tightened zsh test in tests/fm-backend.test.sh fails on base-commit sources and passes at HEAD ✅ pass live fm-backend-test-base-with-new-test.txt (not ok on tmux) vs fm-backend-test-head.txt (ok - zsh: fm_backend_source recognizes known backends...)
Adversarial: a repo under a path containing spaces, sourced by absolute path from / under zsh, still loads every backend cleanly ✅ pass live zsh-bash-adversarial.txt section A
Adversarial: with a sibling library missing, fm_backend_source fails closed under zsh and bash and does not mark the backend loaded (a retry fails again) ✅ pass live zsh-bash-adversarial.txt section B
Unknown backend is still rejected under zsh with the expected error ✅ pass live zsh-interactive-all-backends.clean.txt (error: unknown backend 'bogus'... bogus rc=1)
Standalone sourcing of a sibling library or adapter (the BASH_SOURCE/$0 fallback) works in both zsh and bash ✅ pass live zsh-bash-adversarial.txt section C
Bash behavior unchanged: same loading and fail-closed results, the caller's positional parameters are preserved, and existing bash suites still pass ✅ pass live zsh-bash-adversarial.txt bash sections; fm-backend-test-head.txt; fm-session-lock-ancestry.txt; fm-composer-lib.txt
Real herdr adapter path under bash against an isolated fm-lab herdr session behaves the same as at the base commit ✅ pass live fm-backend-herdr-smoke-bash.txt vs fm-backend-herdr-smoke-bash-BASE.txt
Real tmux backend use from a zsh-sourced fm-backend.sh (fm_backend_tmux_capture against a live pane) ⏸️ untested no tmux is not on PATH on this machine, and the workspace rules forbid installing system packages. Loading the tmux adapter and its siblings under zsh was proven live. Driving a real tmux pane needs a ho…
Evidence: Interactive zsh session loading all five backends and calling sibling functions (ANSI stripped)

Source: Interactive zsh session loading all five backends and calling sibling functions (ANSI stripped)

%                                                                                                                                                                12b492481022% e�echo "zsh $ZSH_VERSION cwd=$PWD"
zsh 5.9.1 cwd=/tmp
%                                                                                                                                                                12b492481022% s�source ~/.no-mistakes/worktrees/450411b3e67c/01M3TDNJ7VYPBSYHBBV05W8Q8S/bin/fm-backend.sh; echo "source rc=$?"
source rc=0
%                                                                                                                                                                12b492481022% f�for b in tmux herdr zellij orca cmux; do ( fm_backend_source $b 2>/tmp/fm-zsh-live/err.$b; echo "[$b] fm_backend_source rc=$? stderr_bytes=$(wc -c   </tmp/fm-zsh-live/err.$b)"; whence -w fm_backend_${b}_capture fm_composer_classify_content fm_backend_hometag fm_transition_record fm_harness_path_name fm_curs oor_path_is_cursor fm_agent_process_classify_name fm_gemini_path_is_gemini | tr '\n' ' '; echo ); done
[tmux] fm_backend_source rc=0 stderr_bytes=0
fm_backend_tmux_capture: function fm_composer_classify_content: function fm_backend_hometag: none fm_transition_record: none fm_harness_path_name: function fm_cursor_path_is_cursor: function fm_agent_process_classify_name: function fm_gemini_path_is_gemini: function 
[herdr] fm_backend_source rc=0 stderr_bytes=0
fm_backend_herdr_capture: function fm_composer_classify_content: function fm_backend_hometag: none fm_transition_record: function fm_harness_path_name: function fm_cursor_path_is_cursor: function fm_agent_process_classify_name: function fm_gemini_path_is_gemini: function 
[zellij] fm_backend_source rc=0 stderr_bytes=0
fm_backend_zellij_capture: function fm_composer_classify_content: function fm_backend_hometag: function fm_transition_record: none fm_harness_path_name: none fm_cursor_path_is_cursor: none fm_agent_process_classify_name: none fm_gemini_path_is_gemini: none 
[orca] fm_backend_source rc=0 stderr_bytes=0
fm_backend_orca_capture: function fm_composer_classify_content: function fm_backend_hometag: none fm_transition_record: none fm_harness_path_name: none fm_cursor_path_is_cursor: none fm_agent_process_classify_name: none fm_gemini_path_is_gemini: none 
[cmux] fm_backend_source rc=0 stderr_bytes=0
fm_backend_cmux_capture: function fm_composer_classify_content: function fm_backend_hometag: function fm_transition_record: none fm_harness_path_name: none fm_cursor_path_is_cursor: none fm_agent_process_classify_name: none fm_gemini_path_is_gemini: none 
%                                                                                                                                                                12b492481022% (�( fm_backend_source tmux && echo "tmux: classify(/usr/bin/claude)=$(fm_agent_process_classify_name /usr/bin/claude) classify(/bin/zsh)=$(fm_agent_ pprocess_classify_name /bin/zsh) harness_name(/x/codex)=$(fm_harness_path_name /x/codex)"; fm_gemini_path_is_gemini /opt/gemini && echo "tmux: gemini lib live";  ffm_backend_source tmux; echo "tmux: second call rc=$?" )
tmux: classify(/usr/bin/claude)=agent classify(/bin/zsh)=shell harness_name(/x/codex)=codex
tmux: gemini lib live
tmux: second call rc=0
%                                                                                                                                                                12b492481022% (�( fm_backend_source herdr && FM_STATE_DIR=/tmp/fm-zsh-live type fm_transition_record | head -1 )
fm_transition_record is a shell function from ~/.no-mistakes/worktrees/450411b3e67c/01M3TDNJ7VYPBSYHBBV05W8Q8S/bin/fm-transition-lib.sh
%                                                                                                                                                                12b492481022% (�( fm_backend_source bogus; echo "bogus rc=$?" )
error: unknown backend 'bogus' (known: tmux herdr zellij orca cmux)
bogus rc=1
%                                                                                                                                                                12b492481022% e�exit
Evidence: Raw pty transcript of the interactive zsh session

Source: Raw pty transcript of the interactive zsh session

�[1m�[7m%�[27m�[1m�[0m                                                                                                                                                                �[0m�[27m�[24m�[J12b492481022% �[K�[?2004he�echo "zsh $ZSH_VERSION cwd=$PWD"�[?2004l
zsh 5.9.1 cwd=/tmp
�[1m�[7m%�[27m�[1m�[0m                                                                                                                                                                �[0m�[27m�[24m�[J12b492481022% �[K�[?2004hs�source ~/.no-mistakes/worktrees/450411b3e67c/01M3TDNJ7VYPBSYHBBV05W8Q8S/bin/fm-backend.sh; echo "source rc=$?"�[?2004l
source rc=0
�[1m�[7m%�[27m�[1m�[0m                                                                                                                                                                �[0m�[27m�[24m�[J12b492481022% �[K�[?2004hf�for b in tmux herdr zellij orca cmux; do ( fm_backend_source $b 2>/tmp/fm-zsh-live/err.$b; echo "[$b] fm_backend_source rc=$? stderr_bytes=$(wc -c �[K  </tmp/fm-zsh-live/err.$b)"; whence -w fm_backend_${b}_capture fm_composer_classify_content fm_backend_hometag fm_transition_record fm_harness_path_name fm_curs �[Koor_path_is_cursor fm_agent_process_classify_name fm_gemini_path_is_gemini | tr '\n' ' '; echo ); done�[?2004l
[tmux] fm_backend_source rc=0 stderr_bytes=0
fm_backend_tmux_capture: function fm_composer_classify_content: function fm_backend_hometag: none fm_transition_record: none fm_harness_path_name: function fm_cursor_path_is_cursor: function fm_agent_process_classify_name: function fm_gemini_path_is_gemini: function 
[herdr] fm_backend_source rc=0 stderr_bytes=0
fm_backend_herdr_capture: function fm_composer_classify_content: function fm_backend_hometag: none fm_transition_record: function fm_harness_path_name: function fm_cursor_path_is_cursor: function fm_agent_process_classify_name: function fm_gemini_path_is_gemini: function 
[zellij] fm_backend_source rc=0 stderr_bytes=0
fm_backend_zellij_capture: function fm_composer_classify_content: function fm_backend_hometag: function fm_transition_record: none fm_harness_path_name: none fm_cursor_path_is_cursor: none fm_agent_process_classify_name: none fm_gemini_path_is_gemini: none 
[orca] fm_backend_source rc=0 stderr_bytes=0
fm_backend_orca_capture: function fm_composer_classify_content: function fm_backend_hometag: none fm_transition_record: none fm_harness_path_name: none fm_cursor_path_is_cursor: none fm_agent_process_classify_name: none fm_gemini_path_is_gemini: none 
[cmux] fm_backend_source rc=0 stderr_bytes=0
fm_backend_cmux_capture: function fm_composer_classify_content: function fm_backend_hometag: function fm_transition_record: none fm_harness_path_name: none fm_cursor_path_is_cursor: none fm_agent_process_classify_name: none fm_gemini_path_is_gemini: none 
�[1m�[7m%�[27m�[1m�[0m                                                                                                                                                                �[0m�[27m�[24m�[J12b492481022% �[K�[?2004h(�( fm_backend_source tmux && echo "tmux: classify(/usr/bin/claude)=$(fm_agent_process_classify_name /usr/bin/claude) classify(/bin/zsh)=$(fm_agent_ �[Kpprocess_classify_name /bin/zsh) harness_name(/x/codex)=$(fm_harness_path_name /x/codex)"; fm_gemini_path_is_gemini /opt/gemini && echo "tmux: gemini lib live";  �[Kffm_backend_source tmux; echo "tmux: second call rc=$?" )�[?2004l
tmux: classify(/usr/bin/claude)=agent classify(/bin/zsh)=shell harness_name(/x/codex)=codex
tmux: gemini lib live
tmux: second call rc=0
�[1m�[7m%�[27m�[1m�[0m                                                                                                                                                                �[0m�[27m�[24m�[J12b492481022% �[K�[?2004h(�( fm_backend_source herdr && FM_STATE_DIR=/tmp/fm-zsh-live type fm_transition_record | head -1 )�[?2004l
fm_transition_record is a shell function from ~/.no-mistakes/worktrees/450411b3e67c/01M3TDNJ7VYPBSYHBBV05W8Q8S/bin/fm-transition-lib.sh
�[1m�[7m%�[27m�[1m�[0m                                                                                                                                                                �[0m�[27m�[24m�[J12b492481022% �[K�[?2004h(�( fm_backend_source bogus; echo "bogus rc=$?" )�[?2004l
error: unknown backend 'bogus' (known: tmux herdr zellij orca cmux)
bogus rc=1
�[1m�[7m%�[27m�[1m�[0m                                                                                                                                                                �[0m�[27m�[24m�[J12b492481022% �[K�[?2004he�exit�[?2004l
Evidence: zsh vs bash: path with spaces, missing sibling, standalone sourcing, positional parameters

Source: zsh vs bash: path with spaces, missing sibling, standalone sourcing, positional parameters

===== zsh (5.9.1) =====
-- A. repo copy under a path with spaces, sourced by absolute path from /:
  tmux rc=0 stderr_bytes=0 composer_fn=yes
  herdr rc=0 stderr_bytes=0 composer_fn=yes
  zellij rc=0 stderr_bytes=0 composer_fn=yes
  orca rc=0 stderr_bytes=0 composer_fn=yes
  cmux rc=0 stderr_bytes=0 composer_fn=yes
-- B. a missing sibling (fm-gemini-lib.sh removed) must fail closed (silent readable pre-check, by design):
  tmux rc=1 gemini_fn=no
  tmux retry rc=1
-- C. relative source from repo root, then standalone sourcing of a sibling lib / adapter directly:
  standalone agent-process rc=0 gemini=yes harness=claude cursor_fn=yes
  standalone herdr.sh rc=0 FM_BACKEND_HERDR_ROOT=~/.no-mistakes/worktrees/450411b3e67c/01M3TDNJ7VYPBSYHBBV05W8Q8S composer_fn=yes
-- D. caller positional parameters survive fm_backend_source (set -- is function-local):
  after: $#=3 args=one two three
===== bash (5.2.37(1)-release) =====
-- A. repo copy under a path with spaces, sourced by absolute path from /:
  tmux rc=0 stderr_bytes=0 composer_fn=yes
  herdr rc=0 stderr_bytes=0 composer_fn=yes
  zellij rc=0 stderr_bytes=0 composer_fn=yes
  orca rc=0 stderr_bytes=0 composer_fn=yes
  cmux rc=0 stderr_bytes=0 composer_fn=yes
-- B. a missing sibling (fm-gemini-lib.sh removed) must fail closed (silent readable pre-check, by design):
  tmux rc=1 gemini_fn=no
  tmux retry rc=1
-- C. relative source from repo root, then standalone sourcing of a sibling lib / adapter directly:
  standalone agent-process rc=0 gemini=yes harness=claude cursor_fn=yes
  standalone herdr.sh rc=0 FM_BACKEND_HERDR_ROOT=~/.no-mistakes/worktrees/450411b3e67c/01M3TDNJ7VYPBSYHBBV05W8Q8S composer_fn=yes
-- D. caller positional parameters survive fm_backend_source (set -- is function-local):
  after: $#=3 args=one two three
Evidence: Tightened test run against base-commit sources (fails before the fix)

Source: Tightened test run against base-commit sources (fails before the fix)

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: HERDR_SOCKET_PATH fallback for containers (valid socket detects herdr, non-socket/missing/empty rejected, HERDR_ENV/TMUX win, HERDR_SOCKET_PATH wins over CMUX_WORKSPACE_ID, signal set correctly)
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
not ok - zsh: fm_backend_source tmux did not load the adapter and its sibling libraries (fm_backend_tmux_capture fm_tmux_strip_ghost fm_composer_classify_content fm_cursor_path_is_cursor fm_harness_path_name fm_agent_process_classify_name fm_gemini_path_is_gemini): 
Evidence: tests/fm-backend.test.sh at HEAD (passes)

Source: tests/fm-backend.test.sh at HEAD (passes)

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: HERDR_SOCKET_PATH fallback for containers (valid socket detects herdr, non-socket/missing/empty rejected, HERDR_ENV/TMUX win, HERDR_SOCKET_PATH wins over CMUX_WORKSPACE_ID, signal set correctly)
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 recognizes known backends and rejects unknown ones
ok - bash: fm_backend_source recognizes known backends and rejects unknown ones
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
Evidence: Live herdr lab smoke test at HEAD (bash)

Source: Live herdr lab smoke test at HEAD (bash)

ok - real herdr: version_check accepts the installed binary's protocol
ok - real herdr: container_ensure starts the isolated session's server, creates the firstmate workspace (fm-lab-backend-smoke-1247907:w1), and reports its seeded default tab id (w1:t1)
ok - real herdr: session status normalizes running and compatible
ok - real herdr: container_ensure is idempotent (reuses/adopts the existing firstmate workspace, reports no seeded default tab on adoption)
ok - real herdr: create_task prunes the freshly-created workspace's seeded default tab, leaving exactly one clean fm-<id> task tab
not ok - REGRESSION: create_task should refuse a duplicate label whose pane hosts a genuinely live registered agent (idle counts as live)
fm-herdr-lab: missing fleet-state tripwire for 'fm-lab-backend-smoke-1247907'; refusing destructive calls
Evidence: Live herdr lab smoke test at the base commit (same failure)

Source: Live herdr lab smoke test at the base commit (same failure)

ok - real herdr: version_check accepts the installed binary's protocol
ok - real herdr: container_ensure starts the isolated session's server, creates the firstmate workspace (fm-lab-backend-smoke-1255736:w1), and reports its seeded default tab id (w1:t1)
ok - real herdr: session status normalizes running and compatible
ok - real herdr: container_ensure is idempotent (reuses/adopts the existing firstmate workspace, reports no seeded default tab on adoption)
ok - real herdr: create_task prunes the freshly-created workspace's seeded default tab, leaving exactly one clean fm-<id> task tab
not ok - REGRESSION: create_task should refuse a duplicate label whose pane hosts a genuinely live registered agent (idle counts as live)
fm-herdr-lab: missing fleet-state tripwire for 'fm-lab-backend-smoke-1255736'; refusing destructive calls
Evidence: zsh before/after
BASE: zsh -c 'source bin/fm-backend.sh && fm_backend_source herdr' -> rc=1; fm_transition_record: none; fm_gemini_path_is_gemini: none
HEAD: [herdr] fm_backend_source rc=0 stderr_bytes=0; fm_transition_record: function; fm_gemini_path_is_gemini: function; fm_composer_classify_content: function
- Outcome: ⚠️ 1 warning across 1 run (5m48s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • 🚨 bin/fm-backend.sh:666 - The stated goal is that fm_backend_source loads sibling libraries under zsh. It still doesn't: the change only fixes the readability pre-check loop. The adapters and libraries it sources still find their siblings through ${BASH_SOURCE[0]}, which zsh leaves unset, so they resolve to . or a wrong root and every sibling . fails. Reproduced with zsh -c &#39;cd $ROOT &amp;&amp; source bin/fm-backend.sh &amp;&amp; fm_backend_source herdr&#39;: rc=0 and fm_backend_herdr_capture is defined, but fm_transition_record, fm_composer_classify and fm_agent_process_state are all none. stderr shows no such file or directory: ~/.no-mistakes/worktrees/bin/fm-transition-lib.sh. The change also makes this worse than before. At the base commit, zsh failed closed (rc=1). Now it returns 0 with a half-loaded adapter and sets FM_BACKEND<NAME>SOURCED=1, so later calls never retry, and calls like fm_backend_herdr_wait_transition hit command-not-found at runtime. Every site with the same invariant (sibling resolution must not depend on BASH_SOURCE): bin/backends/herdr.sh:71 (FM_BACKEND_HERDR_ROOT); bin/backends/zellij.sh:115 (FM_BACKEND_ZELLIJ_ROOT); bin/backends/cmux.sh:110 (FM_BACKEND_CMUX_ROOT); bin/backends/orca.sh:13 (composer lib path); bin/fm-tmux-lib.sh:46-47 (composer/cursor lost for tmux); bin/fm-agent-process-lib.sh:16-17 (session-lock/gemini lost for tmux and herdr, so fm_gemini* is undefined after fm_backend_source tmux); bin/fm-session-lock-lib.sh:21-22 (cursor lib). Fix: resolve sibling paths from the FM_BACKEND_LIB_DIR that fm-backend.sh already computes, the way bin/backends/tmux.sh:22-26 already does, falling back to the BASH_SOURCE form for standalone sourcing, e.g. ${FM_BACKEND_LIB_DIR:-${BASH_SOURCE[0]%/*}}.
  • ⚠️ tests/fm-backend.test.sh:619 - The zsh regression test can't catch the failure the intent describes. The new per-backend loop ignores fm_backend_source's exit status and only fails on the string command not found, so it passes while every adapter prints no such file or directory for its sibling libraries. The herdr check at tests/fm-backend.test.sh:613 sends stderr to /dev/null and only probes fm_backend_herdr_capture, which herdr.sh defines itself, so it passes even though fm-composer-lib, fm-transition-lib and fm-agent-process-lib never loaded. For each backend, assert rc=0, empty stderr, and that a function from at least one sibling library is defined, e.g. whence -w fm_transition_record for herdr, fm_composer_* for all backends, and fm_gemini_* for tmux. With those assertions the test fails until the adapter-level issue above is fixed.

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

⚠️ **Test** - 1 warning
  • ⚠️ tests/fm-backend-herdr-smoke.test.sh:147 - tests/fm-backend-herdr-smoke.test.sh fails at 'REGRESSION: create_task should refuse a duplicate label whose pane hosts a genuinely live registered agent' with herdr 0.9.3. It fails identically on the base commit 44833b2, so this change did not cause it. The test registers its fake agent with herdr pane report-agent, and bin/backends/herdr.sh:2338 already notes that on herdr 0.9.x that command returns rc=0 but never takes effect. This test needs updating for the current herdr version, separately from this PR.
  • Live validation: ✅ go - 8 of 9 scenarios driven live against the product
Scenario Result Live Evidence
In an interactive zsh session (outside the repo), sourcing bin/fm-backend.sh and running fm_backend_source <backend> loads each of tmux/herdr/zellij/orca/cmux with its sibling libraries: rc=0, no stde… ✅ pass live zsh-interactive-all-backends.clean.txt
Regression: the tightened zsh test in tests/fm-backend.test.sh fails on base-commit sources and passes at HEAD ✅ pass live fm-backend-test-base-with-new-test.txt (not ok on tmux) vs fm-backend-test-head.txt (ok - zsh: fm_backend_source recognizes known backends...)
Adversarial: a repo under a path containing spaces, sourced by absolute path from / under zsh, still loads every backend cleanly ✅ pass live zsh-bash-adversarial.txt section A
Adversarial: with a sibling library missing, fm_backend_source fails closed under zsh and bash and does not mark the backend loaded (a retry fails again) ✅ pass live zsh-bash-adversarial.txt section B
Unknown backend is still rejected under zsh with the expected error ✅ pass live zsh-interactive-all-backends.clean.txt (error: unknown backend 'bogus'... bogus rc=1)
Standalone sourcing of a sibling library or adapter (the BASH_SOURCE/$0 fallback) works in both zsh and bash ✅ pass live zsh-bash-adversarial.txt section C
Bash behavior unchanged: same loading and fail-closed results, the caller's positional parameters are preserved, and existing bash suites still pass ✅ pass live zsh-bash-adversarial.txt bash sections; fm-backend-test-head.txt; fm-session-lock-ancestry.txt; fm-composer-lib.txt
Real herdr adapter path under bash against an isolated fm-lab herdr session behaves the same as at the base commit ✅ pass live fm-backend-herdr-smoke-bash.txt vs fm-backend-herdr-smoke-bash-BASE.txt
Real tmux backend use from a zsh-sourced fm-backend.sh (fm_backend_tmux_capture against a live pane) ⏸️ untested no tmux is not on PATH on this machine, and the workspace rules forbid installing system packages. Loading the tmux adapter and its siblings under zsh was proven live. Driving a real tmux pane needs a ho…
  • bash tests/fm-backend.test.sh at HEAD (whole file passes, including the tightened zsh case)
  • Copied the HEAD test file onto an extracted base-commit (44833b2) tree and ran bash tests/fm-backend.test.sh there: the zsh case fails (not ok - zsh: fm_backend_source tmux did not load the adapter and its sibling libraries)
  • Started zsh -f -i in a pty (40x160, empty env, cwd /tmp), sourced bin/fm-backend.sh by absolute path, then ran fm_backend_source for each of the five backends plus whence -w probes for sibling functions, sibling functions called with real inputs, a second (idempotent) call, and bogus
  • zsh and bash, same checks: a copy of the repo under a path containing spaces, sourced from /
  • zsh and bash: fm-gemini-lib.sh removed, fm_backend_source tmux must fail closed and keep failing on retry
  • zsh and bash: sourcing bin/fm-agent-process-lib.sh and bin/backends/herdr.sh standalone (fallback path resolution)
  • zsh and bash: the caller's positional parameters survive fm_backend_source (its set -- stays inside the function)
  • bash tests/fm-backend-herdr-smoke.test.sh against a real herdr 0.9.3 isolated fm-lab session, at HEAD and at the base commit
  • bash tests/fm-session-lock-ancestry.test.sh, bash tests/fm-composer-lib.test.sh, bash tests/fm-tmux-agent-liveness.test.sh (the last one skipped itself because tmux is absent)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

cloud-practitioner and others added 4 commits October 1, 2026 00:26
* fix(bin): run no repository hook when core.hooksPath is empty (kunchenguid#6216)

* fix(bin): run no repository hook when core.hooksPath is empty

The per-task hook wrapper refused every commit in a repository whose own
config sets core.hooksPath to the empty string, because git rev-parse
--git-path hooks fails on it. Plain git reads that setting as no hooks, so
the wrapper now runs none; every other lookup failure still refuses and
shows git's error.

Fixes kunchenguid#6171

* no-mistakes(review): Refuse commits when core.hooksPath is a valueless key

* no-mistakes(document): Document empty core.hooksPath handling in commit attribution docs

* no-mistakes(ci): When the wrapper refuses a commit, Git's hook-lookup error now shows up once instead of twice. That required changing one line in the wrapper, and the tests were extended so both bad-config cases would catch the duplicate. Invariant: when the wrapper refuses, Git's lookup error must appear exactly once. In the failure path, the only Git call besides the deliberate second lookup is the `git config --get --type=path core.hooksPath` check in `runtime_chain_body` (`bin/fm-git-strip-ai-trailers.sh:168`). That check prints the same error, so it was the one place to fix. I added `2>/dev/null` to it. Its exit status still decides the outcome: an empty value still runs no hook, and anything else goes on to the second lookup, which prints Git's error once, and the commit is refused. Tests (`tests/fm-git-strip-ai-trailers.test.sh`): - The unresolvable-path test (`~fm-no-such-user-6171/hooks`) now requires `failed to expand user dir` to appear exactly once in the refused commit's output. - The valueless-key test now requires `missing value for 'core.hookspath'` to appear exactly once. - Pre-existing bug in the unresolvable-path test: its `git add` ran after the bad config was set, so it failed silently (exit 128) and the "refused commit" had nothing staged. The test now stages the file before writing the config, the same way the valueless test does, so a real commit gets refused. - The empty-string test is unchanged and still passes, so an empty `core.hooksPath` still runs no hook. Verification: - With the wrapper change reverted, both new checks fail with `expected '1', got '2'`. With the change in place, the whole suite passes. - `bash -n` passes. shellcheck shows only an info-level SC1091 note about sourcing `lib.sh`, which was already there before this change. - `git status` lists only the two intended files

* fix(bin): let a stale record on a reassigned slot retire records-only (kunchenguid#6213)

* fix(bin): let a stale record on a reassigned slot retire records-only

When a pool slot's owner claim names another task, the stale record's
teardown touches nothing under the slot, so the exclusive-slot record scan
no longer refuses it. Full teardowns of a slot this task still claims, or
one with no claim, keep the refusal.

Fixes kunchenguid#6184

* no-mistakes(document): Note claim-over-record precedence for reassigned teardown slots

* fix(bin): keep the steering doorbell short under deep homes (kunchenguid#6240)

* fix(bin): keep the steering doorbell short under deep homes

The doorbell printed the task inbox's absolute path twice, so under a deep
home it grew to about 290 characters and a Herdr submit reported it never
reached the pane on every re-ring. It now names the inbox once by its short
<task>.inbox name and points at the full path the worker's brief already
gives, so its length no longer depends on the home's depth.

Fixes kunchenguid#6120

* no-mistakes(review): Export FM_TASK_INBOX at launch and name it in doorbell

* no-mistakes(ci): ci-1 (Behavior portable serial 9) was caused by this PR, and I fixed it in the test. tests/fm-claude-trust.test.sh failed with "the launch command did not carry a brief doorbell". Its claude_launch_doorbell helper stripped exactly two leading `export ...;` statements before reading the final prompt argument. This PR adds a third one (`export FM_TASK_INBOX=...`) to every launch, so the helper was reading the wrong command. The invariant: a test that parses the launch command must skip every leading export statement, however many there are. I checked every test that parses the launch this way. The only other ones are the two helpers in tests/fm-spawn-dispatch-profile.test.sh, and they already loop over all exports. The kimi and dispatch-profile exact-string checks were updated earlier in this PR. The fix makes claude_launch_doorbell use the same loop (`while [[ "$command" == export\ *\;* ]]; do command=${command#*; }; done`) and then take the last argument. The ordinary path still works: the claude spawn test and the secondmate-clone spawn test both resolve the brief record through the same helper. Verified locally: `bash tests/fm-claude-trust.test.sh` exits 0 with no failing cases. ci-2 (Behavior tests (Herdr)) was not caused by this change, and I made no code change for it. In tests/fm-backend-herdr-presentation-e2e.test.sh, the concurrent secondmate recovery failed with "herdr presentation recovery could not acquire its session lock; refusing a concurrent resume". Two reasons it is not this PR: - The same failure, in the same test and case, happened on run 36655209015 for the unrelated branch fm/fm-contributions-old-gh-compat about 14 hours earlier. - This PR's change cannot lengthen how long the lock is held. The launch is written to a file and sent to the pane as `. launch.N.sh`, so the extra export changes neither the pane submit nor the lock hold time. The cause is a race that was already there: spawn_herdr_presentation_order_lock_acquire gives up after 5 seconds, and a concurrent real-Herdr recovery can hold the lock longer. Fixing that means changing the product's lock timeout, which is outside this PR. It should be tracked separately, and a rerun of the Herdr job is expected to pass. The only file changed is tests/fm-claude-trust.test.sh

* feat(bin): add opt-in config/wait-no-turns so a waiting worker spends no turns (kunchenguid#4859)

* fix(dod): drive no-mistakes with one foreground call, not a background poll

The brief told workers to background the drive call and poll `axi status`
because one call "routinely outlives what your harness lets a single
command run". That advice contradicts the tool it drives: `no-mistakes
axi run --help` documents `--wait` with an 8m default, existing precisely
"so an agent harness with a 10-minute tool cap gets a structured return
instead of an unbounded hang".

Following the old text, a worker could never idle - a backgrounded call
returns in milliseconds, so it does not wait at all - and each attempt
leaked a live timer that later fired as a paid wake. Tell workers to make
one foreground call, let it block, and repeat it when it returns on
elapsed wait rather than on a gate or outcome.

Also drops the generalisation that told workers on any unestablished
harness to assume a command cap and use the same shape, which exported
the defect to harnesses with no such cap.

* fix(bin): let a waiting worker spend no turns until it is answered

A worker waiting on a decision, a pipeline gate, CI, or a heavy-test slot
kept taking model turns: the brief told it to list its inbox at any natural
checkpoint, and six automatic senders nudged secondmates whatever their open
decisions.

- The ship and scout briefs gain one Waiting section: end the turn after
  needs-decision or blocked, and hold an external wait inside ONE blocking
  command bounded by the harness's own command ceiling. The checkpoint clause
  is deleted. Forbidding the wrong shapes is not enough on its own, so the
  section also names the blocking foreground `until` loop as the wait a Claude
  Code worker may use, because that harness can refuse a sleep-then-check
  command while pointing at backgrounding, which is the one shape a waiting
  worker must not take.
- fm-send --automatic defers (exit 4, nothing written or rung) while the
  target has an open decision or blocker of its own; every automatic sender
  passes it and keeps its retry state, and the pending-reply recovery waits
  the same way.
- The two senders that report the result classified it by matching the text of
  the send's captured output against `deferred:*`. fm-send runs bin/fm-guard.sh
  as a supervision warning, and that guard prints its worktree-tangle banner
  whenever the primary checkout is on a feature branch, which is exactly what a
  CI pull-request checkout is. The banner lands ahead of the `deferred:` line,
  so the match fell through and a waiting mate was reported as a failed send,
  with the banner as the reason. Both senders now classify on fm-send's exit
  status, which is the contract the deferral is actually stated in, and select
  the `deferred:` line out of the output rather than assuming it came first.

The third root cause, a no-mistakes definition of done that backgrounded the
drive call and polled axi status, is fixed by this branch's parent commit
"drive no-mistakes with one foreground call, not a background poll"; this
commit takes that text as is and adds the regression test.

Upstream's spawn abort path no longer calls the lease-return helper at all,
so the fork's missing-helper guard and its pin-feature test line are moot
here and are not ported.

The command ceilings each harness enforces, and the probes behind the named
Claude Code wait, are recorded in docs/verification/runtime-backends.md.

* no-mistakes(review): Exempt captain holds, quiet deferred reconcile, clarify worker pauses

* no-mistakes(document): Document deferred automatic nudges, rereads, and reply recovery

* no-mistakes(document): Ring unlanded fire-and-forget steers exactly once more

* no-mistakes(ci): The failing check, "PR must be raised via no-mistakes", reads the pipeline's attestation record, which says document=skipped. No file in the repository can change that record, so I did not touch the check or the PR body. As you said, the no-mistakes rerun after this run finishes will re-execute the document step and record document=completed. The one change is the documentation sentence you ordered. It adds a line to docs/remote-secondmates.md, right after the line saying the remote host runs no re-ring ladder of its own: "A fire-and-forget record, such as a reconcile ask, gets its single retry ring only on the local plane: the remote steer leg owes no re-ring, so a swallowed remote doorbell for one waits for the next ring into that inbox, and a remote-side retry is known follow-up scope." No behavior changed. Checks: tests/fm-documentation-audiences.test.sh passes (4/4) and bin/fm-lint.sh is clean. The change is left uncommitted in the working tree for the pipeline to pick up

* no-mistakes(review): Hold automatic wakes until a mate's own decision closes

* no-mistakes(document): Document watcher delivery of deferred remote re-read nudges

* no-mistakes(review): Merge duplicate elapsed-wait reattach instructions in DOD

* no-mistakes(test): Resolve merged default decision in remote-reply recovery fixture

* no-mistakes(test): Source classify lib so config-push retry-deferred honors open decisions

* no-mistakes(ci): Fixed a flaky test that also fails on main. Neither this PR's bin/fm-brief.sh nor its bin/fm-dod-lib.sh change is involved: bin/fm-dispatch-resolve.sh sources neither file. Another branch (fm-attended-cutover-smoothing-s1, run 36343879084) failed the same shard 8 check the same way, on a different case ("a rule-criterion match prints one diagnostic line, got 2"). Root cause: `fm_quota_single_provider_for_harness` in bin/fm-quota-axi-lib.sh returned from its `while read` loop as soon as it found a match. That closed the pipe while `fm_quota_single_provider_table`'s `printf` was sometimes still writing. GitHub Actions runners ignore SIGPIPE, so bash printed `fm-quota-axi-lib.sh: line 138: printf: write error: Broken pipe` to the resolver's stderr. That is the extra line. I reproduced it locally by running the test with SIGPIPE ignored: 2 of 20 runs failed, one with the resolver's diagnostic line plus two broken-pipe lines. Invariant: looking up a harness in the provider table must never make the table writer fail. The only reader of that table is this function, and all of the resolver's lookups (line 208 without stderr redirected, line 222 with it) go through it. So the fix is in that one place: read the whole table, then print the match. The same file now shows it reads the full table first, like `fm_control_harness_supported` does. Return values and output are unchanged. Verification: with SIGPIPE ignored, tests/fm-dispatch-resolve.test.sh failed 0 of 30 runs after the fix (2 of 20 before). tests/fm-dispatch-resolve.test.sh, tests/fm-brief.test.sh, tests/fm-send-inbox.test.sh, tests/fm-quota-choose.test.sh and tests/fm-quota-array-dispatch-live-e2e.test.sh all pass, and shellcheck is clean. tests/fm-procevent-quota.test.sh fails locally with or without the change ("process-event state root is not a private directory"), so that failure comes from the local environment, not from this fix. No new test was added: the existing one-diagnostic-line assertions already catch this whenever SIGPIPE is ignored, as it is in CI

* Revert "no-mistakes(ci): Fixed a flaky test that also fails on main. Neither this PR's bin/fm-brief.sh nor its bin/fm-dod-lib.sh change is involved: bin/fm-dispatch-resolve.sh sources neither file. Another branch (fm-attended-cutover-smoothing-s1, run 36343879084) failed the same shard 8 check the same way, on a different case ("a rule-criterion match prints one diagnostic line, got 2"). Root cause: `fm_quota_single_provider_for_harness` in bin/fm-quota-axi-lib.sh returned from its `while read` loop as soon as it found a match. That closed the pipe while `fm_quota_single_provider_table`'s `printf` was sometimes still writing. GitHub Actions runners ignore SIGPIPE, so bash printed `fm-quota-axi-lib.sh: line 138: printf: write error: Broken pipe` to the resolver's stderr. That is the extra line. I reproduced it locally by running the test with SIGPIPE ignored: 2 of 20 runs failed, one with the resolver's diagnostic line plus two broken-pipe lines. Invariant: looking up a harness in the provider table must never make the table writer fail. The only reader of that table is this function, and all of the resolver's lookups (line 208 without stderr redirected, line 222 with it) go through it. So the fix is in that one place: read the whole table, then print the match. The same file now shows it reads the full table first, like `fm_control_harness_supported` does. Return values and output are unchanged. Verification: with SIGPIPE ignored, tests/fm-dispatch-resolve.test.sh failed 0 of 30 runs after the fix (2 of 20 before). tests/fm-dispatch-resolve.test.sh, tests/fm-brief.test.sh, tests/fm-send-inbox.test.sh, tests/fm-quota-choose.test.sh and tests/fm-quota-array-dispatch-live-e2e.test.sh all pass, and shellcheck is clean. tests/fm-procevent-quota.test.sh fails locally with or without the change ("process-event state root is not a private directory"), so that failure comes from the local environment, not from this fix. No new test was added: the existing one-diagnostic-line assertions already catch this whenever SIGPIPE is ignored, as it is in CI"

This reverts commit c719928.

* no-mistakes(review): Retry deferred local instruction nudges via the watcher

* no-mistakes(review): Document watcher retry for deferred local instruction nudges

* no-mistakes(ci): I fixed both review findings you selected (ci-1 and ci-3). I did not touch the deferral check in bin/fm-send.sh. ci-1 (bin/fm-config-push.sh, retry_deferred_rereads) - Rule that must hold: a deferred reread stays flagged until it is actually delivered. - Before the fix, the flag was removed before any of the steps that can skip a mate: the remote lock-path lookup, validate_secondmate_home, the local lock-path lookup, and the lock acquire. A skip at any of those dropped the flag, so the watcher lost track of the reread. - Now the flag is removed in one place only, when the send succeeds (rc 0). A skipped home, a busy lock, a deferred send (rc 4) or a failed send all leave it in place. The re-mark calls on a busy lock and on rc 4 were no longer needed, so I removed them. I updated the comment above the function to match. - Side effect: a send that keeps failing now stays flagged, so the watcher retries it on every poll and logs each failure. That follows your "don't clear until delivered" rule, but it replaces the old behaviour of leaving a failed send to the next config push or session start. - New test in tests/fm-secondmate-sync.test.sh: T8j "a deferred flag survives a skipped invalid home and is retried once it validates". It takes the home's marker away to make validation fail, checks that nothing is sent and the flag stays, then puts the marker back and checks that the nudge is delivered and both the flag and the retry marker are cleared. It fails on the old code and passes now. ci-3 (bin/fm-secondmate-restart.sh) - Rule that must hold: no automatic send wakes a mate that is waiting on its own open decision. - The two automatic sends in this script are the fallback reread nudge (fall_back_to_nudge) and the persist request. Both now pass --automatic. If a persist request is deferred, its correlation is discarded and the mate goes to the fallback nudge, which is also deferred, so the mate is reported as unreached. - New test in tests/fm-secondmate-restart.test.sh: T3b. It gives a mate an open needs-decision and runs a restart. It checks that both sends report as deferred, the mate's doorbell is never rung, its inbox gets no message, nothing is stopped, and the mate is reported as unreached with exit status 3. It fails on the old code and passes now. - The test marks the watcher as alive first. Without that, the watcher-down warning is printed first and becomes the reported reason instead of the deferral message. Verification - tests/fm-secondmate-sync.test.sh passes. - tests/fm-secondmate-restart.test.sh passes. - tests/fm-secondmate-harness.test.sh (the other test that exercises --retry-deferred) passes. - The fm-send-inbox test that covers automatic deferral passes. I only looked at the last lines of that run, not the whole file. - `shellcheck -x` on the four changed files is clean

* Pin autoarm supervision model in secondmate restart T3b

The fresh watcher beat the test writes proves a live watcher only under the
autoarm model; on CI hosts with no detected harness the persistent model
demands a lock-holding watcher, so the watcher-down banner became the
reported reason and the deferral assertion failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep deferred secondmate nudges retryable under the inheritance lock.

A bootstrap instruction nudge could write its deferral flag outside the lock the watcher retry holds, so a concurrent retry could delete a flag that had just been set. A restart fallback that is deferred now records the same marker and flag, so the watcher delivers it once the decision closes.

* no-mistakes(document): Document watcher retry of deferred restart re-read nudges

* Send secondmate reread and restart nudges immediately again.

Deferring those nudges let a later config push drop an incomplete transfer once the decision closed. They now send as they do on main.

* Make the no-turn wait opt-in behind config/wait-no-turns.

Homes that do not create the file keep the previous briefs, drive text, and sends.

* no-mistakes(document): Document wait-no-turns inbox wording change in configuration

* no-mistakes(review): Keep checkpoint inbox check; forbid only polling while waiting

* no-mistakes(ci): Fixed ci-2 (Greptile: a concurrent retry marker gets lost). The rule that was broken: the watcher may remove only the `.retry-ring` mark for the record it just processed. A newer mark written in the meantime is owed its own retry. `fm_task_inbox_clear_retry` is the one shared function that removes the mark, and I fixed it there. In `bin/fm-task-inbox-lib.sh` it now takes the record path. It compares the mark's content with that record's name and removes the mark only when they match. When the mark names a different record it returns success and leaves the mark alone. It still fails only when the processed record's own mark can't be removed. Both callers in `bin/fm-watch.sh` now pass `"$rec"`: the dead or missing pane path and the path after a retry ring. So the fix holds at both removal sites. Tests, in `tests/fm-task-inbox.test.sh`: - I added an optional `FM_RING_MARKS_RETRY` hook to the fake tmux. It writes a newer record's mark while the doorbell is being typed, which reproduces the race deterministically. - I added `test_watcher_retry_keeps_a_newer_mark`. The owed retry rings once, the newer mark survives, and a later check rings the newer record once and then clears its mark. The test fails without the fix ("the spent retry removed a newer record's mark written during its ring") and passes with it. - I updated the direct `clear_retry` call in the existing unit test to pass the record. Results: `tests/fm-task-inbox.test.sh` passes in full and `tests/fm-send-inbox.test.sh` passes 15/15. Shellcheck reports only SC1091 "not following sourced file" notices. As instructed, I didn't change the brief inbox wording

* no-mistakes(document): Fix stale wait-no-turns inbox wording in inbox lib comment

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kun Chen <kunchenguid@users.noreply.github.com>

---------

Co-authored-by: Christopher McKay <101884182+karotkriss@users.noreply.github.com>
Co-authored-by: Tiago <tiagop@hey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kun Chen <kunchenguid@users.noreply.github.com>
@cloud-practitioner
cloud-practitioner force-pushed the fm/fm-backend-source-zsh-split branch from d0ec770 to 8fa636b Compare October 1, 2026 00:44
@cloud-practitioner cloud-practitioner changed the title fix(bin): make fm_backend_source load backend sibling libraries under zsh fix(bin): load backend sibling libraries when fm-backend.sh is sourced from zsh Oct 1, 2026
@cloud-practitioner
cloud-practitioner merged commit f249554 into main Oct 1, 2026
20 checks passed
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