Skip to content

Multiplexer selection: availability-aware defaults + dev-chosen backend (#87)#118

Merged
pbean merged 8 commits into
mainfrom
feat/87-mux-backend-selection
Jul 10, 2026
Merged

Multiplexer selection: availability-aware defaults + dev-chosen backend (#87)#118
pbean merged 8 commits into
mainfrom
feat/87-mux-backend-selection

Conversation

@pbean

@pbean pbean commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Implements #87 (dracic). Selection was first-matches(sys.platform)-wins in registration order, so two same-platform backends — psmux and tmux-windows (#85), both matches == "win32", both driving a binary literally named tmux — would collide by import order. Selection is now availability-aware, per-platform-defaulted, and dev-choosable.

Selection precedence (adapters/multiplexer.py::_select)

  1. BMAD_LOOP_MUX_BACKEND env var (unchanged; message byte-compatible)
  2. new [mux] backend in policy.toml, installed once per CLI invocation by main() via configure_multiplexer() — the one chokepoint that also covers probe/diagnose/attach/stop, which never load policy
  3. the platform default (win32: psmux, elsewhere: tmux) when registered and available()
  4. the first registered platform match that is available() (registration order breaks ties)
  5. the historical fallback, preserved exactly: first platform match regardless of availability, bottoming out at TmuxMultiplexer — a tmux-less POSIX host still selects tmux and validate reports it unavailable

Forced names (1–2) bypass matches()/available() (explicit = trusted) and fail loudly when unregistered — the policy path names the policy file to edit.

bmad-loop mux

  • bare: lists registered backends (platform / available / version / selected + why) via new detect_multiplexers() (never raises; feeds the validate preflight too, which lists backends when >1 is registered and notes env/policy provenance)
  • mux set <name>: persists the choice through a comment-preserving stdlib writer (policy.write_mux_backend, anchored on the template's commented key line — no tomlkit/tomli-w dependency in core); --clear reverts to auto, --force allows a name that only registers on the target machine; unavailable-but-registered persists with a warning
  • no interactive prompts anywhere — runs are unattended

policy.toml is now gitignored by init

Policy is per-machine-per-repo (it carries the [mux] backend choice; the TUI settings editor rewrites it). A .gitignore entry does not untrack an already-committed file, so init best-effort detects a tracked policy.toml and prints the one-time git rm --cached hint; CHANGELOG carries the migration note. (bmad-loop's own worktree-clean preflight already exempted policy.toml via pathspec — this additionally stops inner dev sessions and plain git status from reading a policy edit as dirt.)

Handoff to the Windows backend PRs

The available() discriminators can't be implemented here (both backends are out-of-tree), so the porting guide gains an "Availability discriminators (same-platform backends)" contract: psmux → which("psmux") and which("tmux") and which("pwsh"); tmux-windows → which("tmux") and not which("psmux"); probes must be cheap and side-effect-free, factories plain constructors. ROADMAP reframes psmux/tmux-windows as sibling tmux-family backends behind selection. #85 and the psmux re-cut should rebase onto this and implement the contract; any test pinning "first match wins on win32" needs re-cutting against the new precedence.

Deliberately unchanged

probe.py, diagnostics.py, runs.py, tui/launch.py, tui/data.py, _make_adapters, tmux_base.py — all keep calling get_multiplexer(); the chokepoint changes what it returns. test_match_based_selection_wins_by_order was rewritten (registration order now breaks ties only among available non-default backends — and the old form was nondeterministic across hosts with/without tmux).

Verification

  • full suite: 2110 passed, 1 skipped (linux); trunk check clean
  • new tests: precedence chain, cache/configure semantics, detect_multiplexers guards, writer round-trip/CRLF/byte-identical-diff, mux CLI exit codes, end-to-end chokepoint through cli.main
  • scratch-repo E2E: init (gitignore gains policy.toml) → mux (platform default) → mux set tmux (anchor line replaced in place) → mux set nope (exit 1, known list) → mux set nope --force → next invocation fails loud naming policy.toml → mux set --clear → env override shows forced by BMAD_LOOP_MUX_BACKEND

Closes #87.

Summary by CodeRabbit

  • New Features
    • Added bmad-loop mux and bmad-loop mux set/--clear to list and persist a terminal-multiplexer backend choice.
    • Implemented backend selection precedence (env override, saved policy, platform default, then first available match) with strict, clear failure behavior when forced names can’t be used.
  • Documentation
    • Expanded setup, command reference, upgrade notes, porting/adapter guidance, and TUI settings for [mux].
  • Bug Fixes
    • bmad-loop init now gitignores .bmad-loop/policy.toml, with guidance when it was previously tracked.
  • Tests
    • Added/expanded coverage for selection precedence, CLI behavior, policy read/write, and install gitignore handling.

pbean added 6 commits July 10, 2026 09:44
…d writer

The machine-scoped terminal-multiplexer choice (issue #87) persists as
[mux] backend in policy.toml. MuxPolicy stays data-only (registered-name
existence is checked at selection time); the writer is a targeted line
replace against the template's commented anchor so hand edits and comments
survive without a core TOML-writer dependency.
… platform defaults

get_multiplexer now resolves by precedence — BMAD_LOOP_MUX_BACKEND env,
the policy [mux] backend (installed via configure_multiplexer), the
platform default when registered+available, the first available platform
match — bottoming out at the historical fallback so a tmux-less POSIX
host still returns TmuxMultiplexer for validate to report. Forced names
bypass matches()/available() (explicit = trusted) and raise loudly when
unregistered, naming the policy file for the policy path.
detect_multiplexers() enumerates the registry (never raises) for
bmad-loop mux and the validate preflight. Registration order now breaks
ties only among available non-default backends — two same-platform
backends (psmux / tmux-windows, issue #87) no longer collide by import
order.
…ght detection

main() installs the policy [mux] backend into the multiplexer seam before
dispatch — the one point that covers probe/diagnose/attach/stop, which
never load policy. `bmad-loop mux` lists registered backends
(platform/available/version/selected+why); `mux set <name>` persists the
choice via write_mux_backend (unregistered names error unless --force,
unavailable ones persist with a warning — explicit = trusted). validate's
preflight keeps its existing lines byte-compatible and adds an advisory
multi-backend listing plus env/policy provenance notes. No prompts
anywhere: runs are unattended.
…on hint

policy.toml is per-machine-per-repo (it carries the [mux] backend choice
and the TUI settings editor rewrites it), so init now gitignores it like
runs/ and cache/. A gitignore entry doesn't untrack an already-committed
file, so init best-effort detects a tracked policy.toml and prints the
one-time 'git rm --cached' hint. CHANGELOG documents both the feature and
the migration.
…ator contract

core.toml gains the [mux] section (schema sync-tested against MuxPolicy).
The porting guide documents the five-step precedence and a new
'availability discriminators' contract — psmux and tmux-windows both
drive a binary named tmux, so their available() probes must be pairwise
discriminating (the handoff for PR #85 and the psmux re-cut). ROADMAP
reframes the two as sibling tmux-family backends behind selection rather
than interim-vs-future. README/FEATURES/setup/tui guides pick up the mux
command, the policy.toml gitignore, and the migration hint.
…B110)

The preflight detect block and _configure_mux now assign a fallback in
the except arm instead of try/except/pass; prettier reflowed the ROADMAP
paragraph and table alignment.
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 31baf275-98bc-4b8c-b312-7fc21647cb10

📥 Commits

Reviewing files that changed from the base of the PR and between 0fccc2f and a444641.

📒 Files selected for processing (4)
  • src/bmad_loop/adapters/multiplexer.py
  • src/bmad_loop/policy.py
  • tests/test_backend_registry.py
  • tests/test_policy.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/test_policy.py
  • tests/test_backend_registry.py
  • src/bmad_loop/policy.py

Walkthrough

The change adds machine-scoped multiplexer policy, availability-aware selection and diagnostics, new bmad-loop mux commands, validation reporting, atomic policy updates, gitignore handling, documentation, and comprehensive tests.

Changes

Multiplexer selection and policy

Layer / File(s) Summary
Persisted mux policy contract
src/bmad_loop/policy.py, src/bmad_loop/data/settings/core.toml, tests/test_policy.py, tests/test_settings_schema.py, docs/tui-guide.md
Adds MuxPolicy, TOML parsing and validation, comment-preserving atomic persistence for [mux].backend, settings metadata, and round-trip coverage.
Availability-aware backend selection
src/bmad_loop/adapters/multiplexer.py, tests/test_backend_registry.py, docs/..., README.md, CHANGELOG.md
Implements environment, policy, platform-default, availability, and fallback precedence with guarded probes, diagnostics, cache handling, and related documentation and tests.
Mux commands and preflight reporting
src/bmad_loop/cli.py, tests/test_cli.py, README.md
Adds mux listing, set, clear, and force behavior; loads policy before dispatch; and reports backend availability and selection provenance during validation.
Local policy installation behavior
src/bmad_loop/install.py, tests/test_install.py, docs/setup-guide.md, README.md, CHANGELOG.md, docs/FEATURES.md
Adds .bmad-loop/policy.toml to generated gitignore entries and warns when an existing tracked policy requires untracking.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Developer
  participant CLI
  participant PolicyFile
  participant MultiplexerRegistry
  Developer->>CLI: run bmad-loop mux set name
  CLI->>PolicyFile: write [mux].backend
  CLI->>MultiplexerRegistry: configure selection
  MultiplexerRegistry->>MultiplexerRegistry: probe registered backends
  MultiplexerRegistry-->>CLI: backend diagnostics and selection reason
  CLI-->>Developer: report selected or unavailable backend
Loading

Poem

A rabbit hops through tmux’s gate,
Picks a backend, quick and straight.
Policy tucked where machines may roam,
Env vars guide the journey home.
Gitignore keeps the secrets light—
“Mux set!” thumps carrot-right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.88% 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 summarizes the main change: availability-aware multiplexer selection with a developer-chosen backend.
Linked Issues check ✅ Passed The PR matches #87’s goals: availability-aware selection, override precedence, detection, and a non-committed per-machine backend choice.
Out of Scope Changes check ✅ Passed The changes stay focused on multiplexer selection, policy persistence, and related docs/tests without obvious unrelated additions.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/87-mux-backend-selection

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.

@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: 2

🧹 Nitpick comments (1)
src/bmad_loop/adapters/multiplexer.py (1)

294-352: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Platform-default step doesn't verify matches(sys.platform) for the candidate backend.

_factory_by_name(default) (Line 338) only checks registration by name; it never confirms the backend's own matches() predicate agrees with the current platform — unlike the "first-match" loop right below it (Lines 343-345), which does. Harmless today (only tmux ships, and its matches covers everything but win32), but a future same-platform backend registering under a name that collides with _PLATFORM_DEFAULTS/_DEFAULT_BACKEND for the wrong platform would be selected here purely on available(), bypassing the platform check every other step enforces.

🛡️ Proposed fix: fold the matches() check into the default lookup
     default = _PLATFORM_DEFAULTS.get(sys.platform, _DEFAULT_BACKEND)
-    default_factory = _factory_by_name(default)
-    if default_factory is not None:
-        backend = _instance(default, default_factory)
-        if _usable(backend):
-            return backend, default, "platform-default"
+    for name, matches, factory in _BACKENDS:
+        if name == default and matches(sys.platform):
+            backend = _instance(name, factory)
+            if _usable(backend):
+                return backend, name, "platform-default"
+            break
     for name, matches, factory in _BACKENDS:
         if matches(sys.platform) and _usable(_instance(name, factory)):
             return instances[name], name, "first-match"
🤖 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/bmad_loop/adapters/multiplexer.py` around lines 294 - 352, Update the
platform-default selection in _select so the backend is considered only when its
registered matches predicate returns true for sys.platform, in addition to
passing _usable. Locate the default_factory/_instance block and retain the
existing first-match and fallback behavior for candidates that do not match the
platform.
🤖 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 `@src/bmad_loop/adapters/multiplexer.py`:
- Around line 399-409: In the backend probing logic around the factory
invocation, isolate the availability probe from the version probe: keep
`factory()` and `_usable(backend)` in the block that sets `available`, then call
`backend.version()` in a separate guarded block that only leaves `version` as
`None` on failure. Do not reset `available` when `version()` raises, preserving
the result computed by `_usable` and preventing contradictory diagnostics.

In `@src/bmad_loop/policy.py`:
- Around line 904-980: Update write_mux_backend to preserve any inline comment
and spacing trailing the existing backend value when replacing or clearing the
key. Extract the suffix beginning at the first # after the assignment/value,
retain it alongside the original line ending, and append it to new_line; add or
update tests for backend lines such as `backend = "tmux"  # pinned per teammate
X` to verify the comment survives.

---

Nitpick comments:
In `@src/bmad_loop/adapters/multiplexer.py`:
- Around line 294-352: Update the platform-default selection in _select so the
backend is considered only when its registered matches predicate returns true
for sys.platform, in addition to passing _usable. Locate the
default_factory/_instance block and retain the existing first-match and fallback
behavior for candidates that do not match the platform.
🪄 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: 05671e20-324a-41dd-8bf0-f6556992b2d7

📥 Commits

Reviewing files that changed from the base of the PR and between 92d31a1 and 0fccc2f.

📒 Files selected for processing (18)
  • CHANGELOG.md
  • README.md
  • docs/FEATURES.md
  • docs/ROADMAP.md
  • docs/adapter-authoring-guide.md
  • docs/porting-to-a-new-os.md
  • docs/setup-guide.md
  • docs/tui-guide.md
  • src/bmad_loop/adapters/multiplexer.py
  • src/bmad_loop/cli.py
  • src/bmad_loop/data/settings/core.toml
  • src/bmad_loop/install.py
  • src/bmad_loop/policy.py
  • tests/test_backend_registry.py
  • tests/test_cli.py
  • tests/test_install.py
  • tests/test_policy.py
  • tests/test_settings_schema.py

Comment thread src/bmad_loop/adapters/multiplexer.py
Comment thread src/bmad_loop/policy.py
- detect_multiplexers: isolate the version() probe from the availability
  probe so a cosmetic version() failure can no longer overwrite a
  correctly-computed available=True (contradictory selected/available row)
- write_mux_backend: preserve a hand-added trailing comment on the
  'backend =' anchor line itself when replacing or clearing the value
  (names never contain '#', so '#' after '=' is unambiguously a comment)
- _select platform-default step: enforce the backend's own matches()
  predicate like every other step, via a first-wins-preserving lookup
  (CodeRabbit's proposed loop would have scanned past a non-matching
  first registration to a later duplicate, breaking first-wins)
@pbean

pbean commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

Re the platform-default matches() nitpick: also fixed in a444641, but not with the proposed diff verbatim — that loop would scan past a non-matching first registration named default to a later duplicate under the same name, breaking the documented "duplicate registrations: first wins" contract. Used a first-wins-preserving variant (find the first registration with the default name, check its matches(), then stop either way) + a regression test.

@pbean

pbean commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@pbean pbean merged commit f9a6bf5 into main Jul 10, 2026
8 checks passed
@pbean pbean deleted the feat/87-mux-backend-selection branch July 10, 2026 20:33
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.

Multiplexer selection: per-platform default + detected, dev-chosen backend

1 participant