feat(bin): run on Windows via terminal-multiplexer abstraction#86
Open
kevinamick wants to merge 7 commits into
Open
feat(bin): run on Windows via terminal-multiplexer abstraction#86kevinamick wants to merge 7 commits into
kevinamick wants to merge 7 commits into
Conversation
tmux has no Windows port, so route all multiplexer access through bin/fm-mux.sh (tmux + wezterm backends) and add portable process introspection in bin/fm-proc.sh (MSYS ps lacks -o). Refactor every tmux consumer, harness/lock detection, and bootstrap; pin FM_MUX=tmux in tests; add tests/fm-mux.test.sh; enforce LF via .gitattributes; update README/CONTRIBUTING/AGENTS.
- fm-spawn: add an OSC-7-independent fallback to worktree detection (sentinel $PWD printed into the pane, read back from pane text), gated to the wezterm backend so the tmux path stays byte-identical. - fm-mux: cygpath -w the auto-detected MSYS bash path before passing it to the native wezterm cli spawn (a Windows process cannot resolve /usr/bin/bash). - fm-lock: align HARNESS_RE with fm-harness.sh so it matches the Windows process name pi.exe (and pi with args), not only a bare ^pi$. - fm-bootstrap: emit a per-platform installable token (wezterm on Windows, tmux elsewhere) instead of the unknown MISSING: multiplexer token.
Resolve conflicts between the Windows multiplexer-abstraction port and upstream's doc restructure, verified-submit core, and from-firstmate marker:
- fm-send.sh / fm-supervise-daemon.sh: backend-aware - tmux uses the verified fm-tmux-lib submit/composer core; wezterm uses the fm_mux_* abstraction. Marker and settle pause apply on both.
- README/AGENTS: adopt upstream's terse docs/-based structure; Windows specifics migrated into docs/{architecture,configuration,scripts}.md.
- CONTRIBUTING: combine shellcheck bin+tests with the Windows/fm-mux/fm-proc conventions.
- tests: keep upstream helper sourcing, add FM_MUX=tmux backend pin.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Changed
bin/fm-mux.sh, a multiplexer abstraction withtmux(macOS/Linux) andwezterm(Windows) backends behind a commonfm_mux_*verb set, plus opaque window handles (session:windowfor tmux,wezterm:<pane_id>for wezterm) and auto backend selection.bin/fm-proc.shfor portable process/ancestry introspection, walking the Windows process tree via a PowerShell CIM query where MSYSps -ois unavailable, and rewiredfm-spawn,fm-peek,fm-send,fm-watch,fm-teardown,fm-supervise-daemon,fm-harness,fm-lock, andfm-bootstrapto drive the multiplexer and process helpers instead of callingtmux/psdirectly.tests/fm-mux.test.shcovering both backends via fakes, a.gitattributesfor line-ending normalization, and updatedAGENTS.md,README.md, andCONTRIBUTING.mdto document the multiplexer/process abstraction and Windows support.Risk Assessment
✅ Low: The macOS/Linux (tmux) path is preserved byte-identically and the new wezterm backend is purely additive (no prior Windows support to regress), both backends are test-covered, and the only outstanding item is an info-level forward-compatibility note on a parser whose current behavior the author verified.
Testing
On a real Windows ARM64 + Git Bash + WezTerm host (the target platform; no tmux), the full behavior suite was run and the WezTerm port was exercised end-to-end: fm-mux.sh auto-selected the wezterm backend, spawned a live WezTerm tab, round-tripped a unique marker through send-text/capture, reported pane-path/cursor-y/pane-alive, and killed the tab cleanly; fm-proc.sh walked the real Windows process tree and fm-harness.sh/fm-bootstrap.sh produced the correct Windows behaviors. No reviewer-visible screenshot is included because this is a terminal-multiplexer/CLI change whose end-user surface is the WezTerm pane itself — the pane's input and echoed output are captured verbatim in the e2e transcript, which is the faithful surface artifact (capturing the live WezTerm window would expose the user's unrelated working session). Two suite tests (fm-wake-queue singleton timing, fm-secondmate relative-origin path shape) fail only on a Windows dev box due to MSYS-specific test-harness assumptions; both are green on the project's ubuntu-latest CI and the underlying product behavior was verified correct, so they are reported as non-blocking warnings rather than regressions.
Evidence: WezTerm backend end-to-end on real Windows (spawn -> send -> capture marker round-trip -> kill)
[1] backend auto-selection (no FM_MUX set): fm-mux.sh backend -> wezterm [2] spawn a real WezTerm tab -> handle wezterm:2 [3] pane-path -> /tmp/tmp.xbzLLIRMWl (matches expected pwd -P) [4] pane-alive -> TRUE [5] sent: echo FM_E2E_152618440-OUTPUT [6] pane| $ echo FM_E2E_152618440-OUTPUT pane| FM_E2E_152618440-OUTPUT RESULT: marker round-tripped through the real WezTerm pane -> PASS [7] cursor-y -> 6 [8] pane-alive after kill -> FALSE (tab gone, as expected)Evidence: fm-proc.sh portable introspection walks the real Windows process tree on MSYS
fm_proc_is_windows -> yesps -o comm= -p $$-> ps: unknown option -- o (Unix path cannot work on MSYS) fm_proc_ancestry $$ -> bash.exe -> pwsh.exe -> copilot.exe -> no-mistakes.exe (Windows process tree) fm_proc_alive(live winpid) -> TRUE ; fm_proc_alive(999999) -> FALSEEvidence: fm-harness.sh detection on Windows (env markers, crew override, ancestry fallback)
CLAUDECODE=1 -> claude ; PI_CODING_AGENT=true -> pi crew-harness=codex -> codex ; default+PI -> pi bare (no marker) -> unknown (actual harness copilot.exe is unrecognized; AGENTS.md says ask the captain)Evidence: fm-bootstrap.sh emits the Windows-specific wezterm multiplexer token
Real box (wezterm present): no multiplexer MISSING line Simulated no-multiplexer Windows box: MISSING: wezterm (install: winget install wez.wezterm # or https://wezfurlong.org/wezterm/install/windows.html)Pipeline
Updates from git push no-mistakes
⏭️ **intent** - skipped
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-proc.sh:44- _fm_proc_run_ps leaks a temp file on every Windows invocation.tmp=$(mktemp)creates an empty file (e.g. /tmp/tmp.AbC123), thentmp="$tmp.ps1"reassigns andcat > "$tmp"writes to the distinct .ps1 path; the original mktemp file is never removed (rm -f "$tmp"only deletes the .ps1). Each fm_proc_ancestry/fm_proc_info/fm_proc_alive call on MSYS (lock acquire/status, harness detection) leaks one 0-byte file. Fix: write to the mktemp path directly, ormktemp --suffix=.ps1, or alsorm -f "${tmp%.ps1}". Secondary nit: the mktemp-absent fallback uses a predictable name in a shared temp dir whichcat >would follow through a pre-planted symlink; the primary mktemp path avoids this, so it only matters when mktemp is missing.🔧 Fix: fix temp-dir leak in fm-proc PowerShell helper
1 info still open:
bin/fm-mux.sh:72- _fm_wez_list's perl parser is order-dependent: it flushes a pane record only when it sees the next"pane_id"line, so it assumes pane_id is the FIRST key emitted within each pane object. If a wezterm version emits title/cwd/cursor_x/cursor_y before pane_id, those values are attributed to the PREVIOUS pane (and the first pane's leading fields are lost), silently corrupting fm_mux_pane_path / fm_mux_cursor_y / fm_mux_pane_alive — which in turn would break spawn worktree detection and the afk busy-guard. The code comment only guarantees key-name non-collision, not field ordering, and tests/fm-mux.test.sh fixture hardcodes pane_id-first so a reorder regression would not be caught. Current wezterm output evidently emits pane_id first (author-verified during the port), so this is a robustness/forward-compat concern rather than a present defect. A field-keyed parse (accumulate per object, emit at object close) or an explicit fixture asserting the assumption would harden it.tests/fm-wake-queue.test.sh:303- On this Windows/MSYS box, test_singleton_start fails with 'expected exactly one live watcher, got 2'. Root cause is environment-specific, not a product regression: the test's 0.5s settle (line 299) is too short for Windows fork/exec, and is_live_non_zombie'sps -p PID -o stat=is unsupported by MSYS ps (the very gap fm-proc.sh exists to bridge). I reproduced the same scenario with a 3s settle and the singleton lock works correctly: exactly one watcher stays live and the loser prints 'watcher: already running pid N' and exits. bin/fm-watch.sh's singleton logic is functionally correct on Windows; this test passes on the project's ubuntu-latest CI. No code change warranted.tests/fm-secondmate.test.sh:906- On this Windows/MSYS box, test_home_seed_resolves_relative_source_origins fails at 'relative source origin was not cloned through the resolved path'. The assertion compares git's resolved absolute origin (C:/Users/kamick/AppData/Local/Temp/.../relative-alpha.git) against the test helper'spwd -P(/tmp/.../relative-alpha.git) — two equivalent representations of the same directory via the MSYS /tmp mount. The product behavior is correct: the clone succeeded and origin resolved to an absolute path pointing at the right bare repo. bin/fm-home-seed.sh is not modified by this change, and this test passes on the project's ubuntu-latest CI. Environment-specific path-shape artifact, not a product regression. No code change warranted.for t in tests/*.test.sh; do bash "$t"; done(full behavior suite via Git Bash on Windows; all pass except two Windows-only environment artifacts)bash tests/fm-mux.test.sh(both tmux and wezterm backends via fakes — pass)Real WezTerm backend e2e viabin/fm-mux.sh:backendauto-selects wezterm;new-windowspawns a live tab;pane-path/cursor-y/pane-alive;send-text+send-enterthencaptureround-trips a unique marker;kill-windowthenpane-aliveflips to falseReal MSYS process introspection viabin/fm-proc.sh:fm_proc_ancestry/fm_proc_info/fm_proc_alivewalk the actual Windows process tree (bash.exe -> pwsh.exe -> copilot.exe -> no-mistakes.exe) and confirmps -ois unsupported on MSYSHarness detection viabin/fm-harness.sh: env markers (CLAUDECODE=1 -> claude, PI_CODING_AGENT=true -> pi), crew override (config/crew-harness=codex -> codex), and ancestry fallback (-> unknown, correct for copilot.exe)Bootstrap multiplexer token viabin/fm-bootstrap.sh: emits Windows-specificMISSING: wezterm (install: winget install wez.wezterm ...)when no multiplexer is present, and detects the present wezterm otherwiseReproduced the singleton-watcher scenario with a 3s settle to confirm the lock leaves exactly one live watcher on WindowsReproduced the relative-origin seed to capture actual-vs-expected origin paths and confirm the clone/origin resolution is correct✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.