Skip to content

feat(mux): herdr TUI-launch surface#137

Merged
pbean merged 2 commits into
feat/herdr-mux-backendfrom
feat/herdr-tui-surface
Jul 14, 2026
Merged

feat(mux): herdr TUI-launch surface#137
pbean merged 2 commits into
feat/herdr-mux-backendfrom
feat/herdr-tui-surface

Conversation

@pbean

@pbean pbean commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

PR 2 of the herdr series (#136 = engine run path). Implements the full tui/launch.py surface on the herdr backend, so TUI-launched runs/sweeps/resolves, attach, prune, and the return-to-origin hop all work under BMAD_LOOP_MUX_BACKEND=herdr.

Stacked on #136 (feat/herdr-mux-backend) — GitHub will retarget this to main automatically when #136 merges and its branch is deleted.

What's here

Parked windowsnew_parked_window creates a tab and types a POSIX exec sh -c '<argv>; ec=$?; echo "[bmad-loop exited $ec — press enter]"; read -r; <trailer>' recipe (tmux-identical from the operator's seat). The tmux trailer reads the return option live via show-options; herdr window options live in our sidecar JSON, which a one-line sh trailer can't query — so the option methods mirror the parked window's return option into a per-window return file the trailer cats. The value is resolved to a tab id at write time (Python owns the JSON parsing), keeping the trailer a dumb herdr tab focus. POSIX-only, like the exec launch (pwsh dialect = the win32 follow-up, with agent.start).

Target parsing — one _parse_target helper resolves tmux-style =session[:window] specs (what tui/launch.py builds) vs native pane ids across select_window, kill_window, set/unset/show_window_option, attach_target_argv, switch_client. Window-option sidecar keys are normalized to the native pane id, so set_return_pane("=bmad-loop-ctl:run-X", …) and return_attached_client()'s read by native id agree.

Attach, finalized — outside herdr: ["herdr","terminal","attach",<terminal_id>] (blocks; exits when the pane closes — verified live). Inside a herdr pane: the fire-and-forget ["herdr","tab","focus",<tab_id>], mirroring tmux's in-TMUX switch-client branch. Session-level =session targets resolve to the workspace's active tab, except when that's the root shell and task tabs exist — then the newest tab (task tabs are --no-focus, so an agent workspace keeps root active while the window you want is the newest; ctl attach is unaffected because attach_plan runs select_window first).

switch_client — a tab focus on the target's tab; focusing a tab in another workspace flips workspace focus too (verified 0.7.3), so one verb covers the whole return hop. No "last client" concept in herdr → last_fallback degrades to honest False.

list_windows tmux-field mappingwindow_id/pane_id → pane id, window_name → owning tab's label (bare tab list join — tab list --workspace <absent> RAISES workspace_not_found, same trap as pane list), session_name, @… options → sidecar. Backs ctl_window / _ctl_window_candidates verbatim.

Seam-honesty migration (the two TMUX env reads from the plan, plus one same-pattern read found in tui/app.py): launch.in_ctl_session(), cli.cmd_attach, and app._attach_to_target now ask the seam (current_pane_id()/current_session()) instead of sniffing TMUX. To keep that behavior-preserving, BaseTmuxBackend._display_message gained the TMUX guard: against a live server, display-message would answer for some other client's session and misreport a plain shell as inside tmux. Every call site already guarded on TMUX, so tmux behavior is unchanged (pinned by tests); under herdr these paths now work (HERDR_* env is the inside marker, no subprocess).

Live re-verification (herdr 0.7.3, isolated HERDR_SESSION probes)

  • Parked recipe works end-to-end typed through the user's shell (fish here): exec replaces the shell, banner renders with $ec expanded, pane parks on read -r, Enter runs the trailer, pane+tab close, workspace survives on its root tab.
  • herdr terminal attach exits when its pane closes → the tmux "trailer detaches the throwaway client" move is emulated by pane-close itself.
  • tab focus works headless, updates active_tab_id, and flips workspace focus cross-workspace.
  • Root tab of workspace create gets the numeric label "1" → can never collide with <kind>-<run_id> window names.

Degradations (ledger updated in the module docstring)

  • detach_client stays a no-op (keybinding-only in herdr; no CLI verb — re-checked terminal --help). The post-exit detach return is full-fidelity anyway (pane close ends terminal attach); only the mid-process RETURN_DETACH hand-back (sweep finishing decisions while a plain-terminal client watches) degrades to "stay attached until the parked window closes".
  • switch_client has no last-client fallback.

Tests

  • Unit (FakeHerdr grew tabs/labels/focus/active_tab_id): parked argv+recipe shape, return-option round-trip incl. =target normalization + return-file mirror/unset/kill/kill-session cleanup, target-form matrix for select/attach/switch, session-level attach heuristic, list_windows mapping, in_ctl_session under herdr, tmux _display_message TMUX-guard pins.
  • Gated live (HAVE_HERDR, isolated HERDR_SESSION): launch.start_detached(["validate", …]) → ctl workspace, ctl_window discovery via the tab-label join, parked banner (regex on the expanded exited \d+, since the typed recipe contains the literal $ec), return-file write via =name target, Enter-unpark → pane gone + file consumed + workspace survives. Skips cleanly with herdr off PATH.
  • Full suite 2306 passed / 1 skipped, run both with and without TMUX in the env (this box runs inside tmux; CI doesn't — the guard change made several pre-existing tests' env assumptions explicit).
  • win32-sim (forced BMAD_LOOP_MUX_BACKEND=herdr, herdr binary off PATH) on all touched test files: 539 passed. The new consumer paths are safer on win32 than before: herdr current_* is env-only (no subprocess).
  • trunk check (no filter): clean.

Risks

  • The =session active-vs-newest-tab heuristic is behavior chosen for our two workspace shapes (agent: root+task; ctl: focused-by-select). A user who manually focuses the root tab of an agent workspace then attaches via CLI gets the newest task tab — intended, documented in _session_level_tab.
  • Mid-process detach degradation above; surfaced honestly, not overclaimed.
  • win32 launch (agent.start) and pwsh parked dialect remain the follow-up (Feature Request: Support for Native Windows (Remove tmux dependency for Windows developers) #92 thread).

Summary by CodeRabbit

  • New Features

    • Expanded herdr multiplexer support with parked-window launches, target selection, attachment, window listing, and tab focusing.
    • Added reliable return and detach behavior for launched windows.
    • Improved cleanup when closing windows or sessions.
    • Added end-to-end POSIX support for herdr, including the TUI launch flow.
  • Bug Fixes

    • Improved detection of multiplexer context across supported backends.
    • Prevented incorrect attachment and display behavior when running outside a multiplexer.
  • Documentation

    • Updated the changelog, roadmap, and backend-porting guidance to reflect current herdr capabilities and remaining limitations.

Complete the herdr backend's tui/launch.py surface (PR 2 of the herdr
series): parked orchestrator windows via a typed 'exec sh -c' recipe with
a per-window return file the trailer consumes, one _parse_target helper
for tmux-style '=session[:window]' targets across select/kill/option/
attach/switch methods (window-option sidecar keys normalized to the
native pane id), list_windows tmux-field mapping (window_id/window_name/
@options via a tab-label join), finalized attach_target_argv (terminal
attach outside herdr, fire-and-forget tab focus inside), and
switch_client via tab focus.

Seam-honesty migration: the TUI/CLI attach flows and in_ctl_session now
ask the seam (current_pane_id/current_session) instead of sniffing TMUX;
BaseTmuxBackend._display_message gains the TMUX guard that keeps 'not
inside' honest (behavior-preserving at every call site, pinned by tests).

Unit tests extend FakeHerdr with tabs/focus/active_tab_id; a new
HAVE_HERDR-gated live test drives launch.start_detached(validate)
end-to-end (park -> banner -> return-file -> unpark -> workspace
survives). Full suite green with and without TMUX in the env; win32-sim
(forced herdr, no binary) green on all touched files.
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pbean, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 25ebaaa7-f4bb-410e-8c37-9e8e392cbda6

📥 Commits

Reviewing files that changed from the base of the PR and between 91ad537 and 8ed66e8.

📒 Files selected for processing (6)
  • docs/ROADMAP.md
  • src/bmad_loop/adapters/herdr_backend.py
  • src/bmad_loop/tui/app.py
  • tests/test_cli.py
  • tests/test_herdr_backend.py
  • tests/test_tui_app.py

Walkthrough

Herdr now supports POSIX parked-window launches, return-file handling, target resolution, attachment, tab focus, window listing, cleanup, and backend-derived context detection. Documentation and tests describe and validate the completed TUI/CLI launch surface.

Changes

Herdr TUI launch surface

Layer / File(s) Summary
Parked-window lifecycle and return state
src/bmad_loop/adapters/herdr_backend.py, tests/test_herdr_backend.py, tests/test_herdr_integration.py
Adds parked-window launch recipes, per-window return files, option mirroring, return handling, cleanup, and live integration coverage.
Target resolution and window controls
src/bmad_loop/adapters/herdr_backend.py, tests/test_herdr_backend.py
Resolves native and tmux-style targets, maps window fields, supports attachment and tab focus, and updates window cleanup operations.
Backend-derived context detection
src/bmad_loop/adapters/tmux_base.py, src/bmad_loop/cli.py, src/bmad_loop/tui/*, tests/test_tui_launch.py
Replaces direct TMUX checks in launch paths with multiplexer accessors and guards tmux display-message outside tmux.
Transport model and contract validation
tests/test_herdr_backend.py
Extends the fake herdr transport with tab focus and cascading teardown behavior and validates degradation contracts.
Release and porting documentation
CHANGELOG.md, docs/ROADMAP.md, docs/porting-to-a-new-os.md
Documents the POSIX rollout scope, remaining win32 launch work, and herdr-specific degraded operations.

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

Poem

I’m a rabbit with a parked little pane,
Return files tucked safely from the rain.
Tabs now focus, targets hop in line,
Herdr’s launch path works just fine.
Tmux checks yield to seams so true—
A carrot salute from me to you!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% 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 is concise and accurately summarizes the main change: completing the herdr TUI-launch surface for the 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
  • Commit unit tests in branch feat/herdr-tui-surface

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.

@pbean

pbean commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 14, 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.

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

🤖 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 `@docs/ROADMAP.md`:
- Line 12: Update the later herdr status paragraph in docs/ROADMAP.md to state
that herdr is available end-to-end on POSIX, including both the engine run path
and TUI-launch surface. Keep the remaining win32 agent.start launch follow-up
consistent with the status line.

In `@src/bmad_loop/adapters/herdr_backend.py`:
- Around line 829-862: Update new_parked_window to wrap the post-creation
sidecar update and pane run in failure cleanup: if either operation fails after
tab creation, best-effort close pane_id before re-raising the original error.
Preserve the existing HerdrError conversion for sidecar OSError and ensure
cleanup failures do not replace the original exception.

In `@src/bmad_loop/tui/app.py`:
- Around line 379-392: The attach flows in `_attach_to_target` and the
corresponding `cmd_attach` logic in `launch.py` call `runs.attach_target_argv()`
without handling `MultiplexerError` or `HerdrError`. Wrap both attach-argument
calls in the existing error-handling path, surface a user-facing error message,
and return without launching or crashing when either exception occurs.
🪄 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: 7befd407-eddf-4cbc-ac2f-ec79da408cf9

📥 Commits

Reviewing files that changed from the base of the PR and between 2f9154a and 91ad537.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • docs/ROADMAP.md
  • docs/porting-to-a-new-os.md
  • src/bmad_loop/adapters/herdr_backend.py
  • src/bmad_loop/adapters/tmux_base.py
  • src/bmad_loop/cli.py
  • src/bmad_loop/tui/app.py
  • src/bmad_loop/tui/launch.py
  • tests/test_herdr_backend.py
  • tests/test_herdr_integration.py
  • tests/test_tui_launch.py

Comment thread docs/ROADMAP.md
Comment thread src/bmad_loop/adapters/herdr_backend.py
Comment thread src/bmad_loop/tui/app.py
CodeRabbit round-2 fixes on the TUI-launch surface:

- new_parked_window: the herdr launch is three steps (tab create ->
  sidecar write -> typed recipe) where tmux gets one atomic new-window;
  a failure after tab create now rolls the fresh tab back via the
  existing kill_window teardown (close + sidecar prune + return-file
  cleanup) instead of leaving an untracked idle shell in the ctl
  workspace until workspace close.
- TUI attach: attach_target_argv is a server round-trip on herdr and
  can raise MultiplexerError after the availability/session pre-gates
  pass; _attach_to_target now surfaces it as an error toast instead of
  crashing the app. The cmd_attach half of the finding was declined:
  cli.main's backstop already prints 'error: <msg>' + rc 1 (a new
  regression test pins that).
- ROADMAP: the herdr status paragraph now matches the header line
  (shipped end-to-end on POSIX: run path + TUI-launch surface).

Full suite 2310 green with and without TMUX in the env; trunk clean.
@pbean

pbean commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 14, 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 d42be23 into feat/herdr-mux-backend Jul 14, 2026
9 checks passed
pbean added a commit that referenced this pull request Jul 14, 2026
…herdr

The herdr backend (engine run path #136, TUI-launch surface #137) moves out
of core to pbean/bmad-loop-adapter-herdr before ever shipping in a release.
The adapter registers through the bmad_loop.mux_backends entry-point
discovery (previous commit): co-install it with bmad-loop and 'herdr'
appears in `bmad-loop mux` exactly as before. Core now bundles only tmux —
_load_builtin_backends() is tmux-only again.

Removed: adapters/herdr_backend.py plus its four test files; they continue
as the adapter repo's backend.py + suite, where the live-herdr integration
and E2E tests still run. test_backend_registry gains
test_win32_bottoms_out_at_tmux_with_no_externals pinning the win32 fallback
reversal now that nothing bundled matches win32.

Kept deliberately: the seam docstrings, probe.py gate comment, and
tui/launch.py note that cite herdr as the external worked example, and the
win32-matching rationale comments in tests — an installed external backend
may match win32 (the herdr adapter does), so force_tmux_backend and the
registry/launch tests pin selection against exactly that leak.

Docs: multiplexer-backends.md's herdr section becomes 'External backends'
(install recipe + adapter-repo links); README / FEATURES / docs index /
setup guide / TUI guide / ROADMAP / porting guide / authoring guide now name
herdr as an external adapter and link its operator guide for the seat-level
differences (detach chord ctrl+b q, polled logs, state sidecar). CHANGELOG
[Unreleased] herdr bullets collapse into one extraction entry.

BREAKING CHANGE: bmad_loop.adapters.herdr_backend is gone (it never shipped
in a release); install bmad-loop-adapter-herdr alongside bmad-loop instead.
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