Skip to content

Add a tmux-grid terminal backend so the whole swarm fits one surface - #42

Open
tidoemanuele wants to merge 1 commit into
unclebob:mainfrom
tidoemanuele:feat/tmux-grid-terminal-backend
Open

Add a tmux-grid terminal backend so the whole swarm fits one surface#42
tidoemanuele wants to merge 1 commit into
unclebob:mainfrom
tidoemanuele:feat/tmux-grid-terminal-backend

Conversation

@tidoemanuele

@tidoemanuele tidoemanuele commented Aug 4, 2026

Copy link
Copy Markdown

Closes #41.

Thank you for SwarmForge. Running a six-pack against a real project is the first time a group of agents has behaved like an engineering team for me rather than a set of chat windows, and the reason is the discipline in this repo: role prompts, a layered constitution, worktree isolation, and a handoff queue with real lifecycle states. This PR touches only the presentation layer, and it is opt-in — nothing changes for anyone who does not set the variable.

The problem

Every existing backend opens one window per role. That is the right default: each window is independently trackable, and swarm-window-watchdog.sh can reopen one a role loses. At six roles it also means the swarm has no single surface — six same-sized windows at cascading offsets, overlapping each other, with the window manager deciding which roles you can see.

The change

SWARMFORGE_TERMINAL=tmux-grid tiles the roles into one surface:

+---------------+---------------+
|   Specifier   |     Coder     |
+---------------+---------------+
|    Cleaner    |   Architect   |
+---------------+---------------+
|   Hardender   |      QA       |
+---------------+---------------+

The tiling is done by tmux, so one adapter covers every host SwarmForge already runs on: macOS Terminal.app or iTerm2, Windows Terminal under WSL, and any Linux terminal — including a host with no terminal automation at all, where the grid is attached by hand, which is the normal Linux workflow. It arrives as one more adapter behind the existing contract: no new hook, and no new branch in swarmforge.bb.

No role pane is moved. Handoff delivery targets a bare session name, which tmux resolves to that session's current window's active pane, so join-pane would destroy the role's session and its delivery address. Instead the backend creates one disposable viewer session whose panes each run tmux attach-session -t swarmforge-<role>. Every role session stays one session, one window, one pane. The daemon, the handoff routing and CURRENT_TASK are untouched, and dropping the variable restores one window per role.

The viewer session holds no state. When cleanup kills the role sessions, its attach processes exit and it ends on its own — no addition to swarm-cleanup.sh or close-swarm is needed. There is a test for exactly that.

select-layout tiled decides the geometry, so no layout math is reimplemented here: a six-pack tiles 2x3, a four-pack 2x2, a two-pack side by side. Pane borders carry the project and the role, so two live swarms stay tellable apart. The label comes from SWARMFORGE_LABEL, then .swarmforge/label, then the working directory name.

terminal_backend_tracks_windows is false for this backend. window-ids, windows.tsv and the watchdog assume one window per role, and there is no honest way to reopen one role's window inside a shared grid, so the backend opts out and SwarmForge takes its own already-existing branch: no ids file, no watchdog, and it prints that it has disabled the watchdog. The tmux sessions remain the durable thing; a closed grid or a single role is re-attachable by hand.

Files

  • swarmforge/scripts/terminal-adapters/tmux-grid.sh — new, the whole feature.
  • swarmforge/scripts/swarmforge.bb — one entry added to terminal-helpers so a missing adapter is still caught at startup.
  • README.md — a "Watching The Whole Swarm In One Surface" section under Terminal Behavior.
  • test/swarmforge/script_test.clj — six tests, described below.

No existing backend is modified, and no default changes.

Evidence

Both runs are the same six-pack in the same working directory, with a stub agent standing in for a real backend so the measurement contains only window geometry — no project code, no model traffic.

evidence-before evidence-after

Before, with a stock one-window-per-role backend:

distinct OS windows holding the swarm: 6
  window  frame=69,51    size=865x458  tiles=1
  window  frame=1055,25  size=865x458  tiles=1
  window  frame=162,94   size=865x458  tiles=1
  window  frame=34,77    size=865x458  tiles=1
  window  frame=89,546   size=865x458  tiles=1
  window  frame=746,37   size=865x458  tiles=1

After, with SWARMFORGE_TERMINAL=tmux-grid:

$ tmux -S "$(cat .swarmforge/tmux-socket)" list-panes -t swarmforge-grid \
    -F '#{pane_index} at #{pane_left},#{pane_top} size #{pane_width}x#{pane_height}  #{pane_title}'
0 at 0,1    size 39x5  demo-project | Specifier
1 at 40,1   size 40x5  demo-project | Coder
2 at 0,7    size 39x7  demo-project | Cleaner
3 at 40,7   size 40x7  demo-project | Architect
4 at 0,15   size 39x8  demo-project | Hardender
5 at 40,15  size 40x8  demo-project | Qa

One OS window, six tiles. Every role session survives the change:

$ tmux -S "$(cat .swarmforge/tmux-socket)" list-sessions -F '#{session_name} windows=#{session_windows}'
swarmforge-QA windows=1
swarmforge-architect windows=1
swarmforge-cleaner windows=1
swarmforge-coder windows=1
swarmforge-grid windows=1
swarmforge-hardender windows=1
swarmforge-specifier windows=1

swarmforge-grid is the viewer; the six role sessions are unchanged, so tmux send-keys -t swarmforge-coder still resolves exactly as before.

Startup output, showing the backend declining window tracking rather than half-wiring it:

Opening separate tmux grid surfaces for each session...
tmux grid surfaces are not trackable; window watchdog is disabled for this backend.

The panes above are small because tmux sizes a session to the terminal that attaches it and the demo attached an 80x24 window; the README says to run the grid in a large or full-screen window, and SWARMFORGE_GRID_COLUMNS / SWARMFORGE_GRID_ROWS (default 240x60) size it before anything attaches.

Tests

bb test — 30 tests, 121 assertions, 0 failures. Six are new, and they drive tmux directly rather than a window system, so they run anywhere tmux runs:

  • the backend's declared contract, including that it opts out of window tracking
  • all six adapter hooks are defined
  • the inner-window backend is normalized and can never resolve back to the grid itself
  • the tile label falls back environment -> .swarmforge/label -> directory name
  • a six-role swarm produces one viewer session with one pane per role, tiled across more than one row and more than one column, titled per role, with all six role sessions still alive — and a second role arriving does not build a second grid
  • killing the role sessions ends the viewer session on its own

Notes

  • Only tiled is offered. Named layouts (even-horizontal, main-vertical) would be easy to add later if anyone wants them; they seemed like speculation now.
  • The two diagrams above are drawn, not screenshots, so they carry no project names or paths. Happy to add them to the README too, or not.

Happy to change anything here, including the name of the backend or the variable. Thank you again for the tool, and for the decades of work behind it.

Every existing backend opens one window per role, so a six-pack scatters six
windows across the desktop and leaves no single surface that is the swarm.

Add `SWARMFORGE_TERMINAL=tmux-grid`, which tiles the roles instead. The tiling
is done by tmux rather than by a terminal emulator, so it behaves identically on
macOS, on Windows Terminal under WSL, and on any Linux terminal — including a
host with no terminal automation at all, where the grid is simply attached by
hand.

The grid does not move any role pane. Handoff delivery targets a bare session
name, which tmux resolves to that session's current window's active pane, so
`join-pane` would destroy the role's session and its delivery address. Instead
the backend creates one disposable viewer session whose panes each attach a role
session. Every role session stays one session, one window, one pane, and the
daemon, the handoff routing and CURRENT_TASK are untouched. When cleanup kills
the role sessions the viewer's attach processes exit and the viewer session ends
on its own.

The single OS window is opened by delegating to an existing single-window
backend, auto-detected as usual or named with SWARMFORGE_GRID_TERMINAL, loaded
in a child shell so it cannot overwrite the grid's own adapter functions.
Tiles are labeled with the project and the role, so two live swarms stay
tellable apart.

`terminal_backend_tracks_windows` is false for this backend: window-ids,
windows.tsv and the window watchdog assume one window per role, and there is no
honest way to reopen one role's window inside a shared grid. SwarmForge already
has that branch and reports it at startup.

Tests drive tmux directly, so they assert the real pane layout, the pane titles,
the survival of every role session, and that the viewer session does not outlive
the swarm — with no dependency on any window system.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

Six-pack has no single surface: every backend opens one window per role

1 participant