Skip to content

Windows terminal-multiplexer backend#19

Draft
dracic wants to merge 1 commit into
bmad-code-org:mainfrom
dracic:feat/psmux-backend
Draft

Windows terminal-multiplexer backend#19
dracic wants to merge 1 commit into
bmad-code-org:mainfrom
dracic:feat/psmux-backend

Conversation

@dracic

@dracic dracic commented Jun 28, 2026

Copy link
Copy Markdown

What

Adds a native-Windows terminal-multiplexer backend (PsmuxMultiplexer) so the orchestrator runs on a native Windows host (psmux/ConPTY) without WSL, alongside the existing tmux backend which stays the default on POSIX.

Why

The transport was tmux-only, so native Windows was unsupported. psmux is a protocol-compatible tmux drop-in on Windows, but several behaviours diverge (UTF-8 output, no POSIX sh for the parked-window trailer, env propagation, agent-teams auto-injection) and the process/stop layer had Windows branches that were never exercised.

How

  • New backend (additive): adapters/psmux_backend.py implements the TerminalMultiplexer contract, overriding only the genuinely-divergent methods (pwsh parked-window trailer, UTF-8 subprocess decoding, env injection, teammate-mode isolation) and inheriting the rest.
  • Upstream reference impl touched — adapters/tmux_backend.py: extracted a small _run() subprocess seam (shared timeout) purely so the psmux backend can subclass and add encoding="utf-8". POSIX command strings are unchanged byte-for-byte (pinned by test_portability_guard.py); no behavioural change on Linux/macOS.
  • Backend selection — adapters/multiplexer.py: get_multiplexer() now picks psmux on win32, else tmux (WSL reports linux and correctly keeps tmux). This is the policy seam that was already documented as the selection point.
  • Process/stop machinery — platform_util.py + runs.py: added kill_pid_forced (Win taskkill /F /T, POSIX SIGKILL), pid_identity (psutil start-time) and zombie-aware liveness; stop_run now escalates a non-cooperating process to a forced kill after the graceful window and identity-guards both the polite and forced paths against PID reuse. The engine stays the single writer of stopped.
  • Windows install/validate — install.py, cli.py: hook command branches to uv run --no-project python on Windows (no reliable python3 on PATH); validate preflights psmux + uv presence. Cross-platform absolute-path check (is_absolute_path) replaces Path.is_absolute() in adapters/profile.py.
  • CI — .github/workflows/ci.yml: adds a native-Windows runner so the Windows paths are exercised in CI.

Testing

pytest green on Linux and native Windows; new suites cover the psmux backend, the process layer, and live psmux probes (test_psmux_backend.py, test_platform_util.py, test_psmux_probe.py, test_psmux_live.py), with test_portability_guard.py enforcing that the POSIX backend leaks no Windows-isms and vice-versa. Live end-to-end was human-validated on a real Windows+psmux host (hook env injection, engine-written graceful stop, session teardown).

⚠️ Not for merge

This PR is for detailed testing and review only — please do not merge as-is. The native-Windows paths need broader validation across psmux versions and host configurations (the backend targets a fast-moving psmux that emulates tmux 3.3.6). Treat it as a working reference for evaluating the approach and exercising the Windows transport end-to-end, pending sign-off after thorough testing.

Summary by CodeRabbit

  • New Features
    • Added native Windows terminal/session handling with platform-aware backend selection.
    • Added Windows execution in CI, running the full test suite on Windows with pinned dependencies.
  • Bug Fixes
    • Status validation is now case-insensitive and whitespace-tolerant.
    • Improved cross-platform path handling (absolute/parent-reference detection) and consistent POSIX-style path formatting in generated outputs.
    • Made stopping runs safer by adding PID identity checks and forced-kill escalation safeguards.
  • Chores
    • Expanded cross-platform tests (Windows-focused CLI preflight, portability, and integration coverage) and hardened test scaffolding.

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e76ff906-3272-47e5-a09d-9708e97c87c6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Adds a Windows psmux backend, platform-aware hook and process handling, POSIX path normalization in serialized outputs, and Windows CI/test coverage. Updates validation and run-stopping logic to use normalized status and PID identity checks.

Changes

Windows Portability

Layer / File(s) Summary
platform_util and caller normalization
src/automator/platform_util.py, src/automator/verify.py, src/automator/engine.py, src/automator/sweep.py, src/automator/model.py, src/automator/resolve.py
Adds cross-platform process and path helpers in platform_util, updates verification/status callers to use normalized status parsing, and serializes several paths with POSIX separators.
tmux helper and psmux backend
src/automator/adapters/tmux_backend.py, src/automator/adapters/psmux_backend.py
Centralizes tmux subprocess execution, then adds the Windows psmux backend with PowerShell encoded commands, env prelude handling, parked-window translation, and best-effort pane/session operations.
backend selection, hook interpreter, CLI validation, stop_run
src/automator/adapters/multiplexer.py, src/automator/install.py, src/automator/cli.py, src/automator/runs.py, src/automator/data/plugins/unity/unity_plugin.py
Routes Windows hosts to psmux, makes hook command generation platform-aware, adds Windows validation preflight for hook interpreters and psutil, and rewrites run stopping with PID identity checks and forced-kill escalation.
platform and runtime test updates
tests/conftest.py, tests/test_engine.py, tests/test_engine_plugin.py, tests/test_engine_worktree.py, tests/test_generic_tmux.py, tests/test_hook_bus.py, tests/test_install.py, tests/test_portability_guard.py, tests/test_platform_util.py, tests/test_runs.py, tests/test_sanitize.py, tests/test_tui_app.py, tests/test_tui_launch.py, tests/test_cli.py, tests/test_decisions.py, tests/test_probe.py, tests/test_verify.py, tests/test_psmux_backend.py, tests/test_psmux_live.py, tests/test_psmux_probe.py
Adjusts test scaffolding and platform-specific assertions for launcher generation, hook command quoting, stop/kill behavior, TUI retries, tmux gating, encoding-sensitive file reads, and psmux backend/live probe coverage.
CI Windows job
.github/workflows/ci.yml
Adds a Windows test job to CI and disables credential persistence in the Linux checkout step.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • bmad-code-org/bmad-auto#3: Both PRs touch the engine’s manual-recovery and rollback path, including Engine._pause_for_manual_recovery.
  • bmad-code-org/bmad-auto#11: Both PRs modify the multiplexer seam and the stop/kill path around get_multiplexer() and runs.stop_run().

Poem

🐇 I hop through Windows with psmux in tow,
With $LASTEXITCODE and pwsh, I flow.
Paths stay neat with slashes so bright,
And stop-run knows the right process toाइट?
I nibble a carrot, then test all day —
Cross-platform hops have found the way.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a native Windows terminal-multiplexer backend.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@augmentcode

augmentcode Bot commented Jun 28, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: This PR adds a native Windows terminal-multiplexer backend so the orchestrator can run on Windows without WSL.

Changes:

  • Introduces PsmuxMultiplexer (psmux/ConPTY) by subclassing the existing tmux backend and overriding only Windows-divergent behaviors (UTF-8 reads, pwsh parked-window trailer, env injection, pipe-pane sink, kill-session targeting).
  • Refactors TmuxMultiplexer to route all subprocess calls through a shared _run() seam (centralized timeout + enables psmux to add encoding defaults).
  • Updates multiplexer selection to choose psmux on sys.platform == "win32", otherwise tmux (WSL remains tmux via linux).
  • Extends process-control utilities with forced-kill support, PID identity guarding (psutil start-time), and zombie-aware liveness checks.
  • Enhances stop_run to escalate from graceful termination to forced kill when needed, with PID-reuse defenses and a new StopRunError.
  • Improves Windows portability in install/validate (hooks use uv run --no-project python; validate preflights the hook interpreter).
  • Normalizes several persisted/communicated paths to forward-slash form for cross-OS stability.
  • Adds Windows CI coverage and substantial new deterministic + (guarded) live psmux/platform tests.

Technical Notes: psmux window commands are executed via pwsh -EncodedCommand to survive tmux→ConPTY quoting, and Windows process control relies on taskkill + psutil identity checks to reduce PID-reuse risk.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread src/automator/runs.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/automator/install.py (1)

296-324: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Normalize seed_files before writing git exclude patterns.

The glob path now uses rel.as_posix(), but seed_files still appends the raw rel. On Windows, a seed like .claude\settings.json can be copied but excluded as /.claude\settings.json, which git ignore patterns won’t reliably match, risking seeded config being committed.

Proposed fix
-        seeded.append(rel)
+        seeded.append(Path(rel).as_posix())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/automator/install.py` around lines 296 - 324, Normalize the `seed_files`
entries before they are added to `seeded` in `install.py`, matching the
`seed_globs` behavior. The issue is that `seed_files` currently appends raw
`rel` values, which can use backslashes on Windows and produce git exclude
patterns that don’t reliably match. Update the `seed_files` loop so `seeded`
stores a POSIX-style relative path, consistent with the `seed_globs` handling
and the later git ignore generation.
tests/test_psmux_probe.py (1)

359-467: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Gate the Claude probes on Claude actually being installed.

HAVE_PSMUX only checks for psmux/tmux/pwsh, but H7 asserts that claude.exe resolves as an application. On a host without Claude, H7 fails for the wrong reason, and H7-noprofile stops proving the wrapper-bypass path at all.

Suggested fix
+HAVE_CLAUDE = shutil.which("claude") is not None
+HAVE_CLAUDE_EXE = shutil.which("claude.exe") is not None
+_LIVE_CLAUDE = pytest.mark.skipif(
+    not (HAVE_PSMUX and HAVE_CLAUDE),
+    reason="native Windows + psmux + claude required",
+)
+_LIVE_CLAUDE_EXE = pytest.mark.skipif(
+    not (HAVE_PSMUX and HAVE_CLAUDE_EXE),
+    reason="native Windows + psmux + claude.exe required",
+)
...
-@_LIVE
+@_LIVE_CLAUDE_EXE
 def test_h7_claude_code_fix_tty_injection():
...
-@_LIVE
+@_LIVE_CLAUDE
 def test_h7_noprofile_bypasses_fix_tty_wrapper():
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_psmux_probe.py` around lines 359 - 467, Gate the H7 Claude probe
tests on Claude being installed, since HAVE_PSMUX only validates psmux/tmux/pwsh
and not the Claude binary. Add a Claude availability check in the H7 setup path
used by test_h7_claude_code_fix_tty_injection and
test_h7_noprofile_bypasses_fix_tty_wrapper, and skip or short-circuit when
claude.exe is not resolvable. Keep the existing assertions intact so they still
verify the wrapper/injection behavior only when Claude is present.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Line 56: The checkout step currently leaves Git credentials persisted by
default, which is unnecessary for this job after repository code runs. Update
the actions/checkout step to explicitly disable credential persistence by
setting persist-credentials to false on the checkout configuration. Keep the
change localized to the checkout action invocation in the CI workflow.

In `@src/automator/adapters/profile.py`:
- Line 116: The project-relative path checks in the profile adapter still allow
parent traversal via values like ../..., so tighten the validation in the
profile path handling logic for hooks.config_path and skill_tree. Update the
relevant path-normalization/acceptance code in the profile adapter to reject any
relative path that escapes the project tree, not just absolute paths, and keep
the existing absolute-path rejection in place.

In `@src/automator/cli.py`:
- Around line 171-198: The Windows preflight in the CLI currently validates only
the hook relay, so `validate` can still succeed even when the selected backend
cannot start. Update the `get_multiplexer()`/`PsmuxMultiplexer` path in this
`win32` check to also verify the backend prerequisites (`psmux`, `tmux`, and
`pwsh`) before reporting success, and add clear `problems.append(...)` messages
when any dependency is missing or unusable.

In `@src/automator/plugins/manifest.py`:
- Around line 33-35: The path validation in _check_relative_paths only rejects
empty or absolute values, so manifest-relative entries can still contain ..
segments and escape the plugin/project root. Update _check_relative_paths (and
the related validation path used for seed_files, seed_globs, and
[python].module) to explicitly reject any value containing parent-directory
traversal, alongside the existing absolute-path check, so only safe relative
paths are accepted.

In `@src/automator/runs.py`:
- Around line 227-256: The stop path in stop_run is still trusting the current
process owning the PID instead of the engine that originally wrote the run
state, so a recycled PID on Windows can be targeted by terminate_pid. Update the
run metadata written at start-up to persist the engine’s pid_identity alongside
the raw PID, and change the guard in stop_run to compare the stored identity
against the live process before calling terminate_pid or kill_pid_forced. Use
the existing helpers read_pid, pid_identity, and _same_pid_alive as the main
touchpoints for wiring this through.

In `@tests/test_engine_worktree.py`:
- Around line 537-553: The hook command serialization in hook_cmd and the
plugin.toml assembly currently uses TOML literal strings for cmd, which breaks
when the launcher path contains an apostrophe. Update the command generation in
this test helper to emit cmd as a TOML basic string instead, keeping the
launcher path safely quoted so plugin.toml remains valid across paths with
special characters.

In `@tests/test_psmux_probe.py`:
- Around line 242-274: In test_h3_posix_parked_trailer_non_viable, avoid
asserting that the POSIX sh -c parked trailer never runs when sh is available on
the host, since that makes the probe depend on local configuration rather than
the backend. Gate the native sh_ran expectation on sh_available (or otherwise
skip/soften that check when sh.exe is present), while keeping the pwsh
$LASTEXITCODE path as the real assertion for the backend behavior. Keep the
_record output informative by reporting both sh availability and the pwsh
result.

---

Outside diff comments:
In `@src/automator/install.py`:
- Around line 296-324: Normalize the `seed_files` entries before they are added
to `seeded` in `install.py`, matching the `seed_globs` behavior. The issue is
that `seed_files` currently appends raw `rel` values, which can use backslashes
on Windows and produce git exclude patterns that don’t reliably match. Update
the `seed_files` loop so `seeded` stores a POSIX-style relative path, consistent
with the `seed_globs` handling and the later git ignore generation.

In `@tests/test_psmux_probe.py`:
- Around line 359-467: Gate the H7 Claude probe tests on Claude being installed,
since HAVE_PSMUX only validates psmux/tmux/pwsh and not the Claude binary. Add a
Claude availability check in the H7 setup path used by
test_h7_claude_code_fix_tty_injection and
test_h7_noprofile_bypasses_fix_tty_wrapper, and skip or short-circuit when
claude.exe is not resolvable. Keep the existing assertions intact so they still
verify the wrapper/injection behavior only when Claude is present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ee16d245-5d50-4e7f-a350-0802a9bbfa10

📥 Commits

Reviewing files that changed from the base of the PR and between f702304 and 0ec8151.

📒 Files selected for processing (36)
  • .github/workflows/ci.yml
  • src/automator/adapters/multiplexer.py
  • src/automator/adapters/profile.py
  • src/automator/adapters/psmux_backend.py
  • src/automator/adapters/tmux_backend.py
  • src/automator/cli.py
  • src/automator/data/plugins/unity/unity_plugin.py
  • src/automator/engine.py
  • src/automator/install.py
  • src/automator/model.py
  • src/automator/platform_util.py
  • src/automator/plugins/manifest.py
  • src/automator/resolve.py
  • src/automator/runs.py
  • src/automator/sweep.py
  • src/automator/tui/data.py
  • src/automator/verify.py
  • tests/conftest.py
  • tests/test_cli.py
  • tests/test_decisions.py
  • tests/test_engine.py
  • tests/test_engine_plugin.py
  • tests/test_engine_worktree.py
  • tests/test_generic_tmux.py
  • tests/test_hook_bus.py
  • tests/test_install.py
  • tests/test_platform_util.py
  • tests/test_portability_guard.py
  • tests/test_psmux_backend.py
  • tests/test_psmux_live.py
  • tests/test_psmux_probe.py
  • tests/test_runs.py
  • tests/test_sanitize.py
  • tests/test_tui_app.py
  • tests/test_tui_launch.py
  • tests/test_verify.py

Comment thread .github/workflows/ci.yml
Comment thread src/automator/adapters/profile.py Outdated
Comment thread src/automator/cli.py Outdated
Comment thread src/automator/plugins/manifest.py Outdated
Comment thread src/automator/runs.py Outdated
Comment thread tests/test_engine_worktree.py Outdated
Comment thread tests/test_psmux_probe.py Outdated
@dracic

dracic commented Jun 28, 2026

Copy link
Copy Markdown
Author

Some win32 workarounds could perhaps be handled more elegantly, and I still have some tests named after the tea testing project that I should rename and clean up.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_cli.py (1)

766-782: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Seed these failure-path tests with the same project bootstrap as the success case.

Unlike the success-path test on Lines 740-758, these two tests skip config/init setup, so they only stay green while validate happens to hit the Windows preflight before project checks. That makes them order-dependent instead of focused on the specific failure being asserted.

Also applies to: 802-812

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_cli.py` around lines 766 - 782, The failure-path tests for
validate are missing the same project bootstrap used by the success case, so
they rely on Windows preflight happening before any project checks and become
order-dependent. Update the affected tests to use the same initialized project
setup as the success-path validate test before invoking cli.main, while keeping
the psutil-missing assertion focused on the validate path; apply the same fix to
the other related test around the validate bootstrap helpers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_cli.py`:
- Around line 766-782: The failure-path tests for validate are missing the same
project bootstrap used by the success case, so they rely on Windows preflight
happening before any project checks and become order-dependent. Update the
affected tests to use the same initialized project setup as the success-path
validate test before invoking cli.main, while keeping the psutil-missing
assertion focused on the validate path; apply the same fix to the other related
test around the validate bootstrap helpers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ff1fa157-d5ce-4151-9134-b29317ddb00c

📥 Commits

Reviewing files that changed from the base of the PR and between 0ec8151 and dbc5948.

📒 Files selected for processing (13)
  • .github/workflows/ci.yml
  • src/automator/adapters/profile.py
  • src/automator/cli.py
  • src/automator/install.py
  • src/automator/platform_util.py
  • src/automator/plugins/manifest.py
  • tests/test_cli.py
  • tests/test_decisions.py
  • tests/test_engine_worktree.py
  • tests/test_platform_util.py
  • tests/test_probe.py
  • tests/test_psmux_backend.py
  • tests/test_psmux_probe.py
✅ Files skipped from review due to trivial changes (1)
  • tests/test_decisions.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • .github/workflows/ci.yml
  • src/automator/cli.py
  • src/automator/plugins/manifest.py
  • src/automator/adapters/profile.py
  • src/automator/install.py
  • tests/test_psmux_backend.py
  • tests/test_platform_util.py
  • tests/test_psmux_probe.py

@dracic dracic marked this pull request as draft June 28, 2026 15:25
@pbean

pbean commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Review — thank you, this is strong work 🙏

First off: this is a genuinely high-quality contribution. The psmux backend is careful, the
commenting around why each override exists is excellent, the H1–H7 hazard-probe harness is a
great way to pin behavior against a moving target, and you clearly validated it end-to-end on a
real host. Appreciate that you also flagged it "not for merge yet."

The main ask is a rebase onto main (now 0.7.6). Between your branch point (0.7.5) and now we
landed the seam infrastructure this PR was implicitly asking for — so a backend should now be
a new file + one registration line, with zero edits to core .py bodies. That removes a large
chunk of this diff and resolves the current conflicts. Details below.


1. Core edits that the new seams now supersede — please drop on rebase

main already has these, so the corresponding edits here become redundant/conflicting:

  • multiplexer.py::get_multiplexer() — there's now a registry
    (register_multiplexer(name, matches, factory) + BMAD_AUTO_MUX_BACKEND override). Drop the
    hand-rolled if sys.platform=="win32" branch; register psmux instead (see §3).
  • tmux_backend.py _run extraction — the single spawn primitive now lives in
    adapters/tmux_base.py::BaseTmuxBackend._run(). Please drop your _run extraction entirely and
    re-fit psmux to the base's _run (see §2 — this is the one real piece of rework).
  • platform_util.py kill_pid_forced / pid_identity / _same_pid_alive and the
    runs.py::stop_run escalation — all now live behind process_host.ProcessHost
    (force_kill / identity) and stop_run already does the identity-guarded grace→force-kill
    escalation with StopRunError. Use get_process_host(); drop the duplicates.
  • install.py / cli.py hook-interpreter + list2cmdline quoting — now
    ProcessHost.hook_interpreter() and ProcessHost.shell_quote() (POSIX python3/shlex.quote,
    Windows uv run --no-project python/list2cmdline). install.py and probe.py already call
    through the seam, so the branch is handled.

2. The one real rebase gotcha: the _run contract changed

Your _run was (self, args, **kwargs) where args includes "tmux" and kwargs flow to
subprocess.run. The base's is _run(self, argv, *, check=True) where argv excludes "tmux"
(it prepends it) and capture/text/timeout are fixed. So the psmux methods need re-fitting:

  • Subclass BaseTmuxBackend (from tmux_base), not TmuxMultiplexer (it's just the empty
    POSIX leaf; psmux is a sibling).
  • utf-8: override _run to do
    subprocess.run(["tmux", *argv], capture_output=True, text=True, encoding="utf-8", errors="backslashreplace", timeout=TMUX_TIMEOUT_S) and honor check. This is exactly the
    "tweak binary/decoding/timeout" extension the base's docstring invites.
  • new_session env-strip (psmux #424): the base _run forwards no env=. Cleanest options —
    either (a) have psmux's _run override accept an optional env=None and pass it through, or
    (b) we widen BaseTmuxBackend._run to take env: dict|None=None (arguably belongs on the seam).
    Happy to take (b) on our side if you prefer. Please don't call subprocess.run directly in
    new_session — it bypasses the single-spawn seam and the portability guard.
  • Re-point every self._run(["tmux", …], capture_output=…) to self._run([…], check=…) /
    self._tmux(…) per the base convention.

3. Registration (the only core line that remains)

psmux is bundled, so register it next to tmux in multiplexer._load_builtin_backends():

from .psmux_backend import PsmuxMultiplexer
register_multiplexer("psmux", lambda p: p == "win32", PsmuxMultiplexer)

(tmux is already registered for p != "win32".) That's the entire core footprint.

4. A few smaller things

  • validate preflight: main now has its own validate seam-guard. Please merge your
    Windows psmux/uv/psutil probes into it rather than layering a second preflight.
  • CI pins: match main's existing ubuntu job action versions exactly (checkout/setup-uv/python)
    so the new windows-latest job doesn't drift.
  • portability_guard: after the rebase, confirm it still passes — the pwsh strings should be
    allowed only inside psmux_backend.py, and there should be no raw subprocess/tmux left
    outside the seam.

5. Please keep these — they're great

The psmux backend itself, the windows-latest CI job, the test_psmux_probe.py hazard harness,
and the Windows-specific tests. No correctness issues found in the backend logic.


Heads-up on a few cross-cutting fixes

Several non-psmux fixes you bundled in (case-insensitive spec-status gates / status_of, the
..-traversal + cross-platform absolute-path guards in profile.py/manifest.py, as_posix()
state serialization, and the st_ino watcher signature) are valuable on their own — a couple are
latent bugs/hardening independent of Windows. We'll land those on main separately so you can
drop them from this branch and the rebased PR stays psmux-only. We'll coordinate so you don't
re-conflict on them.

Thanks again — once it's rebased to psmux-only on top of 0.7.6 we'll re-review and exercise the
Windows path. 🚀

dracic pushed a commit to dracic/bmad-auto that referenced this pull request Jun 29, 2026
The kill/liveness primitives branched on sys.platform inline in
platform_util, and stop_run had no way to force-kill a wedged engine — so a
native-Windows backend (PR bmad-code-org#19) would have to edit core. Introduce a
ProcessHost seam (terminate / force_kill / is_alive / identity) with per-OS
impls and a name-keyed registry mirroring the multiplexer backend selection,
so adding an OS is a new subclass + one registration line.

stop_run now escalates: an engine that ignores SIGTERM past the grace window
is SIGKILL'd — but only while host.identity(pid) still matches the value
recorded at stop-time (a pid-reuse guard); otherwise it raises the new
StopRunError rather than risk an unrelated process. This is a deliberate POSIX
behavior change, pinned by new tests. The graceful-first / single-writer-of-
stopped invariant is unchanged.

platform_util keeps terminate_pid/pid_alive as thin back-compat shims over the
host; detach_kwargs stays put. The TUI worker widens its except to surface
StopRunError/ProcessHostError. The portability guard swaps platform_util for
process_host in KILL_PROBE_ALLOW and allows its Linux /proc identity read.

Unity teardown de-dup is deferred: unity_teardown.py runs under a bare python3
and can't reliably import the package.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dracic pushed a commit to dracic/bmad-auto that referenced this pull request Jun 29, 2026
The hook-registration sites in install/probe hardcoded `python3`, and
`cmd_validate` hardcoded a `tmux` PATH check — two more axes a native
backend (psmux PR bmad-code-org#19) would have to branch on `sys.platform` to cross.

Route both through the platform-selected seams so a new OS registers
rather than edits core:

- Add `ProcessHost.hook_interpreter()` (POSIX `python3`; Windows
  `uv run --no-project python`); install/probe interpolate it instead
  of a `python3` literal. POSIX output is byte-for-byte unchanged.
- Extract `_platform_preflight()` in cli.py: ask the multiplexer
  backend (`available()`/`version()`) and name the process host,
  replacing the hardcoded `tmux` probe in `cmd_validate`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dracic pushed a commit to dracic/bmad-auto that referenced this pull request Jun 29, 2026
Spec-frontmatter status gates compared the raw `status:` value, so a
hand-edited spec carrying a stray-cased `Done`/`In-Review` silently failed
to advance a story. Add `verify.status_of()` (strip + lower) and route the
six spec-frontmatter status reads through it (verify dev/review + bundle
gates, engine post-dev sync, sweep bundle). The template and sprint-status
tokens are lowercase, so behavior is unchanged for well-formed specs;
`devcontract` keeps its own lowercasing (it parses skill prose).

Also fix the manual-rollback notice: an empty baseline rendered an invalid
`git reset --hard the run's baseline commit` — use a `<baseline_commit>`
placeholder instead.

Cross-cutting fix surfaced by PR bmad-code-org#19; landed independently of the psmux work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dracic pushed a commit to dracic/bmad-auto that referenced this pull request Jun 29, 2026
The profile- and plugin-manifest "must be project-relative" guards used
`Path(value).is_absolute()`, which is platform-dependent (on Windows a
POSIX-absolute `/etc/passwd` reads as *not* absolute) and never caught
relative `..` escapes — so `../../etc` and cross-OS-absolute paths slipped
through. Add `platform_util.is_absolute_path` (absolute in either POSIX or
Windows terms) and `has_parent_ref` (a `..` segment in either flavor) and
pair them at every guard site in adapters/profile.py and plugins/manifest.py.
Hardening surfaced by PR bmad-code-org#19; landed independently of the psmux work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dracic pushed a commit to dracic/bmad-auto that referenced this pull request Jun 29, 2026
A worktree task's `spec_file` (state.json) and the resolve context's
`resolution_path` were serialized via `str(Path)`, which emits backslashes
on Windows — so a state/context file written on one OS read back with
OS-specific separators. Persist both via `as_posix()` so the cross-OS state
contract is a single forward-slash string everywhere (a no-op on POSIX).
Cross-OS portability surfaced by PR bmad-code-org#19; landed independently of the psmux work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dracic pushed a commit to dracic/bmad-auto that referenced this pull request Jun 29, 2026
The data layer caches a parse while a file's (mtime_ns, size) is unchanged.
On a coarse-mtime filesystem (e.g. WSL2 drvfs) a same-size rewrite within one
mtime tick is invisible to that signature, so the dashboard serves a stale
parse. The engine rewrites state.json atomically (temp + os.replace), so every
write lands on a fresh inode — fold st_ino into _stat_sig to catch the change
regardless of mtime resolution. Surfaced by PR bmad-code-org#19; landed independently.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dracic dracic force-pushed the feat/psmux-backend branch 2 times, most recently from 47e3bfd to c75e34d Compare June 29, 2026 09:53
@dracic

dracic commented Jun 29, 2026

Copy link
Copy Markdown
Author

Rebased psmux-only on top of 0.7.7 and force-pushed (single commit) - cross-cutting non-psmux fixes dropped as discussed; portability_guard still passes.

CI-only fixups since last push: generic probe-test names, trunk fmt (black/isort), and UTF-8 report reads (fixes a Windows cp1252 UnicodeDecodeError).

On Windows, os.kill(pid, 0) is not a harmless liveness probe as on POSIX - signal 0 maps to CTRL_C_EVENT, which injected phantom KeyboardInterrupts into pytest and crashed the session, so PID-liveness must use a Windows-safe check (psutil/OpenProcess) instead.

Suite green on Linux + Windows.

@dracic dracic force-pushed the feat/psmux-backend branch from 645c8cd to fbb485f Compare June 30, 2026 06:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants