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