Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,33 @@ breaking changes may land in a minor release.
forced selection. A tmux-less POSIX host still selects `TmuxMultiplexer` and reports it
unavailable, exactly as before.

- **Herdr TUI-launch surface.** The herdr backend now covers everything `tui/launch.py` drives:
parked orchestrator windows (a typed `exec sh -c '<argv>; banner; read; trailer'` recipe,
tmux-identical from the operator's seat, with the return-to-origin target mirrored into a
per-window return file the trailer consumes), tmux-style `=session[:window]` target parsing
across select/kill/option/attach methods, `list_windows` tmux-field mapping
(`window_id`/`window_name`/`@option`), finalized `attach_target_argv` (blocking
`terminal attach` outside herdr, fire-and-forget `tab focus` inside), and `switch_client`
via `tab focus`. The TUI/CLI attach flows and `in_ctl_session` now ask the seam
(`current_pane_id()`/`current_session()`) instead of sniffing `TMUX`, so they work under any
backend; the tmux backend's `current_*` gained the TMUX guard that keeps "not inside" honest.
Remaining herdr degradations: `detach_client` stays a no-op (the post-exit detach return rides
pane-close ending `terminal attach`; only the mid-process hand-back degrades to staying
attached), and `switch_client` has no last-client fallback.
- **Herdr multiplexer backend (opt-in, non-tmux-family).** A new `herdr` transport implements
the `TerminalMultiplexer` seam _fresh_ (subclassing nothing) over
[herdr](https://herdr.dev)'s cross-platform workspace/tab/pane model — Unix socket on POSIX,
named pipe on Windows — a step toward native Windows (#92) plus herdr's agent-status sidebar for
watching runs. Opt in per run with `BMAD_LOOP_MUX_BACKEND=herdr` or persist with
`bmad-loop mux set herdr`; `_PLATFORM_DEFAULTS` is untouched, so tmux stays the POSIX default and
herdr activates on win32 only by first-match until psmux ships. This PR is the **engine run
path**: session→workspace / window→tab mapping, an `exec`-based launch with tmux-identical
window-death, a content-hash-gated polling `pipe_pane` tee (herdr has no tee), JSON-sidecar
session/window options (herdr has none; writes lock-guarded via a new cross-platform
`platform_util.file_lock`), lazy server autostart, and a protocol-version guard
(fail-below / warn-above). Landed as one registration line plus one sanctioned `probe.py` gate
fix — zero portability-guard allowlist changes. The TUI-launch surface landed as a second PR
(previous entry); the remaining degradations are in the backend module docstring's ledger.
- **Stories mode — a second planning pipeline that drives the loop off a typed `stories.yaml` (folder+id dispatch) instead of `sprint-status.yaml`.** Opt in with `[stories] source = "stories"` + `spec_folder`, or per run with `bmad-loop run --spec <folder>` (overrides policy); `--story` then filters by story id. Each entry dispatches by folder + id — the dev skill creates-or-resumes the story spec at `<folder>/stories/<id>-<slug>.md` and the orchestrator reads that id-keyed path back deterministically (no shared board to line-edit, no result-artifact mtime-scan). Strictly linear schedule (list order, no `depends_on`); `bmad-loop run --dry-run --spec <folder>` and `bmad-loop status` print the board (id · live disk state · checkpoint markers · title). Sprint mode is unchanged and remains the default. Requires a `bmad-dev-auto` new enough for folder+id dispatch — the run preflight checks and remediates.
- **Per-story human checkpoints (stories mode).** Independent `spec_checkpoint` (pause before code to review the plan — dev halts at `ready-for-dev`; approve to implement, or request a replan that resets the spec to `draft`) and `done_checkpoint` (pause after the story commits, skipped when it is the last story); both additive to `gates.mode`. A blocked story escalates + resolves as in sprint mode, with a pre-planning-halt sentinel auto-deleted (a copy preserved under the run dir) on re-arm.
- **TUI human-in-the-loop surface for stories mode.** The sprint tree is replaced by a stories board (id · live disk state · spec/done checkpoint markers · title) when a stories-mode run is selected; paused runs carry a per-run pause-kind badge and the run list shows a global _⚑ N need attention_ count; `p` opens the stage-appropriate viewer — plan-checkpoint spec review (Approve & resume / Request replan), story-checkpoint summary card (Continue / Stop), escalation with story context (Resolve / Re-arm & resume), and a gate spec viewer that the existing spec-approval/epic pauses reuse. The start-run modal gains a source select + spec-folder field with a live schedule preview. Every TUI action calls the same code paths as the CLI.
Expand Down
47 changes: 33 additions & 14 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Status legend: **planned** (agreed, not started) · **exploring** (shape still o

## Native Windows multiplexer backend

**Status:** planned · **Foundation:** the full platform-seam series landed (multiplexer registry + `BaseTmuxBackend` + `ProcessHost` + hook interpreter + validate preflight, v0.7.6; availability-aware backend selection + `bmad-loop mux`, #87; original seam v0.7.0)
**Status:** planned · **Foundation:** the full platform-seam series landed (multiplexer registry + `BaseTmuxBackend` + `ProcessHost` + hook interpreter + validate preflight, v0.7.6; availability-aware backend selection + `bmad-loop mux`, #87; original seam v0.7.0) · a first non-tmux-family, native-Windows-capable backend — **herdr** — now ships end-to-end on POSIX (engine run path + TUI-launch surface; the win32 `agent.start` launch path is the remaining follow-up)

The orchestrator no longer fuses tmux into the engine. All session/window/pane operations
go through a single `TerminalMultiplexer` ABC (`src/bmad_loop/adapters/multiplexer.py`),
Expand All @@ -34,19 +34,38 @@ held by a CI portability guard (`tests/test_portability_guard.py`). **WSL alread
— it _is_ Linux, so it takes every fast path unchanged; this is purely about a future _native_
Windows host.

The remaining work is the native-Windows backend itself. Two candidates are in flight, and
they are **sibling tmux-family backends**, not stages of one plan: `psmux` (drives psmux's
tmux-compatible shim via pwsh) and `tmux-windows` (#85; drives the tmux-windows port) — both
subclass `BaseTmuxBackend`, both register for `win32`, and both invoke a binary literally
named `tmux`, which is exactly why selection is availability-aware with discriminating
`available()` probes (psmux → `which("psmux") and which("tmux") and which("pwsh")`;
tmux-windows → `which("tmux") and not which("psmux")`) and an explicit
`bmad-loop mux set <name>` tie-break. The seams are designed so each slots in as **new files
plus one registration line, with no change to the adapters, `runs.py`, `tui/launch.py`,
`probe.py`, `tui/data.py`, or `cli.py`'s `validate`** (`WindowsProcessHost` and its hook
interpreter are already in place and registered). The end-to-end port path — both build
options, the test-override env vars, and exactly what a native-Windows port costs — is
documented in [Porting bmad-loop to a new OS](porting-to-a-new-os.md); the deep transport
The remaining work is the native-Windows backend itself. Three candidates now exist, and
they are **not stages of one plan**. Two are **sibling tmux-family backends** still in
flight: `psmux` (drives psmux's tmux-compatible shim via pwsh) and `tmux-windows` (#85;
drives the tmux-windows port) — both subclass `BaseTmuxBackend`, both register for `win32`,
and both invoke a binary literally named `tmux`, which is exactly why selection is
availability-aware with discriminating `available()` probes (psmux →
`which("psmux") and which("tmux") and which("pwsh")`; tmux-windows →
`which("tmux") and not which("psmux")`) and an explicit `bmad-loop mux set <name>` tie-break.

The third — **herdr** — has **shipped** end-to-end on POSIX (engine run path +
TUI-launch surface) as a
different-binary-family backend: it implements `TerminalMultiplexer` fresh over herdr's
workspace/tab/pane model rather than subclassing `BaseTmuxBackend`, and probes
`which("herdr")`, so it is pairwise-discriminating against the tmux family by construction
(no `mux set` tie-break needed to tell them apart). It registers with `matches = True` (all
platforms), which keeps the door open for native Windows without disturbing POSIX. On POSIX,
tmux stays the platform default, so herdr activates only on an explicit
`BMAD_LOOP_MUX_BACKEND=herdr` / `bmad-loop mux set herdr`. On **win32**, `_PLATFORM_DEFAULTS`
is **untouched** — psmux remains the declared win32 default — so herdr is picked only by
**first-match** (the fourth precedence step) when it is `available()` and no higher-priority
backend is: once psmux (or tmux-windows) ships and registers for win32, the win32 default
reasserts and outranks herdr's first-match automatically, no code change. herdr's `exec`
launch is POSIX-only for now; the Windows launch path (herdr's `agent.start`) is a follow-up,
and the Phase-0 characterization must be re-run on a real Windows host before #92 is claimed.

The seams are designed so each backend slots in as **new files plus one registration line,
with no change to the adapters, `runs.py`, `tui/launch.py`, `probe.py`, `tui/data.py`, or
`cli.py`'s `validate`** (`WindowsProcessHost` and its hook interpreter are already in place
and registered; herdr proved this end to end — one registration line and one sanctioned
`probe.py` gate fix, zero portability-guard allowlist changes). The end-to-end port path —
both build options, the test-override env vars, and exactly what a native-Windows port costs —
is documented in [Porting bmad-loop to a new OS](porting-to-a-new-os.md); the deep transport
contract is in the
[adapter authoring guide](adapter-authoring-guide.md#the-transport-contract-for-a-backend-author).

Expand Down
20 changes: 20 additions & 0 deletions docs/adapter-authoring-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,26 @@ backend. `window_alive` uses `list-windows` membership, not `display-message`, b
the fastest way to see exactly what a `new_parked_window` or `session_options` must
produce.

For the **implement-fresh** path, `adapters/herdr_backend.py` is the shipped worked
example — a backend over [herdr](https://herdr.dev), a cross-platform,
agent-aware workspace manager whose CLI is a different binary family from tmux. Its
mapping: a bmad-loop **session** is a herdr **workspace** (label == session name), a
**window** is a **tab** (one shell pane, whose `root_pane.pane_id` is the native
window id handed back), and the launched command runs via a typed `exec <argv>`
(`pane run` = type + Enter) so process-exit == pane-close == tab-close ==
tmux-identical window death. Where herdr has no analogue, the backend degrades
honestly rather than faking it: session/window **options** (which herdr lacks
entirely) live in a cross-process JSON **sidecar** (atomic swaps for readers, an OS
advisory lock around each read-modify-write so concurrent writers never lose
updates), and `pipe_pane` — herdr has no
tee — runs a per-window **poller** thread that snapshots `pane read` into the log
whenever the content changes, which is exactly enough to drive the two log consumers
a tmux tee would (`generic._log_activity_key`'s stall re-arm and `probe`'s marker
discovery). Its module docstring is a **degradation ledger** of every such
divergence (sidecar options, poller `pipe_pane`, no-op `detach_client`, the attach
argv, the advisory geometry, the protocol-version policy) — the reference for what
"implement fresh" costs when the host has no tmux-shaped CLI.

The hard part of a new profile isn't the TOML — it's the **facts that live in no
doc**: the CLI's exact hook payload shape (field names and casing, whether
`session_id` / `transcript_path` / `cwd` are present), where it writes its session
Expand Down
23 changes: 23 additions & 0 deletions docs/porting-to-a-new-os.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ version, and the current selection. (The result is cached — see
- **Implement `TerminalMultiplexer` fresh** when the host has no tmux-shaped CLI
at all (e.g. a ConPTY-based window manager). You implement the full contract
directly; `tmux_backend.py` is the reference for what each method must produce.
The shipped worked example is **herdr** (`adapters/herdr_backend.py`) — a
cross-platform, agent-aware workspace manager whose object model
(workspace/tab/pane) and CLI are a different binary family entirely, so it
subclasses nothing and maps the whole contract onto herdr verbs: a bmad-loop
session is a herdr **workspace** (label == session name), a window is a **tab**
(its `root_pane.pane_id` is the native window id), and the launched command runs
via a typed `exec <argv>` so process-exit stays tmux-identical window death.
Where herdr has no analogue for a contract method — options, `pipe_pane`,
the parked-window return hop, detach — it emulates or degrades honestly (a
JSON sidecar for options, a polling tee for `pipe_pane`, a per-window return
file for the parked trailer, a no-op detach); that **degradation ledger** is
the module docstring, and it is the template for what "implement fresh" costs
in practice.

`available()` gates whether the backend is usable on the current host (e.g. its
binary is on PATH); the optional `version()` feeds the diagnostic dump and the
Expand Down Expand Up @@ -123,6 +136,16 @@ default, but `bmad-loop mux` and the validate preflight would list both as
available when only one actually drives the installed binary. A host with an
ambiguous install resolves it explicitly: `bmad-loop mux set <name>`.

A backend from a **different binary family** sidesteps this problem entirely.
The shipped `herdr` backend probes `shutil.which("herdr")` — a distinct binary
that no tmux-family backend claims — so it is **pairwise-discriminating by
construction**: it can never report available on a host where only tmux is
installed, and vice versa, without any explicit tie-break. That `available()`
must stay a pure PATH lookup (it is called by `detect_multiplexers()` on every
listing) — herdr in particular **never** probes or starts its background server
from `available()`, `version()`, or the constructor; server autostart is lazy,
confined to the mutating operations that actually need it.

**Deep contract →** [adapter authoring guide: the transport contract for a backend
author](adapter-authoring-guide.md#the-transport-contract-for-a-backend-author).

Expand Down
Loading
Loading