@mu-crew/mu 3.2.0 → 3.3.0

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 (71) hide show
  1. package/AGENTS.md +208 -599
  2. package/README.md +125 -329
  3. package/dist/cli.js +108 -78
  4. package/dist/cli.js.map +1 -1
  5. package/dist/extension/mu-pi.js +421 -56
  6. package/dist/extension/mu-pi.js.map +1 -1
  7. package/dist/index.d.ts +36 -10
  8. package/dist/index.js +89 -67
  9. package/dist/index.js.map +1 -1
  10. package/docs/ARCHITECTURE.md +176 -1072
  11. package/docs/ROADMAP.md +88 -169
  12. package/docs/VISION.md +205 -415
  13. package/docs/VOCABULARY.md +204 -466
  14. package/docs/architecture/control-socket.md +133 -0
  15. package/docs/architecture/dag.md +113 -0
  16. package/docs/architecture/mux.md +162 -0
  17. package/docs/architecture/ops-log.md +140 -0
  18. package/docs/architecture/sdk.md +185 -0
  19. package/docs/architecture/sync.md +129 -0
  20. package/docs/architecture/tui.md +146 -0
  21. package/docs/guide/README.md +30 -0
  22. package/docs/guide/backends.md +61 -0
  23. package/docs/guide/cleanup.md +74 -0
  24. package/docs/guide/delegate.md +87 -0
  25. package/docs/guide/dispatch.md +144 -0
  26. package/docs/guide/getting-started.md +180 -0
  27. package/docs/guide/recovery.md +135 -0
  28. package/docs/guide/remote.md +74 -0
  29. package/docs/guide/sql.md +76 -0
  30. package/docs/guide/stop-a-worker.md +64 -0
  31. package/docs/guide/sync.md +78 -0
  32. package/docs/guide/tui.md +98 -0
  33. package/docs/guide/upgrade.md +76 -0
  34. package/docs/history/CHANGELOG-pre-3.md +4866 -0
  35. package/docs/reference/env.md +87 -0
  36. package/docs/reference/naming.md +82 -0
  37. package/docs/specs/2026-09-28-agent-state-from-murmur.md +1 -1
  38. package/docs/specs/2026-09-29-task-substates.md +1 -1
  39. package/docs/specs/2026-10-03-keep-driving-nudge.md +142 -0
  40. package/package.json +2 -1
  41. package/prompts/mu-refute.md +8 -0
  42. package/prompts/mu-research.md +8 -0
  43. package/prompts/mu-review.md +8 -0
  44. package/prompts/mu-rules-audit.md +8 -0
  45. package/prompts/mu-tournament.md +9 -0
  46. package/prompts/ultrathink.md +9 -0
  47. package/skills/mu/SKILL.md +126 -255
  48. package/skills/mu/recipes/adversarial-review.md +83 -0
  49. package/skills/mu/recipes/brief.md +53 -0
  50. package/skills/mu/recipes/codemode-driver.md +54 -0
  51. package/skills/mu/recipes/deep-research.md +52 -0
  52. package/skills/mu/recipes/fan-out.md +36 -0
  53. package/skills/mu/recipes/hypothesis-panel.md +40 -0
  54. package/skills/mu/recipes/long-run.md +20 -0
  55. package/skills/mu/recipes/loop-until-done.md +38 -0
  56. package/skills/mu/recipes/orchestrator-loop.md +87 -0
  57. package/skills/mu/recipes/plan.md +54 -0
  58. package/skills/mu/recipes/recovery.md +26 -0
  59. package/skills/mu/recipes/refute.md +49 -0
  60. package/skills/mu/recipes/remote-workers.md +292 -0
  61. package/skills/mu/recipes/review-panel.md +57 -0
  62. package/skills/mu/recipes/rules-audit.md +70 -0
  63. package/skills/mu/recipes/tournament.md +37 -0
  64. package/skills/mu/recipes/triage.md +38 -0
  65. package/skills/mu/recipes/ultrathink.md +73 -0
  66. package/skills/mu/recipes/watcher.md +16 -0
  67. package/skills/mu/recipes/waves.md +19 -0
  68. package/skills/mu/recipes/worker.md +24 -0
  69. package/docs/HANDOVER.md +0 -486
  70. package/docs/USAGE_GUIDE.md +0 -2738
  71. package/skills/mu/REMOTE_WORKERS.md +0 -617
@@ -1,617 +0,0 @@
1
- # Remote workers
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.
6
-
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
- ---
17
-
18
- ## The model
19
-
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
- ---
109
-
110
- ## The recipe
111
-
112
- ```bash
113
- # 1. WORKSPACE — you create it; --workspace does NOT work remotely
114
- ssh dev 'git -C ~/repo worktree add ~/ws/worker-1'
115
-
116
- # 2. RECORD — persist location and per-worker baseline together
117
- mu task note t1 -w big "REMOTE: dev:~/ws/worker-1
118
- REMOTE_BASE: worker-1:$(ssh dev 'cd ~/ws/worker-1 && git rev-parse HEAD')"
119
-
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)
122
- 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
- mu agent spawn worker-1 -w big --command \
125
- "ssh $MU_SSH_ARGS dev -t 'cd ~/ws/worker-1 && $MU_REMOTE_ENV \$SHELL -ilc \"pi --approve\"'"
126
-
127
- # 4. CLAIM + SEND — identical to a local agent
128
- mu task claim t1 -w big --for worker-1 --evidence 'remote on dev'
129
- mu agent send worker-1 -w big --fresh '...'
130
-
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
139
-
140
- # 6. COLLECT — fetch straight from the remote worktree
141
- git fetch "ssh://dev/~/ws/worker-1" HEAD && git cherry-pick FETCH_HEAD
142
- ```
143
-
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:
187
-
188
- ```bash
189
- mule run --max-secs 30 'for d in ~/ws/*/; do printf "%s %s\\n" \
190
- "$(basename $d)" "$(cd $d && git rev-parse HEAD 2>/dev/null || echo unreadable)"; done'
191
- mule wait <job>; mule tail <job>
192
- ```
193
-
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.
211
-
212
- ### Recovering the location
213
-
214
- `mu state` lists exact `REMOTE:` lines as its remote-worker inventory,
215
- and the recovery command is mechanical:
216
-
217
- ```bash
218
- git fetch "ssh://<host>/<path>" HEAD && git cherry-pick FETCH_HEAD
219
- ```
220
-
221
- ### On step 6 — fetching from a worktree
222
-
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.
226
-
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.
233
-
234
- ### Run the merged suite where the workers are
235
-
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.
240
-
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.
245
-
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:
248
-
249
- ```bash
250
- git cherry-pick <sha>
251
- 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
- mule run --cwd ~/hacking/<checkout> --wait \
263
- 'git fetch -q origin && git reset -q --hard origin/main && npm run check'
264
- ```
265
-
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.
269
-
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.
273
-
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.
280
-
281
- ---
282
-
283
- ## Traps
284
-
285
- ### Use the host's real CLI command
286
-
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
- ```
296
-
297
- ### Tell the worker that mu is absent
298
-
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
308
-
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.
313
-
314
- ### A dropped connection reaps the task but NOT the commit
315
-
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.
320
-
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.
325
-
326
- ### A crashed remote worker disappears rather than reporting `crashed`
327
-
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.
334
-
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.
341
-
342
- So do not wait for a crash signal on a remote worker. The durable
343
- traces are the ones mu already gives you:
344
-
345
- - the reaper flipping the task back to `OPEN` with a `[reaper]` note
346
- - the `REMOTE:` task note, which is where the commit is
347
-
348
- `ssh <host> 'tmux ls'` confirms whether the session is really gone.
349
-
350
- ### Cleaning up
351
-
352
- mu will not remove a remote worktree, because it does not know it
353
- exists:
354
-
355
- ```bash
356
- ssh dev 'git -C ~/repo worktree remove ~/ws/worker-1'
357
- ```
358
-
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).
382
-
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:
412
-
413
- ```bash
414
- murmur peer list --json | jq -r '.[] | select(.error) | .name'
415
- ```
416
-
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*".
429
-
430
- ### Reaching a host is two different questions
431
-
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):
434
-
435
- ```bash
436
- murmur peer set <name> --jump-command '<command with {pane}>'
437
- ```
438
-
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.
447
-
448
- ---
449
-
450
- ## When the host limits concurrent sessions
451
-
452
- Rare, but it presents as a credentials bug, so learn to recognise it.
453
-
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.
460
-
461
- **Diagnostic:** if `mu agent read` works fine while a plain `ssh
462
- <host> true` fails, it is session exhaustion, not credentials.
463
-
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.
470
-
471
- Check both, in this order — the second is local and instant:
472
-
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
- ```
477
-
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.
536
-
537
- ```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
540
- ssh dev "tmux new-session -d -s mu-worker-1 -c ~/ws/worker-1 \
541
- '$MU_REMOTE_ENV \$SHELL -ilc \"pi --approve\"'"
542
-
543
- # Attach a local pane to it (forwarding the socket); claim/send are then normal
544
- mu agent spawn worker-1 -w big --command "ssh $MU_SSH_ARGS dev -t 'tmux attach -t mu-worker-1'"
545
-
546
- # COLLECT: detach FIRST to free the session, then fetch
547
- mu agent close worker-1 -w big
548
- git fetch "ssh://dev/~/ws/worker-1" HEAD && git cherry-pick FETCH_HEAD
549
-
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'"
552
- ```
553
-
554
- The pi keeps serving its socket at the remote path while detached; each
555
- attach re-creates the local end of the forward.
556
-
557
- Two consequences, both counterintuitive:
558
-
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.
594
-
595
- ```bash
596
- et dev # or your site's wrapper, e.g. `x2ssh -et dev`
597
- ```
598
-
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.