@mu-crew/mu 3.2.0 → 3.2.1

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.
Files changed (40) hide show
  1. package/AGENTS.md +203 -599
  2. package/README.md +76 -327
  3. package/dist/cli.js.map +1 -1
  4. package/dist/extension/mu-pi.js +178 -45
  5. package/dist/extension/mu-pi.js.map +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/docs/ARCHITECTURE.md +175 -1072
  8. package/docs/ROADMAP.md +88 -169
  9. package/docs/VISION.md +205 -415
  10. package/docs/VOCABULARY.md +202 -466
  11. package/docs/architecture/control-socket.md +119 -0
  12. package/docs/architecture/dag.md +113 -0
  13. package/docs/architecture/mux.md +163 -0
  14. package/docs/architecture/ops-log.md +140 -0
  15. package/docs/architecture/sdk.md +185 -0
  16. package/docs/architecture/sync.md +129 -0
  17. package/docs/architecture/tui.md +146 -0
  18. package/docs/guide/README.md +25 -0
  19. package/docs/guide/backends.md +61 -0
  20. package/docs/guide/cleanup.md +74 -0
  21. package/docs/guide/delegate.md +87 -0
  22. package/docs/guide/dispatch.md +137 -0
  23. package/docs/guide/getting-started.md +177 -0
  24. package/docs/guide/recovery.md +133 -0
  25. package/docs/guide/remote.md +74 -0
  26. package/docs/guide/sql.md +76 -0
  27. package/docs/guide/stop-a-worker.md +64 -0
  28. package/docs/guide/sync.md +78 -0
  29. package/docs/guide/tui.md +98 -0
  30. package/docs/guide/upgrade.md +76 -0
  31. package/docs/history/CHANGELOG-pre-3.md +4866 -0
  32. package/docs/reference/env.md +87 -0
  33. package/docs/reference/naming.md +82 -0
  34. package/docs/specs/2026-09-28-agent-state-from-murmur.md +1 -1
  35. package/docs/specs/2026-09-29-task-substates.md +1 -1
  36. package/package.json +1 -1
  37. package/skills/mu/REMOTE_WORKERS.md +179 -504
  38. package/skills/mu/SKILL.md +116 -185
  39. package/docs/HANDOVER.md +0 -486
  40. package/docs/USAGE_GUIDE.md +0 -2738
@@ -1,189 +1,60 @@
1
1
  # Remote workers
2
2
 
3
- Running mu agents on another machine. A recipe, not a feature: mu has
4
- no remote backend, no ssh code, and no host registry, and it needs
5
- none.
3
+ mu has no remote backend, ssh code, or host registry. This is a recipe
4
+ plus the traps that cost time.
6
5
 
7
- Read this before spawning your first remote agent. The recipe is short;
8
- the traps below it are the part that costs time.
9
-
10
- **If you read one thing:** on a session-capped host, never leave an
11
- attach pane open — it holds the only ssh channel and silently breaks
12
- `git fetch`, `murmur collect` and every other ssh. Attach to look, then
13
- `mu agent close`. See § Never leave an attach pane open. This guidance
14
- concerns interactive attachment, not mu's automatic agent-state reads.
15
-
16
- ---
6
+ **If you read one thing:** on a session-capped host, an open attach pane
7
+ holds the only ssh channel and silently breaks `git fetch`,
8
+ `murmur collect`, and every other ssh. Attach to look, then
9
+ `mu agent close`.
17
10
 
18
11
  ## The model
19
12
 
20
- **The pane is local, the process is remote.** `mu agent spawn
21
- --command 'ssh <host> -t "..."'` starts an ordinary tmux pane whose
22
- foreground process happens to be ssh. Everything mu does is
23
- pane-shaped, so `mu agent send`, `mu agent read`, and the reaper work
24
- unchanged across the network. A pi agent's control socket rides the
25
- same ssh as a `-L` forward, so its state is exact; murmur reports state
26
- only for non-pi agents.
27
-
28
- **One orchestrator DB; panes may be remote.** All state stays on your
29
- machine. Do not run a second mu on the host to "coordinate": ownership
30
- is machine-local by construction — `tasks.owner_id` is an FK into the
31
- `agents` table, which never syncs — so a remote mu could not claim
32
- tasks in your DAG. Two half-views, no benefit.
33
-
34
- **murmur, if you use it, sees the REMOTE pane** — the opposite of the line above. Its
35
- extension runs inside the agent's process, so it claims the pane on the
36
- HOST; mu's local pane holds an ssh client and reports nothing. Two
37
- addresses, one worker.
38
-
39
- murmur ties them back together by looking for the AGENT NAME in your
40
- local pane's command line. When it matches, the remote row shows
41
- `attached here %N` and enter focuses your existing pane instead of
42
- opening a second connection — which matters on a capped host, where the
43
- second one fails.
44
-
45
- Both recipes below work, for the same reason: the agent name is in the
46
- argv either way. A direct spawn carries it in the remote command; the
47
- detached shape carries it in the session name, because you named the
48
- session after the agent. **Keep doing that** — `-s mu-<agent>` is what
49
- makes the attachment findable, and a session named anything else costs
50
- you the hint.
51
-
52
- Do not bother putting `MU_AGENT_NAME=` in front of the attach command
53
- to help it along. An environment prefix is consumed by your shell and
54
- never reaches the process arguments, which is all `ps` reports on macOS.
55
-
56
- What mu does NOT know about a remote agent: the workspace. There is no
57
- `vcs_workspaces` row, so no `mu workspace list / refresh / commits /
58
- free`, no `behind` column, no staleness warning on claim, and no
59
- auto-free on close. You own that bookkeeping.
60
-
61
- ---
62
-
63
- ## Never leave an attach pane open
64
-
65
- On a host that caps sessions per connection, **your attach pane holds
66
- the only ssh channel**. While it is open, `git fetch`, `git push`,
67
- `murmur collect`, `rsync`, and plain ssh can fail with the misleading
68
- `Permission denied (keyboard-interactive)` error.
69
-
70
- **Attach to look, then close immediately.** `mu agent close <name>`
71
- detaches without stopping a detached-tmux agent. mu reads agent state
72
- automatically (pi: the forwarded control socket); use mule for orchestrator commands because
73
- its separate ControlPath cannot contend with the attach pane. See
74
- § When the host limits concurrent sessions for diagnosis and the
75
- remote-agent setup.
76
-
77
- When a collect fails, current murmur names an attachment that holds the
78
- channel. Otherwise inspect it directly:
79
-
80
- ```bash
81
- ps -o pid=,command= -ax | grep "[s]sh <host>"
82
- ```
83
-
84
- ### Better: attach with the peer's jump command
85
-
86
- If murmur knows the host, ask it how to reach the host interactively
87
- rather than hardcoding ssh:
88
-
89
- ```bash
90
- JUMP=$(murmur peer list --json | jq -r '.[]|select(.name=="dev").jump_command')
91
- mu agent spawn worker-1 -w big --command "${JUMP//\{pane\}/mu-worker-1}"
92
- ```
93
-
94
- That command may name a transport taking **no** ssh session at all,
95
- which removes the contention rather than managing it.
96
-
97
- **A spawn that lands on an auth prompt looks exactly like a healthy
98
- one.** Measured: with the slot busy, `ssh dev -t "tmux attach"` fell
99
- back to a fresh connection and stopped at `Enter a passcode:`. The spawn
100
- succeeded, the row appeared, `mu agent list` showed `needs_input` — and
101
- `mu agent send` pasted the whole prompt into the passcode field and
102
- reported success. The agent received nothing.
103
-
104
- So confirm a remote agent actually got the work before trusting it
105
- (`mu agent read <name> -n 20`, or its context percentage), and never
106
- send anything sensitive to an unconfirmed pane.
107
-
108
- ---
13
+ - **The pane is local, the process is remote.** The pane's foreground
14
+ process is ssh. `send`, `read`, and the reaper work unchanged. The pi
15
+ control socket rides the same ssh as a `-L` forward, so pi state,
16
+ `wait`, and `abort` are exact.
17
+ - **One orchestrator DB.** Never run a second mu on the host.
18
+ `tasks.owner_id` is an FK into the machine-local `agents` table, so a
19
+ remote mu cannot claim tasks in your DAG.
20
+ - **mu does not know the remote workspace**: no `mu workspace` verbs,
21
+ staleness warning, or auto-free. You own it.
22
+ - **Everything is orchestrator-pull.** The host usually has no route
23
+ back to your laptop. Push setup out, fetch commits back.
109
24
 
110
25
  ## The recipe
111
26
 
112
27
  ```bash
113
- # 1. WORKSPACE — you create it; --workspace does NOT work remotely
28
+ # 1. WORKSPACE: you create it; --workspace is local-only
114
29
  ssh dev 'git -C ~/repo worktree add ~/ws/worker-1'
115
30
 
116
- # 2. RECORD — persist location and per-worker baseline together
31
+ # 2. RECORD: location and per-worker baseline, in one note
117
32
  mu task note t1 -w big "REMOTE: dev:~/ws/worker-1
118
33
  REMOTE_BASE: worker-1:$(ssh dev 'cd ~/ws/worker-1 && git rev-parse HEAD')"
119
34
 
120
- # 3. SPAWN — env goes INSIDE the command (tmux -e stops at the hop);
121
- # remote-env prints it plus the control-socket forward (runs nothing)
35
+ # 3. SPAWN: env goes INSIDE the command (tmux -e stops at the hop).
36
+ # remote-env prints the socket forward and the identity env; it runs nothing.
37
+ # $SHELL -ilc: ssh's non-interactive shell skips ~/.zshrc (PATH, provider env).
122
38
  eval "$(mu agent remote-env worker-1 -w big --shell)" # MU_SSH_ARGS, MU_REMOTE_ENV
123
- # $SHELL -ilc: ssh's non-interactive shell skips ~/.zshrc (PATH, provider env)
124
39
  mu agent spawn worker-1 -w big --command \
125
40
  "ssh $MU_SSH_ARGS dev -t 'cd ~/ws/worker-1 && $MU_REMOTE_ENV \$SHELL -ilc \"pi --approve\"'"
126
41
 
127
- # 4. CLAIM + SEND — identical to a local agent
42
+ # 4. CLAIM + SEND: as for a local agent
128
43
  mu task claim t1 -w big --for worker-1 --evidence 'remote on dev'
129
44
  mu agent send worker-1 -w big --fresh '...'
130
45
 
131
- # 5. WAIT — run the claim's one-shot Next: command once per turn
132
- # Set a deadline; at expiry read the pane, then release or re-dispatch.
133
- job=$(mule run --host dev --max-secs 30 \
134
- 'cd ~/ws/worker-1 && git rev-parse HEAD 2>/dev/null || echo unreadable')
135
- mule wait "$job" >/dev/null && sha=$(mule tail "$job")
136
- case "$sha" in (*[!0-9a-fA-F]*|'') :;; (*) [ "${#sha}" -eq 40 ] && \
137
- { [ "$sha" = 24481fe24481fe24481fe24481fe24481fe2448 ] || \
138
- mu task close t1 -w big --evidence "worker-1 committed $sha"; };; esac
46
+ # 5. WAIT: run the claim's one-shot Next: command once per turn
139
47
 
140
- # 6. COLLECT — fetch straight from the remote worktree
48
+ # 6. COLLECT: fetch straight from the remote worktree
141
49
  git fetch "ssh://dev/~/ws/worker-1" HEAD && git cherry-pick FETCH_HEAD
142
50
  ```
143
51
 
144
- Local and remote agents mix freely in one workstream. The DAG, tracks,
145
- `mu state` and `mu task wait` do not care where a pane's process runs
146
- — task status is a row in YOUR database, written by you, so a wait on
147
- it is exact.
148
-
149
- **Agent state comes from the forwarded control socket** for pi agents.
150
- `-L <local>:<remote>` in `$MU_SSH_ARGS` makes the remote pi's mu
151
- extension answer at the local path mu always uses, so send, state,
152
- `mu agent wait --first` and `mu agent abort` are exact, as for a local
153
- agent. Two prerequisites:
154
-
155
- - The host has the extension: run `mu link pi` there (or copy
156
- `dist/extension/mu-pi.js` into its pi extensions dir).
157
- - The ssh is a direct connection. `$MU_SSH_ARGS` carries
158
- `-o ControlMaster=no -o ControlPath=none`: with `ControlMaster auto`
159
- in `~/.ssh/config` the pane's ssh joins an existing master and the
160
- `-L` unix forward never binds locally (spawn reports `ctl missing`).
161
- - sshd allows the forward. `ExitOnForwardFailure=yes` makes a refused
162
- forward kill the pane at spawn, loudly; check
163
- `AllowStreamLocalForwarding` in the host's `sshd_config`.
164
-
165
- Spawn reports `ctl ok` when both hold; `mu doctor` has a ctl row per
166
- agent. When the ssh dies, ssh leaves the local socket file behind: it
167
- probes as `refused` (never a stale `ok`), and close or the reaper
168
- deletes it.
169
-
170
- Non-pi remote agents still get state from murmur, which lags by its
171
- collect floor (30 seconds ± 10 seconds) plus a 10-second cache. Without
172
- murmur their state is `unknown`, which `mu agent wait` and stall
173
- detection never fire on.
174
-
175
- The reaper is right for a direct spawn — the connection dying really
176
- does kill that agent — and wrong for a detached-tmux one, where the
177
- agent outlives the ssh but mu reaps the task anyway. See § A dropped
178
- connection reaps the task but NOT the commit.
179
-
180
- ### On step 2 — the note is load-bearing
181
-
182
- The task note survives connection loss and context compaction. Keep the
183
- location as `REMOTE: <host>:<path>` and each baseline as
184
- `REMOTE_BASE: <agent>:<sha>`. `mu task claim --for <agent>` uses both to
185
- print a complete one-shot poll-and-close command. For a crew, record one
186
- baseline per worker and use one bounded mule job for every path:
52
+ ### Step 2: the note is load-bearing
53
+
54
+ Keep the exact forms `REMOTE: <host>:<path>` and
55
+ `REMOTE_BASE: <agent>:<sha>`. `mu task claim --for` builds the
56
+ poll-and-close command from them, and `mu state` lists `REMOTE:` lines.
57
+ For a crew, poll every path in one bounded mule job:
187
58
 
188
59
  ```bash
189
60
  mule run --max-secs 30 'for d in ~/ws/*/; do printf "%s %s\\n" \
@@ -191,427 +62,231 @@ mule run --max-secs 30 'for d in ~/ws/*/; do printf "%s %s\\n" \
191
62
  mule wait <job>; mule tail <job>
192
63
  ```
193
64
 
194
- Each line names the worker and sha. Accept only 40 hex characters;
195
- `unreadable` or an empty field means retry next turn, never progress.
196
-
197
- ### On step 5 — wait on commits
198
-
199
- **Poll once per turn, never sleep in a tool call.** A `while true` loop
200
- with `sleep` holds the call; aborting it can leave remote work and a
201
- capped ssh channel running. Set a wall-clock deadline before dispatch.
202
- At expiry, read the pane, then release or re-dispatch instead of extending
203
- the wait silently.
204
-
205
- On a session-capped host, run the claim's `Next:` command through mule
206
- between other work. Bare ssh polls can be refused with an empty sha,
207
- which looks like progress. The command captures the new sha and closes
208
- the task with that sha as evidence. The commit tells you;
209
- the DAG stays IN_PROGRESS until the orchestrator closes it. This feeds
210
- `mu task wait` so blocked tasks advance; it does not replace it.
65
+ Accept only 40 hex characters. `unreadable` or an empty field means
66
+ retry next turn, not progress.
211
67
 
212
- ### Recovering the location
68
+ ### Step 3: the control-socket forward
213
69
 
214
- `mu state` lists exact `REMOTE:` lines as its remote-worker inventory,
215
- and the recovery command is mechanical:
70
+ Spawn reports `ctl ok` when all of these hold (`mu doctor` has a ctl
71
+ row per agent):
216
72
 
217
- ```bash
218
- git fetch "ssh://<host>/<path>" HEAD && git cherry-pick FETCH_HEAD
219
- ```
73
+ - The host has the mu extension: run `mu link pi` there.
74
+ - The ssh is direct. `$MU_SSH_ARGS` sets `ControlMaster=no
75
+ ControlPath=none`; a multiplexed ssh never binds the forward and spawn
76
+ reports `ctl missing`.
77
+ - sshd allows the forward. A refused forward kills the pane at spawn
78
+ (`ExitOnForwardFailure=yes`). Check `AllowStreamLocalForwarding`.
220
79
 
221
- ### On step 6 — fetching from a worktree
80
+ A dead ssh leaves the socket file, which probes `refused`, never a
81
+ stale `ok`. Non-pi agents get state from murmur, lagging 30 s ± 10 s
82
+ plus a 10 s cache.
222
83
 
223
- `git fetch "ssh://<host>/<path>" HEAD` reads a remote worktree
224
- **directly**. No shared remote, no push, no bare repo in between. This
225
- is the part people expect to be hard and it is not.
84
+ ### Step 5: poll once per turn
226
85
 
227
- - It exits **128** on failure, so `&&` chaining is safe.
228
- - **Quote the URL.** `~` is legal in an `ssh://` URL (git's own docs
229
- list `ssh://host/~user/path`), but unquoted it is expanded by your
230
- LOCAL shell into your laptop's home.
231
- - Two workers editing one file still conflict on cherry-pick, exactly
232
- as locally. Bucket work by file cluster, not by machine.
86
+ - Never `sleep` in a tool call. Aborting the loop can leave remote work
87
+ and a capped channel running.
88
+ - Set a deadline before dispatch. At expiry, read the pane, then release
89
+ or re-dispatch.
90
+ - On a capped host, run `Next:` through mule. A refused bare ssh returns
91
+ an empty sha that looks like progress.
233
92
 
234
- ### Run the merged suite where the workers are
93
+ ### Step 6: fetch from the worktree
235
94
 
236
- The reason to send work to a big machine is to stop paying for it on a
237
- small one. Cherry-picking remote work and then running the whole suite
238
- locally gives that back — and it is the default thing to do, so say the
239
- other thing explicitly.
95
+ - No shared remote or bare repo is needed. It exits 128 on failure, so
96
+ `&&` chaining is safe.
97
+ - Quote the URL. Unquoted, your local shell expands `~` to your laptop's
98
+ home.
99
+ - Bucket work by file cluster, not by machine.
240
100
 
241
- **Do not re-run what the worker ran.** Its green on its own tree is the
242
- evidence you asked for. What is unverified is the MERGE: the worker
243
- forked from a base that has since moved, so the only new information is
244
- in the combination.
101
+ ### Run the merged gate on the host
245
102
 
246
- So cherry-pick locally (it is a few seconds of IO), then push the merged
247
- head to the same host and run the gate there:
103
+ Do not re-run what the worker ran. Only the merge is unverified.
104
+ Cherry-pick locally, push the merged head to the host, and run the gate
105
+ there with its warm dependencies:
248
106
 
249
107
  ```bash
250
108
  git cherry-pick <sha>
251
109
  git push -q "ssh://dev/~/hacking/<repo>.git" HEAD:refs/heads/main
252
- ssh dev 'cd ~/hacking/<checkout> && git fetch -q origin \
253
- && git reset -q --hard origin/main && npm run check'
254
- ```
255
-
256
- **On a session-capped host, run that gate through mule instead.** The
257
- last line holds the channel for the whole suite — minutes — which is
258
- precisely when your other tooling starts failing with a credentials
259
- error that has nothing to do with credentials:
260
-
261
- ```bash
262
110
  mule run --cwd ~/hacking/<checkout> --wait \
263
111
  'git fetch -q origin && git reset -q --hard origin/main && npm run check'
264
112
  ```
265
113
 
266
- Same work, dispatched detached on mule's own connection, and `--wait`
267
- exits with the suite's own code. See § When the host limits concurrent
268
- sessions.
114
+ `--wait` exits with the suite's code. A bare ssh would hold the capped
115
+ channel for minutes. Keep two gates local: platform-sensitive tests (macOS `ps` returns argv,
116
+ Linux `ps -e` appends the environment, and a real bug lived there), and
117
+ the final gate before the push that matters.
269
118
 
270
- That host already has a checkout and warm dependencies — the same ones
271
- the worker used — so the marginal cost is near zero, while the same run
272
- on a laptop is minutes of CPU per integration.
119
+ ## Traps
273
120
 
274
- **Keep two things local.** A **platform-sensitive** subset, because a
275
- remote green does not prove a local green when the bug is
276
- platform-shaped: macOS `ps` returns argv where Linux `ps -e` appends the
277
- environment, and a real bug lived in exactly that gap. And the **final**
278
- gate before the push that matters, since that one is about your tree
279
- rather than the worker's.
121
+ ### A spawn on an auth prompt looks healthy
280
122
 
281
- ---
282
-
283
- ## Traps
123
+ With the channel busy, the attach ssh stopped at `Enter a passcode:`.
124
+ Spawn succeeded, the agent showed `needs_input`, and the prompt went
125
+ into the passcode field. Confirm a remote agent got the work
126
+ (`mu agent read <name> -n 20`) before trusting it. Send nothing
127
+ sensitive to an unconfirmed pane.
284
128
 
285
129
  ### Use the host's real CLI command
286
130
 
287
- The wrapper you run locally is not what a bare `pi` gives you on the
288
- host. If your local `$MU_PI_COMMAND` is `pi-meta --pi-meta-no-solo
289
- --approve`, spawning plain `pi` remotely yields a live pane, a healthy
290
- status, and an agent with **no models configured** — it looks fine
291
- until you send it work. Check first:
292
-
293
- ```bash
294
- ssh dev 'command -v pi-meta'
295
- ```
131
+ If your local `$MU_PI_COMMAND` is a wrapper such as `pi-meta
132
+ --pi-meta-no-solo --approve`, plain `pi` on the host gives a live pane
133
+ with **no models configured**. Check with `ssh dev 'command -v pi-meta'`.
296
134
 
297
135
  ### Tell the worker that mu is absent
298
136
 
299
- `mu` is usually not installed on the host, and the DB is on your
300
- laptop regardless. The in-pane worker loop (`mu task claim` / `note` /
301
- `close`) therefore cannot run. Say so in the prompt and have the
302
- worker print its sha instead; YOU claim, note and close from the
303
- orchestrator. Omit this and the worker burns a turn on `command not
304
- found`, or worse, silently fails to close and your `mu task wait`
305
- hangs.
306
-
307
- ### Everything is orchestrator-PULL
137
+ mu is usually not on the host, so the in-pane loop cannot run. Say so
138
+ in the prompt and have the worker print its sha. You claim, note, and
139
+ close. Otherwise `mu task wait` hangs.
308
140
 
309
- The host frequently cannot resolve your laptop at all — a corporate
310
- devserver typically has no route back to a NAT'd machine. So: you push
311
- setup out, you fetch commits back. Never write a recipe in which the
312
- host reaches you, and never assume a peer can `git fetch` from you.
141
+ ### A dropped connection reaps the task, not the commit
313
142
 
314
- ### A dropped connection reaps the task but NOT the commit
143
+ The reaper reverts the task to OPEN, but the commit is still on the
144
+ host. Fetch from the `REMOTE:` path before re-dispatching.
315
145
 
316
- If the ssh client dies — network drop, laptop sleep, VPN blip — the
317
- pane dies with it, and mu behaves exactly as for a dead local agent:
318
- the agent row goes, and the reaper reverts the task `IN_PROGRESS →
319
- OPEN` with a `[reaper]` note. That is correct and desirable.
146
+ ### A dead non-pi remote agent vanishes
320
147
 
321
- But the worker's **commit is still on the host**, and mu has no record
322
- of where. Before re-dispatching, fetch from the path in the task note
323
- and look: re-running the task blind duplicates work that already
324
- exists. This is the strongest argument for step 2.
148
+ murmur records `crashed` only when a pane outlives its process. Remotely
149
+ tmux reaps the session with the agent, so SIGKILL gives zero rows, not
150
+ `crashed`. (A dead pi agent probes `refused`.) Trust the `[reaper]`
151
+ note and `ssh <host> 'tmux ls'`.
325
152
 
326
- ### A crashed remote worker disappears rather than reporting `crashed`
153
+ ### Stopping and cleaning up
327
154
 
328
- murmur records `crashed` only when a **pane outlives the process** in
329
- it: that is how an unreported death leaves a trace. Locally it holds —
330
- a pi exits inside a shell pane and the pane stays. It does not hold
331
- here, because the agent is the remote session's only process, so tmux
332
- reaps the session with it and the row is simply gone at the next
333
- collect.
155
+ - `mu agent abort <name>` stops a pi turn through the forwarded socket.
156
+ - `mu agent kick` signals the local ssh client, not the remote agent.
157
+ For an unresponsive agent, `mu agent close` and respawn.
158
+ - mu does not remove remote worktrees:
159
+ `ssh dev 'git -C ~/repo worktree remove ~/ws/worker-1'`.
334
160
 
335
- Measured: SIGKILL a remote agent and murmur reports **zero rows and
336
- zero crashed**, not a crash. Nothing distinguishes "it died" from "it
337
- finished and I closed it". A second consequence of the same rule: the
338
- crash path only fires for an agent killed **mid-turn**, since an idle
339
- agent is already `stopped` and a stopped owner dying reads as a normal
340
- finish.
161
+ ## murmur
341
162
 
342
- So do not wait for a crash signal on a remote worker. The durable
343
- traces are the ones mu already gives you:
163
+ pi agents need no murmur. [murmur](https://github.com/mu-crew/murmur)
164
+ reports state for non-pi agents on tmux, and adds status pills,
165
+ attachment hints, and `murmur peer list`. Without it, non-pi agents
166
+ report `unknown`; everything else works.
344
167
 
345
- - the reaper flipping the task back to `OPEN` with a `[reaper]` note
346
- - the `REMOTE:` task note, which is where the commit is
168
+ | Question | Ask |
169
+ | --- | --- |
170
+ | What happens next, who owns it, is it done | mu: the DAG, `claim`, `task wait` |
171
+ | What is a pi agent doing | mu: the control socket |
172
+ | What is a non-pi agent doing; is anything blocked on me | murmur |
173
+ | Which host can I reach | murmur: `peer list` |
347
174
 
348
- `ssh <host> 'tmux ls'` confirms whether the session is really gone.
175
+ **`$MU_REMOTE_ENV` goes inside the ssh command.** Without it the agent
176
+ reports `driver=human` and shows in your own status bar.
349
177
 
350
- ### Cleaning up
178
+ **murmur sees the remote pane** and links it to your local pane by the
179
+ agent name in the local argv (`attached here %N`; focus instead of a
180
+ second connection). The detached shape carries the name only in the
181
+ session name, so name it `mu-<agent>`. An env prefix does not help:
182
+ macOS `ps` reports argv, not env.
351
183
 
352
- mu will not remove a remote worktree, because it does not know it
353
- exists:
184
+ ### Picking a host
354
185
 
355
186
  ```bash
356
- ssh dev 'git -C ~/repo worktree remove ~/ws/worker-1'
187
+ murmur peer list --json | jq -r '.[] | select(.error) | .name'
357
188
  ```
358
189
 
359
- To stop a remote pi agent's turn, `mu agent abort <name>`: it goes
360
- through the forwarded socket. `mu agent kick` signals the local pane's
361
- foreground process group — that is the ssh client, not the remote
362
- agent. For an unresponsive one, `mu agent close` and respawn.
363
-
364
- ---
365
-
366
- ## mu and murmur, and what you lose without it
367
-
368
- pi agents, local or remote, need no murmur: mu reads their state from
369
- the control socket. [murmur](https://github.com/mu-crew/murmur) provides
370
- agent state for non-pi agents (claude-code, codex) on tmux. Without it
371
- those report `unknown`; the DAG, claims, task completion waits,
372
- workspaces, spawn, send, read, the reaper, this remote recipe, and
373
- `git fetch` collection still work. Agent-state consumers such as
374
- `mu agent wait`, idle flags, and task-stall detection wait rather than
375
- treating `unknown` as completion or a stall.
376
-
377
- murmur also provides the tmux status pills, `prefix+a`, attachment hints,
378
- and `murmur peer list` for host reachability. The division is: **mu owns
379
- the work; murmur reports what an agent is doing.** murmur never places
380
- work. Its interfaces used by mu are in murmur's
381
- [stable contract](https://github.com/mu-crew/murmur/blob/main/ARCHITECTURE.md#contract).
190
+ - `ssh` is last-known reachability. `error` is the current attempt.
191
+ - It exits 0 either way. Branch on `.error`, not on presence.
192
+ - `murmur collect` prints one line per unreachable host, now.
382
193
 
383
- | question | ask |
384
- | --- | --- |
385
- | what should happen next | mu — the DAG |
386
- | who owns this task | mu — `claim` / `close` |
387
- | is the task done | mu — `task wait`, a DB poll |
388
- | what is this pi agent doing right now | mu — the control socket |
389
- | what is a non-pi agent doing right now | murmur |
390
- | is anything blocked on me, anywhere | murmur |
391
- | which host can I reach | murmur — `peer list` |
392
-
393
- **The seam is three env vars, and it is load-bearing.** `mu agent
394
- spawn` injects `MU_MANAGED_AGENT=1`, `MU_AGENT_NAME` and
395
- `MU_WORKSTREAM` (plus `MU_CTL_SOCK` for mu's own extension); pi
396
- inherits them and murmur's extension reads them.
397
- That one mechanism gives you:
398
-
399
- 1. `driver=orchestrated`, so crew stays out of the human's status bar
400
- unless it is blocked or crashed — true for local and remote alike
401
- 2. the workstream and agent name on the row, for grouping
402
- 3. the attachment back-reference for a remote worker
403
-
404
- For a remote worker they go **inside** the ssh command (tmux `-e` stops
405
- at the hop); `$MU_REMOTE_ENV` from `mu agent remote-env` carries them. Miss them and the agent reports `driver=human`: it appears
406
- in your own counts as if it were yours.
407
-
408
- ## Picking a host
409
-
410
- mu does not track hosts and should not; that is murmur's job. If it is
411
- installed:
194
+ Attach with the peer's jump command instead of hardcoding ssh. It may
195
+ use a transport that takes no ssh session:
412
196
 
413
197
  ```bash
414
- murmur peer list --json | jq -r '.[] | select(.error) | .name'
198
+ murmur peer set <name> --jump-command '<command with {pane}>'
199
+ JUMP=$(murmur peer list --json | jq -r '.[]|select(.name=="dev").jump_command')
200
+ mu agent spawn worker-1 -w big --command "${JUMP//\{pane\}/mu-worker-1}"
415
201
  ```
416
202
 
417
- Read it correctly, because it is **best-effort by design**:
418
-
419
- - `ssh` is **last-known** reachability; `error` is the **current**
420
- attempt. A host can read `warm` and be failing right now.
421
- - The command exits **0** either way — a fleet with sleeping machines
422
- is the normal state, not a fault.
423
- - So branch on `.error`, never on presence in the list.
424
-
425
- `murmur status` and `pick` are polling paths and stay silent whatever
426
- the fleet is doing. `murmur collect` is the deliberate dial: it prints
427
- one line per host it could not reach, so it is the one to run for "can I
428
- reach this host *right now*".
203
+ ## When the host limits concurrent sessions
429
204
 
430
- ### Reaching a host is two different questions
205
+ Most sshd allow 10 sessions per connection (`MaxSessions`). A hardened
206
+ host may set `MaxSessions 1`. Then the ssh holding your agent takes the
207
+ only channel, and every other ssh, including `git fetch`, fails with a
208
+ misleading `Permission denied (keyboard-interactive)`.
431
209
 
432
- A peer carries `target` (for a COMMAND — always ssh, used by the
433
- collector) and a jump command (for a HUMAN — need not be ssh):
210
+ Diagnose in this order. The first check is local and instant:
434
211
 
435
212
  ```bash
436
- murmur peer set <name> --jump-command '<command with {pane}>'
213
+ ssh-add -l # "Error connecting to agent" => dead ssh agent, NOT the cap
214
+ ssh <host> true # fails while `mu agent read` works => the cap
437
215
  ```
438
216
 
439
- Opaque template: murmur substitutes `{pane}` and runs the rest
440
- unparsed. The default reproduces `ssh -t <target> tmux attach`, so an
441
- unconfigured peer behaves as before.
442
-
443
- On a session-capped host this is the difference between holding the one
444
- slot for your whole visit and holding nothing (see the ET section).
445
- Don't hardcode `et` — a site wrapper may add VPN selection and its own
446
- binary resolution, which is why the value is opaque.
217
+ A dead `SSH_AUTH_SOCK` gives the same error via an invisible 2FA prompt;
218
+ one session lost an hour treating it as the cap. To find what holds the
219
+ channel:
220
+ `ps -o pid=,command= -ax | grep "[s]sh <host>"`.
447
221
 
448
- ---
449
-
450
- ## When the host limits concurrent sessions
222
+ `ControlMaster no` does not help (it governs master creation only).
223
+ There is no client fix.
451
224
 
452
- Rare, but it presents as a credentials bug, so learn to recognise it.
225
+ ### Run commands through mule
453
226
 
454
- Most sshd allow 10 sessions per connection (`MaxSessions`, default 10)
455
- and the recipe above is all you need. A hardened host may set
456
- **`MaxSessions 1`**: then the long-lived ssh holding your AGENT consumes
457
- the only channel, and every other ssh — including `git fetch` — is
458
- refused with the misleading 2FA error described in § Never leave an
459
- attach pane open.
227
+ [mule](https://github.com/mu-crew/mule) dispatches detached jobs on its
228
+ own ssh ControlPath. On a `MaxSessions 1` host, eight bare `rev-parse`
229
+ polls returned one sha and seven empty results; mule ran all eight.
460
230
 
461
- **Diagnostic:** if `mu agent read` works fine while a plain `ssh
462
- <host> true` fails, it is session exhaustion, not credentials.
231
+ - Use mule for long work and for any command whose refusal could look
232
+ like success. Keep worktree setup and `murmur collect` bare: they fail
233
+ loudly.
234
+ - A mule job is entirely remote. It cannot fetch from, push to, or rsync
235
+ with your laptop.
236
+ - For an interactive agent under mule's lifecycle, use `mule run --tui`
237
+ (plain `mule run` has no live TUI). It prints the attach command.
238
+ Closing that pane does not stop the job.
463
239
 
464
- **But do not stop there — that test is necessary, not sufficient.** The
465
- same `Permission denied (keyboard-interactive)` is also what a **dead
466
- ssh agent** produces: `SSH_AUTH_SOCK` points at a socket that no longer
467
- exists, key auth cannot be attempted, and sshd falls back to 2FA and
468
- sits on a passcode prompt you cannot see. One session cost an hour to
469
- this, diagnosed as session exhaustion throughout.
240
+ | mule exit | Meaning | Do |
241
+ | --- | --- | --- |
242
+ | 3 | No ssh master; opening one may need a hardware-key touch | Hand back: ask the operator to run the printed command. Never retry, run `ssh -MNf`, or fall back to `ssh <host> <cmd>` |
243
+ | 4, 6 | Not yet | Poll again |
244
+ | 5 | No result will ever arrive | Stop polling |
470
245
 
471
- Check both, in this order — the second is local and instant:
246
+ None of these means the work failed. Full table: `mule --help`.
472
247
 
473
- ```bash
474
- ssh-add -l # "Error connecting to agent" => dead agent, NOT the cap
475
- ssh <host> true # fails while `mu agent read` works => the cap
476
- ```
248
+ ### Run the agent in detached remote tmux
477
249
 
478
- And note the prompt is **invisible unless the ssh runs inside a pane you
479
- can capture**. That is what finally exposed it: the refusal looks like a
480
- silent failure because the question is being asked somewhere with no
481
- terminal attached.
482
-
483
- `ControlMaster no` looks like it should help and does not. It governs
484
- master *creation* only; a refused channel falls back regardless.
485
- Measured with `no` set: three of four concurrent calls still produced
486
- the misleading 2FA error. There is no client-side fix — `MaxSessions`
487
- exists only in `sshd_config`, and the client discovers the cap only by
488
- being refused.
489
-
490
- ### Fix: run commands through mule
491
-
492
- [mule](https://github.com/mu-crew/mule) opens a separate ssh
493
- ControlPath and dispatches detached jobs. On a `MaxSessions 1` host,
494
- five concurrent bare calls produced **1 of 5** successes; mule produced
495
- **5 of 5**. A multi-minute mule job also left a concurrent plain ssh
496
- working.
497
-
498
- Route commands by failure mode, not duration. Use mule for long work
499
- and whenever refusal could look like success. Eight concurrent bare
500
- `rev-parse` polls returned one sha and seven empty results; mule
501
- dispatched all eight. Keep worktree setup and `murmur collect` bare
502
- because they fail loudly. See `mule --help` and the relevant subcommand
503
- help for flags, job control, warnings, and exit codes.
504
-
505
- **A mule job must be entirely remote.** The host has no route back to
506
- your laptop, so a local-endpoint `git fetch`, `git push`, or rsync
507
- cannot run inside the job. Collect from the orchestrator as described
508
- in § Everything is orchestrator-PULL.
509
-
510
- **Use `mule run --tui` for an interactive/full-screen agent when you want
511
- mule's timeout, runtime, transcript, and cleanup.** Plain `mule run` pipes
512
- output into the artifact, so attaching finds the pane but no live TUI.
513
- mule prints the exact murmur jump and `mu agent spawn --command` lines after
514
- TUI dispatch. mu still needs that local attach pane for read/send/wait; closing
515
- it detaches without stopping the mule job. Use plain detached remote tmux when
516
- you need none of mule's artifacts or lifecycle controls.
517
-
518
- #### mule exit 3 is a handback
519
-
520
- A missing ssh master can require a human to touch a hardware key. Ask
521
- the operator to run the command mule prints. Do not retry, run
522
- `ssh -MNf` yourself, or fall back to `ssh <host> <command>`; each avoids
523
- the required handback or recreates the capped-channel failure.
524
-
525
- #### Exit 4 is "not yet"; exit 5 is "never"
526
-
527
- Exit 4 and 6 mean poll again because the job may still complete. Exit 5
528
- means no result will arrive. None means the work itself failed. Read the
529
- full table in `mule --help`.
530
-
531
- ### Fix: detached remote tmux (for the agent itself)
532
-
533
- Run the agent in its own tmux session on the host, and attach to
534
- *that*. The agent's lifetime is then decoupled from the connection, so
535
- the session can be released.
250
+ Decouple the agent from the connection to release the channel:
536
251
 
537
252
  ```bash
538
- eval "$(mu agent remote-env worker-1 -w big --shell)" # MU_SSH_ARGS, MU_REMOTE_ENV
539
- # Agent runs DETACHED on the host; the ssh returns immediately
253
+ eval "$(mu agent remote-env worker-1 -w big --shell)"
254
+ # Agent runs detached on the host; this ssh returns at once
540
255
  ssh dev "tmux new-session -d -s mu-worker-1 -c ~/ws/worker-1 \
541
256
  '$MU_REMOTE_ENV \$SHELL -ilc \"pi --approve\"'"
542
257
 
543
- # Attach a local pane to it (forwarding the socket); claim/send are then normal
258
+ # Attach a local pane (with the socket forward); claim and send as usual
544
259
  mu agent spawn worker-1 -w big --command "ssh $MU_SSH_ARGS dev -t 'tmux attach -t mu-worker-1'"
545
260
 
546
- # COLLECT: detach FIRST to free the session, then fetch
261
+ # COLLECT: detach first to free the channel, then fetch
547
262
  mu agent close worker-1 -w big
548
263
  git fetch "ssh://dev/~/ws/worker-1" HEAD && git cherry-pick FETCH_HEAD
549
264
 
550
- # Reattach — same session, LLM context intact (same --command as above)
551
- mu agent spawn worker-1 -w big --command "ssh $MU_SSH_ARGS dev -t 'tmux attach -t mu-worker-1'"
265
+ # Reattach later with the same spawn command; LLM context is intact
552
266
  ```
553
267
 
554
- The pi keeps serving its socket at the remote path while detached; each
555
- attach re-creates the local end of the forward.
268
+ - An attached pane blocks other ssh exactly like a direct one. Close it
269
+ before fetching, or use mule.
270
+ - `mu agent close` detaches. It does not stop the agent. To stop it:
271
+ `ssh dev 'tmux kill-session -t mu-worker-1'`. `ssh dev 'tmux ls'` is
272
+ the only inventory of these sessions.
273
+ - A dropped connection no longer reaps the agent. Each attach
274
+ re-creates the socket forward.
275
+ - Keep the session name `mu-<agent>`: mu records neither, so it is the
276
+ only link. On an uncapped host this shape buys nothing.
556
277
 
557
- Two consequences, both counterintuitive:
278
+ ### Use ET for your own shell
558
279
 
559
- - **An attached pane blocks concurrent ssh just as much as a direct
560
- one.** Nesting does not make the host concurrent; it makes detaching
561
- cheap and non-destructive. You must close the pane BEFORE fetching —
562
- or use mule, which is on its own channel and does not care.
563
- - **`mu agent close` detaches, it does not stop the agent** — the
564
- inverse of local semantics, and the whole point. To actually stop
565
- one: `ssh dev 'tmux kill-session -t mu-worker-1'`. Skip that and you
566
- accumulate orphaned remote sessions mu cannot see; `ssh dev 'tmux
567
- ls'` is the only inventory.
568
-
569
- This adds a THIRD address: local pane → ssh → remote tmux session →
570
- agent pane. `kick` reaches only the first, `ssh dev 'tmux ls'` is the
571
- only view of the third. Keep the session name equal to the agent name
572
- — mu records neither, so it is the only handle tying them together.
573
-
574
- The session name is load-bearing beyond readability: murmur finds your
575
- attachment by spotting the agent name in the local pane's argv, and
576
- with this shape the session name is the only place it appears. `-s
577
- mu-worker-1` for agent `worker-1` gives you `attached here %N` on the
578
- remote row; `-s scratch` silently does not.
579
-
580
- The upside beyond unblocking `git fetch`: a dropped connection no
581
- longer reaps the task, since the agent outlives the ssh session, and a
582
- reattach preserves full LLM context.
583
-
584
- Use this shape only where you need it. On an ordinary host it is
585
- pointless indirection.
586
-
587
- ### For your own interactive work, use ET instead
588
-
589
- The recipe above is for mu agents, which need a pane whose process mu
590
- controls. Your own shell on the host has an easier answer: **Eternal
591
- Terminal holds no ssh session at all.** It bootstraps over ssh and then
592
- hands off to `etserver` on its own transport, so `MaxSessions` never
593
- counts it.
280
+ Eternal Terminal moves off ssh to `etserver`, so `MaxSessions` never
281
+ counts it, even with a nested `tmux attach`. It makes a good jump
282
+ command but cannot serve `git fetch`.
594
283
 
595
284
  ```bash
596
- et dev # or your site's wrapper, e.g. `x2ssh -et dev`
285
+ et dev # or your site's wrapper, such as `x2ssh -et dev`
597
286
  ```
598
287
 
599
- **An aborted ET or `x2ssh` leaves local processes behind.** Both survive
600
- the abort of the tool call that started them and go on holding state, so
601
- a retry stacks a second one on top. `pkill -f x2ssh` before retrying.
602
-
603
- Verified on a `MaxSessions 1` host, twice: an interactive `ssh dev`
604
- starved every other ssh for as long as it stayed open, while an ET
605
- session left `ssh dev true` succeeding throughout — and the same held
606
- with a nested `tmux attach` live inside it, with a concurrent `murmur
607
- collect` reaching the host. So ET costs the capped slot nothing even
608
- while you are sitting in a remote agent, which is what makes it usable
609
- as a murmur jump command.
610
-
611
- So the clean split on a capped host is ET for you, one `ssh -MNf <host>`
612
- master for tooling, and detached tmux for mu agents. The three do not
613
- compete.
614
-
615
- Note ET cannot serve murmur or `git fetch` — it exposes no multiplexing
616
- socket to attach to. That is exactly why it pairs well: it takes none of
617
- the capped slots those tools need.
288
+ An aborted `et` or `x2ssh` leaves local processes behind. Run
289
+ `pkill -f x2ssh` before retrying.
290
+
291
+ On a capped host: ET for you, mule for tooling, detached tmux for mu
292
+ agents. None of them competes for the slot.