@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,2738 +0,0 @@
1
- # mu — Usage Guide
2
-
3
- A practical, copy-pasteable tour of mu. Terms are canonical — see
4
- [VOCABULARY.md](VOCABULARY.md); the canonical verb list is
5
- `mu --help`, with the gotchas in `## CLI overview` of
6
- [skills/mu/SKILL.md](../skills/mu/SKILL.md).
7
-
8
- > **Status:** 1.0 (pre-release). Typed verbs span 7 namespaces
9
- > (`workstream`, `agent`, `task`, `workspace`, `log`, `me`, `db`) plus
10
- > bare top-level verbs (`state`, `doctor`, `sql`, `undo`, `sync`,
11
- > `rebuild`). Every verb accepts `--json`. Schema v11. See
12
- > [CHANGELOG.md](../CHANGELOG.md).
13
-
14
- **In a hurry? Start at [§ 0. Common scenarios](#0-common-scenarios).**
15
-
16
- *If anything below disagrees with `mu --help`, trust `mu --help`.*
17
-
18
- ---
19
-
20
- ## Table of contents
21
-
22
- 0. [Common scenarios (start here)](#0-common-scenarios)
23
- 1. [Setup](#1-setup)
24
- 2. [Get oriented (`mu doctor`)](#2-get-oriented)
25
- 3. [Create a workstream (`mu workstream init`)](#3-create-a-workstream)
26
- 4. [Plan some work as a DAG (`mu task add`)](#4-plan-some-work-as-a-dag)
27
- 5. [See the graph (dashboard + state API)](#5-see-the-graph-dashboard--state-api)
28
- 5b. [The TUI dashboard (interactive)](#5b-the-tui-dashboard-interactive)
29
- 6. [Spawn a crew (`mu agent spawn`)](#6-spawn-a-crew)
30
- 7. [Watch the crew live (`tmux attach`)](#7-watch-the-crew-live)
31
- 8. [Send work to an agent (`mu agent send`)](#8-send-work-to-an-agent)
32
- 9. [Read what an agent did (`mu agent read`)](#9-read-what-an-agent-did)
33
- 10. [The claim protocol from inside a pane (`mu task claim`)](#10-the-claim-protocol--from-inside-an-agents-pane)
34
- 11. [Drop notes (durable context) (`mu task note`)](#11-drop-notes-durable-context)
35
- 12. [Close out a task](#12-close-out-a-task)
36
- 13. [The SQL escape hatch (`mu sql`)](#13-the-sql-escape-hatch-is-your-friend)
37
- 14. [Recovery scenarios](#14-recovery-scenarios)
38
- 15. [Cleanup](#15-cleanup)
39
- 15.6. [Multi-machine sync](#156-multi-machine-sync)
40
- 16. [One-shot demo script](#16-one-shot-demo-script)
41
- 17. [Mental model in three sentences](#mental-model-in-three-sentences)
42
- 18. [What's NOT in mu](#whats-not-in-mu-and-how-to-work-around-it)
43
- 19. [Where to go from here](#where-to-go-from-here)
44
- 20. [Multiplexer backends (tmux and herdr)](#20-multiplexer-backends-tmux-and-herdr)
45
-
46
- ---
47
-
48
- ## 0. Common scenarios
49
-
50
- Six worked paths, every command run live. Output is verbatim (colour
51
- stripped). Set `MU_DB_PATH=/tmp/mu-play.db` first if you want to try
52
- them without touching your real registry.
53
-
54
- ### 0.1 First 5 minutes
55
-
56
- Init a workstream, plan two tasks with a dependency, see what's ready.
57
-
58
- ```bash
59
- mu workstream init auth
60
- mu task add design -w auth --title "Design auth module" --impact 80 --effort-days 2
61
- mu task add build -w auth --title "Build auth module" --impact 80 --effort-days 5 --blocked-by design
62
- mu task next -w auth
63
- mu task tree build -w auth
64
- ```
65
-
66
- ```
67
- Created workstream auth (mux session mu-auth)
68
- Next:
69
- Attach the session : tmux attach -t mu-auth
70
- Plan tasks : mu task add -w auth --title "..." --impact 50 --effort-days 1
71
- Spawn an agent : mu agent spawn <name> -w auth
72
- See state : mu state -w auth
73
-
74
- Added task design (workstream=auth, impact=80, effort=2)
75
-
76
- Added task build (workstream=auth, impact=80, effort=5)
77
- blocked by: design
78
-
79
- ┌────────┬────────┬────────────────────┬────────┬────────┬──────┬───────┐
80
- │ name │ status │ title │ impact │ effort │ ROI │ owner │
81
- ├────────┼────────┼────────────────────┼────────┼────────┼──────┼───────┤
82
- │ design │ OPEN │ Design auth module │ 80 │ 2 │ 40.0 │ — │
83
- └────────┴────────┴────────────────────┴────────┴────────┴──────┴───────┘
84
-
85
- Tree of build (blockers below; --down for dependents)
86
- build OPEN Build auth module
87
- └── design OPEN Design auth module
88
- ```
89
-
90
- `build` is absent from `mu task next` because `design` blocks it.
91
- Close `design` and `build` becomes ready. Details: [§ 3](#3-create-a-workstream),
92
- [§ 4](#4-plan-some-work-as-a-dag).
93
-
94
- ### 0.2 The dispatch loop: spawn, claim, send, wait, close
95
-
96
- ```bash
97
- mu agent spawn worker-1 -w auth --cli sh # --cli pi for a real agent
98
- mu task claim design -w auth --for worker-1
99
- mu agent send worker-1 -w auth 'Design the auth module, then: mu task close design --evidence "..."'
100
- mu task wait design -w auth --timeout 60 --on-stall exit # blocks until CLOSED
101
- mu agent close worker-1 -w auth
102
- ```
103
-
104
- ```
105
- Spawned worker-1 (sh) in window worker-1 of mu-auth, pane %88
106
-
107
- Claimed design for worker-1 (OPEN → IN_PROGRESS)
108
- Next:
109
- Drop a note (single-quote to defer shell expansion) : mu task note design 'FILES: ...\nDECISION: ...' -w auth
110
- Close with grounding : mu task close design --evidence "..." -w auth
111
- Release if blocked : mu task release design -w auth
112
-
113
- sent 23 bytes to worker-1
114
-
115
- all-of 1 reached CLOSED in 4094ms
116
- ✓ design (CLOSED)
117
-
118
- Closed worker-1
119
- ```
120
-
121
- Exit codes from `mu task wait`: `0` met, `5` timeout, `6` the owning
122
- pane died, `7` stalled (with `--on-stall exit`). See
123
- [§ Wait exit codes](#wait-exit-codes-mu-task-wait).
124
-
125
- `mu agent send` verifies delivery. If it prints `warning: ... was NOT
126
- submitted`, the text is sitting unsubmitted in the pane — the agent has
127
- not seen it. That is the failure mode `exit 0` used to hide; check
128
- before you wait on the task.
129
-
130
- ### 0.3 Laptop ↔ devserver
131
-
132
- Same `MU_SYNC_DIR` on both machines. No export step, no import step, no
133
- sync verb in the loop.
134
-
135
- ```bash
136
- # both machines, in your shell rc
137
- export MU_SYNC_DIR=$HOME/Sync/mu
138
-
139
- # laptop
140
- mu workstream init app
141
- mu task add auth_fix -w app -t "Fix the auth redirect" -i 80 -e 2
142
-
143
- # devserver — the ordinary command already ingested it
144
- mu task list -w app
145
- mu task close auth_fix -w app --evidence "shipped in #412"
146
-
147
- # laptop
148
- mu task show auth_fix -w app
149
- ```
150
-
151
- ```
152
- ┌──────────┬────────┬───────────────────────┬────────┬────────┬──────┬───────┐
153
- │ name │ status │ title │ impact │ effort │ ROI │ owner │
154
- ├──────────┼────────┼───────────────────────┼────────┼────────┼──────┼───────┤
155
- │ auth_fix │ OPEN │ Fix the auth redirect │ 80 │ 2 │ 40.0 │ — │
156
- └──────────┴────────┴───────────────────────┴────────┴────────┴──────┴───────┘
157
-
158
- Closed auth_fix (OPEN → CLOSED)
159
- evidence: shipped in #412
160
-
161
- auth_fix — Fix the auth redirect
162
- workstream : app
163
- status : CLOSED
164
- ...
165
- Notes (1)
166
- 2026-08-02T05:49:15.361Z worker-2
167
- CLOSE: shipped in #412
168
- ```
169
-
170
- **Both machines edit at once.** Merges are per-FIELD, so different
171
- fields of the same task both survive:
172
-
173
- ```bash
174
- # laptop # devserver
175
- mu task update t1 -w app --impact 95 mu task close t1 -w app
176
- ```
177
-
178
- ```
179
- # both machines, after both have synced:
180
- t1 — Concurrent demo
181
- status : CLOSED
182
- impact : 95
183
- ```
184
-
185
- Same field on both machines: the newer HLC wins, silently. Peer status
186
- is `mu sync`; a torn transfer is `mu sync --repair <peer-prefix>`. Full
187
- section: [§ 15.6](#156-multi-machine-sync).
188
-
189
- ### 0.4 I made a mistake
190
-
191
- Every user-visible action is one **group** of ops. Find it, preview,
192
- apply.
193
-
194
- ```bash
195
- mu task update build -w auth --impact 5 # oops
196
- mu undo # what can I undo?
197
- mu undo 6380dd3d # preview (dry run)
198
- mu undo 6380dd3d --yes # apply
199
- ```
200
-
201
- ```
202
- mu undo — most recent undoable action:
203
- 6380dd3d task.update — 1 op
204
-
205
- recent groups (newest first):
206
- * 6380dd3d task.update — 1 op
207
- 1abb5038 task.add — 2 ops
208
- 4f93f1df task.add — 1 op
209
- 70af8fb8 workstream.init — 1 op
210
-
211
- mu undo 6380dd3d — would revert task.update
212
- restore task auth/build impact=80 updated_at=2026-08-02T05:49:34.545Z
213
-
214
- (dry-run; rerun with --yes to apply)
215
-
216
- Undid 6380dd3d (task.update)
217
- task auth/build impact=80 updated_at=2026-08-02T05:49:34.545Z
218
- 1 row change(s), recorded as group 3814ed17
219
- This undo is itself undoable: mu undo 3814ed17 --yes
220
- ```
221
-
222
- The undo is itself a group — `mu undo 3814ed17 --yes` is redo. Any
223
- unique prefix of a group id works. Undo reverts ROWS: killed panes and
224
- freed workspace dirs do not come back. See
225
- [§ Undoing one action](#undoing-one-action-mu-undo).
226
-
227
- ### 0.5 Something looks wrong
228
-
229
- ```bash
230
- mu doctor # exit 0 healthy, 5 on drift
231
- mu doctor --deep # full rebuild + field diff; slower
232
- mu doctor --disk # + per-workspace byte usage (walks every checkout)
233
- ```
234
-
235
- ```
236
- mu doctor
237
-
238
- environment
239
- tmux : ok (tmux 3.7b)
240
- $TMUX : set
241
- $TMUX_PANE : %21
242
- $MU_SESSION : auth
243
- agent state : ok agent state from murmur 1.0.0
244
-
245
- db
246
- path : /tmp/mu-play.db
247
- schema : ok (10 tables)
248
- schema_version : 10
249
- journal_mode : wal
250
- foreign_keys : on
251
-
252
- workstream
253
- current : auth
254
-
255
- state (workstream=auth)
256
- agents : 0
257
- tasks : 2 (ready 1, blocked 1, in-progress 0)
258
- ops rows : 1
259
- ghosts : none
260
- orphan panes : 0
261
-
262
- fleet
263
- db-vs-sync : ok MU_SYNC_DIR not set (no sync configured)
264
- db-filesystem : ok DB is on local (0x1021994)
265
- name-case : ok no case-colliding workstream names
266
-
267
- housekeeping
268
- ws-dormant : note 2 dormant workstream(s): 2 with open tasks
269
-
270
- Idle 60+ days but still holding UNCLOSED tasks — look before tearing down:
271
- infer-rs 95d idle, 19 of 202 unclosed
272
- modelbridge 89d idle, 8 of 75 unclosed
273
-
274
- Removes the task rows and notes — the plan, not the code (no checkout
275
- is touched, and `mu undo <group>` puts the rows back). The risk is
276
- losing sight of what was left to do, so read it first:
277
- mu task list -w infer-rs --status OPEN
278
-
279
- disk
280
- ws-rows : ok every workspace row has its dir
281
- ws-dirs : ok no orphan workspace dirs
282
- ws-empty : ok no empty workstream dirs
283
- db-copies : WARN 7 stray DB file(s), 49M — nothing reads these
284
- exports : ok no leftover export dirs
285
- locks : ok no stale lock dirs
286
- (run `mu doctor --disk` for per-workspace byte usage — walks every checkout)
287
-
288
- ops log
289
- drift (shallow) : ok every live row has ops (1ms) — run `mu doctor --deep` for the full rebuild diff
290
- ```
291
-
292
- The `disk` section is the only one that reads the filesystem, and it
293
- reconciles **both directions** — the two can disagree either way:
294
-
295
- | Row | Means | Why it matters |
296
- | --- | --- | --- |
297
- | `ws-rows` | a `vcs_workspaces` row whose path is gone | every read surface reports it as healthy; the next send fails inside the VCS backend instead of at the row that lied. Usually a hand-run `rm -rf` where `mu workspace free` was wanted |
298
- | `ws-dirs` | a workspace dir with no row (**workspace orphan**) | blocks the next `--workspace` spawn for that agent name. `stranded` = the workstream row is gone too, so no workstream-scoped verb can reach it |
299
- | `ws-empty` | a per-workstream dir holding no checkouts | `ok` severity — `workspace free` leaves the parent behind. Reported, not faulted |
300
- | `db-copies` | `mu.db*` files that aren't the live WAL triple | hand-made copies and pre-upgrade saves. Nothing reads them, nothing prunes them, and they are usually the largest dead-byte category |
301
- | `exports` | leftovers from the removed `mu workstream export` <!-- doc-cli-drift:skip --> | 1.0 deleted the verb, not its output directory |
302
- | `locks` | advisory-lock dirs older than 1h | not a deadlock (`withFileLock` steals at 30s) but evidence of a spawn or flush that died mid-critical-section |
303
-
304
- **Report-only, always.** Every row names its own cleanup command in the
305
- remediation block and mu runs none of them. An orphan or stranded dir
306
- may hold the only copy of uncommitted work, so the decision is yours (or
307
- your agent's) — a diagnostic that deleted checkouts because a readdir
308
- raced a spawn would be a worse bug than the residue.
309
-
310
- ### Which workstreams can I tear down? (`housekeeping`)
311
-
312
- `mu workstream list` prints row counts and no dates, so the workstream
313
- you finished in June looks exactly like the one you worked on an hour
314
- ago. The `housekeeping` section answers "what can go" — and splits it
315
- in two, because one list would give actively bad advice:
316
-
317
- | Bucket | Test | Advice |
318
- | --- | --- | --- |
319
- | **finished** | every task `CLOSED`, idle ≥ 14 days | safe to tear down; the remediation is the `teardown` command |
320
- | **abandoned** | idle ≥ 60 days *with* unclosed tasks | look first; the remediation lists the open tasks, because those are the record of what was left to do |
321
-
322
- The thresholds differ on purpose. A fortnight away from a project is a
323
- holiday, not abandonment, so anything still holding open tasks has to be
324
- cold for two months before it is mentioned at all. A closed-out
325
- workstream is not suspicious — that is the success state — so it earns
326
- its row by the work also being cold.
327
-
328
- Excluded: the workstream you are in right now, `scratch` (ephemeral by
329
- design), anything with a live agent or a registered workspace, and
330
- task-less workstreams (those are `mu workstream teardown --empty`'s job,
331
- which sweeps test litter — zero tasks, agents and workspaces — and never
332
- matches a workstream with real history).
333
-
334
- Neither bucket is a data-loss warning. Teardown removes **task rows and
335
- notes — the plan, not the code**: no commit, branch or checkout is
336
- touched, and `mu undo <group>` puts the rows back. (That claim holds
337
- because this list excludes anything with a registered workspace;
338
- teardown *does* free real checkouts when they exist.) The reason to look
339
- before tearing down an abandoned workstream is that its unclosed tasks
340
- are the only record of what was still outstanding — lose sight of them
341
- and you re-derive that context from scratch later.
342
-
343
- Severity is always `ok`: a tidy-up opportunity is not a fault, so this
344
- section never makes `mu doctor` claim something needs attention.
345
- Teardown is reversible — tombstone ops are written, so `mu undo <group>`
346
- puts the rows back and `mu workstream list --torn-down` finds the group
347
- months later.
348
-
349
- `--disk` adds recursive byte accounting per checkout, with the orphan
350
- share called out as reclaimable:
351
-
352
- ```
353
- workspace bytes : 758M across 4 checkout(s) — 212M in orphan dirs
354
- 212M auth-peers/worker-2
355
- 212M auth-peers/worker-1
356
- 167M leanhl-runner/worker-1
357
- 167M leanhl-runner/worker-2
358
- ```
359
-
360
- It is a separate flag because its cost scales with the size of your
361
- checkouts rather than with mu's own state (~2s for 758M here), and
362
- `mu doctor` has to stay cheap enough to run reflexively. The
363
- reconciliation rows above always run: they are `readdir` + `stat` only,
364
- depth 2, ~1ms.
365
-
366
- **On drift**, `--deep` names the exact table, key and field:
367
-
368
- ```
369
- ops log
370
- drift : FAIL 1 divergence(s) (16ms)
371
- tasks auth/build.title: live=TAMPERED log=Build auth module
372
- ```
373
-
374
- Do not rebuild reflexively — which side is right depends on the cause.
375
- In order:
376
-
377
- ```bash
378
- mu db backup /tmp/mu-drift-evidence.db # 1. preserve the evidence
379
- mu rebuild /tmp/mu-rebuilt.db # 2. materialize what the log believes
380
- MU_DB_PATH=/tmp/mu-rebuilt.db mu sql "SELECT local_id, title FROM tasks" # 3. compare
381
- ```
382
-
383
- Then report it: drift is a capture/apply bug, and the named
384
- table/key/field is the reproduction. See
385
- [§ What to do when drift is reported](#what-to-do-when-drift-is-reported).
386
-
387
- ### 0.6 Upgrading an older DB
388
-
389
- `mu` refuses to open a pre-v11 DB (`SchemaTooOldError`, exit 4) or a DB
390
- newer than it understands (`SchemaTooNewError`, exit 4) and leaves the
391
- file alone. The retained `scripts/migrate.ts` sidecar auto-detects v7,
392
- v8, v9, v10, or v11 and writes a fresh v11 DB. It never migrates in place.
393
- Legacy `REJECTED` tasks become `CLOSED/rejected` and `DEFERRED` tasks
394
- become `OPEN/parked`; their original op payloads remain unchanged.
395
- Pre-1.0 archives restore as live workstreams under their original
396
- `source_workstream` names unless you pass `--drop-archives`.
397
-
398
- Full backup, migration, verification, and swap recipe:
399
- [scripts/README.md](../scripts/README.md).
400
-
401
- ---
402
-
403
- ## 1. Setup
404
-
405
- From npm (the common path):
406
-
407
- ```bash
408
- npm install -g @mu-crew/mu
409
- mu --version # → the current version
410
- ```
411
-
412
- Update later via `npm install -g @mu-crew/mu@latest`.
413
-
414
- From a local checkout (when hacking on mu itself):
415
-
416
- ```bash
417
- npm install -g . # `prepare` script auto-builds; `mu` lands on $PATH
418
- mu --version # → the current version (see package.json)
419
- ```
420
-
421
- To update the source-installed copy: pull from upstream, then
422
- `npm install -g .` from inside the checkout. The `prepare` script
423
- rebuilds before linking the new dist/.
424
-
425
- ### Link pi (`mu link pi`)
426
-
427
- For pi agents, one verb installs both pieces mu needs:
428
-
429
- ```bash
430
- mu link pi # extension shim + skill symlink; then restart pi (or /reload)
431
- mu link pi --copy # inline the extension instead (pinned; relink after upgrades)
432
- ```
433
-
434
- - `~/.pi/agent/extensions/mu.ts` — a one-line shim that re-exports the
435
- installed `dist/extension/mu-pi.js`, so `npm install -g` upgrades the
436
- extension with no relink.
437
- - `~/.agents/skills/mu` — a symlink to the package's `skills/mu`.
438
-
439
- mu refuses to replace a real directory at the skill path (exit 4), and
440
- replaces a symlink pointing elsewhere (e.g. a dev checkout) only with
441
- `--force`. `--extension-only` / `--skill-only` install one piece.
442
- `MU_PI_HOME` overrides the home directory both paths are rooted at.
443
-
444
- ### Install the bundled skill (other agents)
445
-
446
- Mu ships a skill at `skills/mu/SKILL.md` that teaches the LLM running
447
- inside an agent pane how to use mu. The canonical install path is the
448
- [skills CLI](https://github.com/vercel-labs/skills), which auto-detects
449
- every supported agent (pi, claude-code, codex, opencode, cursor, ...)
450
- and installs into the right per-agent location:
451
-
452
- ```bash
453
- npx skills add mu-crew/mu # interactive: pick scope + agents
454
- npx skills add mu-crew/mu -g -y # global, no prompts (pi: ~/.pi/agent/skills/mu/)
455
- npx skills update mu # later, to refresh
456
- ```
457
-
458
- Hacking on the skill itself? Point the skills CLI at your checkout so
459
- edits flow straight through:
460
-
461
- ```bash
462
- npx skills add ./skills/mu # local-path source format (symlinks)
463
- ```
464
-
465
- Without the skills CLI, the skill is just a directory with a
466
- `SKILL.md` — symlink or copy it into the agent's skills dir. For pi:
467
- `~/.pi/agent/skills/mu/` (global) or `.pi/skills/mu/` (per-project).
468
- The cross-tool `~/.agents/skills/mu/` is picked up by pi and several
469
- others:
470
-
471
- ```bash
472
- # From an npm-global install
473
- mkdir -p ~/.agents/skills
474
- ln -sf "$(npm root -g)/@mu-crew/mu/skills/mu" ~/.agents/skills/mu
475
-
476
- # Or from a checkout
477
- ln -sf "$PWD/skills/mu" ~/.agents/skills/mu
478
- ```
479
-
480
- ### For mu hackers: alias to the build output
481
-
482
- Fastest iteration when hacking on mu itself:
483
-
484
- ```bash
485
- npm install # deps only
486
- npm run build # produces dist/
487
- alias mu="node $PWD/dist/cli.js"
488
- ```
489
-
490
- See [README.md § Install](../README.md#install) for the full set of
491
- install patterns.
492
-
493
- mu requires a terminal multiplexer. tmux ≥ 3.0 is the backend the rest
494
- of this guide assumes; herdr also supports spawn, send, and read (see
495
- [§ 20](#20-multiplexer-backends-tmux-and-herdr)). On tmux, install
496
- murmur for runtime agent state. Make sure you're inside a session before
497
- proceeding:
498
-
499
- ```bash
500
- tmux # if you're not already in one
501
- ```
502
-
503
- ---
504
-
505
- ## 2. Get oriented
506
-
507
- For a human at a terminal, bare `mu` is the home base: it launches the
508
- read-only TUI with every workstream on the machine loaded as tabs.
509
- Initial tab focus ladder: `$MU_SESSION` naming a loaded workstream →
510
- current tmux session named `mu-<workstream>` → cwd inside a registered
511
- workspace → cwd at the VCS-derived project root of any loaded
512
- workstream (newest activity wins ties) → tab 0. With no workstream yet
513
- it prints help plus the start command:
514
-
515
- ```bash
516
- mu
517
- # Get started: mu workstream init <name>
518
- ```
519
-
520
- For scripts, agents, CI, and pipes, bare `mu` does NOT enter the TUI:
521
- when stdout is not a TTY it prints `mu --help`. Use explicit typed
522
- verbs and `--json` for the API surface:
523
-
524
- ```bash
525
- mu state -w <workstream> --json
526
- MU_NO_TUI=1 mu # force the non-TTY/help path even in a terminal
527
- ```
528
-
529
- Run `mu doctor` to check tmux + DB health — full annotated output in
530
- [§ 0.5 Something looks wrong](#05-something-looks-wrong). Three families
531
- of checks are the ones to know.
532
-
533
- **Mixed-fleet hazards** (the `fleet` section) — cheap, and every one is
534
- something you can fix before it costs you data:
535
-
536
- | Row | Means |
537
- | --- | --- |
538
- | `db-vs-sync` | **FAIL** if `MU_DB_PATH` is inside `MU_SYNC_DIR`. Never do this: a live WAL-mode SQLite DB is three files (`mu.db`, `-wal`, `-shm`) whose mutual consistency IS its durability, and a file-syncer copying them out of order — or resurrecting a peer's stale `-wal` — silently corrupts the database. mu syncs append-only per-machine **segments** so the DB file never has to travel. |
539
- | `db-filesystem` | **WARN** if the DB is on NFS/SMB/sshfs. WAL needs working advisory locks and a shared-memory file; network mounts provide neither reliably. Symptom is `database is locked` with no contention, or corruption with it. |
540
- | `name-case` | **WARN** if two workstream names differ only by case. They coexist on Linux but collide on macOS (APFS) and Windows, and a workstream name IS a tmux session name and seeds workspace paths — so a Mac joining the fleet sees one session where Linux sees two. |
541
-
542
- **Disk↔DB reconciliation** (the `disk` section) — the state dir and the
543
- database can disagree in both directions, and mu had no surface for
544
- either before. Row-by-row table in
545
- [§ 0.5](#05-something-looks-wrong); the short version is `ws-rows` for a
546
- row whose directory is gone, `ws-dirs` for a directory with no row, and
547
- `db-copies` / `exports` / `locks` for bytes nothing references. Always
548
- report-only — the remediation block names the command, you or your agent
549
- run it. `--disk` adds per-checkout byte usage.
550
-
551
- **Ops-log drift** (the `ops log` section) — is the projection still
552
- faithful to the log? Undo, sync and history are all
553
- projections of the ops log, so a capture bug breaks all four at once,
554
- silently. Two tiers:
555
-
556
- ```bash
557
- mu doctor # shallow: every live row must have >=1 op. ~3ms.
558
- mu doctor --deep # full rebuild + field-level diff. ~0.6ms per op.
559
- ```
560
-
561
- The default is shallow so `mu doctor` stays reflexive; the deep check
562
- rebuilds the whole log (~2.3s on a 1000-task DB). Shallow catches an
563
- uncaptured INSERT or DELETE and is blind to an uncaptured UPDATE (the
564
- row's key still has ops). Run `--deep` when you suspect something, in
565
- CI, or before a release. Either tier exits **5** on drift.
566
-
567
- ### What to do when drift is reported
568
-
569
- Drift names the exact table, key and field, e.g.:
570
-
571
- ```
572
- drift : FAIL 2 divergence(s) (12ms)
573
- tasks demo/a.title: live=TAMPERED log=A
574
- tasks demo/a.impact: live=7 log=60
575
- ```
576
-
577
- **Do not rebuild reflexively.** Drift means the log and the tables
578
- disagree, and which side is right depends on the cause:
579
-
580
- - If **capture** missed a mutation, the LIVE tables hold real work the
581
- log never recorded — rebuilding would discard it.
582
- - If **apply** is lossy, the log is authoritative and a rebuild fixes it.
583
-
584
- So, in order:
585
-
586
- ```bash
587
- mu db backup /tmp/mu-drift-evidence.db # 1. preserve the evidence FIRST
588
- mu rebuild /tmp/mu-rebuilt.db # 2. materialize what the log believes
589
- # 3. compare the named keys in both files and decide which side is correct:
590
- MU_DB_PATH=/tmp/mu-rebuilt.db mu sql "SELECT local_id, title, impact FROM tasks"
591
- ```
592
-
593
- Then report it. Drift is a capture/apply **bug**, not operator error, and
594
- the named table/key/field is the reproduction.
595
-
596
- ### Undoing one action (`mu undo`)
597
-
598
- Undo emits **inverse ops** for one **group** (one user-visible action).
599
-
600
- ```bash
601
- mu undo # what would I undo? lists recent groups + ids
602
- mu undo 1a2a94eb # preview the inverse (dry run)
603
- mu undo 1a2a94eb --yes # apply it
604
- ```
605
-
606
- Three properties:
607
-
608
- - **Granular.** Only the rows that action touched. A cascade close wrote
609
- N task ops under one group, so undoing it reopens exactly those N.
610
- - **Itself an op.** The undo lands in its own group, so it syncs to peers
611
- and is **itself undoable** — that is redo:
612
- `mu undo <the-undo-group> --yes`.
613
- - **Refuses to clobber newer work.** If a later action changed the same
614
- fields, undo exits 4 and names the conflict:
615
-
616
- ```
617
- WARNING: this group has been SUPERSEDED by later work.
618
- demo/a: impact was changed since by e04df0d5 (task.update)
619
- Undoing would DISCARD that newer work. Pass --force with --yes to do it anyway.
620
- ```
621
-
622
- `--force` is the override, and it says what it destroys.
623
-
624
- Rows only: undo does not resurrect killed tmux panes or freed workspace
625
- directories. For whole-DB recovery from the log, use `mu rebuild <file>`.
626
-
627
- Get the full command list:
628
-
629
- ```bash
630
- mu --help
631
- ```
632
-
633
- Every verb's `--help` is exhaustive (flags, defaults,
634
- interactions). Every successful invocation also prints a dim
635
- `Next:` block of suggested follow-up commands at the bottom —
636
- you never have to leave the terminal to learn what to do next.
637
-
638
- Every verb accepts `--json` for machine-readable output. Errors
639
- in `--json` mode emit a `{ error, message, nextSteps, exitCode }`
640
- record to stderr; the `nextSteps` array carries resolutions you can
641
- `eval` directly.
642
-
643
- ### CLI conventions: validation errors
644
-
645
- Every operator-error path — missing required option, unknown option,
646
- unknown subcommand, missing positional, type-coercion failure, mutex
647
- flags, range checks — produces a uniform surface:
648
-
649
- - **Human path**: red `error: <msg>` on stderr, then the failing
650
- subcommand's `--help` block (same text as `mu <verb> --help`),
651
- then exit **2**.
652
- - **`--json` path**: a structured envelope on stderr:
653
-
654
- ```json
655
- {
656
- "error": "UsageError",
657
- "message": "--self and --for are mutually exclusive",
658
- "nextSteps": [],
659
- "exitCode": 2,
660
- "usage": {
661
- "command": "mu task claim",
662
- "synopsis": "mu task claim [options] <id>",
663
- "description": "...",
664
- "args": [{"name": "id", "required": true, "variadic": false, "description": ""}],
665
- "options": [{"flags": "--self", "description": "...", "mandatory": false, "valueRequired": false}, ...]
666
- }
667
- }
668
- ```
669
-
670
- `usage.options[].mandatory` is `true` when the operator MUST pass
671
- the option (`.requiredOption()` in commander terms). `valueRequired`
672
- is `true` when the option's argument can't be omitted if the flag
673
- IS passed (`<value>` form vs bare flag). The two are independent.
674
-
675
- Exit 2 is the consistent code for the whole operator-error class —
676
- commander mistakes and handler-thrown `UsageError`s alike. Other
677
- classes keep their own codes (3 = not found, 4 = conflict, 5 =
678
- substrate, 6 = reaper, 7 = stall).
679
-
680
- ### CLI conventions: `--json` collection envelope
681
-
682
- Collection-read verbs emit a canonical `{items: T[], count: number}`
683
- shape on stdout:
684
-
685
- ```bash
686
- $ mu task list -w foo --json
687
- {"items":[{"name":"a",...},{"name":"b",...}],"count":2}
688
- ```
689
-
690
- `count` is `items.length` pre-computed so `jq '.count'` is one less hop
691
- than `jq '.items | length'`. `mu workspace commits --json` adds `vcs`,
692
- `baseRef`, and `workspacePath` siblings.
693
-
694
- Applies to: `mu task list / next / owned-by / notes`,
695
- `mu workstream list`, `mu workstream teardown --empty` (dry-run),
696
- `mu workspace list / orphans / commits`,
697
- `mu undo` (group list), `mu log -n N` (read).
698
-
699
- Two carve-outs:
700
- - **`mu sql --json`** keeps bare-array rows. The verb is the typed-
701
- escape hatch; row shape is per-query, not part of the typed
702
- contract.
703
- - **`mu log --tail --json`** emits NDJSON (one JSON object per line)
704
- because it's a stream, not a collection. Stream consumers want one
705
- envelope per row, not a single envelope that grows forever.
706
-
707
- Singleton verbs (`mu task show`, `mu agent show`, `mu workstream init`,
708
- `mu task close`, ...) use object envelopes with named top-level fields
709
- (`{task, blockers, dependents, notes}`, `{taskName, ..., nextSteps}`).
710
- `items + count` is for collection reads only.
711
-
712
- ### CLI conventions: multi-value flags
713
-
714
- Multi-value flags accept repeated invocations
715
- (`--blocked-by a --blocked-by b`), a comma-separated value
716
- (`--blocked-by a,b`), or any mix. The signal is `<value...>` in the
717
- help-text metavar. Variadic positionals (`mu task wait a b c`) stay
718
- space-separated — operands are not commas, and single-valued flags
719
- (`-w`, `--title`) stay single.
720
-
721
- - `--by` on `mu task block` / `mu task unblock` is multi-value:
722
- `mu task block c --by a,b` adds both edges in one call.
723
- - `--status` on `mu task list` / `mu task next` is multi-value and
724
- returns the union. Omitting it applies no filter.
725
- - `mu task wait --status` stays single — "wait until reaches THIS
726
- status".
727
-
728
- **Empty vs blank fragments.** An *empty* fragment is dropped: a
729
- trailing or doubled comma is a typing artifact, so `--status "OPEN,"`
730
- means `[OPEN]`, and `mu task reparent --blocked-by ''` is the
731
- documented "clear every blocker" sentinel. A *blank* fragment — one
732
- that is entirely whitespace, like `--status " "` or the tail of
733
- `--status "OPEN, "` — is a usage error (exit 2) naming the flag. Nobody
734
- means "filter by the space character", and dropping it silently would
735
- return a different answer than the one typed. Same rule for `-w`,
736
- `--by`, and `--blocked-by`; see docs/VOCABULARY.md § Empty vs blank
737
- flag fragments.
738
-
739
- ### CLI conventions: flag vs positional
740
-
741
- The rule the CLI follows:
742
-
743
- > **The primary entity a verb acts on is POSITIONAL. Everything
744
- > else — scoping, modifiers, payload — is a flag.**
745
-
746
- So `mu task close <id>`, `mu agent send <name> <text>`,
747
- `mu workstream init <name>`. The workstream
748
- is a *scope* for most verbs (`-w`) — but for `mu workstream <verb>` it
749
- IS the primary entity, so it may be positional:
750
-
751
- ```bash
752
- mu workstream init v2 # positional
753
- mu workstream teardown v2 --yes # positional (aliases -w)
754
- mu workstream teardown -w v2 --yes # -w still works
755
- ```
756
-
757
- Passing both a positional and a disagreeing `-w` is a usage error
758
- (exit 2), not a silent pick-one.
759
-
760
- Where sibling verbs supply a payload two ways, the flag form is an
761
- ADDITIVE alias so muscle memory carries across:
762
-
763
- ```bash
764
- mu task add t -t T -i 5 -e 1 --note "context" # note as a flag
765
- mu task note t "more context" # positional
766
- mu task note t --text "more context" # --text alias
767
- ```
768
-
769
- Supplying both shapes at once is a usage error (exit 2).
770
-
771
- ---
772
-
773
- ## 3. Create a workstream
774
-
775
- A **workstream** is mu's unit of organization. One workstream = one
776
- tmux session = one logical project. Multiple workstreams on the same
777
- machine are isolated (partitioned in the SQLite registry by
778
- `session_id`); they never see each other's agents.
779
-
780
- ```bash
781
- mu workstream init auth-refactor
782
- ```
783
-
784
- ```
785
- Created workstream auth-refactor (mux session mu-auth-refactor)
786
- Next:
787
- Attach the session : tmux attach -t mu-auth-refactor
788
- Plan tasks : mu task add -w auth-refactor --title "..." --impact 50 --effort-days 1
789
- Spawn an agent : mu agent spawn <name> -w auth-refactor
790
- See state : mu state -w auth-refactor
791
- ```
792
-
793
- Behind the scenes: `tmux new-session -d -s mu-auth-refactor` plus a
794
- placeholder window so the session is non-empty. It sits detached,
795
- waiting for agents.
796
-
797
- To see what's already on the machine before picking a name:
798
-
799
- ```bash
800
- mu workstream list
801
- ```
802
-
803
- ```
804
- ┌────────────────────────────────────────┬───────┬────────┬───────┬───────┬───────┐
805
- │ name │ tmux │ agents │ tasks │ edges │ notes │
806
- ├────────────────────────────────────────┼───────┼────────┼───────┼───────┼───────┤
807
- │ auth │ — │ 0 │ 2 │ 1 │ 0 │
808
- ├────────────────────────────────────────┼───────┼────────┼───────┼───────┼───────┤
809
- │ mu │ alive │ 0 │ 0 │ 0 │ 0 │
810
- └────────────────────────────────────────┴───────┴────────┴───────┴───────┴───────┘
811
- ```
812
-
813
- The list is the **union** of three sources: distinct workstreams in
814
- `agents`, in `tasks`, and tmux sessions matching `mu-*`. A freshly
815
- `init`'d workstream with no tasks shows up via its session; one whose
816
- session was killed externally shows up via its surviving DB rows, so
817
- you can still `mu workstream teardown` it.
818
-
819
- ### How mu finds your active workstream
820
-
821
- Every command after `init` needs to know which workstream you're in.
822
- Resolution order, first match wins:
823
-
824
- 1. **`--workstream <name>` flag** explicitly
825
- 2. **`MU_SESSION` env var** (`export MU_SESSION=auth-refactor`)
826
- 3. **Current tmux session name** (mu reads `tmux display-message -p '#S'` and strips the `mu-` prefix)
827
- 4. Error if none of the above
828
-
829
- The third option is the most ergonomic. Once you `tmux attach -t
830
- mu-auth-refactor`, every command "just works" without flags.
831
-
832
- ### Off-the-cuff agents: the `scratch` workstream
833
-
834
- When you just want one helper agent you can keep talking to — not a
835
- crew, not a task DAG, not per-agent workspaces — skip the `init` step
836
- entirely and spawn into the reserved `scratch` workstream:
837
-
838
- ```bash
839
- mu agent spawn helper -w scratch # auto-creates mu-scratch
840
- mu agent send helper 'Investigate the failing test in foo.spec.ts'
841
- mu agent read helper -n 50 # check on it anytime
842
- mu agent close helper -w scratch # done
843
- ```
844
-
845
- `scratch` is a shared, ephemeral bucket: it auto-creates on first
846
- spawn, tasks are optional, and idle scratch agents get a staleness
847
- nudge in `mu state` and the TUI so they don't pile up. The name is
848
- **reserved** — `mu workstream init scratch` is rejected;
849
- `mu workstream teardown scratch` works normally.
850
-
851
- ```
852
- $ mu workstream init scratch
853
- error: workstream name "scratch" is reserved: it is the off-the-cuff bucket and auto-creates on first spawn. Don't 'init' it.
854
- Next:
855
- Just spawn into it (auto-creates) : mu agent spawn <name> -w scratch
856
- Use a durable workstream instead : mu workstream init <name>
857
- ```
858
-
859
- **When to use which:** `mu workstream init <name>` the moment
860
- coordination *is* the work — multiple agents, dependencies, gated
861
- review. `scratch` for a single driveable helper. A **delegate** when
862
- you only need a one-shot answer back: the `mu_delegate` tool (shipped
863
- in the extension `mu link pi` installs) spawns a scratch agent, sends
864
- the task, returns at once, and delivers the answer as a follow-up
865
- message. From a shell it is `mu agent spawn -w scratch` + `send` +
866
- `mu agent wait --json` (see [`mu agent wait`](#mu-agent-wait-the-task-less-counterpart-to-mu-task-wait)).
867
- Unlike a hidden subagent, the delegate's pane stays attachable: watch
868
- it, steer it, abort it, or pass `keep: true` and keep talking to it.
869
- Each delegate costs one pane and one pi process.
870
-
871
- ### The log ledger: durable watcher dedupe + memory
872
-
873
- A watcher loop reacting to external state — a PR's CI status, a queue
874
- depth, a file's mtime — must remember *what it last saw* so it doesn't
875
- act twice. Don't keep that in the agent's chat context: it evaporates
876
- on compaction and dies with the loop. Use a custom `--kind` tag on the
877
- activity log as an append-only **ledger**:
878
-
879
- ```bash
880
- # Watcher tick: record what you saw and what you did about it.
881
- mu log -w scratch --kind pr-state 'pr=1234 sha=abc ci=red -> spawned fixer-1'
882
-
883
- # Next tick: reconstruct the last-seen state (latest entry of that kind).
884
- mu log -w scratch --kind pr-state -n 1 --json
885
- # {"items":[{"seq":8695,...,"kind":"pr-state",
886
- # "payload":"pr=1234 sha=abc ci=red -> spawned fixer-1"}],"count":1}
887
- ```
888
-
889
- The dedupe rule: read the latest `pr-state`, compare its `sha`/`ci` to
890
- what you observed, act only on a difference. The ledger lives in the
891
- `ops` log, so it survives loop death and context compaction.
892
-
893
- - **Pick a stable `--kind`** per ledger (`pr-state`, `queue-depth`).
894
- `mu log --kind <tag>` filters to it, so ledgers coexist in one
895
- workstream.
896
- - **One line per tick, latest wins.** `-n 1` for the most recent;
897
- `--since <seq>` replays history a dead watcher missed.
898
- - **Free-form payload.** mu doesn't parse it. Ledger lines have no
899
- **intent**, so `mu log` prints them verbatim rather than rendering
900
- them as a verb.
901
-
902
- A *convention*, not a feature — `mu log` already has `--kind`,
903
- `--since`, and `--json`. See **log ledger** in
904
- [VOCABULARY.md](VOCABULARY.md).
905
-
906
- `--kind` is the operator's channel tag; `--intent` filters on what *mu*
907
- recorded (`--intent task.close`). Two axes, two flags.
908
-
909
- ---
910
-
911
- ## 4. Plan some work as a DAG
912
-
913
- Tasks have **mandatory** `impact` (1–100) and `effort-days` (>0).
914
- Edges are blocks-relationships, expressed as **`--blocked-by`** on
915
- `mu task add` (and `mu task reparent`): `--blocked-by design` means
916
- "can't start until `design` closes". Tasks are **scoped to a
917
- workstream**.
918
-
919
- ```bash
920
- # -w can be omitted inside the workstream's tmux session, or with
921
- # $MU_SESSION exported.
922
- mu task add design -w auth-refactor --title "Design auth module" --impact 80 --effort-days 2
923
- mu task add build -w auth-refactor --title "Build auth module" --impact 80 --effort-days 5 --blocked-by design
924
- mu task add review -w auth-refactor --title "Review auth module" --impact 60 --effort-days 1 --blocked-by build
925
- ```
926
-
927
- See [§ 0.1](#01-first-5-minutes) for the output. Add initial context at
928
- creation time with `--note` (shell escapes like `\n` are translated as
929
- in `mu task note`); `--note-author <name>` overrides the actor label:
930
-
931
- ```bash
932
- mu task add bugfix \
933
- --workstream auth-refactor \
934
- --title "Fix token refresh race" \
935
- --impact 70 --effort-days 1 \
936
- --note 'REPRO: login, wait 24h, refresh\nSCOPE: auth refresh only' \
937
- --note-author orchestrator
938
- ```
939
-
940
- Task creation + the initial note are one transaction: if either part
941
- fails, neither lands.
942
-
943
- Ids validate against `/^[a-z][a-z0-9_-]{0,63}$/` and reject duplicates.
944
- `mu task add x --blocked-by y` when `y` already transitively depends on
945
- `x` refuses with a `CycleError`.
946
-
947
- **Task ids are per-workstream unique.** Cross-workstream references use
948
- the qualified form `<workstream>/<id>`. Blocks-edges are always
949
- same-workstream — a blocker outside the target workstream is a
950
- `CrossWorkstreamEdgeError`.
951
-
952
- ### Modeling external dependencies
953
-
954
- When a task is waiting on something outside this repo (an upstream PR
955
- shipping, a vendor releasing v3 of an API, a coworker's review), don't
956
- reach for a new status — add a **placeholder task** for the external
957
- thing and `--blocked-by` it. The DAG already encodes "blocked":
958
-
959
- ```bash
960
- mu task add upstream_react_19_lands -w gchatui-node \
961
- --title "Wait for React 19 release (vendor)" \
962
- --impact 30 --effort-days 0.1
963
- mu task note upstream_react_19_lands -w gchatui-node \
964
- "Tracking https://github.com/facebook/react/issues/...
965
- Last checked: 2026-05-15.
966
- When this lands: bump react in package.json + re-run upgrade tasks."
967
-
968
- mu task add migrate_to_use_action -w gchatui-node \
969
- --title "Migrate ChatInput to React 19 useActionState" \
970
- --impact 60 --effort-days 1 \
971
- --blocked-by upstream_react_19_lands
972
- ```
973
-
974
- When the upstream lands, `mu task close upstream_react_19_lands
975
- --evidence "shipped 2026-08-12"` — the dependent flips from blocked
976
- to ready in the same render. If the upstream gets cancelled, close the
977
- placeholder anyway (`mu task close upstream_react_19_lands --evidence
978
- "cancelled, won't ship"`) and note the reason on each dependent, or
979
- `mu task delete` the placeholder and its dependents' edges if the work
980
- is truly abandoned.
981
-
982
- Why this beats a `BLOCKED` status: the placeholder's notes are the
983
- audit trail, one placeholder blocks N downstream tasks, and the
984
- placeholder's own status carries the detail (`OPEN` = someone external
985
- is on it, a note records a parked reason, `IN_PROGRESS` = you're
986
- chasing it).
987
-
988
- ---
989
-
990
- ## 5. See the graph (dashboard + state API)
991
-
992
- `mu` exposes one logical "what's going on?" view with two renderers:
993
-
994
- | Surface | Use it for |
995
- | -------------------- | --------------------------------------------------------------- |
996
- | **bare `mu`** | A human at a terminal — launches the interactive TUI dashboard. |
997
- | **`mu state --tui`** | Same TUI, explicitly opt-in. Useful in scripts / aliases. |
998
- | **`mu state`** | Static text card. JSON-friendly; pipeable; `watch`-able. |
999
- | **`mu state --json`** | The canonical full snapshot. Agents and scripts read this. |
1000
-
1001
- The interactive surface has its own section:
1002
- [§ 5b. The TUI dashboard](#5b-the-tui-dashboard-interactive), below.
1003
- The rest of this section is the static / JSON contract.
1004
-
1005
- ### Static state card (`mu state`)
1006
-
1007
- For an agent/script or a static capture, use explicit `mu state`:
1008
-
1009
- ```bash
1010
- mu state -w auth-refactor # human-readable card
1011
- mu state -w auth-refactor --json # full snapshot
1012
- mu state --all --json # every workstream on this machine
1013
- ```
1014
-
1015
- The static card carries every section the TUI cards summarize: agents +
1016
- orphans + tracks + ready / in-progress / blocked / recent-closed tasks
1017
- + workspaces + recent events. It also lists **remote workers** recorded by
1018
- exact `REMOTE: <host>:<path>` task-note lines; malformed lines and prose that
1019
- merely mentions `REMOTE:` are ignored.
1020
-
1021
- **JSON shapes**
1022
-
1023
- - single-ws: flat `{ workstreamName, agents, orphans, tracks, ready,
1024
- blocked, inProgress, recentClosed, workspaces, remoteWorkers, recent }`.
1025
- - multi-ws: wrapped `{ workstreams: [{...}, ...] }`.
1026
- - bare `mu --json` prints `--help`; use `mu state --json`.
1027
- - `--tui` is render-only and incompatible with `--json`.
1028
-
1029
- **Multi-workstream**: repeat `-w`, or `--all`. In static mode N≥2
1030
- stacks one card per workstream.
1031
-
1032
- `tmux display-popup -E 'mu state -w X'` works for popup-card use.
1033
-
1034
- ---
1035
-
1036
- ## 5b. The TUI dashboard (interactive)
1037
-
1038
- The interactive TUI is mu's human surface. It is **read-only** —
1039
- every act-intent `y`anks the canonical `mu` command to
1040
- your clipboard so you run mutations from your shell, with one
1041
- documented escape (`t` in git-show drills runs `tuicr` in the project
1042
- root, see below).
1043
-
1044
- ### Launch
1045
-
1046
- ```bash
1047
- mu # bare; opens the TUI when stdout is a TTY
1048
- mu state --tui # explicit; same surface
1049
- mu state --tui -w a,b # restrict to specific workstreams
1050
- mu state --tui --all # all workstreams (default for bare `mu`)
1051
- MU_NO_TUI=1 mu # force the help path even in a TTY
1052
- mu --json # also forces help; pipe `mu state --json`
1053
- ```
1054
-
1055
- Quit with `q` or `Ctrl-C`. The dashboard restores your scrollback
1056
- on exit (alt-screen).
1057
-
1058
- **Initial active tab** is picked from this ladder:
1059
- `$MU_SESSION` → current tmux session name (`mu-<ws>`) → cwd inside a
1060
- registered workspace → cwd at the VCS-derived project root of any
1061
- workstream (newest activity wins ties) → tab 0.
1062
-
1063
- ### Layout: 10 cards, responsive columns
1064
-
1065
- The dashboard renders 10 toggleable cards with rounded borders and
1066
- section headers inset into the top border line (lazygit / btop / k9s
1067
- convention):
1068
-
1069
- | Slot | Card | Toggle | Popup | Content |
1070
- | ---- | ------------- | ------ | --------- | ---------------------------------------------------- |
1071
- | 0 | Commits | `0` | `Shift+0` | Recent project-root commits (git / jj / sl) |
1072
- | 1 | Agents | `1` | `Shift+1` | Active agents + runtime state + cli + role |
1073
- | 2 | Tracks | `2` | `Shift+2` | Parallel tracks (union-find clusters) |
1074
- | 3 | Ready (Tasks) | `3` | `Shift+3` | Ready-to-claim tasks (no open blockers) |
1075
- | 4 | Activity log | `4` | `Shift+4` | Recent ops rendered as prose |
1076
- | 5 | Workspaces | `5` | `Shift+5` | Per-agent VCS workspaces + behind/dirty status |
1077
- | 6 | In-progress | `6` | `Shift+6` | IN_PROGRESS tasks owned by agents |
1078
- | 7 | Blocked | `7` | `Shift+7` | Tasks with at least one open blocker |
1079
- | 8 | Recent | `8` | `Shift+8` | Recently CLOSED tasks |
1080
- | 9 | Doctor | `9` | `Shift+9` | Cheap health checks (schema, ghosts, orphans, …) |
1081
- | — | DAG | — | `g` | Full task DAG forest (keybind-only) |
1082
- | — | All tasks | — | `t` | Sortable / filterable list of every task |
1083
-
1084
- Digit toggles HIDE / SHOW the card on the dashboard; `Shift+digit`
1085
- opens the matching fullscreen popup. **Single-popup invariant**: only
1086
- one popup is visible at a time; `Esc` / `q` returns to the dashboard
1087
- with all toggles + tick rate preserved.
1088
-
1089
- **Responsive layout**: cards stack below 120 cols, then reflow into
1090
- pair-aware 2 / 3 / 4-column layouts at 120 / 180 / 240 cols. Each
1091
- visible card gets a dynamic row budget so a noisy list cannot crowd
1092
- out its siblings; overflow shows as `+N more · Shift+N` inset into
1093
- the bottom border. On short panes the dashboard culls low-priority
1094
- cards (Doctor → Recent → Workspaces → …) and shows
1095
- `+N cards hidden · resize taller`.
1096
-
1097
- Ordering is slot-stable: within each column, non-stream cards go by
1098
- toggle digit ascending; stream cards (Commits, Activity log) trail,
1099
- with slot 0 last.
1100
-
1101
- ### Multi-workstream tabs
1102
-
1103
- With N≥2 workstreams (bare `mu` on a multi-workstream machine, or
1104
- `mu state --tui -w a,b,c`), a tab strip renders above the cards:
1105
-
1106
- ```
1107
- workstreams: ▸ auth-refactor · ui-rewrite · demo (Tab / Shift-Tab)
1108
- ```
1109
-
1110
- - `Tab` cycles forward, `Shift-Tab` backward (suppressed inside popups
1111
- that bind the key locally).
1112
- - The active tab name shows in the status bar next to the tick rate.
1113
- - Cards / popups operate on the active tab — no per-row workstream
1114
- column. For N=1 the strip renders nothing.
1115
- - Wider than the terminal: the strip windows around the active tab and
1116
- shows `‹N` / `›N` counters for hidden workstreams.
1117
- - `*` prefix (e.g. `*scratch`) marks the ephemeral `scratch` bucket.
1118
- - If a loaded workstream is torn down while the TUI runs, its launch-time tab
1119
- remains in place but appears dimmed with strikethrough. This also surfaces the
1120
- otherwise hidden tab in a single-workstream TUI. Recreating the same name
1121
- restores the tab on the next fast tick.
1122
-
1123
- ### Popup drills
1124
-
1125
- `Enter` in any list popup drills into the focused row. Where the
1126
- row is itself an entity (a task), a further `Enter` chains into the
1127
- shared read-only task-detail leaf (notes timeline):
1128
-
1129
- - **Tracks popup (`Shift+2`)**: list of tracks → `Enter` opens the
1130
- track's task list → `Enter` opens that task's notes timeline.
1131
- - **Ready / In-progress / Blocked / Recent / All-tasks**:
1132
- list of tasks → `Enter` opens notes; `y` yanks `mu task show <id>`
1133
- (or `mu task claim` / `mu task close` / `mu task tree` depending
1134
- on popup).
1135
- - **Activity log popup (`Shift+4`)**: list of events → `Enter`
1136
- drills into the full untruncated payload of the focused event;
1137
- `y` yanks `mu log --since <seq-1> -n 1 -w <ws>`.
1138
- - **Workspaces popup (`Shift+5`)**: list of workspaces → `Enter`
1139
- opens the commits-since-fork list → `Enter` on a commit opens
1140
- the inline `git show <sha> --stat -p` view; `y` yanks `git show
1141
- <sha>`; `t` launches `tuicr -r <sha>`.
1142
- - **Commits popup (`Shift+0`)**: project-root recent commits →
1143
- `Enter` opens the backend's show view; `y` yanks the show
1144
- command; `t` launches `tuicr`.
1145
- - **Doctor popup (`Shift+9`)**: list of checks → `Enter` opens
1146
- the remediation paragraph for the focused check.
1147
- - **DAG popup (`g`)**: keybind-only; renders the active workstream's
1148
- full task DAG forest (one ASCII subtree per root, diamond-collapse
1149
- marker on repeated nodes).
1150
-
1151
- One `Esc` / `q` backs out per recursion level. Drills auto-refresh in
1152
- step with the dashboard tick (fast 1s for SQL bodies like notes, slow
1153
- 10s for subprocess git-show / scrollback). Scroll position survives
1154
- refreshes, and subprocess loaders keep the prior body visible until the
1155
- new one arrives.
1156
-
1157
- ### Search / filter
1158
-
1159
- `/` inside any list popup enters an incremental case-insensitive
1160
- substring filter (lazygit / k9s convention). Printable characters
1161
- append, `Backspace` pops, `Esc` cancels, `Enter` commits (keeps the
1162
- filter, resumes `j/k` navigation), and `/` again refines. The filter
1163
- blob is per-popup — agent name/status/cli/role; track head id + title;
1164
- task name/title/status/owner; log verb/payload/source — and dies with
1165
- the popup.
1166
-
1167
- Task-list popups add **per-status toggles** (`o` / `i` / `c` for OPEN /
1168
- IN_PROGRESS / CLOSED; default all-on). The All-tasks popup adds **sort
1169
- cycle** on `s`: `roi` → `recency` → `age` → `id`.
1170
-
1171
- ### Mouse
1172
-
1173
- Navigation-in only: double-click a dashboard card to open its popup,
1174
- double-click a popup row to drill one level deeper, and scroll-wheel
1175
- inside a popup list / drill body to move the cursor or scroll. There is
1176
- **no mouse back binding** — use `Esc` / `q`.
1177
-
1178
- ### Yank contract (`y`) and the `tuicr` escape (`t`)
1179
-
1180
- Every popup row exposes one canonical `mu` command via `y`, which goes
1181
- to your system clipboard (pbcopy / wl-copy / xclip / xsel / clip.exe,
1182
- OSC-52 fallback). You run it in your shell; the TUI never mutates.
1183
-
1184
- The one escape is **`t`** inside a `git show` drill (Workspaces or
1185
- Commits popup): mu suspends its alt-screen, runs `tuicr -r <sha>` in
1186
- the project root / workspace cwd, and restores the dashboard when tuicr
1187
- exits. The operator drives another TUI tool; mu still performs no
1188
- mutation.
1189
-
1190
- Status cells colour-code as in the static CLI tables: OPEN cyan,
1191
- IN_PROGRESS yellow, CLOSED green.
1192
-
1193
- ### Polling tiers
1194
-
1195
- The dashboard has two refresh tiers:
1196
-
1197
- - **Fast tick** (default 1s, adjustable with `+` / `-` / `=` /
1198
- `0`): SQL-only. Refreshes tasks, tracks, workspace registry rows,
1199
- and the activity log.
1200
- - **Slow tick** (10s, fixed): subprocess-backed. Refreshes
1201
- tmux-derived agent liveness / orphans, workspace dirty flags,
1202
- recent project commits, and the Doctor summary.
1203
-
1204
- The last slow-tier result is merged into every fast render so cards
1205
- do not flicker through a loading state. `r` / F5 refreshes both
1206
- tiers immediately. Tab / Shift-Tab triggers an eager slow refresh
1207
- for the newly active workstream.
1208
-
1209
- ### Keymap reference
1210
-
1211
- | Mode | Keys | Action |
1212
- | --------- | ---------------------------- | -------------------------------------------------------------- |
1213
- | dashboard | `0`-`9` | toggle card visibility |
1214
- | dashboard | `Shift+0`-`Shift+9` | open the matching popup |
1215
- | dashboard | `g` | open DAG popup (keybind-only) |
1216
- | dashboard | `t` | open All-tasks popup (keybind-only) |
1217
- | dashboard | `Tab` / `Shift-Tab` | cycle workstream tabs (N≥2) |
1218
- | dashboard | `+` / `-` / `=` / `0` | adjust fast tick rate (faster / slower / default / pause) |
1219
- | dashboard | `r` / `F5` | force refresh both tiers |
1220
- | dashboard | `c` | clear footer (last yank) |
1221
- | dashboard | `?` / `F1` | open help overlay |
1222
- | any | `q` / `Ctrl-C` | quit (or back out of popup; quits at dashboard) |
1223
- | popup | `j` / `k` | move cursor / scroll |
1224
- | popup | `g` / `G` | jump top / bottom |
1225
- | popup | `Ctrl-D` / `Ctrl-U` | half-page down / up |
1226
- | popup | `PgDn` / `PgUp` | full page |
1227
- | popup | `Enter` | drill into focused row |
1228
- | popup | `Esc` / `q` | back out one level |
1229
- | popup | `y` | yank canonical `mu` command for focused row |
1230
- | popup | `/` | enter filter mode |
1231
- | popup | `a` | attach to focused agent's mux pane (Agents popup only) |
1232
- | popup | `l` | launch lazygit in the project root (Commits popup only) |
1233
- | filter | (printable) / `Backspace` | edit query |
1234
- | filter | `Esc` | cancel (clear query) |
1235
- | filter | `Enter` | commit (keep filter, return to nav) |
1236
- | task popup| `o` / `i` / `c` | toggle OPEN / IN_PROGRESS / CLOSED |
1237
- | task popup| `p` / `w` | toggle OPEN/parked / closed-not-done (DAG + All-tasks) |
1238
- | All-tasks | `b` | blocked filter cycle (all → only → hide) |
1239
- | All-tasks | `s` | cycle sort key (roi → recency → age → id) |
1240
- | git-show | `t` | launch `tuicr -r <sha>` (alt-screen handoff) |
1241
-
1242
- `?` shows the same table as a scrollable overlay (j/k/Ctrl-D/U/g/G
1243
- also work inside the overlay).
1244
-
1245
- ### Read-only invariant
1246
-
1247
- The TUI never executes a mutation — a load-bearing pledge in
1248
- `docs/ROADMAP.md`, not an implementation detail. If a TUI gesture
1249
- tempts you to mutate state from the SDK, file a roadmap entry first.
1250
- Yank-and-run is the cost of keeping the TUI inspectable, scriptable
1251
- and recoverable from any shell.
1252
-
1253
- ---
1254
-
1255
- ## 6. Spawn a crew
1256
-
1257
- For a demo with live agent state, install and link murmur, then spawn pi agents:
1258
-
1259
- ```bash
1260
- mu agent spawn worker-1 --workstream auth-refactor # default --cli is pi
1261
- ```
1262
-
1263
- To play around without needing pi installed, use `--cli sh`:
1264
-
1265
- ```bash
1266
- mu agent spawn worker-1 --workstream auth-refactor --cli sh
1267
- mu agent spawn worker-2 --workstream auth-refactor --cli sh
1268
- ```
1269
-
1270
- ```
1271
- Spawned worker-1 (sh) in window worker-1 of mu-auth-refactor, pane %15
1272
- ```
1273
-
1274
- What just happened:
1275
-
1276
- 1. mu checked the agents table — no `worker-1` yet, OK to proceed
1277
- 2. mu created a tmux window named `worker-1` in the `mu-auth-refactor`
1278
- session
1279
- 3. mu set the pane title to `worker-1` via `tmux select-pane -T worker-1`
1280
- — **this is the claim protocol identity**
1281
- 4. mu inserted a row in `agents` with `pane_id=%15`; the deprecated `status` column remains `spawning`
1282
-
1283
- If the DB insert fails after the pane was created, mu kills the pane
1284
- to avoid leaking. If the same name was already taken, mu rejects
1285
- **before** calling tmux.
1286
-
1287
- ### Naming convention (lint, not a rule)
1288
-
1289
- mu accepts any name matching `/^[a-z][a-z0-9_-]{0,31}$/`, but the
1290
- recommended shape is **`<role>-<n>`** — a lowercase role plus the
1291
- smallest unused integer suffix (e.g. `worker-1`, `reviewer-2`,
1292
- `scout-12`). Names that diverge (`worker-tests`, `alice`, `db-leader`,
1293
- `x-y-1`) still spawn successfully but trigger a one-line stderr hint:
1294
-
1295
- ```
1296
- hint: agent name "worker-tests" does not match the smallest-unused-suffix
1297
- convention (<role>-<n>; e.g. worker-1, reviewer-2). Accepted; consider
1298
- renaming if you spawn additional workers.
1299
- ```
1300
-
1301
- The hint is suppressed under `--json` so script callers stay clean.
1302
-
1303
- ### Multiple agents in one window (split panes)
1304
-
1305
- Give them a shared `--tab`:
1306
-
1307
- ```bash
1308
- mu agent spawn reviewer-1 --workstream auth-refactor --cli sh --tab Review --role read-only
1309
- mu agent spawn audit --workstream auth-refactor --cli sh --tab Review
1310
- ```
1311
-
1312
- The `Review` window holds whichever agents share `--tab Review`.
1313
-
1314
- ### Spawn options
1315
-
1316
- | Flag | Meaning |
1317
- | ---------------------------- | ------------------------------------------------------- |
1318
- | `--cli <name>` | Logical CLI family (effectively always `pi`; the flag exists as a key for `MU_<UPPER_CLI>_COMMAND` resolution) |
1319
- | `--command <cmd>` | Executable launched in the pane. Defaults to `$MU_<UPPER_CLI>_COMMAND` (e.g. `MU_PI_COMMAND=pi-alt`) and finally to the `--cli` value |
1320
- | `--tab <name>` | Group with other agents under this window name |
1321
- | `--role <full-access\|read-only>` | Capability flag; stored but not yet enforced |
1322
- | `--cwd <path>` | Initial working directory for the pane |
1323
- | `-w, --workstream <name>` | Required if not auto-detectable |
1324
-
1325
- If your `pi` binary lives under a different name, set
1326
- `MU_PI_COMMAND=<name>` once in your shell rc: every
1327
- `mu agent spawn --cli pi` execs it, and reconcile treats that binary's
1328
- panes as agent-worthy when surfacing orphans. `MU_PI_COMMAND` (and
1329
- `--command`) accept a multi-word string — tmux execs it via a shell, so
1330
- `MU_PI_COMMAND="pi-alt --some-flag"` works. Same pattern for
1331
- `MU_CLAUDE_COMMAND` / `MU_CODEX_COMMAND`.
1332
-
1333
- **Project-trust prompt (gotcha).** pi asks whether to trust a project
1334
- folder on interactive startup, before loading `AGENTS.md` and `.pi/`
1335
- resources. A freshly-spawned agent stuck at `needs_input` is usually
1336
- waiting on this. Spawn with `--command 'pi --approve'` (or
1337
- `MU_PI_COMMAND="pi --approve"`) to auto-trust. Caveat: `--approve`
1338
- trusts whatever directory the pane lands in — including `--workspace`
1339
- forks — so use it for crews on your own code only. Otherwise the
1340
- decision is saved per-directory in `~/.pi/agent/trust.json`, or run
1341
- `/trust` in the pane once.
1342
-
1343
- ### Adopt an existing tmux pane
1344
-
1345
- You launched a `pi` by hand and decided mid-flow it belongs in the
1346
- graph, or `mu` crashed mid-spawn and left an orphan pane with no DB
1347
- row. Either way:
1348
-
1349
- ```bash
1350
- mu agent list -w auth-refactor # surfaces orphans at the bottom
1351
- # Orphan panes (1)
1352
- # %15 title=worker-2 cli=pi
1353
-
1354
- mu agent adopt %15 -w auth-refactor # adopt by pane id
1355
- mu agent adopt worker-2 -w auth-refactor # adopt by pane title (same effect)
1356
- mu agent adopt %15 --name investigator -w auth-refactor # adopt and rename the pane
1357
- ```
1358
-
1359
- The pane title becomes the agent name (the claim-protocol invariant),
1360
- so a pane titled `worker-2` registers as agent `worker-2` with no
1361
- further config. Use `--name` when the title isn't a valid agent name.
1362
-
1363
- Adopt is **idempotent** (twice on the same pane is a no-op) and
1364
- **scope-aware**: the pane must be in the `mu-<workstream>` tmux
1365
- session, or the adopt is rejected — no silent cross-session moves.
1366
-
1367
- ---
1368
-
1369
- ## 7. Watch the crew live
1370
-
1371
- Attach the workstream's tmux session and you see everything.
1372
-
1373
- ```bash
1374
- tmux attach -t mu-auth-refactor
1375
- ```
1376
-
1377
- You see one tmux window per agent (or a window with split panes if
1378
- they share a `--tab`).
1379
-
1380
- | Tmux key | What it does |
1381
- | -------------- | --------------------------------------------- |
1382
- | `Ctrl+b w` | Pick a window (interactive list) |
1383
- | `Ctrl+b n`/`p` | Cycle next/previous window |
1384
- | `Ctrl+b d` | Detach from the session (mu doesn't care) |
1385
-
1386
- mu does not require you to be attached. Detach freely.
1387
-
1388
- ---
1389
-
1390
- ## 8. Send work to an agent
1391
-
1392
- From any shell with mu on `$PATH`:
1393
-
1394
- ```bash
1395
- mu agent send worker-1 "echo hello from outside"
1396
- ```
1397
-
1398
- **Which transport.** A pi agent (`--cli pi`, a command whose argv0
1399
- is `pi` / `pi-meta`, or any agent whose control socket file exists)
1400
- gets the text through its **control socket**: the
1401
- mu pi extension calls `pi.sendUserMessage` inside the pane's own pi, so
1402
- nothing is pasted or scraped. When pi is busy the message queues as a
1403
- follow-up; `--steer` interrupts the current run instead. If the socket
1404
- does not answer, the send fails with `AgentCtlUnreachableError` (exit 1)
1405
- and pastes nothing; `--via mux` pastes explicitly. A pi started before
1406
- you upgraded mu still runs the old extension: an op it lacks (say
1407
- `--fresh`) fails with `AgentExtensionOutdatedError` (exit 4). Type
1408
- `/reload` in its pane (`mu agent send <name> '/reload' --via mux`) to
1409
- load the new extension and keep the session, or respawn it. `mu doctor`'s
1410
- `ctl` row flags such agents before you hit the error. Text starting with
1411
- `/` (`/new`, `/compact`) is a TUI command that `sendUserMessage` would
1412
- hand to the model as plain text, so it always goes through the mux.
1413
- Non-pi CLIs always go through the mux. `--json` reports `transport:
1414
- "ctl"|"mux"` (and pi's `state` for ctl). An adopted pi pane was started
1415
- without `MU_CTL_SOCK`; `mu agent adopt` warns and prints the path to
1416
- restart pi with.
1417
-
1418
- **Fresh session + prompt.** For a pi agent, `mu agent send <name>
1419
- --fresh '<prompt>'` starts a new session and sends the prompt into it
1420
- as one operation inside pi (the extension's internal `/mu-fresh`
1421
- command runs `newSession` and sends from the new session). It returns
1422
- once the prompt's run has started, so a send right after it lands in
1423
- the new session. Use it instead of sending `/new` then the prompt. A
1424
- busy pi refuses with exit 4 (`mu agent abort` first, or `--force` to
1425
- abandon the running turn). Non-pi CLIs have no control socket: use the
1426
- two-step `/new` + prompt there and verify the prompt landed.
1427
-
1428
- For the mux path, mu uses the **canonical bracketed-paste protocol**:
1429
-
1430
- 0. wait for the pane to stop being mid-modal (`MU_SEND_READINESS_MS`,
1431
- default 15000; 0 disables)
1432
- 1. `tmux copy-mode -q` (silent if not in copy mode)
1433
- 2. `tmux set-buffer` (loads text into a uniquely-named buffer)
1434
- 3. `tmux paste-buffer -p -d -r` (`-p` = bracketed paste, `-d` = delete
1435
- buffer after paste, `-r` = preserve LF)
1436
- 4. wait `MU_SEND_DELAY_MS` ms (default 500)
1437
- 5. `tmux send-keys Enter`
1438
- 6. confirm the Enter took; re-send it if the text is still sitting
1439
- unsubmitted in the input box
1440
- 7. when the text is exactly `/new`, wait for the pane to visibly change and
1441
- stabilize before returning, so a following send cannot race the delayed
1442
- session replacement
1443
-
1444
- Special characters (`/`, `?`, `!`, `$`, `&&`, `|`, `*`, …) therefore
1445
- arrive at the agent's CLI **literally**. Naive `tmux send-keys` would
1446
- let the agent's TUI hijack `/` for "search forward" and similar.
1447
-
1448
- The send delay is configurable per call:
1449
-
1450
- ```bash
1451
- MU_SEND_DELAY_MS=300 mu agent send worker-1 "..." # faster, less safe
1452
- MU_SEND_DELAY_MS=1000 mu agent send worker-1 "..." # slow remote
1453
- ```
1454
-
1455
- **Steps 0, 6, and 7 make `exit 0` mean "submitted" and safe for a
1456
- following send.** A TUI rendering a modal accepts a bracketed paste but
1457
- *swallows the Enter after it*,
1458
- leaving the prompt typed-but-unsubmitted. The visible symptom is an
1459
- agent idle at `needs_input` with 0.0% context on work it never
1460
- received. No `sleep` between a clear and a prompt is needed.
1461
-
1462
- If a send cannot be confirmed it prints `warning: ... was NOT
1463
- submitted` to stderr (and `"delivered": false` under `--json`).
1464
- **Check for that warning before you wait on the task.**
1465
-
1466
- Sending to a **busy** agent is not delayed — that input queues
1467
- normally; only a modal/re-init spinner is waited out.
1468
-
1469
- A **stale** target workspace (≥10 commits behind main — the red bucket
1470
- in `mu workspace list` and the TUI Workspaces card) warns on stderr but
1471
- still sends:
1472
-
1473
- ```bash
1474
- WARN: worker-1 workspace is 14 commits behind main (≥10 = stale)
1475
- Next:
1476
- Refresh first : mu workspace refresh worker-1 -w auth-refactor
1477
- ```
1478
-
1479
- `--strict-staleness` refuses instead:
1480
-
1481
- ```bash
1482
- mu agent send worker-1 "..." -w auth-refactor --strict-staleness
1483
- ```
1484
-
1485
- Agents without workspaces are skipped (common for read-only roles).
1486
- `--json` carries `staleness: null` or `{agentName, workstreamName,
1487
- commitsBehindMain, isStale}`.
1488
-
1489
- ---
1490
-
1491
- ## 9. Read what an agent did
1492
-
1493
- ```bash
1494
- mu agent read worker-1 # full scrollback
1495
- mu agent read worker-1 -n 50 # last 50 lines
1496
- ```
1497
-
1498
- Both go through `tmux capture-pane`. No state change.
1499
-
1500
- ---
1501
-
1502
- ## 10. The claim protocol — from inside an agent's pane
1503
-
1504
- An agent (the LLM running in a pane) runs `mu task claim foo` **with no
1505
- agent name argument** — mu derives "worker-1" from the pane title. In
1506
- worker-1's pane:
1507
-
1508
- ```bash
1509
- mu task claim design
1510
- ```
1511
-
1512
- ```
1513
- Claimed design for worker-1 (OPEN → IN_PROGRESS)
1514
- ```
1515
-
1516
- Behind the scenes: mu reads `$TMUX_PANE`, asks tmux for that pane's
1517
- title (`tmux display-message -t %15 -p '#{pane_title}'`), and runs one
1518
- atomic conditional UPDATE guarded on `owner IS NULL OR owner = <me>`.
1519
- If 0 rows change, mu distinguishes "task doesn't exist" from "already
1520
- owned by someone else" and throws the right typed error.
1521
-
1522
- Two agents claiming the same task → the second fails with "already
1523
- owned by worker-1". Re-claim by the same agent is idempotent.
1524
-
1525
- You can also claim explicitly from outside any pane:
1526
-
1527
- ```bash
1528
- mu task claim build --for worker-2
1529
- ```
1530
-
1531
- `--for` accepts either a bare worker name (`worker-2`, resolved in
1532
- the task's workstream) or a qualified ref `<workstream>/<name>` for
1533
- **cross-workstream dispatch**:
1534
-
1535
- ```bash
1536
- # Task lives in mufeedback-v03; worker-1 lives in roadmap-v0-3.
1537
- mu task claim some-task -w mufeedback-v03 --for roadmap-v0-3/worker-1
1538
- ```
1539
-
1540
- The agent stays in its own workstream — only `tasks.owner_id` points
1541
- across the boundary (an INTEGER FK to `agents.id`). A bad qualifier
1542
- surfaces typed errors and writes nothing:
1543
- `WorkstreamNotFoundError` (exit 3) on a missing prefix,
1544
- `AgentNotFoundError` (exit 3) when the worker doesn't live there.
1545
-
1546
- When `--for` targets an agent with a **stale** workspace (≥10 commits
1547
- behind main), the claim warns on stderr and succeeds:
1548
-
1549
- ```bash
1550
- mu task claim build -w auth-refactor --for worker-2
1551
- # stderr: WARN: worker-2 workspace is 14 commits behind main (≥10 = stale)
1552
- # Next: Refresh first : mu workspace refresh worker-2 -w auth-refactor
1553
- ```
1554
-
1555
- `--strict-staleness` refuses instead with `TaskClaimStaleWorkspaceError`
1556
- (exit 4) — for scripts that must never dispatch onto a stale parent.
1557
- `--json` carries `staleness: null` or `{agentName, workstreamName,
1558
- commitsBehindMain, isStale}`. Bare in-pane and `--self` claims skip the
1559
- check; they don't assign work to a named agent.
1560
-
1561
- ### The orchestrator pattern: `--self`
1562
-
1563
- Not every action comes from a registered worker pane. An
1564
- *orchestrator* (a top-level pi session, a human at a shell, a deploy
1565
- script) often does small work directly. Two roles:
1566
-
1567
- - **Worker** — a pane mu spawned (or you adopted). Has a row in the
1568
- `agents` table. Identity = pane title. Claims with bare
1569
- `mu task claim <id>`. `tasks.owner_id` points at the worker row.
1570
-
1571
- - **Actor** — anything that *causes* a state change, including the
1572
- orchestrator. May or may not have a row in `agents`. Always recorded
1573
- on the op (`ops.actor`).
1574
-
1575
- If the orchestrator tries `mu task claim some-task` directly:
1576
-
1577
- ```
1578
- conflict: claimer 'pi-mu' (pane %6441) is not a registered mu agent.
1579
- Working directly? Pass --self to attribute via log instead.
1580
- Dispatching to a worker? Pass --for <worker> to assign.
1581
- Want full registration? Run: mu agent adopt %6441
1582
- ```
1583
-
1584
- Pick one based on intent:
1585
-
1586
- ```bash
1587
- # Orchestrator does the work itself (most common):
1588
- mu task claim some-task --self --evidence "trivial 5-line fix"
1589
- # -> tasks.owner_id stays NULL
1590
- # -> the task.claim op records actor='pi-mu' (anonymous claim)
1591
- # -> mu task show surfaces it as 'owner: (self: pi-mu)'
1592
-
1593
- # Orchestrator dispatches to a worker:
1594
- mu task claim some-task --for worker-1
1595
- # -> tasks.owner_id points at worker-1
1596
-
1597
- # Orchestrator wants to BE a registered worker (rare):
1598
- mu agent adopt %6441 -w <ws> # only if pane is in mu-<ws> session
1599
- mu task claim some-task # now works as a normal worker claim
1600
- ```
1601
-
1602
- `--self` is **only** for unregistered actors. Workers continue to
1603
- claim with bare `mu task claim` — nothing changes for them. The
1604
- `--actor <name>` flag overrides the auto-detected actor name (defaults
1605
- to pane title, or `$USER`, or `unknown`):
1606
-
1607
- ```bash
1608
- mu task claim deploy --self --actor deploy-bot --evidence "prod release"
1609
- ```
1610
-
1611
- When `tasks.owner_id IS NULL` because of `--self`, `mu task show` looks
1612
- up the most recent `task claim` event for that task and surfaces it:
1613
-
1614
- ```
1615
- owner : (self: pi-mu)
1616
- ```
1617
-
1618
- So provenance is preserved — it just lives in `ops.actor` rather
1619
- than being conflated with the FK that points at registered workers.
1620
-
1621
- ---
1622
-
1623
- ## 11. Drop notes (durable context)
1624
-
1625
- Notes are append-only and survive sessions and agent restarts — the
1626
- cure for LLM context loss: the next agent reads the full history.
1627
-
1628
- ```bash
1629
- mu task note design "DECISION: JWT, 24h expiry, refresh via cookie"
1630
- mu task note design "FILES: src/auth.rs:45-120"
1631
- ```
1632
-
1633
- Read them via the typed verb:
1634
-
1635
- ```bash
1636
- mu task notes design # all notes, oldest first
1637
- mu task notes design --tail 3 # only the last 3 (alias --last)
1638
- mu task notes design --since 2026-01-01 # only notes after an ISO 8601 cutoff
1639
- mu task notes design --since-claim # only notes since the most recent
1640
- # 'task claim' event for this task
1641
- # (auto-resolved from the ops log)
1642
- mu task notes design --tail 5 --json # collection envelope {items, count}
1643
- ```
1644
-
1645
- Filters compose: `--tail` slices the last N of whatever survived
1646
- the timestamp filter. `--since` and `--since-claim` are mutually
1647
- exclusive (both define a cutoff) — pick one. With no filters you get
1648
- every note, oldest-first.
1649
-
1650
- `--since-claim` is the orchestrator form: dispatch flows drop a
1651
- multi-screen SPEC note BEFORE claiming, then the worker appends
1652
- progress notes AFTER. `--since-claim` slices off the SPEC so you see
1653
- only the worker's reports. With no claim event it degrades to no
1654
- filter.
1655
-
1656
- For ad-hoc shape, the SQL escape hatch:
1657
-
1658
- ```bash
1659
- mu sql "SELECT n.author, n.content, n.created_at
1660
- FROM task_notes n
1661
- JOIN tasks t ON t.id = n.task_id
1662
- JOIN workstreams w ON w.id = t.workstream_id
1663
- WHERE t.local_id='design' AND w.name='auth-refactor'
1664
- ORDER BY n.id"
1665
- ```
1666
-
1667
- Convention for note content: `KEY: value` lines. Common keys are
1668
- `FILES`, `DECISION`, `VERIFIED`, `BLOCKED`, `NEXT`. Mu doesn't
1669
- enforce these — they're for the agents reading them.
1670
-
1671
- ---
1672
-
1673
- ## 12. Close out a task
1674
-
1675
- ```bash
1676
- mu task close design # OPEN/IN_PROGRESS → CLOSED/done
1677
- mu task close umbrella --if-ready # close ONLY if every blocker
1678
- # is CLOSED; else no-op + list
1679
- # the still-blocking ids
1680
- mu task close spike --as wontfix --why "superseded by the v2 design"
1681
- mu task open design # CLOSED → OPEN (e.g. closed by mistake)
1682
- mu task park polish --why "after the release" # OPEN → OPEN/parked
1683
- mu task unpark polish # OPEN/parked → OPEN/todo
1684
- ```
1685
-
1686
- Every task carries a **substate** next to its status, rendered as a pair.
1687
- `OPEN` is `todo` or `parked`; `IN_PROGRESS` is `active`; `CLOSED` is
1688
- `done`, `rejected`, `wontfix`, `duplicate`, or `superseded`. `--as` picks
1689
- the closed substate: `rejected` when the proposal itself was declined,
1690
- `wontfix` when it is valid but not worth doing. Anything but `done` requires `--why`, stored as a note in the
1691
- same transaction. Any `CLOSED/*` satisfies a blocker, so a non-`done`
1692
- close prints the dependents it unblocked.
1693
-
1694
- To keep dependents waiting, park the task instead of closing it. A
1695
- parked task leaves `mu task next` and the ready set, and `mu task claim`
1696
- refuses it without `--force`. It stays in goals, and a track whose non-closed
1697
- tasks are all parked is marked `(parked)`. Park refuses `IN_PROGRESS`
1698
- (release first) and `CLOSED` (open first). Filter with
1699
- `mu task list --substate parked`.
1700
-
1701
- Both are idempotent (closing an already-CLOSED task prints a no-op and
1702
- exits 0). Owner is left intact — use `mu task release <id>` to clear
1703
- ownership when an agent bails mid-flight. `IN_PROGRESS` flips back to
1704
- `OPEN` so the task re-enters the ready set. `--reopen` forces `OPEN`
1705
- from `CLOSED`.
1706
-
1707
- When the closing actor has a per-agent workspace and that workspace
1708
- has uncommitted edits, a successful close adds one extra `Next:` hint
1709
- reminding the actor to commit before the next wave:
1710
-
1711
- ```bash
1712
- cd $(mu workspace path worker-1 -w auth-refactor) && git commit -am 'Design auth module'
1713
- ```
1714
-
1715
- The hint is best-effort: no workspace, a clean workspace, the `none`
1716
- backend, or a failed VCS dirty check simply omit it. The same
1717
- `nextSteps` entry is present in `--json` output.
1718
-
1719
- `--if-ready` is the umbrella-on-wave-done shape: fire
1720
- `mu task close <umbrella> --if-ready` after each wave-task finishes.
1721
- It's a no-op while any blocker is OPEN / IN_PROGRESS, printing the
1722
- still-blocking ids + a `mu task wait` hint; once the last blocker is
1723
- CLOSED, the same command closes the umbrella. JSON on the no-op path:
1724
- `{ skipped: "not_ready", changed: false, blockingIds: ["..."], ... }`.
1725
- Exit 0 either way — the no-op is success.
1726
-
1727
- ```bash
1728
- mu task release design # clear owner; IN_PROGRESS → OPEN
1729
- # (CLOSED preserved)
1730
- mu task release design --reopen # clear owner AND force status to OPEN
1731
- # (un-close + release in one verb)
1732
- ```
1733
-
1734
- With `design` closed, `build` is ready:
1735
-
1736
- ```
1737
- $ mu task next -w auth-refactor
1738
- ┌───────┬────────┬───────────────────┬────────┬────────┬──────┬───────┐
1739
- │ name │ status │ title │ impact │ effort │ ROI │ owner │
1740
- ├───────┼────────┼───────────────────┼────────┼────────┼──────┼───────┤
1741
- │ build │ OPEN │ Build auth module │ 80 │ 5 │ 16.0 │ — │
1742
- └───────┴────────┴───────────────────┴────────┴────────┴──────┴───────┘
1743
- ```
1744
-
1745
- ---
1746
-
1747
- ## 13. The SQL escape hatch is your friend
1748
-
1749
- Most routine operations have a typed verb — prefer those, with `--json`
1750
- for scripting. `mu sql` covers ad-hoc joins, manual recovery, and
1751
- schema exploration. The schema is 10 tables — 4 **portable**
1752
- (`workstreams`, `tasks`, `task_edges`, `task_notes`: what crosses
1753
- machines) and 6 **machine-local** (`agents`, `vcs_workspaces`,
1754
- `machine_identity`, `sync_peers`, `schema_version`, and the `ops` log,
1755
- which is carrier not cargo) — plus three views (`ready`, `blocked`,
1756
- `goals`):
1757
-
1758
- ```bash
1759
- mu sql "SELECT name FROM sqlite_master WHERE type IN ('table','view') ORDER BY type, name"
1760
- ```
1761
-
1762
- ### Prefer the typed verb where one exists
1763
-
1764
- | Want | Typed verb |
1765
- | ----------------------------------------------------- | --------------------------------------- |
1766
- | Tasks owned by an agent (current workstream) | `mu task owned-by <agent> [--json]` |
1767
- | Tasks owned by ANY same-named worker (all workstreams)| `mu task owned-by <agent> --all [--json]`|
1768
- | Highest-ROI ready task | `mu task next [-w] [-n K] [--json]` |
1769
- | What did I touch most recently / what's stale | `mu task list --sort recency` / `--sort age` |
1770
- | Visualise what blocks what | `mu task tree <id> [--json]` |
1771
- | Show row + edges + notes | `mu task show <id> [--json]` |
1772
- | Delete + cascade edges/notes (two-phase: bare = dry-run; `--yes` commits) | `mu task delete <id>` / `mu task delete <id> --yes` |
1773
- | Add / remove a single edge | `mu task block` / `mu task unblock` |
1774
- | Replace all blockers atomically | `mu task reparent <id> --blocked-by ...` |
1775
- | Modify scalar fields | `mu task update <id> [--title ...]` |
1776
- | Read the activity log / subscribe to events | `mu log [--tail] [--intent task.close]` |
1777
- | Block until tasks reach a status (orchestrator wait) | `mu task wait <ref> [<ref>...] [--first|--any] [--timeout S] --on-stall exit` |
1778
- | Block until agents finish working (task-less wait) | `mu agent wait <name> [<name>...] [--first|--any] [--timeout S]` |
1779
- ### `mu agent wait`: the task-less counterpart to `mu task wait`
1780
-
1781
- Scratch helpers usually own no task, so `mu task wait` has nothing to
1782
- watch. `mu agent wait` blocks on the agent's runtime state: an agent
1783
- **fires** when it goes **busy → any other state**. It must be observed
1784
- busy first, so an already-idle agent does NOT fire instantly — you're
1785
- waiting for *this* work to finish. Replaces `sleep` polling loops.
1786
-
1787
- ```bash
1788
- mu agent spawn helper-1 -w scratch
1789
- mu agent send helper-1 'Investigate X. Report findings.'
1790
- mu agent wait helper-1 -w scratch --first # blocks until pi finishes
1791
- mu agent read helper-1 -w scratch -n 80 # now read the result
1792
- ```
1793
-
1794
- Mirrors `mu task wait`'s shape: `--any`/`--first` fire on the first
1795
- agent (default: all must finish); `--first` prints the firing agent's
1796
- ref; `--json` carries `nextSteps`; refs may be qualified
1797
- `<workstream>/<name>`. Exit codes: `0` met, `5` timeout, `6` a watched
1798
- agent's pane died (for a pi agent: its control socket stopped
1799
- answering). For pi agents the wait is one request to the mu extension,
1800
- answered when pi settles, so it returns immediately rather than on the
1801
- next poll, and `--json` adds each settled pi agent's `lastText`: the
1802
- text of its final assistant message, verbatim (`""` when the run ended
1803
- without text; capped at 64 KiB). For other CLIs murmur (tmux) or herdr
1804
- supplies the state, and `lastText` is absent; read the pane instead.
1805
- Every `--json` agent row also carries `outcome`: `done` (settled with
1806
- text), `empty` (settled without text, or no `lastText` source), `died`,
1807
- `timeout`, or `pending` (`--any` fired on another agent). The
1808
- `mu_delegate` tool in the mu pi extension is this verb plus spawn and
1809
- send: it formats `outcome` into a follow-up message.
1810
- `unknown` never fires the wait; run `mu doctor` for the missing-source
1811
- reason.
1812
-
1813
- **Don't reach for this verb to catch a worker that asked a question.**
1814
- It fires on `busy → needs_input`, so it does detect the case — but it
1815
- keys on **agents**, which means a task-DAG orchestrator would have to
1816
- track the task→agent mapping itself and run two waits concurrently to
1817
- catch both "task closed" and "worker asked something". `mu task wait
1818
- --stuck-after` / `--on-stall` covers that from the task side; see
1819
- [attention detection](#mu-task-wait-attention-detection---stuck-after----on-stall).
1820
-
1821
- ### `mu task wait`: cross-workstream refs + `--first` returns WHICH
1822
-
1823
- Each `<ref>` is either a bare task name (resolves via `-w` /
1824
- `$MU_SESSION` / tmux session) or a qualified `<workstream>/<name>`
1825
- ref. When all refs are qualified, `-w` is not required; mixed lists
1826
- are allowed (bare uses `-w`, qualified uses its prefix).
1827
-
1828
- ```bash
1829
- # All-bare with -w
1830
- mu task wait build_a build_b -w mufeedback-v03 --timeout 1200 --on-stall exit
1831
-
1832
- # All-qualified — cross-workstream wait, no -w needed
1833
- mu task wait roadmap-v0-3/archive_phase2 mufeedback-v03/cli_audit --timeout 1800 --on-stall exit
1834
-
1835
- # Mixed — bare uses -w; qualified ignores it
1836
- mu task wait cli_audit roadmap-v0-3/archive_phase2 -w mufeedback-v03 --on-stall exit
1837
- ```
1838
-
1839
- `--first` is an alias for `--any` that ALSO prints the firing ref's
1840
- qualified id to stdout (and populates the `firing` field in `--json`,
1841
- which `--any` leaves null). Use
1842
- it to drive a single-shot dispatch loop — one wait, one cherry-pick,
1843
- one verify, one workspace recycle:
1844
-
1845
- ```bash
1846
- # The dispatch-pipeline recipe: cycle until in_flight is empty.
1847
- in_flight=( mufeedback-v03/foo mufeedback-v03/bar roadmap-v0-3/baz )
1848
- while (( ${#in_flight[@]} > 0 )); do
1849
- res=$(mu task wait "${in_flight[@]}" --first --timeout 90 --on-stall exit --json)
1850
- closed=$(jq -r '.firing.qualifiedId // empty' <<<"$res")
1851
- if [[ -z "$closed" ]]; then break; fi # timeout or exit 6 — see below
1852
-
1853
- worker=$(jq -r '.firing.owner // empty' <<<"$res")
1854
- ws=${closed%%/*}
1855
-
1856
- # 1. Inspect, then run, the sha-pinned apply hint from nextSteps.
1857
- # When the worker has commits since its fork point, the command is
1858
- # `git cherry-pick <sha>` (or `<first>^..<last>` for multiple
1859
- # commits). When the worker closed without committing, nextSteps
1860
- # says so and points at manual `git diff` / `git apply` rescue.
1861
- apply=$(jq -r '.nextSteps[0].command' <<<"$res")
1862
- printf 'apply hint: %s\n' "$apply"
1863
- if [[ "$apply" == git\ cherry-pick* ]]; then
1864
- eval "$apply"
1865
- else
1866
- echo "manual rescue required; inspect the worker workspace before continuing"
1867
- break
1868
- fi
1869
-
1870
- # 2. Verify
1871
- npm run typecheck && npm run lint && npm run test:fast && npm run test && npm run build
1872
-
1873
- # 3. Refresh the workspace for the next dispatch (rebases onto
1874
- # fresh main WITHOUT killing the worker's LLM context). Default
1875
- # base = origin/HEAD (git) / trunk() (jj/sl); --from <ref>
1876
- # overrides. Refuses on dirty WC; conflicts exit 5 with a `cd`
1877
- # hint to resolve in-place.
1878
- mu workspace refresh "$worker" -w "$ws"
1879
- # Alt: `mu workspace free "$worker" -w "$ws"` throws the workspace
1880
- # away entirely (the lossy escape). The next
1881
- # `mu agent spawn --workspace` allocates a fresh one. Use when
1882
- # you don't care about replaying the worker's commits.
1883
-
1884
- # 4. Drop $closed from in_flight, dispatch the next task, repeat.
1885
- in_flight=( "${in_flight[@]/$closed}" )
1886
- done
1887
- ```
1888
-
1889
- The `--json` shape on success is `{ firing, all, timedOut, nextSteps,
1890
- ... }`:
1891
-
1892
- * `firing` — `{ workstreamName, name, qualifiedId, status, owner }`
1893
- on **`--first`** success. `null` in every other case: on `--any`
1894
- success, on `--all` success, and on timeout. `--any` exits `0` with
1895
- `firing: null`, so a script that reads `.firing.qualifiedId` after
1896
- `--any` dereferences null on a **successful** wait. Pass `--first`
1897
- when you need to know WHICH ref fired; `--any` only tells you that
1898
- one did.
1899
- * `all` — array of refs that REACHED the target (with
1900
- `qualifiedId` + `reachedAt`).
1901
- * `timedOut` — array of refs that did NOT reach the target. Empty on
1902
- clean success; populated on partial-progress timeout.
1903
- * `nextSteps`— the same hint list printed to stdout (cherry-pick,
1904
- verify, refresh, or `mu task show` for unmet refs — except a
1905
- `stuck` ref, which gets `mu agent read <owner>` because its pane,
1906
- not its task row, holds the reason it stopped).
1907
-
1908
- ### Wait exit codes (`mu task wait`)
1909
-
1910
- `mu task wait` polls the watched tasks every second (cheap indexed
1911
- SELECT + a per-poll reconcile of every workstream in the wait set)
1912
- and exits with one of:
1913
-
1914
- | Exit | Meaning |
1915
- |------|-------------------------------------------------------------------------|
1916
- | `0` | The wait condition was met (`--all` reached, or `--any` / `--first` saw at least one). |
1917
- | `5` | `--timeout` expired before the condition was met. `--json` payload still includes `all` (refs that did reach) and `timedOut` (refs that didn't). |
1918
- | `6` | **REAPER_DETECTED.** A WATCHED task transitioned `IN_PROGRESS → OPEN` between polls because the reconciler detected the owning pane was dead and the reaper flipped the task back. Scoped to the wait set: a reaper-flip in some other workstream (or some other task in the same workstream) does NOT trigger exit 6. Fires only when the wait target is `CLOSED` (the default) — with `--status OPEN` a reaper-flip TO open IS the success and the wait returns `0`. Re-dispatch a worker (`mu agent spawn ... && mu task claim --for ...`) and re-run the wait. (`task_wait_reconcile_dead_panes` + `task_wait_cross_workstream`) |
1919
- | `7` | **STALL_DETECTED.** Only with `--on-stall exit`. The existing `--stuck-after` predicate fired on a watched task (IN_PROGRESS, owner alive but in `needs_input` for `>= --stuck-after` seconds) and the wait threw instead of polling forward. Same target=CLOSED carve-out as exit 6 (with `--status OPEN`/etc the worker reaching `needs_input` might BE the success path; `--on-stall exit` is downgraded to warn-only). Stderr names the task + owner + age. Exit 7 is the **ambiguous** sibling of exit 6: dead pane (6) is unambiguous (re-dispatch); idle agent (7) might be transient, or be waiting on an answer — read the pane before deciding. If both fire in the same poll, exit 6 wins (reaper-flip moves status off `IN_PROGRESS`, so the stuck-check's predicate naturally fails). (`task_wait_stall_action_flag`) |
1920
-
1921
- The per-poll reconcile means a worker pane that died **before** you
1922
- ran `mu task wait` is also reaped on the first tick — you'll see exit
1923
- `6` in well under a second instead of running out the `--timeout`.
1924
- For cross-workstream waits the reconcile loops over every workstream
1925
- in the wait set (so a dead pane in workstream B is reaped while you
1926
- wait on its task there too).
1927
-
1928
- ### `mu task wait`: attention detection (`--stuck-after` + `--on-stall`)
1929
-
1930
- For unattended waits, always pass `--on-stall exit`; otherwise the default
1931
- warning leaves the wait polling. Two orthogonal flags govern the behaviour:
1932
-
1933
- * `--stuck-after <seconds>` — the **trigger**. An IN_PROGRESS task
1934
- whose owner has been in `needs_input` for `>= N` seconds according
1935
- to the state source is marked as needing attention. `unknown` never
1936
- counts. Default `300` (5 min); pass `0` to disable detection entirely
1937
- (no warn AND no exit).
1938
- * `--on-stall <action>` — the **action** when the trigger fires.
1939
- Two values:
1940
- * `warn` (default) — yellow attention warning to stderr (deduped per
1941
- task per wait call), corroborating `agent stalled <name> owns
1942
- <task> for <secs>s` event in the ops log, and `wait` keeps
1943
- polling.
1944
-
1945
- **`needs_input` has at least three causes, and the trigger cannot tell
1946
- them apart:**
1947
-
1948
- | Cause | Response |
1949
- | --- | --- |
1950
- | Worker finished and committed, skipped `mu task close` | close it, merge the work |
1951
- | Worker asked a question and is waiting on an answer | read the pane, answer it |
1952
- | Worker hit an approval prompt or a wedged tool call | intervene or `mu agent kick` |
1953
-
1954
- So the warning states the observation and points at `mu agent read
1955
- <owner>`, which is the next move in all three cases — the evidence is
1956
- in the pane, not in the task row. `--json` carries `"stuck": true` per
1957
- task, and a stuck ref's `nextSteps` entry reads the owner's pane
1958
- instead of offering `mu task show`.
1959
-
1960
- A worker stopping to ask is **desirable behaviour**, not negligence:
1961
- the questions that get asked are usually the ones that prevent
1962
- rework. Treat an attention warning as "go look", not "the worker
1963
- misbehaved".
1964
- * `exit` — same emit + persist, then **exit 7**
1965
- (`STALL_DETECTED`). The unattended-orchestrator escape: a
1966
- wrapping policy can branch on 7 (idle, ambiguous — poke vs
1967
- release) vs 6 (dead pane, unambiguous — re-dispatch). Suppressed
1968
- when `--status` is anything other than `CLOSED` (mirrors
1969
- exit-6's carve-out: with `--status OPEN` reaching `needs_input`
1970
- might BE the success path).
1971
-
1972
- ```bash
1973
- # Default: warn at 5 min, keep polling.
1974
- mu task wait build_a build_b -w mufeedback-v03 --timeout 1800
1975
-
1976
- # Tune the trigger; same warn-only action.
1977
- mu task wait build_a -w mufeedback-v03 --stuck-after 60
1978
-
1979
- # Exit on stall (cron-driven wrapper):
1980
- mu task wait build_a -w mufeedback-v03 --on-stall exit
1981
- # exit 0 → closed
1982
- # exit 5 → timeout
1983
- # exit 6 → dead pane (re-dispatch)
1984
- # exit 7 → idle agent (poke or release — inspect first)
1985
-
1986
- # Tune both. Exit at 60s of needs_input:
1987
- mu task wait build_a -w mufeedback-v03 --stuck-after 60 --on-stall exit
1988
-
1989
- # Disable both warn AND exit (--stuck-after 0 wins):
1990
- mu task wait build_a -w mufeedback-v03 --stuck-after 0 --on-stall exit
1991
- ```
1992
-
1993
- ### Common ad-hoc queries
1994
-
1995
- ```bash
1996
- # Set task to IN_PROGRESS without claiming (claim does this automatically;
1997
- # this covers the rare manual case). local_id is per-workstream unique,
1998
- # so always scope by workstream_id to avoid hitting a same-named task in
1999
- # another workstream.
2000
- mu sql "UPDATE tasks SET status='IN_PROGRESS'
2001
- WHERE local_id='build'
2002
- AND workstream_id=(SELECT id FROM workstreams WHERE name='mufeedback-v03')"
2003
-
2004
- # What's blocking what (open tasks only) — same data as `mu task tree`
2005
- # but as a flat join when you want a wider report. task_edges is keyed
2006
- # by tasks.id, not local_id; join workstreams to scope the report.
2007
- mu sql "SELECT b.local_id AS blocked, t.local_id AS by_task
2008
- FROM tasks b
2009
- JOIN workstreams w ON w.id = b.workstream_id
2010
- JOIN task_edges e ON e.to_task_id = b.id
2011
- JOIN tasks t ON t.id = e.from_task_id
2012
- WHERE w.name='mufeedback-v03'
2013
- AND t.status != 'CLOSED' AND b.status = 'OPEN'"
2014
-
2015
- # Recursive CTE: every task that transitively blocks `launch` in a
2016
- # given workstream (or use `mu task tree launch --json` for the same
2017
- # data structured). local_id is per-workstream, so resolve the seed
2018
- # under a workstream filter.
2019
- mu sql "WITH RECURSIVE prereqs(id) AS (
2020
- SELECT t.id FROM tasks t
2021
- JOIN workstreams w ON w.id = t.workstream_id
2022
- WHERE t.local_id='launch' AND w.name='mufeedback-v03'
2023
- UNION
2024
- SELECT e.from_task_id FROM task_edges e, prereqs
2025
- WHERE e.to_task_id = prereqs.id
2026
- )
2027
- SELECT t.local_id, t.title, t.status
2028
- FROM prereqs JOIN tasks t ON t.id = prereqs.id"
2029
- ```
2030
-
2031
- `mu sql` accepts both reads and writes. Reads are pretty-printed as a
2032
- table; writes report `<n> rows affected`.
2033
-
2034
- ---
2035
-
2036
- ## 14. Recovery scenarios
2037
-
2038
- ### An agent's pane dies externally
2039
-
2040
- You killed it from another tmux client, or its CLI crashed:
2041
-
2042
- ```bash
2043
- mu agent list # worker-1's row prunes itself (ghost detected)
2044
- ```
2045
-
2046
- Reconciliation runs on every `mu agent list` / `mu`. Two steps:
2047
-
2048
- 1. **Prune ghost rows** — DB row whose `pane_id` no longer exists in
2049
- the mux gets deleted
2050
- 2. **Surface orphan panes** — panes in the workstream's session whose
2051
- `pane.command` looks like an agent CLI but that aren't in the
2052
- registry. **Not** auto-adopted; mu lists them under "Orphan panes"
2053
- with the `mu agent adopt <pane-id>` hint
2054
-
2055
- ### A pi worker on another machine
2056
-
2057
- `mu agent remote-env` prints the ssh pieces; mu itself runs no ssh:
2058
-
2059
- ```bash
2060
- eval "$(mu agent remote-env worker-1 --shell)" # sets MU_SSH_ARGS, MU_REMOTE_ENV
2061
- mu agent spawn worker-1 --command \
2062
- "ssh $MU_SSH_ARGS dev -t 'cd ~/ws/worker-1 && $MU_REMOTE_ENV \$SHELL -ilc \"pi --approve\"'"
2063
- ```
2064
-
2065
- `$SHELL -ilc` runs pi in an interactive login shell, so the remote
2066
- `~/.zshrc` / `~/.bashrc` (PATH, provider env) loads; ssh's own
2067
- non-interactive shell skips it. `MU_SSH_ARGS` also forces a direct
2068
- connection (`-o ControlMaster=no -o ControlPath=none`): a multiplexed
2069
- ssh never binds the local end of the forward.
2070
-
2071
- `MU_SSH_ARGS` forwards the remote pi's control socket to the local path
2072
- mu always connects to, so send, state, `mu agent wait` and
2073
- `mu agent abort` behave as for a local agent. The host needs
2074
- `mu link pi`, and its sshd must allow the forward
2075
- (`AllowStreamLocalForwarding`). Full recipe and traps:
2076
- `skills/mu/REMOTE_WORKERS.md`.
2077
-
2078
- ### A worker is wedged on an unbounded tool subprocess
2079
-
2080
- A worker ran `find / -maxdepth 6 ...` (30-60 minutes on a populated
2081
- home directory) or a busy-wait loop. `mu agent send` queues steering
2082
- messages until the tool returns; `tmux send-keys C-c` against the
2083
- pane doesn't propagate (the wrapping pi/claude/codex CLI catches it
2084
- as TUI input). For a pi agent, abort the turn first:
2085
-
2086
- ```bash
2087
- mu agent abort worker-1 # Esc through the control socket; waits for pi to settle
2088
- ```
2089
-
2090
- `mu agent abort` kills the running tool (the bash child dies with it)
2091
- and returns once pi settles (exit 5 after `--timeout`, default 30s).
2092
- An idle agent is left alone. A follow-up queued before the abort does
2093
- NOT run: pi puts it back into the pane's editor box, unsent. Re-send
2094
- it if you still want it. Non-pi agents, or a pi whose socket is
2095
- gone, need the signal escape hatch:
2096
-
2097
- ```bash
2098
- mu agent kick worker-1 # SIGINT (graceful, default)
2099
- mu agent kick worker-1 --signal SIGTERM # polite escalation
2100
- mu agent kick worker-1 --signal SIGKILL # hammer
2101
- ```
2102
-
2103
- `mu agent kick` looks up the pane's TTY (`tmux display-message -p
2104
- '#{pane_tty}'`), asks `ps -t <tty>` for the foreground process group
2105
- (the row whose `stat` contains `+`), and signals the whole pgrp.
2106
- Refuses with `NoForegroundProcessError` when the foreground IS the
2107
- wrapping CLI — use `mu agent close` then.
2108
-
2109
- Prevention: don't prompt workers to run filesystem-wide `find`, broad
2110
- `grep -r /`, or unbounded busy-wait loops. Scope to `$WORKSPACE`.
2111
-
2112
- ### You closed your terminal session
2113
-
2114
- The workstream's tmux session keeps running detached. Reconnect with
2115
- `tmux a -t mu-auth-refactor`. Agents are alive; the DB has the
2116
- registry; everything resumes. mu is daemon-free — every `mu`
2117
- invocation is a short-lived process that re-reads from
2118
- `~/.local/state/mu/mu.db`.
2119
-
2120
- ### The mu DB seems wrong
2121
-
2122
- ```bash
2123
- mu doctor # quick health check (exit 5 = drift)
2124
- mu doctor --deep # rebuild + field-level diff
2125
- mu doctor --disk # + per-workspace byte usage
2126
- sqlite3 ~/.local/state/mu/mu.db .schema # inspect
2127
- rm ~/.local/state/mu/mu.db # nuke (last resort; loses task graph and registry)
2128
- ```
2129
-
2130
- ### You ran a destructive verb and want to undo it
2131
-
2132
- Every change any verb makes is captured as **ops** under one
2133
- **group**, so undo is per-action rather than per-file.
2134
-
2135
- ```bash
2136
- mu undo # list recent undoable groups (newest first)
2137
- mu undo 4a1a6305 # preview the inverse of one group (dry run)
2138
- mu undo 4a1a6305 --yes # apply it
2139
- mu undo -n 20 # list more groups
2140
- ```
2141
-
2142
- A group id may be given as any unique prefix. Full semantics —
2143
- granularity, undo-as-an-op (which is also "redo"), and the
2144
- superseded-work refusal plus its `--force` override — are in
2145
- [§ Undoing one action](#undoing-one-action-mu-undo).
2146
-
2147
- Two important caveats:
2148
-
2149
- - **Only rows are reverted.** Undo emits inverse ops against the
2150
- portable tables. It does not resurrect a killed tmux pane, an
2151
- agent registry row, or a workspace directory `mu workspace free`
2152
- removed — none of those are portable state, so none of them are
2153
- in the log.
2154
- - **For whole-DB recovery, use `mu rebuild <file>`**, which replays
2155
- the entire ops log into a NEW file. It never writes in place.
2156
-
2157
- ### Workspace orphans (dirs on disk with no DB row)
2158
-
2159
- A `--workspace` spawn that aborted partway, or a manual `rm` of
2160
- `vcs_workspaces` rows, can leave dirs in
2161
- `<state-dir>/workspaces/<workstream>/<agent>/` with no DB row. They're
2162
- invisible to `mu workspace list` but they BLOCK subsequent
2163
- `--workspace` spawns under the same name.
2164
-
2165
- ```bash
2166
- mu state -w <workstream> # 'Workspace orphans' section in yellow
2167
- mu workspace orphans -w <workstream> # focused list + cleanup recipe
2168
- ```
2169
-
2170
- For each orphan, the cleanup is one of:
2171
-
2172
- ```bash
2173
- # git-backed workspace: also prunes the worktree registry
2174
- (cd <project-root> && git worktree remove --force <orphan-path>)
2175
-
2176
- # any backend (last resort)
2177
- rm -rf <orphan-path>
2178
- ```
2179
-
2180
- The `Next:` block from `mu workspace orphans` interpolates the
2181
- actual paths so you can copy-paste.
2182
-
2183
- ### You typo'd a workstream name and want to rename it
2184
-
2185
- The `workstreams.name` column has `ON UPDATE CASCADE` on every
2186
- child-table foreign key, so renaming a workstream is a single SQL
2187
- statement that propagates atomically through `agents`, `tasks`,
2188
- `task_notes`, and `vcs_workspaces`:
2189
-
2190
- ```bash
2191
- # 1. Validate the new name fits the rules (or mu will reject it on
2192
- # next use). Lowercase alpha first, then alnum/_/-, ≤32 chars,
2193
- # no '.' or ':' (tmux mangles them), no 'mu-' prefix.
2194
- # 2. Rename in the DB. Single statement; cascades to every child.
2195
- mu sql "UPDATE workstreams SET name='auth-refactor' WHERE name='auth-refator'"
2196
-
2197
- # 3. Rename the tmux session too (only if it's currently alive).
2198
- tmux rename-session -t mu-auth-refator mu-auth-refactor
2199
- ```
2200
-
2201
- There is no typed `mu workstream rename` verb <!-- doc-cli-drift:skip -->: the
2202
- schema does the work, and wrapping one safe statement buys nothing (no
2203
- atomicity to preserve, no validation to add). The recipe above is the
2204
- canonical answer, and the same `ON UPDATE CASCADE` makes `mu sql`
2205
- renames safe for `tasks.local_id` and `agents.name` too.
2206
-
2207
- ---
2208
-
2209
- ## 15. Cleanup
2210
-
2211
- ### Close individual agents
2212
-
2213
- ```bash
2214
- mu agent close worker-1 # kills pane + drops registry row
2215
- ```
2216
-
2217
- Idempotent: a missing pane and a missing row are both fine.
2218
-
2219
- If the agent has a workspace:
2220
-
2221
- - **Clean** (no uncommitted changes AND no commits since fork) —
2222
- auto-freed alongside the close.
2223
- - **Dirty** — close refuses with `WorkspacePreservedError` (exit 4).
2224
- Either `mu workspace free <agent>` first (optionally `--commit` to
2225
- capture pending changes) then close, or
2226
- `mu agent close <agent> --discard-workspace` to do both (lossy).
2227
-
2228
- ### Tear down the whole workstream
2229
-
2230
- `mu workstream teardown` is the counterpart of `mu workstream init`: it
2231
- kills the workstream's tmux session AND deletes every DB row tagged
2232
- with the workstream name (edges and notes go via FK cascade on tasks).
2233
- The workstream resolves as everywhere else: `--workstream <name>` >
2234
- `$MU_SESSION` > current tmux session (`mu-` prefix stripped).
2235
-
2236
- Two-phase by default: a bare `mu workstream teardown` prints a dry-run
2237
- summary and exits without touching anything. Pass `-y` / `--yes` to
2238
- tear it down.
2239
-
2240
- ```bash
2241
- mu workstream teardown --workstream auth-refactor # dry-run: shows counts, exits
2242
- mu workstream teardown --workstream auth-refactor --yes # actually does it
2243
-
2244
- # Or, from inside the workstream's tmux session:
2245
- mu workstream teardown --yes # workstream auto-detected
2246
-
2247
- # Sweep every empty workstream (zero tasks, agents, vcs_workspaces)
2248
- # in one call. Tmux session presence and audit-only log entries do NOT
2249
- # disqualify. Also surfaces unregistered `mu-*` tmux sessions (test
2250
- # litter, or a partial teardown that dropped the DB row but left the
2251
- # session). ONLY `mu-`-prefixed sessions are touched. Mutually
2252
- # exclusive with -w. Dry-run lists what WOULD go
2253
- # (created_at renders `—` for mux-only entries); --yes tears them down.
2254
- mu workstream teardown --empty # dry-run: table of empties
2255
- mu workstream teardown --empty --yes # tear them all down
2256
- ```
2257
-
2258
- ```
2259
- Workstream auth-refactor (mux session mu-auth-refactor)
2260
- mux session : alive (will be killed)
2261
- agents : 1
2262
- tasks : 1 (edges: 0, notes: 0)
2263
- workspaces : 0
2264
-
2265
- Destroyed auth-refactor: killed mux=true, agents=1, tasks=1, edges=0, notes=0, workspaces=0/0
2266
- ```
2267
-
2268
- Idempotent on every leg: a missing tmux session is fine, zero DB rows
2269
- is fine, and a repeat teardown prints "nothing to tear down" and exits 0.
2270
-
2271
- Destroy writes TOMBSTONE ops rather than erasing history, so the work
2272
- stays in the ops log. `mu undo <group> --yes` reverses the row
2273
- deletions — but not the killed tmux session or freed workspace dirs.
2274
- See
2275
- [§ 14: You ran a destructive verb and want to undo it](#you-ran-a-destructive-verb-and-want-to-undo-it).
2276
-
2277
- The tmux session is killed BEFORE the DB rows so an unexpected tmux
2278
- failure leaves the registry intact (you can retry); if you only want
2279
- the DB cleared, use `mu sql` directly:
2280
-
2281
- ```bash
2282
- mu sql "DELETE FROM tasks
2283
- WHERE workstream_id=(SELECT id FROM workstreams WHERE name='auth-refactor')" # cascades
2284
- mu sql "DELETE FROM agents
2285
- WHERE workstream_id=(SELECT id FROM workstreams WHERE name='auth-refactor')"
2286
- ```
2287
-
2288
- Or nuke the entire DB:
2289
-
2290
- ```bash
2291
- rm ~/.local/state/mu/mu.db # next mu invocation re-creates an empty schema
2292
- ```
2293
-
2294
- ### Before tearing down a workstream
2295
-
2296
- A workstream's task graph + notes IS the project memory.
2297
- `mu workstream teardown` removes the live rows; the ops log keeps them,
2298
- and `mu undo <group> --yes` reverses the row deletions. **A pre-flight
2299
- `mu db backup` is not warranted for a teardown** — the log already is
2300
- the backup.
2301
-
2302
- Finding the group id later is what `--torn-down` is for:
2303
-
2304
- ```bash
2305
- mu workstream list --torn-down
2306
- # name group torn down tasks edges notes
2307
- # gchatui-node 7a40e6cc 52m ago 220 103 714
2308
- # surface-audit 98007873 3d ago 51 51 201 ← recreated since
2309
-
2310
- mu undo 7a40e6cc # preview
2311
- mu undo 7a40e6cc --yes # restore the rows
2312
- ```
2313
-
2314
- One entry per teardown, newest first, read straight from the ops log —
2315
- so a name torn down twice lists twice (the group is the identity, not
2316
- the name) and teardowns from before the 1.2.0 rename are included.
2317
- `← recreated since` means a later op put that name back, so undoing it
2318
- would be a no-op; the printed `Next:` hint always names the newest
2319
- entry that is still restorable. What does NOT come back is what was
2320
- never in the DB: killed panes and freed workspace dirs.
2321
-
2322
- There is no markdown export. Use the typed surfaces instead:
2323
-
2324
- | Need | Verb |
2325
- | ---- | ---- |
2326
- | A safety copy before a destructive change | `mu db backup <file>` (`VACUUM INTO`; never overwrites) |
2327
- | Find a past teardown's group id | `mu workstream list --torn-down` |
2328
- | Reverse a teardown you regret | `mu undo <group> --yes` |
2329
- | Laptop ↔ devserver handoff | Ambient **sync** — set `MU_SYNC_DIR` and every command carries it (§ 15.6) |
2330
- | Peer status / a torn segment | `mu sync`, `mu sync --repair <peer>` |
2331
- | Disaster recovery from the ops log | `mu rebuild <file>` |
2332
-
2333
- Anything else — reading the graph out for grep, review or a git-checked-in
2334
- artifact — goes through `--json` and whatever renderer you prefer:
2335
-
2336
- ```bash
2337
- mu task list -w auth-refactor --json
2338
- mu task notes <id> -w auth-refactor --json
2339
- ```
2340
-
2341
- ---
2342
-
2343
- ## 15.6 Multi-machine sync
2344
-
2345
- **One env var, on every machine.** That is the whole configuration:
2346
-
2347
- ```bash
2348
- export MU_SYNC_DIR=$HOME/Sync/mu # a folder something else keeps in step
2349
- ```
2350
-
2351
- Now every `mu` invocation flushes your ops into
2352
- `$MU_SYNC_DIR/<machine_id>.jsonl` and reads every OTHER `.jsonl` in that
2353
- folder. Nothing else to set up: no peer list, no host names, no daemon.
2354
- Drop a third machine in and it appears.
2355
-
2356
- The end-to-end walkthrough with real output is
2357
- [§ 0.3 Laptop ↔ devserver](#03-laptop--devserver). There is no export
2358
- step, no import step, and no "which side is authoritative" decision.
2359
-
2360
- ### `mu sync` — the status report
2361
-
2362
- Sync already happened on your last command, so `mu sync`'s job is to
2363
- tell you *who your peers are and whether transport is working*:
2364
-
2365
- ```console
2366
- $ mu sync
2367
- flushed 14 ops · ingested 22 from 1 peer
2368
- ┌──────────┬───────────┬────────┬─────────┐
2369
- │ machine │ last seen │ behind │ │
2370
- ├──────────┼───────────┼────────┼─────────┤
2371
- │ 7f3a91c2 │ 2m ago │ 0 │ │
2372
- ├──────────┼───────────┼────────┼─────────┤
2373
- │ c8ec0feb │ 3d ago │ 47 │ ← stale │
2374
- └──────────┴───────────┴────────┴─────────┘
2375
- Next:
2376
- Pull fresh segments (1 stale peer) : rsync -av <host>:/home/me/Sync/mu/ /home/me/Sync/mu/
2377
- Or stop doing it by hand : # share /home/me/Sync/mu with Syncthing
2378
- ```
2379
-
2380
- Machines are named by `machine_id` prefix, not hostname — a hostname is
2381
- machine-local and never travels. `behind` is how many lines of that
2382
- peer's segment you *hold but have not applied*; non-zero means either a
2383
- transfer caught mid-flight (it resolves itself) or a defect that
2384
- stopped the read (see `--repair`).
2385
-
2386
- Two flags, and only two:
2387
-
2388
- ```bash
2389
- # Read a peer's DB directly — an sshfs mount, or a file you scp'd.
2390
- # A different READER: its SQLite ops table, not a JSONL segment.
2391
- mu sync --from /mnt/devserver/.local/state/mu/mu.db
2392
-
2393
- # Re-read a peer's segment from zero, after a torn transfer.
2394
- # Safe to run any time: ingest is idempotent.
2395
- mu sync --repair c8ec
2396
- ```
2397
-
2398
- A one-off directory needs no flag at all — the env var already does it:
2399
-
2400
- ```bash
2401
- MU_SYNC_DIR=/media/usb-stick mu state # ingest from a USB stick
2402
- ```
2403
-
2404
- ### Transport is yours
2405
-
2406
- mu reads and writes files. It never runs `ssh`, `scp`, or `rsync` for
2407
- you — that would mean owning ssh config, jump hosts, `ProxyCommand`,
2408
- non-standard ports, identity files, and password prompts. When a peer
2409
- looks stale, mu prints the command and you paste it.
2410
-
2411
- Pick any file-mover; segments are **append-only and
2412
- single-writer-per-file**, so anything works:
2413
-
2414
- | Tier | How | Notes |
2415
- | ---- | --- | ----- |
2416
- | **Recommended: Syncthing** | Share `$MU_SYNC_DIR` on every machine | Continuous, peer-to-peer, no server, no cloud account. Its `*.sync-conflict-*` copies are ingested rather than ignored — they are still valid op logs, and dedupe makes reading them free. |
2417
- | `rsync` in a loop or cron | `rsync -av host:$MU_SYNC_DIR/ $MU_SYNC_DIR/` | Fine. Run it in both directions, or one way from each box. |
2418
- | sshfs / NFS mount of the FOLDER | Point `MU_SYNC_DIR` at the mount | OK for segments. **Never** for `MU_DB_PATH`. |
2419
- | Fully manual (`scp`, USB stick) | Copy the `.jsonl` files whenever | Still converges, just less often. |
2420
-
2421
- Manual copying is a first-class tier, not a fallback:
2422
-
2423
- 1. **Direction-free.** No source-vs-target decision. Copy either way,
2424
- or both ways. You converge.
2425
- 2. **Idempotent.** Copying the same file ten times equals copying it
2426
- once, so re-copy freely when unsure.
2427
- 3. **Interruption-safe.** A killed `scp` leaves a truncated last line
2428
- that the parser skips; the next copy completes it.
2429
-
2430
- ### Two things not to do
2431
-
2432
- **Never put `MU_DB_PATH` inside `MU_SYNC_DIR`.** THE footgun of the
2433
- design. `mu doctor` reports it, with the full remediation paragraph:
2434
-
2435
- ```console
2436
- $ mu doctor
2437
- fleet
2438
- db-vs-sync : FAIL DB is INSIDE MU_SYNC_DIR — this WILL corrupt it (/tmp/mu-sync-demo/mu.db)
2439
- ```
2440
-
2441
- A live WAL-mode SQLite database is three files (`mu.db`, `-wal`,
2442
- `-shm`) whose mutual consistency *is* its durability. A file-syncer
2443
- copies them whole-file and out of order, and will resurrect a peer's
2444
- stale `-wal` — yielding a database that opens fine and is silently
2445
- corrupt. Two machines writing the same synced file is worse:
2446
- last-writer-wins on the whole FILE, so one machine's entire history
2447
- disappears. Keep the DB on local disk; only the regenerable segments
2448
- travel.
2449
-
2450
- **Avoid iCloud Drive and Dropbox for `MU_SYNC_DIR` on macOS.** Both do
2451
- *dataless-placeholder eviction*: an untouched file becomes a stub, and
2452
- the `stat()` that materialises it blocks on the network. mu stats the
2453
- sync dir at the top of *every* command, so `mu task add` becomes a
2454
- multi-second hang — or a failure when offline. Syncthing keeps real
2455
- bytes on disk. (Google Drive File Stream and OneDrive Files On-Demand
2456
- have the same problem.)
2457
-
2458
- ### What does and does not travel
2459
-
2460
- Only **portable** state syncs: workstreams, tasks, edges, notes. Ops
2461
- about **agents** and **workspaces** are recorded locally — `mu log`
2462
- still shows them — but never leave the machine, because a `pane_id`
2463
- like `%17` and a path like `/home/me/...` are meaningless (and often
2464
- actively wrong) on another box.
2465
-
2466
- One consequence: **task ownership does not sync.**
2467
- `tasks.owner_id` points into the machine-local `agents` table, so a
2468
- task claimed by `worker-2` on the devserver shows as unowned on your
2469
- laptop. That is correct — `worker-2` is a tmux pane that does not exist
2470
- there.
2471
-
2472
- ### Concurrent editing
2473
-
2474
- Edit one workstream on two machines at once. Ops are semantic partial
2475
- updates merged by **per-field last-writer-wins**, so a devserver crew
2476
- closing a task while you re-prioritise it on the laptop keeps *both*
2477
- edits:
2478
-
2479
- ```bash
2480
- # laptop # devserver
2481
- mu task update t1 --impact 95 -w app mu task close t1 -w app
2482
- # after both sync: impact=95 AND status=CLOSED, on both machines
2483
- ```
2484
-
2485
- Only the same *field* of the same row edited on two machines needs a
2486
- winner, and the newer HLC takes it.
2487
-
2488
- ---
2489
-
2490
- ## 15.7 Upgrading a v7, v8, v9, or v10 DB
2491
-
2492
- `mu` refuses to write a pre-v11 DB and leaves it untouched. Use the
2493
- single retained sidecar against a backup:
2494
-
2495
- ```bash
2496
- DB=${MU_DB_PATH:-$HOME/.local/state/mu/mu.db}
2497
- BACKUP="$HOME/mu-old-backup-$(date +%Y%m%d-%H%M%S).db"
2498
- sqlite3 "$DB" ".backup '$BACKUP'"
2499
- npx tsx scripts/migrate.ts "$BACKUP" --out "${DB}.v11"
2500
- MU_DB_PATH="${DB}.v11" mu doctor --deep
2501
- mv "$DB" "${DB}.old-kept" && mv "${DB}.v11" "$DB"
2502
- mu doctor
2503
- ```
2504
-
2505
- The script auto-detects v7, v8, v9, v10, or v11, opens it read-only, compares
2506
- its SHA-256 before and after, and never overwrites a target unless
2507
- `--force` is explicit. v9/v10 history is retained unchanged; legacy
2508
- statuses map onto substates (`REJECTED` → `CLOSED/rejected`, `DEFERRED` →
2509
- `OPEN/parked`) while projecting into v11, and no migration notes are
2510
- written. Where an undo or a note hid the legacy status, the script
2511
- records a `migrate.substate` op; `--recover <db>` reruns that recovery
2512
- on an already-migrated DB. Valid v9 agents, workspaces, owners, peer
2513
- watermarks, and machine identity carry across, but pane ids and absolute
2514
- workspace paths cannot be proven live until `mu doctor` reconciles them.
2515
- The v7/v8 path keeps the older conservative omissions for agents and
2516
- workspaces, and restores pre-1.0 `archived_*` rows as live workstreams
2517
- by default (`--drop-archives` to skip).
2518
-
2519
- **Full recipe, every flag, and the precise carry/omit rules:
2520
- [scripts/README.md](../scripts/README.md).**
2521
-
2522
- ---
2523
-
2524
- ## 16. One-shot demo script
2525
-
2526
- End-to-end against a throwaway DB, so your real registry is untouched.
2527
-
2528
- ```bash
2529
- export MU_DB_PATH=/tmp/mu-demo.db
2530
- tmux kill-session -t mu-demo 2>/dev/null
2531
-
2532
- # Plan
2533
- mu workstream init demo
2534
- mu task add design --title "Design" -w demo --impact 80 --effort-days 2
2535
- mu task add build --title "Build" -w demo --impact 80 --effort-days 5 --blocked-by design
2536
- mu task add ship --title "Ship" -w demo --impact 90 --effort-days 1 --blocked-by build
2537
-
2538
- # Crew
2539
- mu agent spawn worker-1 --workstream demo --cli sh
2540
- mu agent spawn worker-2 --workstream demo --cli sh
2541
-
2542
- # Assign + observe
2543
- mu task claim design -w demo --for worker-1 --evidence "demo assignment"
2544
- mu state -w demo
2545
-
2546
- # Watch live (Ctrl+b d to detach)
2547
- tmux attach -t mu-demo
2548
-
2549
- # Cleanup
2550
- mu workstream teardown --workstream demo --yes
2551
- rm -f /tmp/mu-demo.db
2552
- ```
2553
-
2554
- ---
2555
-
2556
- ## Mental model in three sentences
2557
-
2558
- 1. **One workstream is one tmux session full of agent panes.** Mu
2559
- manages the lifecycle; tmux is the substrate. Workstreams on the
2560
- same machine are isolated by `session_id` in the SQLite registry.
2561
-
2562
- 2. **The task DAG decides what can be worked on.** The `Ready` table +
2563
- parallel-tracks union-find answer "what's next?" and "what can I
2564
- parallelize?" deterministically. Diamond patterns (two goals sharing
2565
- a prerequisite) collapse into one merged track so two agents never
2566
- collide on shared deps.
2567
-
2568
- 3. **Agents claim tasks via their pane title — zero config.**
2569
- `mu task claim foo` from inside `worker-1`'s pane sets the task's
2570
- `owner_id` to the `worker-1` agent row atomically. mu reads the pane
2571
- title via `tmux display-message -t $TMUX_PANE -p '#{pane_title}'`,
2572
- set on spawn. Two agents cannot claim the same task.
2573
-
2574
- Everything else (`mu sql`, send/read, the bracketed-paste protocol,
2575
- agent-state sources, ghost reconciliation) is plumbing in service of those three.
2576
-
2577
- ---
2578
-
2579
- ## What's NOT in mu (and how to work around it)
2580
-
2581
- The full roadmap with promotion criteria lives in
2582
- [ROADMAP.md](ROADMAP.md). The short list of gaps you might hit
2583
- in real use:
2584
-
2585
- | Want | Workaround | Status |
2586
- | --------------------------------------------- | ----------------------------------------------------------------------- | ------------- |
2587
- | Pi extension (typed tools, HUD, wakeups) | `mu state --tui` (interactive) covers the dashboard use-case; plain `mu state` (static) is the `watch` / `tmux display-popup` / `status-right` substrate. Other extension tools deferred. | partially shipped |
2588
- | Markdown agent-definition discovery | Spawn accepts `--cli` and `--command` directly; no template registry | dropped |
2589
- <!-- doc-cli-drift:skip-start -->
2590
- | `mu run script.ts` (JS DSL) | Use `--json` + bash + jq | rejected |
2591
- | Sync to GitHub Issues / Linear / Asana | Not in scope; explicitly rejected | — |
2592
- | ~~`mu task blocked`~~ (removed; the `blocked` SQL view is the abstraction) | `mu sql "SELECT b.local_id, b.status, b.title FROM blocked b JOIN workstreams w ON w.id=b.workstream_id WHERE w.name='X'"` | removed-with-recipe |
2593
- | ~~`mu task goals`~~ (removed; same shape as `blocked` — view is the abstraction) | `mu sql "SELECT g.local_id, g.status, g.title FROM goals g JOIN workstreams w ON w.id=g.workstream_id WHERE w.name='X'"` | removed-with-recipe |
2594
- | ~~`mu task search <pat>`~~ (removed; case-insensitive LIKE is one SQL line) | `mu sql "SELECT t.local_id, t.status, t.title FROM tasks t JOIN workstreams w ON w.id=t.workstream_id WHERE w.name='X' AND LOWER(t.title) LIKE '%pat%'"` (add `LEFT JOIN task_notes` for the old `--in-notes`; drop the workstream join/filter for the old `--all`) | removed-with-recipe |
2595
- <!-- doc-cli-drift:skip-end -->
2596
-
2597
- Anything in this table that bites you in real use is a candidate
2598
- for **promotion**. Criteria: proven friction in ≥2 real workflows +
2599
- fits in <300 LOC + no major refactor of the load-bearing pillars.
2600
- The most useful feedback is "I tried to do X and had to fall back
2601
- to `mu sql`, twice in one session". File it in [ROADMAP.md](ROADMAP.md).
2602
-
2603
- ---
2604
-
2605
- ## Where to go from here
2606
-
2607
- | Doc | What's in it |
2608
- | -------------------------------------------- | ------------------------------------------------------- |
2609
- | [README.md](../README.md) | Project overview, install, comparison vs hidden subagents |
2610
- | [CHANGELOG.md](../CHANGELOG.md) | Release notes |
2611
- | [ROADMAP.md](ROADMAP.md) | What's next, with promotion criteria + rejected ideas |
2612
- | [VOCABULARY.md](VOCABULARY.md) | Canonical terms — source of truth for every word |
2613
- | [VISION.md](VISION.md) | The eight load-bearing pillars + design principles |
2614
- | [ARCHITECTURE.md](ARCHITECTURE.md) | Module map, reconciliation algorithm, layered design |
2615
- | [skills/mu/SKILL.md](../skills/mu/SKILL.md) | What an LLM running inside an agent pane sees |
2616
-
2617
- If you're trying mu and something doesn't work as documented, file an
2618
- issue with: the exact `mu` command, the full output (set
2619
- `MU_DB_PATH=/tmp/mu-debug.db` to isolate from your real registry),
2620
- your multiplexer version (the `environment` block of `mu doctor`), and
2621
- your platform.
2622
-
2623
- ---
2624
-
2625
- ## 20. Multiplexer backends (tmux and herdr)
2626
-
2627
- mu drives exactly one multiplexer per invocation. tmux is the
2628
- incumbent; [herdr](https://github.com/herdrdev/herdr) is the second
2629
- backend. Spawn, send, and read work on both; agent state comes from herdr
2630
- on herdr and from murmur on tmux. The
2631
- remaining gaps are narrow and listed under
2632
- [Known limits on herdr](#known-limits-on-herdr) — the notable one is
2633
- that `mu agent kick` is Linux-only there.
2634
-
2635
- ### Which backend am I on?
2636
-
2637
- ```bash
2638
- mu doctor # the `environment` block names the active backend
2639
- mu doctor --json # .environment.mux = { name, ok, version, env, remediation }
2640
- ```
2641
-
2642
- `.environment.tmux` is kept in `--json` as an alias reporting the
2643
- *active* backend, so existing scripts keep working. Read
2644
- `.environment.mux.name` if you care which one it is.
2645
-
2646
- ### The detection ladder
2647
-
2648
- 1. `MU_MUX=<name>` — explicit override, wins over everything. An
2649
- unknown value fails the invocation (`error: unknown mux backend:
2650
- nope`, exit 1) rather than quietly running on tmux.
2651
- 2. `HERDR_ENV=1` → herdr. Compared literally against `1`, as herdr
2652
- documents.
2653
- 3. `$TMUX` or `$TMUX_PANE` set → tmux. Either one proves the caller is
2654
- in a tmux pane; some setups (`sudo -E`, ssh with a restrictive
2655
- `SendEnv`) pass one and not the other.
2656
- 4. Availability — whichever binary actually runs, tmux first.
2657
- 5. `NoMultiplexerError`, exit 5.
2658
-
2659
- Rung 2 outranks rung 3 because it is the narrower claim: herdr panes
2660
- routinely host a tmux server, so both signals can be live at once, and
2661
- only `HERDR_ENV` says "herdr manages *this* pane". Rungs 2–3 answer
2662
- "which mux is the caller in", rung 4 answers "which mux works here" —
2663
- mu can create a detached session from a plain shell, so rung 4 alone
2664
- is enough to operate.
2665
-
2666
- The backend is resolved once per process and cached.
2667
-
2668
- ### What differs between the two
2669
-
2670
- | | tmux | herdr |
2671
- | --- | --- | --- |
2672
- | Mux session (holds one workstream) | tmux session `mu-<ws>` | herdr *workspace* labelled `mu-<ws>`. herdr's own "session" is server-level and is NOT the workstream unit. |
2673
- | Window | tmux window | herdr tab |
2674
- | Agent | pane | pane |
2675
- | Pane id | `%15` | `w1:p1` (workspace-qualified, so a pane moved between workspaces gets a new id) |
2676
- | Pane title | `select-pane -T` | `herdr pane rename` writes the pane *label* |
2677
- | Attach hint | `tmux a -t mu-<ws>` | `herdr session attach mu-<ws>` |
2678
- | Pane borders | 4-side border showing agent name + status glyph | no-op — herdr owns its pane chrome; mu-managed panes carry the label instead |
2679
- | Layout | `select-layout` | no-op — herdr splits are explicit, geometry is yours |
2680
- | Agent state | pi agents: the control socket. Others: murmur pane options for local agents; `murmur status --json` for remote agents | pi agents: the control socket. Others: herdr reports state via `paneStatus()` |
2681
- | Focus | mu creates detached | `--no-focus` on every mutating call, always. `detached: false` still gets you a detached workspace; run `herdr workspace focus` yourself. |
2682
- | Isolation seam | `MU_TMUX_SOCKET` (`-L <name>`) | `MU_HERDR_SESSION` (`--session <name>`, its own socket) |
2683
- | `mu agent send` / `read` | six-step paste/Enter protocol | one atomic `agent prompt --wait` |
2684
- | `mu agent spawn` | one `new-window`/`split-window` carrying the command | create-then-start: bare pane, then `agent start` |
2685
-
2686
- `MU_TMUX_SOCKET` is tmux-only and ignored under herdr;
2687
- `MU_HERDR_SESSION` is its herdr analogue. Both exist so a test run can
2688
- never observe or destroy your real panes.
2689
-
2690
- ### Known limits on herdr
2691
-
2692
- - **Spawn is two steps, not one.** herdr has no create-and-run form, so
2693
- mu creates a bare pane and then calls `agent start` in it. The
2694
- creation verbs refuse a command rather than silently dropping it —
2695
- dropping would leave an empty shell that mu records as an agent,
2696
- which is the worst available failure. If step two fails, mu closes
2697
- the pane and writes no agent row.
2698
- - **`--lines` cannot recover scrolled-off rows from an
2699
- alternate-screen pane.** A pane on the alternate screen does not
2700
- spill into host scrollback, so there is nothing behind the viewport
2701
- to read.
2702
- - **`mu agent kick` is Linux-only on herdr.** herdr reports a shell
2703
- pid, not a tty, so `paneTTY` resolves `/proc/<pid>/fd/0`. On macOS
2704
- that path does not exist and the call fails with a `HerdrError`
2705
- naming the limitation rather than lying about the tty.
2706
- - **`herdr pane list` has no foreground-command field**, so a pane's
2707
- command reads as empty from a listing. The authoritative answer
2708
- costs one `herdr pane process-info` round trip per pane. mu does not
2709
- fake it.
2710
- - **`MU_<UPPER_CLI>_COMMAND` has no herdr equivalent.** herdr resolves
2711
- the agent binary itself by kind. The override is honoured on tmux
2712
- only; on herdr it stays moot while spawn is unimplemented, and the
2713
- spawn work has to either pass it through or refuse it explicitly.
2714
-
2715
- ### A degraded backend reports, it does not crash
2716
-
2717
- With no herdr server running, `mu doctor` still prints a full report
2718
- (exit 0) and any verb that genuinely needs the mux fails with exit 5
2719
- and herdr's own message plus remediation steps:
2720
-
2721
- ```bash
2722
- MU_MUX=herdr mu doctor # exit 0; `herdr: ok (0.9.0)` from the client
2723
- MU_MUX=herdr mu agent list -w foo # exit 5: no herdr server is running at ...
2724
- MU_MUX=herdr mu task claim t1 -w foo --self # exit 0 — identity is best-effort
2725
- ```
2726
-
2727
- That last line is the load-bearing / best-effort split. Verbs that ARE
2728
- the mux (spawn, send, kill, reconcile, session create/destroy) fail
2729
- loudly. Verbs where the mux is decoration (identity fallback, pane
2730
- titles, banners, `mu workstream list`) degrade and carry on. Reconcile
2731
- is pointedly in the first group: treating an unreachable mux as "zero
2732
- panes" would prune every agent as a ghost.
2733
-
2734
- One error is deliberately outside that scheme. herdr returns argument
2735
- errors with exit 2, which mu raises as `HerdrSyntaxError` — not a
2736
- `MuxError`. That is CLI drift after a herdr upgrade, i.e. a bug in mu,
2737
- and bucketing it as "herdr is down" would send you chasing a healthy
2738
- server.