zswarm 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,17 @@
1
1
  # zswarm
2
2
 
3
- Meta-package for a global install. Puts `zswarm` (CLI) and `zswarm-mcp` (MCP server) on your PATH.
3
+ Meta-package for a global install. Puts `zswarm` (CLI) and `zswarm-mcp` (MCP server) on your PATH, and bundles `skills/zswarm` for AI agent harnesses.
4
4
 
5
5
  ```bash
6
6
  npm i -g zswarm
7
7
  zswarm list
8
8
  ```
9
9
 
10
- Ops, env, MCP setup, and the event bus: the [repo README](../../README.md).
10
+ The bundled skill can be copied from `$(npm root -g)/zswarm/skills/zswarm` into your harness skill directory (e.g. `~/.claude/skills/zswarm`, `~/.codex/skills/zswarm`, or `~/.cursor/skills/zswarm`).
11
+
12
+ Ops, env, MCP setup, and the event bus: the [repo README](https://github.com/brandonkramer/zswarm#readme).
11
13
 
12
14
  ## License
13
15
 
14
16
  MIT
17
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zswarm",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Coordinate CLI agent crews in Zellij panes",
6
6
  "engines": {
@@ -19,14 +19,17 @@
19
19
  },
20
20
  "files": [
21
21
  "bin",
22
- "README.md"
22
+ "README.md",
23
+ "skills"
23
24
  ],
24
25
  "dependencies": {
25
- "@zswarm/cli": "0.2.0",
26
- "@zswarm/mcp": "0.1.10"
26
+ "@zswarm/cli": "0.2.2",
27
+ "@zswarm/mcp": "0.1.12"
27
28
  },
28
29
  "publishConfig": {
29
30
  "access": "public"
30
31
  },
31
- "scripts": {}
32
+ "scripts": {
33
+ "build": "node -e \"const fs=require('node:fs'); fs.cpSync('../../skills', 'skills', {recursive:true})\""
34
+ }
32
35
  }
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: zswarm
3
+ description: >-
4
+ Coordinate CLI crews in Zellij panes via zSwarm MCP (list, send, dump, tail,
5
+ wait, status, keys, interrupt, spawn, close, broadcast, signal, signals,
6
+ await, log, worktrees, unworktree, rename, focus, tabs, layout, stack, diff,
7
+ checkpoint, bus, serve). Use when messaging another Codex, Claude Code, Cursor
8
+ CLI, pi, OpenCode, or agy session in a Zellij pane, waiting for one to finish,
9
+ broadcasting to a crew, signalling barriers,
10
+ interrupting, opening a new crew pane, isolating peers in git worktrees,
11
+ reviewing peer diffs/checkpoints, renaming/focusing panes, or dumping short
12
+ scrollback. Same host as Zellij, or ZSWARM_SSH / ZSWARM_SERVE for a remote
13
+ crew — not IDE side-panel chat.
14
+ ---
15
+
16
+ # zSwarm
17
+
18
+ Talk to **CLI** sessions in Zellij panes (Codex, Claude Code, Cursor CLI, pi, OpenCode, agy, shells).
19
+ Delivery is `zellij action paste` + Enter. Not IDE side-panel chat.
20
+
21
+ ## Happy path
22
+
23
+ ```text
24
+ zswarm({ op: "list" })
25
+ zswarm({ op: "send", to: "reviewer", body: "please review the plan" })
26
+ zswarm({ op: "wait", to: "reviewer", for: "idle" })
27
+ ```
28
+
29
+ `session` when more than one Zellij session is live, or set `ZSWARM_SESSION`.
30
+ Resolution: arg → `ZSWARM_SESSION` → `ZELLIJ_SESSION_NAME` → the sole live session.
31
+ MCP hosts inherit neither PATH nor Zellij env — absolute interpreter + `ZSWARM_SESSION`.
32
+ CLI backup: `zswarm <op>` (`@zswarm/cli`).
33
+
34
+ ## Read when needed
35
+
36
+ - Ops, submit/expect, bus: [references/ops.md](references/ops.md)
37
+ - Remote SSH (Linux / macOS / Windows) / serve: [references/remote.md](references/remote.md)
38
+ - Barriers, worktrees, review, wait: [references/workflows.md](references/workflows.md)
39
+ - Harness notes: [references/harness.md](references/harness.md)
40
+
41
+ ## Prefix
42
+
43
+ Unless `raw: true`:
44
+
45
+ ```text
46
+ [zswarm from=<sender>]
47
+ <body>
48
+ ```
49
+
50
+ `<sender>` is `from` if you pass it, else `ZSWARM_FROM`, else the sending
51
+ pane's title when `ZELLIJ_PANE_ID` / `ZSWARM_SELF_PANE` is visible, else
52
+ `swarm`. MCP hosts that drop Zellij env need those vars listed (or set
53
+ `from` / `ZSWARM_FROM`).
54
+
55
+ ## Rules
56
+
57
+ 1. Target **pane ids or names** from `list` — do not invent transports.
58
+ 2. Keep the session + pane ID returned by `spawn`. Use names once `alias.observed` is true; retry observation before creating a duplicate.
59
+ 3. Prefer `send` + `wait` over polling. Prefer `tail` over repeated `dump`.
60
+ 4. Check `submitted` on `send`; `false` means the peer never got it.
61
+ 5. Prefer `diff` / `checkpoint` over reading a worktree by hand.
62
+ 6. Zellij terminal panes only — not IDE side-panel chats.
63
+ 7. Same host as Zellij, or `ZSWARM_SSH` / `ZSWARM_SERVE`. Linux/macOS SSH is
64
+ the same user + `$TMPDIR`. Windows pane attach: `ZSWARM_SSH_MODE=interactive`
65
+ or `zswarm serve` next to Zellij. `file:` wasm stays on the box that owns
66
+ Zellij. Details: [references/remote.md](references/remote.md).
67
+ 8. Writes refuse own pane (`self_target`) and exited panes (`pane_exited`).
68
+ Override with `allowSelf` / `force` only when you mean it.
69
+ 9. Policy env vars can block writes (`policy_denied` names the env var).
70
+ 10. `spawn` is executable + argv — no shell, so no pipes or `&&`.
71
+ 11. Prefer `worktree` on `spawn` when peers should not share one dirty tree.
72
+ Tear down with `unworktree` after the pane is closed.
73
+ 12. Crew barriers: `broadcast` + `signal` + `await`, not ad-hoc dumps.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "zSwarm"
3
+ short_description: "Coordinate CLI crews in Zellij panes"
4
+ default_prompt: "Use $zswarm to list panes, send to a peer, and wait until idle. Remote crew: read references/remote.md."
@@ -0,0 +1,21 @@
1
+ # Harness notes
2
+
3
+ Verified live against **codex**, **cursor**, **pi**, **opencode**, and **gemini** (`agy`).
4
+
5
+ - **Read** (`dump`, `tail`, `status`, `wait --for idle`) works on all five.
6
+ - **Send** lands on all five (`codex` auto-detects `submit=double-enter`, others `auto`). The `expect` guard works on all five.
7
+ - **Replies can take > 60s.** Budget timeouts; a quiet pane is rarely a failed send.
8
+ - **`wait --match` on a redrawing TUI is viewport-and-moment dependent.** Those apps
9
+ own the alternate screen (no scrollback; `--full` is identical). Prefer
10
+ `wait --for idle` or match text the harness keeps pinned.
11
+ - Re-test: `node scripts/harness-check.mjs <panes...>`
12
+
13
+ Reaching zswarm from a harness is separate from driving one:
14
+
15
+ - **4 of 5 have an MCP client** — they reach zswarm once session and interpreter
16
+ path are explicit in the server config.
17
+ - **1 ships no MCP client** — use `@zswarm/cli`, or the generic MCP bridge at
18
+ `packages/pi/extensions/zswarm-mcp.ts` (`ZSWARM_MCP_SERVERS`).
19
+ - **Being a target needs no integration.** All five are drivable with
20
+ `send` / `dump` / `tail` / `wait` / `status`.
21
+
@@ -0,0 +1,90 @@
1
+ # Ops
2
+
3
+ MCP: `zswarm({ op, ... })`. CLI: `zswarm <op>`. Same surface.
4
+
5
+ | op | Purpose |
6
+ |----|---------|
7
+ | `list` | Terminal panes (id, title, command, tab); `verbose` adds cwd/flags |
8
+ | `send` | Paste body + Enter (`to` = id / title / command). `from` labels `[zswarm from=…]` (default: `ZSWARM_FROM`, else the sending pane's title, else `swarm`). `submit`: `auto` (default; Codex panes default to `double-enter`) / `double-enter` / `none`. Result `submitted: true\|false\|"unverified"`. `expect` refuses unless the screen already shows that substring |
9
+ | `dump` | Full-screen read; capped at 8000 chars (tail) — expensive vs `tail` |
10
+ | `tail` | Incremental read since last cursor; `reset: true` returns the whole screen |
11
+ | `wait` | Block until quiet or `match`; returns `reason` + a 2000-char tail. Bus holds one pipe for the wait |
12
+ | `status` | busy / waiting / idle / exited / running / unknown, with tab names/IDs, tab summary and waiting evidence; `free[]` = idle ids. `sampleMs: 0` skips sampling. `sinceLast: true` skips the 400ms gap |
13
+ | `keys` | `expect` is checked before any input. Key specs (`keys: ["Ctrl c"]`) or literal `chars` (+ `enter`) |
14
+ | `interrupt` | `Esc`; `hard: true` sends `Ctrl c` |
15
+ | `spawn` | New pane (`newTab: true` for a fresh tab) with `command`, `cwd`, `name`, `direction`, `floating`; `tab` = tab name; `worktree` isolates on a branch |
16
+ | `close` | Close a pane |
17
+ | `rename` | Retitle a pane (`to` + `name`) or a tab (`tab` + `name`) |
18
+ | `focus` | Focus a pane; already-focused is a no-op success |
19
+ | `tabs` | List tabs with pane counts |
20
+ | `layout` | Dump the session layout as KDL |
21
+ | `stack` | Stack a comma list of panes (needs 2+) |
22
+ | `broadcast` | One body to many panes (`to` list, `tab`, or `all`; narrow with `group`) |
23
+ | `signal` | Post to a channel (`channel`, optional `payload`); `clear` resets |
24
+ | `signals` | List channels with cumulative counts |
25
+ | `await` | Block until a channel reaches `count` posts |
26
+ | `log` | Delivery log for send/broadcast/keys/interrupt/close |
27
+ | `worktrees` | List repo git worktrees, annotated with panes working in them |
28
+ | `unworktree` | Remove a worktree (`path` or `branch`; `worktree` aliases `branch`) |
29
+ | `diff` | What a peer changed in its worktree |
30
+ | `checkpoint` | Commit a peer worktree (`message`); clean tree is not an error |
31
+ | `sessions` | Live Zellij session names |
32
+ | `bus` | Event-bus status; `install: true` loads the plugin, `clear: true` forgets it |
33
+ | `serve` | Listen for remote zswarm (`--listen`). `--install` / `--clear` installs/removes a service (Windows logon task, Linux systemd user unit, macOS launchd job). MCP cannot listen; set `ZSWARM_SERVE` on the client. See [remote.md](remote.md) |
34
+ | `doctor` | Inspect-only route/SSH/serve/Zellij/bus diagnostics |
35
+
36
+ `submit=auto` retries Enter if the paste is still sitting in a TUI composer.
37
+ `submit=double-enter` always sends the extra Enter; `submit=none` skips the check.
38
+ Check `submitted` — a queued composer used to report success.
39
+
40
+ **Breaking:** `spawn`'s boolean `tab` is now `newTab`; `tab` is a tab **name**.
41
+
42
+ ## expect
43
+
44
+ A pane that dropped back to a shell will **run** your message as a command.
45
+ Name something the screen must already show:
46
+
47
+ ```text
48
+ zswarm({ op: "send", to: "reviewer", body: "…", expect: "Add a follow-up" })
49
+ ```
50
+
51
+ Failure is `expect_missing` and nothing is written.
52
+
53
+ ## Event bus
54
+
55
+ `zswarm({ op: "bus", install: true })` once per Zellij session. After that
56
+ `list` and `status` read a pushed manifest (`source: "plugin"` or `"zellij"`).
57
+ Off until installed; any failure falls back silently — speed, not a dependency.
58
+ Do not pass `force` to recover from a quiet bus: that used to stack WASM copies.
59
+ Approve the existing pane's permission prompt, or `force` only when you mean to
60
+ close every bus plugin pane and load one replacement. Keep the floating pane
61
+ open; closing it unloads the bus.
62
+
63
+ The manifest has no pane command or cwd, so a bus-served `list` omits `command`,
64
+ and `list` with `verbose` / `status --to <command>` keep polling. `status`
65
+ sampling and `wait` use the plugin; `dump` / `tail` stay on the CLI.
66
+
67
+ ## Reliable spawn and handoffs
68
+
69
+ Keep the returned session + pane ID. Spawn creates once and observes for up to
70
+ `observeMs` (default 3000), within one `timeoutMs` budget (default 30000).
71
+ `created` is the creation acknowledgment; `observed`, `exited`, `observation`,
72
+ `alias.observed`, and `ready` describe what was actually seen. `live` means
73
+ observed and not exited; it does not establish application readiness. An
74
+ observation timeout can accompany `ok: true`: retry reads before spawning again.
75
+ New-tab correlation stays within the returned tab and ambiguous layouts remain
76
+ unresolved. `newTab` + `name` also establishes the terminal alias when observed.
77
+ Pane lookups retry absence for 1000ms; `observeMs: 0` disables retries.
78
+
79
+ CLI: `send --body-file PATH` reads a local UTF-8 file before remote forwarding;
80
+ `--body-file -` reads stdin. Do not combine with `--body`/`--text`/a positional
81
+ body. Newlines are preserved. MCP uses `body`, never its protocol stdin. Paste
82
+ still uses argv, so reference a shared file for very large handoffs.
83
+
84
+ For menus: wait for a match and check `reason`, perform one
85
+ `keys --expect TEXT --key Enter`, then wait for the resulting state. `expect`
86
+ also applies to `chars` and `interrupt`. It is a fresh case-insensitive screen
87
+ check, not an atomic transaction. Serialize input per pane and avoid blind
88
+ retries. Waiting evidence is a screen heuristic, not authorization to approve.
89
+
90
+ Ordinary status uses bus changes without a sample gap by default; first/missing observations are unknown unless a prompt is visible. `sampleMs` explicitly selects two-sample observation (0 = metadata only). Bus change history is shared by the plugin instance. Read-only listings use a 500ms process cache; `fresh: true` bypasses it, and `ZSWARM_CACHE_TTL_MS=0` disables reuse. Writes/spawn settling use fresh metadata. Serve retains caches across client CLI calls.
@@ -0,0 +1,121 @@
1
+ # Remote crews
2
+
3
+ Client (MCP or CLI) talks to Zellij on another machine. `file:` wasm stays on
4
+ the host that owns Zellij.
5
+
6
+ ## Linux / macOS
7
+
8
+ Same user, same `$TMPDIR` — SSH is enough:
9
+
10
+ ```bash
11
+ ZSWARM_SSH=user@host zswarm list
12
+ ZSWARM_SSH=user@host zswarm send --to reviewer --body "please review"
13
+ ```
14
+
15
+ If the remote Zellij uses a different socket dir than the SSH login's `$TMPDIR`:
16
+
17
+ ```bash
18
+ ZSWARM_SSH=user@host ZSWARM_TMP=auto zswarm list
19
+ # or set ZSWARM_TMP to that directory explicitly
20
+ ```
21
+
22
+ `auto` reads live `zellij --server` paths (`ps` on Unix). `serve` (below) is
23
+ optional when SSH already sees the sockets.
24
+
25
+ ## Windows
26
+
27
+ OpenSSH is session 0; live Zellij is usually the desktop session. Named pipes
28
+ live there, so SSH + TEMP can **list sessions** but not attach panes.
29
+
30
+ ```text
31
+ ZSWARM_SSH=user@host ZSWARM_TMP=auto → sessions
32
+ ZSWARM_SSH=user@host ZSWARM_SSH_MODE=interactive → list/send
33
+ ```
34
+
35
+ `interactive` is Windows-only: `schtasks /IT` in the desktop session. Discovers
36
+ TEMP unless `ZSWARM_TMP` is set. `ZSWARM_REMOTE_BIN=zellij.exe` if `zellij` is
37
+ not on the remote PATH. `ZSWARM_REMOTE_SHELL=cmd|sh` overrides quoting.
38
+
39
+ ## Serve (any OS)
40
+
41
+ Run zswarm **next to Zellij**; the client talks over a tunnel. Use this when
42
+ SSH is a different session than Zellij, or when MCP should not spawn `ssh` per
43
+ op.
44
+
45
+ **Native Windows + Tailscale:** the default recipe is
46
+ [`zswarm serve --install`](../../../docs/tailscale.md) on the already-logged-in
47
+ desktop (Interactive/Limited logon task, loopback + token), then
48
+ `--serve 'ssh://user@host?servePort=9419'` from the controller (OpenSSH over
49
+ the tailnet — not Tailscale SSH). That guide also covers an **optional**
50
+ verified Tailscale-IP bind for direct TCP, and **private raw TCP Tailscale
51
+ Serve** (`tailscale serve --tcp=…` over a loopback backend). Readiness, token
52
+ storage, bus approval, and login/reboot limits are documented there.
53
+
54
+ ```bash
55
+ # On the host, in the session that owns Zellij (default loopback):
56
+ ZSWARM_SERVE_TOKEN=secret zswarm serve --listen 127.0.0.1:9419
57
+ # Token is required on loopback too: another local OS user can connect to 127.0.0.1.
58
+ # Optional: bind a verified local Tailscale IP (see docs/tailscale.md):
59
+ # zswarm serve --listen "$(tailscale ip -4):9419"
60
+ # Windows verified logon task: zswarm serve --install --listen 127.0.0.1:9419 --session crew --timeout-ms 30000
61
+ # (Start-ScheduledTask is asynchronous; install waits for authenticated hello + host visibility.)
62
+ # Linux/macOS service (systemd user unit + linger / launchd job; token in ~/.zswarm/serve/):
63
+ # zswarm serve --install --listen 127.0.0.1:9419 --session crew
64
+ # Keep the session itself running from a layout: zswarm crew up crew --layout crew.kdl
65
+
66
+ # On the client (concise default is ssh://; token already in this process):
67
+ zswarm --serve 'ssh://user@host?servePort=9419' status --session crew
68
+ # Or a manual LocalForward to an existing endpoint:
69
+ ssh -fN -L 9419:127.0.0.1:9419 user@host
70
+ ZSWARM_SERVE=127.0.0.1:9419 ZSWARM_SERVE_TOKEN=secret zswarm list
71
+ # Direct endpoint when the host bound its Tailscale IP:
72
+ # zswarm --serve '<host-tailscale-ip>:9419' status --session crew
73
+ # Private TCP Tailscale Serve (frontend port; backend remains 127.0.0.1:9419):
74
+ # zswarm --serve 'tcp://crew-host:19419' status --session crew
75
+ ```
76
+
77
+ `ZSWARM_SSH` only forwards Zellij. Git worktree ops (`worktrees`, `unworktree`,
78
+ `diff`, `checkpoint`, `spawn --worktree`) and barrier ops (`signal`, `signals`,
79
+ `await`) are refused over plain SSH without serve (`ssh_git_unsupported`) — use
80
+ `serve` on the host that owns the repo and session if those ops should run there.
81
+
82
+ Direct SSH cannot `bus --install` or `bus --clear` (`bus_remote_unsupported`):
83
+ the plugin `file:` URL and the install marker belong on the Zellij host. Run
84
+ those on that host, or through `serve` over a tunnel. A remote `bus` report
85
+ still explains that the bus is unavailable. `--local` overrides inherited SSH
86
+ when this machine owns Zellij.
87
+
88
+ MCP: set `ZSWARM_SERVE` in the MCP server env. Pass `session` explicitly or
89
+ configure `ZSWARM_SESSION` on the host running `serve`. Do **not** call
90
+ `op: "serve"` with `listen` — that is CLI-only. `serve` with `install` or `clear`
91
+ works via MCP. Other ops forward.
92
+ A client with `ZSWARM_SERVE` never sends a local wasm path across the tunnel.
93
+
94
+ ## Invocation routing
95
+
96
+ `--local` clears SSH, serve, and remote IPC for this call only. It preserves the
97
+ parent environment and inherited session selection; use `--session` explicitly
98
+ when switching hosts. `--ssh user@host` overrides the inherited destination and
99
+ serve for this call. Passing both flags is a usage error.
100
+
101
+ Normal responses carry `context`: transport, host, resolved session, and the
102
+ origin of those settings. Serve responses also identify the server's context
103
+ when supported, because the endpoint may be a local tunnel. Interactive CLI
104
+ routing notices go to stderr; stdout stays JSON. Scope remote env vars to the
105
+ remote launcher and give local crew wrappers fixed `--local --session` flags.
106
+ Configure each MCP server's environment explicitly.
107
+
108
+ Dedicated crew Zellij configs can disable startup distractions with
109
+ `show_startup_tips false` and `show_release_notes false`. Correct TERM on the
110
+ host that starts workers. Terminal screen reads do not prove the presence or
111
+ absence of all Zellij overlays; inspect held exit output before recovery.
112
+
113
+ For frequent status polling, prefer serve beside Zellij with an SSH tunnel. Use `--serve 127.0.0.1:9419 --session crew` (MCP: `serveAddress`) to select an already-open endpoint, or `--serve 'ssh://user@host?servePort=9419'` for a process-owned LocalForward that probes hello before ops. Keep `ZSWARM_SERVE_TOKEN` configured on both sides. Direct SSH status reports its bus limitation and a serve recommendation. `host:port` does not start a tunnel; `ssh://` starts only the local SSH child, never remote serve. Native Windows recipe: [Tailscale crew](../../../docs/tailscale.md). See also [performance guidance](../../../docs/performance.md).
114
+
115
+ ## Agent hosts (CLI subcommands)
116
+
117
+ `zswarm host install|doctor`, `zswarm crew up|status|down`, `zswarm relay`
118
+ (hand a remote pane work, get a message in a local pane when a done line
119
+ appears; `zswarm relay --wait ID` is the pull path) and `zswarm slot`
120
+ (shared build slots) set up and keep a remote crew running. See
121
+ docs/hosts.md in the zswarm repo.
@@ -0,0 +1,76 @@
1
+ # Workflows
2
+
3
+ ## Barriers
4
+
5
+ Broadcast a task, each peer signals when done, await N:
6
+
7
+ ```text
8
+ zswarm({ op: "signal", channel: "done", clear: true })
9
+ zswarm({ op: "broadcast", body: "finish your slice, then signal done", all: true, group: "claude" })
10
+ zswarm({ op: "await", channel: "done", count: 3, timeoutMs: 600000 })
11
+ ```
12
+
13
+ `status` to see who is free; `tail` (not `dump`) to poll what a peer printed.
14
+ State under `ZSWARM_STATE_DIR` (default `~/.zswarm`): `log.jsonl`, `signals.json`,
15
+ `cursors.json`. `ZSWARM_LOG=0` disables the delivery log; past 4MB it is trimmed to
16
+ its last 512KB, which is all `log` reads.
17
+
18
+ `broadcast` skips plugin / exited / own pane (never errors on those);
19
+ `force` / `allowSelf` override. Empty selection → `no_targets`.
20
+
21
+ ## Worktrees
22
+
23
+ `spawn` with `worktree=<branch>` gives the peer its own git worktree + branch,
24
+ setting the pane's working directory to the worktree path. `cwd` selects the base
25
+ repository the worktree is created from (defaulting to the current working
26
+ directory). Optional `worktreeRoot`, `baseRef`. Defaults: worktrees under
27
+ `<repo>-worktrees` beside the repo (`ZSWARM_WORKTREE_ROOT`). Existing worktree
28
+ at the target path is reused. Git binary: `ZSWARM_GIT_BIN`.
29
+
30
+ ```text
31
+ zswarm({ op: "spawn", command: "claude", worktree: "review-auth", cwd: "/path/to/repo", name: "reviewer" })
32
+ zswarm({ op: "worktrees", cwd: "/path/to/repo" })
33
+ zswarm({ op: "unworktree", branch: "review-auth", cwd: "/path/to/repo" })
34
+ ```
35
+
36
+ `unworktree` refuses the main worktree (`worktree_is_main`), a worktree still
37
+ used by a pane (`worktree_busy`), or one with uncommitted changes
38
+ (`worktree_dirty`). `force: true` overrides busy/dirty only.
39
+
40
+ Note on remote SSH: Over plain SSH (`ZSWARM_SSH` without `serve`), git worktree ops
41
+ and signal/await barrier ops are refused with `ssh_git_unsupported`. Run `zswarm serve`
42
+ on the host that owns the session and connect via `ZSWARM_SERVE`.
43
+
44
+ ## Review loop
45
+
46
+ Isolate a peer, name it, task it, save its work, tear down:
47
+
48
+ ```text
49
+ zswarm({ op: "spawn", command: "claude", worktree: "review-auth", name: "reviewer" })
50
+ zswarm({ op: "rename", to: "terminal_11", name: "reviewer" })
51
+ zswarm({ op: "send", to: "reviewer", body: "review the auth changes" })
52
+ zswarm({ op: "wait", to: "reviewer", for: "idle" })
53
+ zswarm({ op: "diff", branch: "review-auth", cwd: "/path/to/repo" })
54
+ zswarm({ op: "checkpoint", branch: "review-auth", cwd: "/path/to/repo", message: "review checkpoint" })
55
+ zswarm({ op: "close", to: "reviewer" })
56
+ zswarm({ op: "unworktree", branch: "review-auth", cwd: "/path/to/repo" })
57
+ ```
58
+
59
+ Prefer `diff` / `checkpoint` over reading the worktree by hand. Clean
60
+ `checkpoint` returns `committed: false`, `nothingToCommit: true` — not an error.
61
+
62
+ ## Waiting
63
+
64
+ `wait` polls the pane screen instead of you re-dumping it.
65
+
66
+ - `for: "idle"` (default) — screen unchanged for `idleMs` (2000).
67
+ - `match: "…"` — substring, or `regex: true`; sets `for` to `match`.
68
+ - `for: "either"` — whichever lands first. `timeoutMs` defaults to 60000.
69
+
70
+ ```text
71
+ zswarm({ op: "send", to: "reviewer", body: "run the tests" })
72
+ zswarm({ op: "wait", to: "reviewer", for: "either", match: "FAIL", idleMs: 4000 })
73
+ ```
74
+
75
+ Prefer `tail` for cheap incremental polls; reserve `dump` for a one-shot full
76
+ screen (or `tail` with `reset: true`).