@martintrojer/mu 0.3.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.
@@ -0,0 +1,1631 @@
1
+ # mu — Usage Guide
2
+
3
+ A practical, copy-pasteable tour of mu (current main; v0.3-track).
4
+ Everything below works against the built CLI. Terms are canonical
5
+ — see [VOCABULARY.md](VOCABULARY.md) for definitions; the complete
6
+ current verb list is in `## CLI — complete verb list` of
7
+ [skills/mu/SKILL.md](../skills/mu/SKILL.md).
8
+
9
+ > **Status:** v0.3 wave (pre-1.0). ~60 typed verbs across 8
10
+ > namespaces (`workstream`, `agent`, `task`, `workspace`, `log`,
11
+ > `snapshot`, `archive`, `me`) plus bare top-level verbs
12
+ > (`state`, `doctor`, `sql`, `undo`, `adopt`). Every verb accepts
13
+ > `--json` (one allow-listed exception, `mu agent attach`),
14
+ > per-agent VCS workspaces (jj/sl/git/none), activity log with
15
+ > `--tail` subscription, canonical state card (`mu state` —
16
+ > default / `--hud` / `--mission` render modes), whole-DB
17
+ > snapshots auto-captured before destructive verbs +
18
+ > `mu undo` / `mu snapshot {list,show}`, evidence on lifecycle
19
+ > verbs, schema v7 (v5 surrogate INTEGER PKs + per-workstream
20
+ > UNIQUE on operator-facing names; v6 added the `archive_*`
21
+ > family additively; v7 dropped the dead `approvals` table).
22
+ > See [CHANGELOG.md](../CHANGELOG.md) for the release entry,
23
+ > and [§ Not in 0.3.0](#whats-not-in-030-and-how-to-work-around-it)
24
+ > at the bottom for the gaps that still need workarounds.
25
+
26
+ *If anything below disagrees with `mu --help`, trust `mu --help`.*
27
+
28
+ ---
29
+
30
+ ## Table of contents
31
+
32
+ 1. [Setup](#1-setup)
33
+ 2. [Get oriented (`mu doctor`)](#2-get-oriented)
34
+ 3. [Create a workstream (`mu workstream init`)](#3-create-a-workstream)
35
+ 4. [Plan some work as a DAG (`mu task add`)](#4-plan-some-work-as-a-dag)
36
+ 5. [See the graph (mission control)](#5-see-the-graph-mission-control)
37
+ 6. [Spawn a crew (`mu agent spawn`)](#6-spawn-a-crew)
38
+ 7. [Watch the crew live (`tmux attach`)](#7-watch-the-crew-live)
39
+ 8. [Send work to an agent (`mu agent send`)](#8-send-work-to-an-agent)
40
+ 9. [Read what an agent did (`mu agent read`)](#9-read-what-an-agent-did)
41
+ 10. [The claim protocol from inside a pane (`mu task claim`)](#10-the-claim-protocol--from-inside-an-agents-pane)
42
+ 11. [Drop notes (durable context) (`mu task note`)](#11-drop-notes-durable-context)
43
+ 12. [Close out a task](#12-close-out-a-task)
44
+ 13. [The SQL escape hatch (`mu sql`)](#13-the-sql-escape-hatch-is-your-friend)
45
+ 14. [Recovery scenarios](#14-recovery-scenarios)
46
+ 15. [Cleanup](#15-cleanup)
47
+ 15.5. [Archives — cross-workstream preservation](#155-archives--cross-workstream-preservation-of-task-graphs)
48
+ 16. [One-shot demo script](#16-one-shot-demo-script)
49
+ 17. [Mental model in three sentences](#mental-model-in-three-sentences)
50
+ 18. [What's NOT in 0.3.0](#whats-not-in-030-and-how-to-work-around-it)
51
+ 19. [Where to go from here](#where-to-go-from-here)
52
+
53
+ ---
54
+
55
+ ## 1. Setup
56
+
57
+ From npm (the common path):
58
+
59
+ ```bash
60
+ npm install -g @martintrojer/mu
61
+ mu --version # → the current version
62
+ ```
63
+
64
+ Update later via `npm install -g @martintrojer/mu@latest`.
65
+
66
+ From a local checkout (when hacking on mu itself):
67
+
68
+ ```bash
69
+ npm install -g . # `prepare` script auto-builds; `mu` lands on $PATH
70
+ mu --version # → the current version (see package.json)
71
+ ```
72
+
73
+ To update the source-installed copy: pull from upstream, then
74
+ `npm install -g .` from inside the checkout. The `prepare` script
75
+ rebuilds before linking the new dist/.
76
+
77
+ ### Install the bundled skill
78
+
79
+ Mu ships a skill at `skills/mu/SKILL.md` that teaches the LLM
80
+ running inside an agent pane how to use mu (the in-pane working
81
+ loop, the subscribe-vs-poll pattern, the verb-list reference).
82
+ The canonical install path is the
83
+ [skills CLI](https://github.com/vercel-labs/skills) — it auto-
84
+ detects every supported agent on your system (pi, claude-code,
85
+ codex, opencode, cursor, ...) and installs into the right per-agent
86
+ location:
87
+
88
+ ```bash
89
+ npx skills add martintrojer/mu # interactive: pick scope + agents
90
+ npx skills add martintrojer/mu -g -y # global, no prompts (pi: ~/.pi/agent/skills/mu/)
91
+ npx skills update mu # later, to refresh
92
+ ```
93
+
94
+ If you installed mu from a local checkout (hacking on the skill
95
+ itself), point the skills CLI at the checkout instead so edits flow
96
+ straight through:
97
+
98
+ ```bash
99
+ npx skills add ./skills/mu # local-path source format (symlinks)
100
+ ```
101
+
102
+ If you'd rather not use the skills CLI, mu's skill is just a
103
+ directory with a `SKILL.md` — symlink or copy it into the agent's
104
+ skills dir directly. For pi, that's `~/.pi/agent/skills/mu/` (per-
105
+ user global) or `.pi/skills/mu/` (per-project). The convention
106
+ `~/.agents/skills/mu/` (cross-tool location) is also picked up by
107
+ pi and several other agents:
108
+
109
+ ```bash
110
+ # From an npm-global install
111
+ mkdir -p ~/.agents/skills
112
+ ln -sf "$(npm root -g)/@martintrojer/mu/skills/mu" ~/.agents/skills/mu
113
+
114
+ # Or from a checkout
115
+ ln -sf "$PWD/skills/mu" ~/.agents/skills/mu
116
+ ```
117
+
118
+ ### For mu hackers: alias to the build output
119
+
120
+ If you're hacking on mu itself and want fastest iteration, alias
121
+ directly to the build output instead:
122
+
123
+ ```bash
124
+ npm install # deps only
125
+ npm run build # produces dist/
126
+ alias mu="node $PWD/dist/cli.js"
127
+ ```
128
+
129
+ See [README.md § Install](../README.md#install) for the full set of
130
+ install patterns.
131
+
132
+ mu requires tmux ≥ 3.0. Make sure you're inside a tmux session before
133
+ proceeding:
134
+
135
+ ```bash
136
+ tmux # if you're not already in one
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 2. Get oriented
142
+
143
+ ```bash
144
+ mu doctor
145
+ ```
146
+
147
+ Expected output:
148
+
149
+ ```
150
+ mu doctor
151
+ tmux: ok (tmux 3.6a)
152
+ $TMUX: set
153
+ db: ok (/Users/you/.mu/mu.db)
154
+ workstream: none (pass --workstream or run inside an mu-<name> tmux session)
155
+ ```
156
+
157
+ The "workstream: none" line is expected — we haven't joined one yet.
158
+
159
+ Get the full command list:
160
+
161
+ ```bash
162
+ mu --help
163
+ ```
164
+
165
+ Every verb's `--help` is exhaustive (flags, defaults,
166
+ interactions). Every successful invocation also prints a dim
167
+ `Next:` block of suggested follow-up commands at the bottom —
168
+ you never have to leave the terminal to learn what to do next.
169
+
170
+ Every verb accepts `--json` for machine-readable output. Errors
171
+ in `--json` mode emit a `{ error, message, nextSteps, exitCode }`
172
+ record to stderr; the `nextSteps` array carries actionable
173
+ resolutions you can `eval` directly. (One verb opts out:
174
+ `mu agent attach`, which prints a `tmux attach` command for a
175
+ human to copy.)
176
+
177
+ ### CLI conventions: validation errors
178
+
179
+ Every operator-error path — missing required option, unknown option,
180
+ unknown subcommand, missing positional, type-coercion failure, mutex
181
+ flags, range checks — produces a uniform surface:
182
+
183
+ - **Human path**: red `error: <msg>` on stderr, then the failing
184
+ subcommand's `--help` block (same text as `mu <verb> --help`),
185
+ then exit **2**.
186
+ - **`--json` path**: a structured envelope on stderr:
187
+
188
+ ```json
189
+ {
190
+ "error": "UsageError",
191
+ "message": "--self and --for are mutually exclusive",
192
+ "nextSteps": [],
193
+ "exitCode": 2,
194
+ "usage": {
195
+ "command": "mu task claim",
196
+ "synopsis": "mu task claim [options] <id>",
197
+ "description": "...",
198
+ "args": [{"name": "id", "required": true, "variadic": false, "description": ""}],
199
+ "options": [{"flags": "--self", "description": "...", "mandatory": false, "valueRequired": false}, ...]
200
+ }
201
+ }
202
+ ```
203
+
204
+ `usage.options[].mandatory` is `true` when the operator MUST pass
205
+ the option (`.requiredOption()` in commander terms). `valueRequired`
206
+ is `true` when the option's argument can't be omitted if the flag
207
+ IS passed (`<value>` form vs bare flag). The two are independent.
208
+
209
+ Exit 2 is the consistent code for the whole operator-error class —
210
+ commander mistakes and handler-thrown `UsageError`s alike. Other
211
+ classes keep their own codes (3 = not found, 4 = conflict, 5 =
212
+ substrate, 6 = reaper, 7 = stall).
213
+
214
+ ### CLI conventions: `--json` collection envelope
215
+
216
+ Collection-read verbs emit a canonical `{items: T[], count: number}`
217
+ shape on stdout:
218
+
219
+ ```bash
220
+ $ mu task list -w foo --json
221
+ {"items":[{"name":"a",...},{"name":"b",...}],"count":2}
222
+ ```
223
+
224
+ `count` is `items.length` pre-computed so `jq '.count'` is one less
225
+ hop than `jq '.items | length'`. Future siblings (e.g. `baseRef` on
226
+ `mu workspace commits`, `behindCount` on `mu workspace list`) layer
227
+ on without breaking the existing two fields.
228
+
229
+ Applies to: `mu task list / next / owned-by / notes`,
230
+ `mu workstream list`, `mu workstream destroy --empty` (dry-run),
231
+ `mu archive list / search`, `mu workspace list / orphans / commits`,
232
+ `mu snapshot list`, `mu log -n N` (read).
233
+
234
+ Two deliberate carve-outs:
235
+ - **`mu sql --json`** keeps bare-array rows. The verb is the typed-
236
+ escape hatch; row shape is per-query, not part of the typed
237
+ contract.
238
+ - **`mu log --tail --json`** emits NDJSON (one JSON object per line)
239
+ because it's a stream, not a collection. Stream consumers want one
240
+ envelope per row, not a single envelope that grows forever.
241
+
242
+ Singleton verbs (`mu task show`, `mu agent show`, `mu workstream
243
+ init`, `mu task close`, ...) keep their existing object envelopes
244
+ with named top-level fields (`{task, blockers, dependents, notes}`,
245
+ `{taskName, ..., nextSteps}`, etc.). The `items + count` envelope is
246
+ for collection reads only.
247
+
248
+ ### CLI conventions: multi-value flags
249
+
250
+ Multi-value flags accept either repeated invocations
251
+ (`--blocked-by a --blocked-by b`) or a comma-separated value
252
+ (`--blocked-by a,b`) or any mix (`--blocked-by a,b --blocked-by c`).
253
+ All three forms collapse to the same list internally. The
254
+ syntactic signal is `<value...>` in the help-text metavar (the
255
+ triple-dot); the parenthetical "repeat or comma-separate; or both"
256
+ reinforces it. Variadic positionals (e.g. `mu task wait a b c`) keep
257
+ their Unix-style space-separated shape — operands are not commas.
258
+ Single-valued flags (`-w`, `--by`, `--title`, ...) stay single. The
259
+ `--status` filter on `mu task list` and `mu task next` accepts the
260
+ same multi-value shape (`--status OPEN,IN_PROGRESS`,
261
+ `--status OPEN --status CLOSED`, or any mix) and returns the union.
262
+ Missing `--status` keeps today's no-filter shape (no auto-default).
263
+ `mu task wait --status` stays single — the verb means "wait until
264
+ reaches THIS status".
265
+
266
+ ---
267
+
268
+ ## 3. Create a workstream
269
+
270
+ A **workstream** is mu's unit of organization. One workstream = one
271
+ tmux session = one logical project. Multiple workstreams on the same
272
+ machine are isolated (partitioned in the SQLite registry by
273
+ `session_id`); they never see each other's agents.
274
+
275
+ ```bash
276
+ mu workstream init auth-refactor
277
+ ```
278
+
279
+ ```
280
+ Created workstream auth-refactor (tmux session mu-auth-refactor)
281
+ Attach with: tmux a -t mu-auth-refactor
282
+ Spawn agents with: mu agent spawn <name> -w auth-refactor
283
+ ```
284
+
285
+ Behind the scenes: `tmux new-session -d -s mu-auth-refactor` plus a
286
+ placeholder window so the session is non-empty. The session sits there
287
+ detached, waiting for agents.
288
+
289
+ To see what's already on the machine before picking a name:
290
+
291
+ ```bash
292
+ mu workstream list
293
+ ```
294
+
295
+ ```
296
+ ┌──────┬───────┬────────┬───────┬───────┬───────┐
297
+ │ name │ tmux │ agents │ tasks │ edges │ notes │
298
+ ├──────┼───────┼────────┼───────┼───────┼───────┤
299
+ │ r6a │ alive │ 0 │ 2 │ 1 │ 1 │
300
+ │ r6b │ — │ 0 │ 0 │ 0 │ 0 │
301
+ └──────┴───────┴────────┴───────┴───────┴───────┘
302
+ ```
303
+
304
+ The list is the **union** of three sources: distinct
305
+ `agents.workstream`, distinct `tasks.workstream`, and tmux sessions
306
+ matching `mu-*`. So a freshly-`init`'d workstream with no tasks/agents
307
+ still shows up (via its tmux session), and a workstream whose tmux
308
+ session was killed externally still shows up (via its surviving DB
309
+ rows) so you can `mu workstream destroy` to clean up properly.
310
+
311
+ ### How mu finds your active workstream
312
+
313
+ Every command after `init` needs to know which workstream you're in.
314
+ Resolution order, first match wins:
315
+
316
+ 1. **`--workstream <name>` flag** explicitly
317
+ 2. **`MU_SESSION` env var** (`export MU_SESSION=auth-refactor`)
318
+ 3. **Current tmux session name** (mu reads `tmux display-message -p '#S'` and strips the `mu-` prefix)
319
+ 4. Error if none of the above
320
+
321
+ The third option is the most ergonomic. Once you `tmux a -t
322
+ mu-auth-refactor`, every command "just works" without flags.
323
+
324
+ ---
325
+
326
+ ## 4. Plan some work as a DAG
327
+
328
+ Tasks have **mandatory** `impact` (1–100) and `effort-days` (>0).
329
+ Edges are blocks-relationships, modelled as **`--blocked-by`** on `mu
330
+ task add` (and `mu task reparent`): `--blocked-by design` means "this
331
+ task is blocked by `design`; it can't start until `design` closes."
332
+ Tasks are **scoped to a workstream** — mission control only shows
333
+ tasks for the workstream you're in.
334
+
335
+ ```bash
336
+ # --workstream can be omitted if you're inside the workstream's tmux
337
+ # session or have $MU_SESSION exported.
338
+ mu task add design \
339
+ --workstream auth-refactor \
340
+ --title "Design auth module" \
341
+ --impact 80 --effort-days 2
342
+
343
+ mu task add build \
344
+ --workstream auth-refactor \
345
+ --title "Build auth module" \
346
+ --impact 80 --effort-days 5 \
347
+ --blocked-by design
348
+
349
+ mu task add review \
350
+ --workstream auth-refactor \
351
+ --title "Review auth module" \
352
+ --impact 60 --effort-days 1 \
353
+ --blocked-by build
354
+ ```
355
+
356
+ Each task validates its id (`/^[a-z][a-z0-9_-]{0,63}$/`) and rejects
357
+ duplicates. If you tried `mu task add x --blocked-by y` while `y`
358
+ already transitively depended on `x`, mu would refuse with a `CycleError`.
359
+
360
+ **Task ids are globally unique** (PRIMARY KEY across all workstreams)
361
+ but tasks are scoped to one workstream. Cross-workstream blocks-edges
362
+ are forbidden — if `--blocks foo` resolves to a task in a different
363
+ workstream, mu refuses with a `CrossWorkstreamEdgeError`.
364
+
365
+ ---
366
+
367
+ ## 5. See the graph (mission control)
368
+
369
+ ```bash
370
+ mu --workstream auth-refactor
371
+ # or, if your tmux session is mu-auth-refactor:
372
+ mu
373
+ ```
374
+
375
+ ```
376
+ mu-auth-refactor
377
+
378
+ Agents (0)
379
+ (no agents)
380
+
381
+ Tracks (1)
382
+ Track 1: review (3 tasks, 1 ready, track)
383
+
384
+ Ready (1)
385
+ ┌────────┬─────────────────────┬────────┬────────┬──────┬───────┐
386
+ │ id │ title │ impact │ effort │ ROI │ owner │
387
+ ├────────┼─────────────────────┼────────┼────────┼──────┼───────┤
388
+ │ design │ Design auth module │ 80 │ 2 │ 40.0 │ │
389
+ └────────┴─────────────────────┴────────┴────────┴──────┴───────┘
390
+ ```
391
+
392
+ This is the answer to **"what should I work on next?"** without
393
+ asking an LLM. Three sections:
394
+
395
+ - **Agents** — registry rows, status detected from each pane's
396
+ scrollback, post-reconciliation
397
+ - **Tracks** — independent subtrees the parallel-track union-find
398
+ found. When two goals share a prerequisite, mu collapses them into
399
+ ONE track ("merged") so two agents are never assigned tasks that
400
+ share a dependency
401
+ - **Ready** — actionable now, sorted by ROI (impact / effort)
402
+
403
+ ### `mu state` render modes (default, `--hud`, `--mission`)
404
+
405
+ `mu state` is one verb with three render modes — same data set,
406
+ different presentation strategy. The flag picks the renderer; the
407
+ JSON shape (`--json`) follows render mode (full vs stripped).
408
+
409
+ ```bash
410
+ mu state # default: full top-to-bottom card
411
+ mu state --hud # dynamic-fit budget renderer (watch / popup / status-bar)
412
+ mu state --mission # stripped 5-col glance card
413
+ mu # bare alias for `mu state --mission`
414
+ ```
415
+
416
+ - **default (full card)** — every section: agents + orphans + tracks +
417
+ ready / in-progress / blocked / recent-closed + workspaces + recent
418
+ events. JSON-first by design (per Ilya's council critique: state
419
+ cards as the default attention surface; SQL/raw verbs as the
420
+ escape hatch underneath).
421
+
422
+ - **`--hud`** — dynamic table layout that fills the terminal (or tmux
423
+ pane) height + width with as much useful data as fits.
424
+ `watch -n 5 mu state --hud -w X` for a refreshing pane;
425
+ `tmux display-popup -E 'mu state --hud -w X'` for an on-demand
426
+ popup; `#(mu state --hud -w X --json) | jq ...` for tmux
427
+ status-bar interpolation. Sections (priority order):
428
+ header / agents / ready / in-progress / tracks / recent-events.
429
+ Truncated tables get a `… +N more (<verb>)` footer; lower-priority
430
+ sections that can't fit are skipped entirely. Drops blocked /
431
+ recent-closed / workspaces (not glanceable); operator drops
432
+ `--hud` to see them.
433
+
434
+ - **`--mission`** — stripped 5-col glance card (agents + orphans +
435
+ tracks + ready). The bare-`mu` muscle-memory orient call
436
+ ("what's going on?"). The full card with blocked / recent-closed /
437
+ workspaces is too much for that intent; `--mission` is the
438
+ intentional minimum-viable orient view.
439
+
440
+ `--hud` and `--mission` are mutually exclusive.
441
+
442
+ Multi-workstream: pass `-w` multiple values to render N workstreams
443
+ in one card. `-w a,b,c`, `-w a -w b`, or any mix all work — see
444
+ [CLI conventions](#cli-conventions-multi-value-flags). `--all` is
445
+ sugar for "every workstream on this machine" (mutually exclusive with
446
+ `-w`). N≥2 in `--hud` mode unions the per-ws sections with a leading
447
+ `workstream` column; in default + `--mission` modes N≥2 stacks one
448
+ per-workstream card after another. The `--json` envelope wraps in
449
+ `{ workstreams: [...] }` when N≥2.
450
+
451
+ JSON shapes (per render mode):
452
+
453
+ - `mu state --json` (single-ws): flat `{ workstreamName, agents,
454
+ orphans, tracks, ready, blocked, inProgress, recentClosed,
455
+ workspaces, recent }`.
456
+ - `mu state --hud --json`: SAME flat shape (`--hud` is a render flag;
457
+ it doesn't change the machine view).
458
+ - `mu state --mission --json`: STRIPPED — only `{ workstreamName,
459
+ agents, orphans, tracks, ready }`.
460
+ - bare `mu --json`: same as `--mission --json`.
461
+
462
+ > **Migrating from `mu hud`**: drop the `hud` verb and add `--hud`
463
+ > to `mu state`. `tmux display-popup -E 'mu hud -w X'` becomes
464
+ > `tmux display-popup -E 'mu state --hud -w X'`. The `mu hud` verb
465
+ > was removed in v0.3 — see [CHANGELOG.md](../CHANGELOG.md).
466
+
467
+ ---
468
+
469
+ ## 6. Spawn a crew
470
+
471
+ For a real demo with status detection, spawn pi agents:
472
+
473
+ ```bash
474
+ mu agent spawn worker-1 --workstream auth-refactor # default --cli is pi
475
+ ```
476
+
477
+ To play around without needing pi installed, use `--cli sh`:
478
+
479
+ ```bash
480
+ mu agent spawn worker-1 --workstream auth-refactor --cli sh
481
+ mu agent spawn worker-2 --workstream auth-refactor --cli sh
482
+ ```
483
+
484
+ ```
485
+ Spawned worker-1 (sh) in window worker-1 of mu-auth-refactor, pane %15
486
+ ```
487
+
488
+ What just happened:
489
+
490
+ 1. mu checked the agents table — no `worker-1` yet, OK to proceed
491
+ 2. mu created a tmux window named `worker-1` in the `mu-auth-refactor`
492
+ session
493
+ 3. mu set the pane title to `worker-1` via `tmux select-pane -T worker-1`
494
+ — **this is the claim protocol identity**
495
+ 4. mu inserted a row in `agents` with `pane_id=%15`, `status=spawning`
496
+
497
+ If the DB insert fails after the pane was created, mu kills the pane
498
+ to avoid leaking. If the same name was already taken, mu rejects
499
+ **before** calling tmux.
500
+
501
+ ### Naming convention (lint, not a rule)
502
+
503
+ mu accepts any name matching `/^[a-z][a-z0-9_-]{0,31}$/`, but the
504
+ recommended shape is **`<role>-<n>`** — a lowercase role plus the
505
+ smallest unused integer suffix (e.g. `worker-1`, `reviewer-2`,
506
+ `scout-12`). Names that diverge (`worker-tests`, `alice`, `db-leader`,
507
+ `x-y-1`) still spawn successfully but trigger a one-line stderr hint:
508
+
509
+ ```
510
+ hint: agent name "worker-tests" does not match the smallest-unused-suffix
511
+ convention (<role>-<n>; e.g. worker-1, reviewer-2). Accepted; consider
512
+ renaming if you spawn additional workers.
513
+ ```
514
+
515
+ The hint is suppressed under `--json` so script callers stay clean.
516
+
517
+ ### Multiple agents in one window (split panes)
518
+
519
+ Give them a shared `--tab`:
520
+
521
+ ```bash
522
+ mu agent spawn reviewer-1 --workstream auth-refactor --cli sh --tab Review --role read-only
523
+ mu agent spawn audit --workstream auth-refactor --cli sh --tab Review
524
+ ```
525
+
526
+ The `Review` window holds whichever agents share `--tab Review`.
527
+
528
+ ### Spawn options
529
+
530
+ | Flag | Meaning |
531
+ | ---------------------------- | ------------------------------------------------------- |
532
+ | `--cli <name>` | Logical CLI family (effectively always `pi`; the flag exists as a key for `MU_<UPPER_CLI>_COMMAND` resolution) |
533
+ | `--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 |
534
+ | `--tab <name>` | Group with other agents under this window name |
535
+ | `--role <full-access\|read-only>` | Capability flag; stored but not yet enforced |
536
+ | `--cwd <path>` | Initial working directory for the pane |
537
+ | `-w, --workstream <name>` | Required if not auto-detectable |
538
+
539
+ On systems where the local `pi` binary is installed under a different
540
+ name, set `MU_PI_COMMAND=<name>` once in your shell rc and every
541
+ `mu agent spawn --cli pi` will exec the right binary; reconcile
542
+ also treats that binary's panes as agent-worthy when surfacing orphans.
543
+
544
+ `MU_PI_COMMAND` (and `--command`) accept a multi-word string — tmux
545
+ exec's it via a shell, so embedded flags survive intact. If your pi
546
+ build needs extra flags (e.g. to skip a single-instance lock), set
547
+ `MU_PI_COMMAND="pi-alt --some-flag"` and every spawn picks them up.
548
+ Same pattern for `MU_CLAUDE_COMMAND` / `MU_CODEX_COMMAND` once those
549
+ land.
550
+
551
+ ### Adopt an existing tmux pane
552
+
553
+ Not every agent gets born via `mu agent spawn`. Sometimes you
554
+ launched a `pi` (or `claude`, or `codex`) by hand for a one-off
555
+ task, decided mid-flow it deserves to be in the graph, and now
556
+ want to drive it via `mu`. Or `mu` crashed mid-spawn and left an
557
+ orphan pane with no DB row. Either way:
558
+
559
+ ```bash
560
+ mu agent list -w auth-refactor # surfaces orphans at the bottom
561
+ # Orphan panes (1)
562
+ # %15 title=worker-2 cli=pi
563
+
564
+ mu adopt %15 -w auth-refactor # adopt by pane id
565
+ mu adopt worker-2 -w auth-refactor # adopt by pane title (same effect)
566
+ mu adopt %15 --name investigator -w auth-refactor # adopt and rename the pane
567
+ ```
568
+
569
+ The pane title becomes the agent name (`mu`'s claim protocol
570
+ invariant), so adopting a pane titled `worker-2` registers it as
571
+ agent `worker-2` with no further config. Use `--name` when the
572
+ pane's current title isn't a valid agent name (or when you want a
573
+ different name).
574
+
575
+ Adopt is **idempotent**: running it twice on the same pane is a
576
+ no-op. It's also **scope-aware**: the pane must be in the
577
+ `mu-<workstream>` tmux session, otherwise the adopt is rejected
578
+ (no silent cross-session moves).
579
+
580
+ ---
581
+
582
+ ## 7. Watch the crew live
583
+
584
+ The killer property: you can attach the workstream's tmux session and
585
+ see everything.
586
+
587
+ ```bash
588
+ tmux attach -t mu-auth-refactor
589
+ ```
590
+
591
+ You see one tmux window per agent (or a window with split panes if
592
+ they share a `--tab`).
593
+
594
+ | Tmux key | What it does |
595
+ | -------------- | --------------------------------------------- |
596
+ | `Ctrl+b w` | Pick a window (interactive list) |
597
+ | `Ctrl+b n`/`p` | Cycle next/previous window |
598
+ | `Ctrl+b d` | Detach from the session (mu doesn't care) |
599
+
600
+ mu does not require you to be attached. Detach freely.
601
+
602
+ ---
603
+
604
+ ## 8. Send work to an agent
605
+
606
+ From any shell with mu on `$PATH`:
607
+
608
+ ```bash
609
+ mu agent send worker-1 "echo hello from outside"
610
+ ```
611
+
612
+ mu uses the **canonical bracketed-paste protocol** internally:
613
+
614
+ 1. `tmux copy-mode -q` (silent if not in copy mode)
615
+ 2. `tmux set-buffer` (loads text into a uniquely-named buffer)
616
+ 3. `tmux paste-buffer -p -d -r` (`-p` = bracketed paste, `-d` = delete
617
+ buffer after paste, `-r` = preserve LF)
618
+ 4. wait `MU_SEND_DELAY_MS` ms (default 500)
619
+ 5. `tmux send-keys Enter`
620
+
621
+ This means special characters (`/`, `?`, `!`, `$`, `&&`, `|`, `*`,
622
+ …) arrive at the agent's CLI **literally** — not interpreted by tmux's
623
+ copy-mode or by the agent's TUI shortcuts. Naive `tmux send-keys`
624
+ would let the agent's TUI hijack `/` for "search forward" and similar.
625
+
626
+ The send delay is configurable per call:
627
+
628
+ ```bash
629
+ MU_SEND_DELAY_MS=300 mu agent send worker-1 "..." # faster, less safe
630
+ MU_SEND_DELAY_MS=1000 mu agent send worker-1 "..." # slow remote
631
+ ```
632
+
633
+ ---
634
+
635
+ ## 9. Read what an agent did
636
+
637
+ ```bash
638
+ mu agent read worker-1 # full scrollback
639
+ mu agent read worker-1 -n 50 # last 50 lines
640
+ ```
641
+
642
+ Both go through `tmux capture-pane`. No state change.
643
+
644
+ ---
645
+
646
+ ## 10. The claim protocol — from inside an agent's pane
647
+
648
+ This is where mu's design really shines. An agent (the LLM running in
649
+ a pane) can run `mu task claim foo` **with no agent name argument** — mu
650
+ figures out it's "worker-1" from the pane title.
651
+
652
+ To try this manually, attach to the workstream and switch to worker-1's
653
+ window:
654
+
655
+ ```bash
656
+ tmux attach -t mu-auth-refactor # if not attached
657
+ # Ctrl+b w, pick "worker-1" interactively
658
+ ```
659
+
660
+ Then in worker-1's pane (a real shell, since `--cli sh`):
661
+
662
+ ```bash
663
+ mu task claim design
664
+ ```
665
+
666
+ ```
667
+ Claimed design for worker-1 (OPEN → IN_PROGRESS)
668
+ ```
669
+
670
+ What happened behind the scenes:
671
+
672
+ 1. mu reads `$TMUX_PANE` (set by tmux for every pane in the session)
673
+ to get the pane id (e.g. `%15`)
674
+ 2. Calls `tmux display-message -t %15 -p '#{pane_title}'` → returns
675
+ `worker-1`
676
+ 3. Atomic SQLite transaction:
677
+ ```sql
678
+ UPDATE tasks
679
+ SET owner = 'worker-1',
680
+ status = CASE WHEN status = 'OPEN' THEN 'IN_PROGRESS' ELSE status END,
681
+ updated_at = ?
682
+ WHERE local_id = 'design'
683
+ AND (owner IS NULL OR owner = 'worker-1')
684
+ ```
685
+ 4. If 0 rows changed, mu distinguishes "task doesn't exist" from
686
+ "already owned by someone else" and throws the right typed error
687
+
688
+ Two agents trying to claim the same task → second one fails with
689
+ "already owned by worker-1." Re-claim by the same agent is idempotent.
690
+
691
+ You can also claim explicitly from outside any pane:
692
+
693
+ ```bash
694
+ mu task claim build --for worker-2
695
+ ```
696
+
697
+ `--for` accepts EITHER a bare worker name (`worker-2`, resolved in
698
+ the task's workstream — today's behaviour) OR a qualified ref
699
+ `<workstream>/<name>` for **cross-workstream dispatch**
700
+ (`task_claim_for_cross_workstream`):
701
+
702
+ ```bash
703
+ # Task lives in mufeedback-v03; worker-1 lives in roadmap-v0-3.
704
+ # Per-workstream worker pools mean the orchestrator routinely has a
705
+ # free worker in one workstream and a queued task in another.
706
+ mu task claim some-task -w mufeedback-v03 --for roadmap-v0-3/worker-1
707
+ ```
708
+
709
+ The agent stays in its own workstream — only `tasks.owner_id`
710
+ points across the boundary (it's an INTEGER FK to `agents.id`,
711
+ workstream-agnostic at the schema level). A bad qualifier surfaces
712
+ typed errors: `WorkstreamNotFoundError` (exit 3) on a missing
713
+ workstream prefix, `AgentNotFoundError` (exit 3, message names the
714
+ workstream) when the named worker doesn't live there. Nothing is
715
+ written on either failure.
716
+
717
+ ### The orchestrator pattern: `--self`
718
+
719
+ Not every action comes from a registered worker pane. Often the
720
+ *orchestrator* (a top-level pi session, a human at a shell, a
721
+ deploy script) wants to do small work directly without spinning up
722
+ a worker pane just for a 5-minute job. Two patterns split here:
723
+
724
+ - **Worker** — a pane mu spawned (or you adopted). Has a row in the
725
+ `agents` table. Identity = pane title. Claims with bare
726
+ `mu task claim <id>`. `tasks.owner` is set to the worker's name.
727
+
728
+ - **Actor** — anything that *causes* a state change. Includes
729
+ workers, but also includes the orchestrator. May or may not have
730
+ a row in `agents`. The actor is *always* recorded in the
731
+ auto-emitted `agent_logs` event for every state change
732
+ (the `source` field).
733
+
734
+ If the orchestrator tries `mu task claim some-task` directly:
735
+
736
+ ```
737
+ conflict: claimer 'pi-mu' (pane %6441) is not a registered mu agent.
738
+ Working directly? Pass --self to attribute via log instead.
739
+ Dispatching to a worker? Pass --for <worker> to assign.
740
+ Want full registration? Run: mu adopt %6441
741
+ ```
742
+
743
+ Three actionable next steps. Pick one based on intent:
744
+
745
+ ```bash
746
+ # Orchestrator does the work itself (most common):
747
+ mu task claim some-task --self --evidence "trivial 5-line fix"
748
+ # -> tasks.owner stays NULL
749
+ # -> agent_logs records 'task claim some-task by pi-mu --self (anonymous)'
750
+ # -> mu task show surfaces it as 'owner: (self: pi-mu)'
751
+
752
+ # Orchestrator dispatches to a worker:
753
+ mu task claim some-task --for worker-1
754
+ # -> tasks.owner = 'worker-1'
755
+
756
+ # Orchestrator wants to BE a registered worker (rare):
757
+ mu adopt %6441 -w <ws> # only if pane is in mu-<ws> session
758
+ mu task claim some-task # now works as a normal worker claim
759
+ ```
760
+
761
+ `--self` is **only** for unregistered actors. Workers continue to
762
+ claim with bare `mu task claim` — nothing changes for them. The
763
+ `--actor <name>` flag overrides the auto-detected actor name (defaults
764
+ to pane title, or `$USER`, or `unknown`):
765
+
766
+ ```bash
767
+ mu task claim deploy --self --actor deploy-bot --evidence "prod release"
768
+ ```
769
+
770
+ When `tasks.owner IS NULL` because of `--self`, `mu task show` looks
771
+ up the most recent `task claim` event for that task and surfaces it:
772
+
773
+ ```
774
+ owner : (self: pi-mu)
775
+ ```
776
+
777
+ So provenance is preserved — it just lives in `agent_logs` rather
778
+ than being conflated with the FK that points at registered workers.
779
+
780
+ ---
781
+
782
+ ## 11. Drop notes (durable context)
783
+
784
+ Notes are append-only. They survive across sessions and across agent
785
+ restarts. This is the cure for LLM context loss: when the next agent
786
+ picks up a task, they can read the full history.
787
+
788
+ ```bash
789
+ mu task note design "DECISION: JWT, 24h expiry, refresh via cookie"
790
+ mu task note design "FILES: src/auth.rs:45-120"
791
+ ```
792
+
793
+ Read them via the SQL escape hatch:
794
+
795
+ ```bash
796
+ mu sql "SELECT author, content, created_at FROM task_notes WHERE task_id='design' ORDER BY id"
797
+ ```
798
+
799
+ Convention for note content: `KEY: value` lines. Common keys are
800
+ `FILES`, `DECISION`, `VERIFIED`, `BLOCKED`, `NEXT`. Mu doesn't
801
+ enforce these — they're for the agents reading them.
802
+
803
+ ---
804
+
805
+ ## 12. Close out a task
806
+
807
+ ```bash
808
+ mu task close design # OPEN/IN_PROGRESS → CLOSED
809
+ mu task close umbrella --if-ready # close ONLY if every blocker
810
+ # is terminal (CLOSED / REJECTED
811
+ # / DEFERRED); else no-op + list
812
+ # the still-blocking ids
813
+ mu task open design # CLOSED → OPEN (e.g. closed by mistake)
814
+ ```
815
+
816
+ Both are idempotent (closing an already-CLOSED task prints a no-op
817
+ message and exits 0). Owner is intentionally left intact — use
818
+ `mu task release <id>` to clear ownership when an agent bails on a
819
+ task mid-flight. `IN_PROGRESS` auto-flips back to `OPEN` so the
820
+ task re-enters the ready set (the canonical "hand it back to the
821
+ pool" workflow). `--reopen` is the escape hatch for forcing `OPEN`
822
+ from `CLOSED` / `REJECTED` / `DEFERRED`.
823
+
824
+ `--if-ready` is the umbrella-on-wave-done shape: an orchestrator
825
+ fires `mu task close <umbrella> --if-ready` after each wave-task
826
+ finishes (or unconditionally as a final action). It's a no-op while
827
+ any blocker is still OPEN / IN_PROGRESS, and prints the still-
828
+ blocking ids + a `mu task wait` Next: hint so the operator can pick
829
+ back up. Once the last blocker reaches a terminal status (CLOSED /
830
+ REJECTED / DEFERRED), the same command closes the umbrella.
831
+ JSON shape on the no-op path: `{ skipped: "not_ready", changed:
832
+ false, blockingIds: ["..."], ... }`. Exit code 0 either way — the
833
+ no-op is success.
834
+
835
+ ```bash
836
+ mu task release design # clear owner; IN_PROGRESS → OPEN
837
+ # (CLOSED / REJECTED / DEFERRED preserved)
838
+ mu task release design --reopen # clear owner AND force status to OPEN
839
+ # (un-close + release in one verb)
840
+ ```
841
+
842
+ Now run `mu` again — `build` has become ready (its only blocker
843
+ `design` is now closed):
844
+
845
+ ```
846
+ Ready (1)
847
+ ┌───────┬──────────────────┬────────┬────────┬──────┬───────┐
848
+ │ id │ title │ impact │ effort │ ROI │ owner │
849
+ ├───────┼──────────────────┼────────┼────────┼──────┼───────┤
850
+ │ build │ Build auth module│ 80 │ 5 │ 16.0 │ │
851
+ └───────┴──────────────────┴────────┴────────┴──────┴───────┘
852
+ ```
853
+
854
+ ---
855
+
856
+ ## 13. The SQL escape hatch is your friend
857
+
858
+ Most routine operations have a typed verb — prefer those (and prefer
859
+ `--json` for scripting). `mu sql` is for the rare cases the typed
860
+ verbs don't cover: ad-hoc joins, manual recovery, exploring schema.
861
+ The schema is 8 core tables (`workstreams`, `agents`, `tasks`,
862
+ `task_edges`, `task_notes`, `agent_logs`, `vcs_workspaces`,
863
+ `snapshots`), 5 archive tables (`archives`, `archived_tasks`,
864
+ `archived_edges`, `archived_notes`, `archived_events`), 1 meta table
865
+ (`schema_version`), plus three views (`ready`, `blocked`, `goals`):
866
+
867
+ ```bash
868
+ mu sql "SELECT name FROM sqlite_master WHERE type IN ('table','view') ORDER BY type, name"
869
+ ```
870
+
871
+ ### Prefer the typed verb where one exists
872
+
873
+ | Want | Typed verb |
874
+ | ----------------------------------------------------- | --------------------------------------- |
875
+ | Tasks owned by an agent (current workstream) | `mu task owned-by <agent> [--json]` |
876
+ | Tasks owned by ANY same-named worker (all workstreams)| `mu task owned-by <agent> --all [--json]`|
877
+ | Highest-ROI ready task | `mu task next [-w] [-n K] [--json]` |
878
+ | What did I touch most recently / what's stale | `mu task list --sort recency` / `--sort age` |
879
+ | Visualise what blocks what | `mu task tree <id> [--json]` |
880
+ | Show row + edges + notes | `mu task show <id> [--json]` |
881
+ | Delete + cascade edges/notes (two-phase: bare = dry-run; `--yes` commits) | `mu task delete <id>` / `mu task delete <id> --yes` |
882
+ | Add / remove a single edge | `mu task block` / `mu task unblock` |
883
+ | Replace all blockers atomically | `mu task reparent <id> --blocked-by ...` |
884
+ | Modify scalar fields | `mu task update <id> [--title ...]` |
885
+ | Read the activity log / subscribe to events | `mu log [--tail] [--kind event]` |
886
+ | Block until tasks reach a status (orchestrator wait) | `mu task wait <ref> [<ref>...] [--first|--any] [--timeout S]` |
887
+
888
+ ### `mu task wait`: cross-workstream refs + `--first` returns WHICH
889
+
890
+ Each `<ref>` is either a bare task name (resolves via `-w` /
891
+ `$MU_SESSION` / tmux session) or a qualified `<workstream>/<name>`
892
+ ref. When all refs are qualified, `-w` is not required; mixed lists
893
+ are allowed (bare uses `-w`, qualified uses its prefix).
894
+
895
+ ```bash
896
+ # All-bare with -w — today's classic shape, unchanged
897
+ mu task wait build_a build_b -w mufeedback-v03 --timeout 1200
898
+
899
+ # All-qualified — cross-workstream wait, no -w needed
900
+ mu task wait roadmap-v0-3/archive_phase2 mufeedback-v03/cli_audit --timeout 1800
901
+
902
+ # Mixed — bare uses -w; qualified ignores it
903
+ mu task wait cli_audit roadmap-v0-3/archive_phase2 -w mufeedback-v03
904
+ ```
905
+
906
+ `--first` is an alias for `--any` that ALSO prints the firing ref's
907
+ qualified id to stdout (and adds a `firing` field to `--json`). Use
908
+ it to drive a single-shot dispatch loop — one wait, one cherry-pick,
909
+ one verify, one workspace recycle:
910
+
911
+ ```bash
912
+ # The dispatch-pipeline recipe: cycle until in_flight is empty.
913
+ in_flight=( mufeedback-v03/foo mufeedback-v03/bar roadmap-v0-3/baz )
914
+ while (( ${#in_flight[@]} > 0 )); do
915
+ closed=$(mu task wait "${in_flight[@]}" --first --timeout 90 --json \
916
+ | jq -r '.firing.qualifiedId // empty')
917
+ if [[ -z "$closed" ]]; then break; fi # timeout or exit 6 — see below
918
+
919
+ # 1. Cherry-pick the worker's HEAD (the worker is named in the
920
+ # nextSteps array — or use `mu task show` to look up).
921
+ worker=$(mu task show "${closed##*/}" -w "${closed%%/*}" --json | jq -r .ownerName)
922
+ ws=${closed%%/*}
923
+ # `mu workspace commits --json` knows the workspace's parent_ref
924
+ # so this is one verb instead of `cd $(mu workspace path) && git log`.
925
+ sha=$(mu workspace commits "$worker" -w "$ws" --json | jq -r '.[-1].sha')
926
+ git cherry-pick "$sha"
927
+
928
+ # 2. Verify
929
+ npm run typecheck && npm run lint && npm run test && npm run build
930
+
931
+ # 3. Refresh the workspace for the next dispatch (rebases onto
932
+ # fresh main WITHOUT killing the worker's LLM context). Default
933
+ # base = origin/HEAD (git) / trunk() (jj/sl); --from <ref>
934
+ # overrides. Refuses on dirty WC; conflicts exit 5 with a `cd`
935
+ # hint to resolve in-place.
936
+ mu workspace refresh "$worker" -w "$ws"
937
+
938
+ # 4. Drop $closed from in_flight, dispatch the next task, repeat.
939
+ in_flight=( "${in_flight[@]/$closed}" )
940
+ done
941
+ ```
942
+
943
+ The `--json` shape on success is `{ firing, all, timedOut, nextSteps,
944
+ ... }`:
945
+
946
+ * `firing` — `{ workstreamName, name, qualifiedId, status, owner }`
947
+ on `--first` / `--any` success; `null` on `--all` success or on
948
+ timeout.
949
+ * `all` — array of refs that REACHED the target (with
950
+ `qualifiedId` + `reachedAt`).
951
+ * `timedOut` — array of refs that did NOT reach the target. Empty on
952
+ clean success; populated on partial-progress timeout.
953
+ * `nextSteps`— the same hint list printed to stdout (cherry-pick,
954
+ verify, free + recreate, or `mu task show` for unmet refs).
955
+
956
+ ### Wait exit codes (`mu task wait`)
957
+
958
+ `mu task wait` polls the watched tasks every second (cheap indexed
959
+ SELECT + a per-poll reconcile of every workstream in the wait set)
960
+ and exits with one of:
961
+
962
+ | Exit | Meaning |
963
+ |------|-------------------------------------------------------------------------|
964
+ | `0` | The wait condition was met (`--all` reached, or `--any` / `--first` saw at least one). |
965
+ | `5` | `--timeout` expired before the condition was met. `--json` payload still includes `all` (refs that did reach) and `timedOut` (refs that didn't). |
966
+ | `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`) |
967
+ | `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 (operator decides poke vs release). 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`) |
968
+
969
+ The per-poll reconcile means a worker pane that died **before** you
970
+ ran `mu task wait` is also reaped on the first tick — you'll see exit
971
+ `6` in well under a second instead of running out the `--timeout`.
972
+ For cross-workstream waits the reconcile loops over every workstream
973
+ in the wait set (so a dead pane in workstream B is reaped while you
974
+ wait on its task there too).
975
+
976
+ ### `mu task wait`: stall detection (`--stuck-after` + `--on-stall`)
977
+
978
+ Two orthogonal flags govern the stall behaviour:
979
+
980
+ * `--stuck-after <seconds>` — the **trigger**. An IN_PROGRESS task
981
+ whose owner has been in `needs_input` for `>= N` seconds is marked
982
+ stuck. Default `300` (5 min); pass `0` to disable detection
983
+ entirely (no warn AND no exit).
984
+ * `--on-stall <action>` — the **action** when the trigger fires.
985
+ Two values:
986
+ * `warn` (default) — yellow `STUCK` warning to stderr (deduped per
987
+ task per wait call), corroborating `agent stalled <name> owns
988
+ <task> for <secs>s` event in `agent_logs`, and `wait` keeps
989
+ polling. The behaviour pre-`task_wait_stall_action_flag`,
990
+ byte-for-byte.
991
+ * `exit` — same emit + persist, then **exit 7**
992
+ (`STALL_DETECTED`). The unattended-orchestrator escape: a
993
+ wrapping policy can branch on 7 (idle, ambiguous — poke vs
994
+ release) vs 6 (dead pane, unambiguous — re-dispatch). Suppressed
995
+ when `--status` is anything other than `CLOSED` (mirrors
996
+ exit-6's carve-out: with `--status OPEN` reaching `needs_input`
997
+ might BE the success path).
998
+
999
+ ```bash
1000
+ # Default: warn at 5 min, keep polling. Today's behaviour.
1001
+ mu task wait build_a build_b -w mufeedback-v03 --timeout 1800
1002
+
1003
+ # Tune the trigger; same warn-only action.
1004
+ mu task wait build_a -w mufeedback-v03 --stuck-after 60
1005
+
1006
+ # Exit on stall (cron-driven wrapper):
1007
+ mu task wait build_a -w mufeedback-v03 --on-stall exit
1008
+ # exit 0 → closed
1009
+ # exit 5 → timeout
1010
+ # exit 6 → dead pane (re-dispatch)
1011
+ # exit 7 → idle agent (poke or release — inspect first)
1012
+
1013
+ # Tune both. Exit at 60s of needs_input:
1014
+ mu task wait build_a -w mufeedback-v03 --stuck-after 60 --on-stall exit
1015
+
1016
+ # Disable both warn AND exit (--stuck-after 0 wins):
1017
+ mu task wait build_a -w mufeedback-v03 --stuck-after 0 --on-stall exit
1018
+ ```
1019
+
1020
+ ### Common ad-hoc queries
1021
+
1022
+ ```bash
1023
+ # Set task to IN_PROGRESS without claiming (claim does this automatically;
1024
+ # this covers the rare manual case). local_id is per-workstream unique,
1025
+ # so always scope by workstream_id to avoid hitting a same-named task in
1026
+ # another workstream.
1027
+ mu sql "UPDATE tasks SET status='IN_PROGRESS'
1028
+ WHERE local_id='build'
1029
+ AND workstream_id=(SELECT id FROM workstreams WHERE name='mufeedback-v03')"
1030
+
1031
+ # What's blocking what (open tasks only) — same data as `mu task tree`
1032
+ # but as a flat join when you want a wider report. task_edges is keyed
1033
+ # by tasks.id, not local_id.
1034
+ mu sql "SELECT b.local_id AS blocked, t.local_id AS by_task
1035
+ FROM tasks b
1036
+ JOIN task_edges e ON e.to_task_id = b.id
1037
+ JOIN tasks t ON t.id = e.from_task_id
1038
+ WHERE t.status != 'CLOSED' AND b.status = 'OPEN'"
1039
+
1040
+ # Recursive CTE: every task that transitively blocks `launch` in a
1041
+ # given workstream (or use `mu task tree launch --json` for the same
1042
+ # data structured). local_id is per-workstream, so resolve the seed
1043
+ # under a workstream filter.
1044
+ mu sql "WITH RECURSIVE prereqs(id) AS (
1045
+ SELECT t.id FROM tasks t
1046
+ JOIN workstreams w ON w.id = t.workstream_id
1047
+ WHERE t.local_id='launch' AND w.name='mufeedback-v03'
1048
+ UNION
1049
+ SELECT e.from_task_id FROM task_edges e, prereqs
1050
+ WHERE e.to_task_id = prereqs.id
1051
+ )
1052
+ SELECT t.local_id, t.title, t.status
1053
+ FROM prereqs JOIN tasks t ON t.id = prereqs.id"
1054
+ ```
1055
+
1056
+ `mu sql` accepts both reads and writes. Reads are pretty-printed as a
1057
+ table; writes report `<n> rows affected`.
1058
+
1059
+ ---
1060
+
1061
+ ## 14. Recovery scenarios
1062
+
1063
+ ### An agent's pane dies externally
1064
+
1065
+ You killed it from another tmux client, or its CLI crashed:
1066
+
1067
+ ```bash
1068
+ mu agent list # worker-1's row prunes itself (ghost detected)
1069
+ ```
1070
+
1071
+ Reconciliation runs on every `mu agent list` / `mu`. Three steps:
1072
+
1073
+ 1. **Prune ghost rows** — DB row whose `pane_id` no longer exists in
1074
+ tmux gets deleted
1075
+ 2. **Detect status from scrollback** — for survivors, capture the
1076
+ pane and re-derive status (busy / needs_input / needs_permission /
1077
+ spawning) per the pi-status detector
1078
+ 3. **Surface orphan panes** — panes in the workstream's tmux session
1079
+ whose `pane.command` looks like an agent CLI (pi) but
1080
+ that aren't in the registry. **Not** auto-adopted; mu shows them
1081
+ under "Orphan panes" and tells you `mu adopt <pane-id>` to register
1082
+
1083
+ ### You closed your terminal session
1084
+
1085
+ The workstream's tmux session keeps running detached. Reconnect with
1086
+ `tmux a -t mu-auth-refactor`. Agents are alive; the DB has the
1087
+ registry; everything resumes. mu is daemon-free — every `mu`
1088
+ invocation is a short-lived process that re-reads from
1089
+ `~/.local/state/mu/mu.db`.
1090
+
1091
+ ### The mu DB seems wrong
1092
+
1093
+ ```bash
1094
+ sqlite3 ~/.local/state/mu/mu.db .schema # inspect
1095
+ sqlite3 ~/.local/state/mu/mu.db .tables # list
1096
+ mu doctor # quick health check
1097
+ rm ~/.local/state/mu/mu.db # nuke (last resort; loses task graph and registry)
1098
+ ```
1099
+
1100
+ ### You ran a destructive verb and want to undo it
1101
+
1102
+ Every destructive verb (`mu task delete`, `mu workstream destroy
1103
+ --yes`, `mu task close/reject/defer/release`, `mu agent close`,
1104
+ `mu workspace free`) auto-captures a
1105
+ whole-DB snapshot before it mutates. Restore the latest with
1106
+ `mu undo`:
1107
+
1108
+ ```bash
1109
+ mu undo # dry-run: shows the snapshot summary, does NOT restore
1110
+ mu undo --yes # commit the restore
1111
+ mu undo --to 12 --yes # restore a specific snapshot id
1112
+
1113
+ mu snapshot list # newest-first: id / ver / label / workstream / size
1114
+ mu snapshot show 12 # full metadata for one snapshot
1115
+
1116
+ # Manual cleanup (auto-GC also runs on every capture)
1117
+ mu snapshot prune # dry-run summary of the GC policy
1118
+ mu snapshot prune --yes # apply the GC policy now
1119
+ mu snapshot prune --keep-last 50 --yes
1120
+ mu snapshot prune --older-than 7d --yes
1121
+ mu snapshot prune --stale-version --yes # drop schema_version != current rows
1122
+ mu snapshot prune --all --yes # nuke everything (auto-snapshots a safety-net first)
1123
+ mu snapshot delete 12 # surgical removal of one row + its .db file
1124
+ ```
1125
+
1126
+ The `ver` column in `mu snapshot list` shows each snapshot's
1127
+ `schema_version`; rows whose version doesn't match the live DB
1128
+ (post-schema-bump) render dimmed and are unrestorable
1129
+ (`mu undo` raises `SnapshotVersionMismatchError`). Drop them in
1130
+ bulk with `mu snapshot prune --stale-version --yes`.
1131
+
1132
+ Two important caveats:
1133
+
1134
+ - **Tmux state is NOT rolled back.** A snapshot is a copy of
1135
+ `mu.db` only. After restore, mu reconciles every workstream and
1136
+ reports `agents pruned` (DB row → dead pane) and `orphan panes
1137
+ surfaced` (live pane the restored DB doesn't know about) so you
1138
+ can see exactly where DB and tmux disagree. On-disk workspace
1139
+ dirs that `mu workspace free` removed are NOT recreated either.
1140
+ - **Each restore captures a pre-restore snapshot first.** That
1141
+ means a second `mu undo` rolls forward to the snapshot taken
1142
+ just before the previous restore — there is no separate
1143
+ `mu redo`, and there doesn't need to be.
1144
+
1145
+ Snapshots live next to the live DB at
1146
+ `<state-dir>/snapshots/<id>.db`. They GC opportunistically:
1147
+ on every capture, drop any row past the count cap OR past the
1148
+ age cap (whichever fires first). Defaults: keep the 100 newest
1149
+ + everything from the last 14 days. Override with
1150
+ `MU_SNAPSHOT_KEEP_LAST` (default 100) / `MU_SNAPSHOT_MAX_AGE_DAYS`
1151
+ (default 14); typo'd values fall back to the default.
1152
+
1153
+ ### Workspace orphans (dirs on disk with no DB row)
1154
+
1155
+ A `--workspace` spawn that aborted partway, an `mu agent close`
1156
+ from an earlier mu version, or a manual `rm` of `vcs_workspaces`
1157
+ rows can leave dirs in `<state-dir>/workspaces/<workstream>/<agent>/`
1158
+ that have no DB row. They're invisible to `mu workspace list` but
1159
+ they BLOCK subsequent `--workspace` spawns under the same name.
1160
+
1161
+ ```bash
1162
+ mu state -w <workstream> # 'Workspace orphans' section in yellow
1163
+ mu workspace orphans -w <workstream> # focused list + cleanup recipe
1164
+ ```
1165
+
1166
+ For each orphan, the cleanup is one of:
1167
+
1168
+ ```bash
1169
+ # git-backed workspace: also prunes the worktree registry
1170
+ (cd <project-root> && git worktree remove --force <orphan-path>)
1171
+
1172
+ # any backend (last resort)
1173
+ rm -rf <orphan-path>
1174
+ ```
1175
+
1176
+ The `Next:` block from `mu workspace orphans` interpolates the
1177
+ actual paths so you can copy-paste.
1178
+
1179
+ ### You typo'd a workstream name and want to rename it
1180
+
1181
+ The `workstreams.name` column has `ON UPDATE CASCADE` on every
1182
+ child-table foreign key, so renaming a workstream is a single SQL
1183
+ statement that propagates atomically through `agents`, `tasks`,
1184
+ `agent_logs`, and `vcs_workspaces`:
1185
+
1186
+ ```bash
1187
+ # 1. Validate the new name fits the rules (or mu will reject it on
1188
+ # next use). Lowercase alpha first, then alnum/_/-, ≤32 chars,
1189
+ # no '.' or ':' (tmux mangles them), no 'mu-' prefix.
1190
+ # 2. Rename in the DB. Single statement; cascades to every child.
1191
+ mu sql "UPDATE workstreams SET name='auth-refactor' WHERE name='auth-refator'"
1192
+
1193
+ # 3. Rename the tmux session too (only if it's currently alive).
1194
+ tmux rename-session -t mu-auth-refator mu-auth-refactor
1195
+ ```
1196
+
1197
+ Mu doesn't ship a typed `mu workstream rename` verb because the
1198
+ schema does the work — wrapping a single safe statement adds
1199
+ surface area without buying anything (no atomicity to preserve, no
1200
+ validation to add, no side effects beyond the optional `tmux
1201
+ rename-session`). The recipe above is the canonical answer.
1202
+
1203
+ The same `ON UPDATE CASCADE` makes future `mu sql` renames safe
1204
+ for `tasks.local_id` and `agents.name` too, if you ever need to
1205
+ untypo those.
1206
+
1207
+ ---
1208
+
1209
+ ## 15. Cleanup
1210
+
1211
+ ### Close individual agents
1212
+
1213
+ ```bash
1214
+ mu agent close worker-1 # kills pane + drops registry row
1215
+ mu agent close worker-2
1216
+ mu agent close reviewer-1
1217
+ ```
1218
+
1219
+ `mu agent close` is idempotent: `killPane` swallows "pane already gone"
1220
+ errors; `deleteAgent` returns false (not throws) on a missing row.
1221
+
1222
+ ### Tear down the whole workstream
1223
+
1224
+ `mu workstream destroy` is the symmetric counterpart of `mu workstream init`: it kills the
1225
+ workstream's tmux session AND deletes every DB row tagged with the
1226
+ workstream name (agents, tasks, edges, notes — edges and notes go via
1227
+ FK cascade on tasks). The workstream resolves the same way as every
1228
+ other verb: `--workstream <name>` flag > `$MU_SESSION` > current tmux
1229
+ session (with the `mu-` prefix stripped).
1230
+
1231
+ The verb is two-phase by default: a bare `mu workstream destroy` prints a dry-run
1232
+ summary so you can verify what's about to disappear, and exits without
1233
+ touching anything. Pass `-y` / `--yes` to actually destroy.
1234
+
1235
+ ```bash
1236
+ mu workstream destroy --workstream auth-refactor # dry-run: shows counts, exits
1237
+ mu workstream destroy --workstream auth-refactor --yes # actually does it
1238
+
1239
+ # Or, from inside the workstream's tmux session:
1240
+ mu workstream destroy --yes # workstream auto-detected
1241
+
1242
+ # Atomic: archive THEN destroy. Refuses if the archive label
1243
+ # doesn't already exist (run `mu archive create <label>` first).
1244
+ mu workstream destroy -w auth-refactor --archive v0-3-wave --yes
1245
+
1246
+ # Sweep every empty workstream (zero tasks, agents, vcs_workspaces)
1247
+ # in one call. Tmux session presence and audit-only
1248
+ # agent_logs do NOT disqualify. Also surfaces unregistered `mu-*`
1249
+ # tmux sessions (test litter or remnants from a partial destroy that
1250
+ # nuked the DB row but left the session behind) — the matching
1251
+ # predicate is narrow on purpose: ONLY sessions starting with `mu-`,
1252
+ # arbitrary tmux sessions the operator runs for unrelated work are
1253
+ # never touched. Mutually exclusive with -w and --archive. Dry-run
1254
+ # lists what WOULD be destroyed (created_at renders as `—` for
1255
+ # tmux-only entries); --yes captures ONE snapshot for the whole
1256
+ # batch and best-effort destroys each.
1257
+ mu workstream destroy --empty # dry-run: table of empties
1258
+ mu workstream destroy --empty --yes # destroy them all
1259
+ ```
1260
+
1261
+ ```
1262
+ Workstream auth-refactor (tmux session mu-auth-refactor)
1263
+ tmux session : alive (will be killed)
1264
+ agents : 3
1265
+ tasks : 10 (edges: 12, notes: 7)
1266
+
1267
+ Destroyed auth-refactor: killed tmux=true, agents=3, tasks=10, edges=12, notes=7
1268
+ ```
1269
+
1270
+ It's idempotent on every leg: missing tmux session is fine, zero DB
1271
+ rows is fine, repeated `mu workstream destroy` against an already-gone workstream
1272
+ prints "nothing to destroy" and exits 0.
1273
+
1274
+ A whole-DB snapshot is captured before the destroy runs. If you
1275
+ regret it, `mu undo --yes` restores the DB — but the tmux session
1276
+ that was killed and any per-agent workspace dirs that were freed
1277
+ are NOT brought back. See
1278
+ [§ 14: You ran a destructive verb and want to undo it](#you-ran-a-destructive-verb-and-want-to-undo-it).
1279
+
1280
+ The tmux session is killed BEFORE the DB rows so an unexpected tmux
1281
+ failure leaves the registry intact (you can retry); if you only want
1282
+ the DB cleared, use `mu sql` directly:
1283
+
1284
+ ```bash
1285
+ mu sql "DELETE FROM tasks WHERE workstream='auth-refactor'" # cascades
1286
+ mu sql "DELETE FROM agents WHERE workstream='auth-refactor'"
1287
+ ```
1288
+
1289
+ Or nuke the entire DB:
1290
+
1291
+ ```bash
1292
+ rm ~/.local/state/mu/mu.db # next mu invocation re-creates an empty schema
1293
+ ```
1294
+
1295
+ ### Preserve the conversation as markdown before destroying
1296
+
1297
+ A workstream's task graph + notes IS the project memory — the
1298
+ durable record of what was decided and why. `mu workstream destroy`
1299
+ blows that away (a snapshot is taken, but it's a binary `.db` only
1300
+ readable through `mu undo`). For code review, project handoff,
1301
+ git-checked-in artifacts, or just `grep`, render the workstream as
1302
+ plain markdown first.
1303
+
1304
+ Exports use a **bucket** layout (`bucketVersion: 2`, mu ≥ 0.3):
1305
+ the `--out` directory is a multi-source bucket whose top-level
1306
+ contains a bucket-wide README/INDEX/manifest, and one
1307
+ subdirectory per source workstream:
1308
+
1309
+ ```
1310
+ <bucket>/
1311
+ README.md # bucket-level summary (every source-ws + dates + totals)
1312
+ INDEX.md # union of all task tables; first column = source-ws
1313
+ manifest.json # bucketVersion: 2 + per-source-ws sha256 + per-task sha256
1314
+ <source-ws>/
1315
+ README.md # per-source-ws (counts)
1316
+ INDEX.md # per-source-ws (table of every task)
1317
+ tasks/<id>.md # one .md per task; YAML frontmatter + notes
1318
+ ```
1319
+
1320
+ Bucket exports are **additive**: `mu workstream export -w X --out
1321
+ <bucket>` creates the bucket scaffolding plus `X/` on first use,
1322
+ and a follow-up call with `-w Y --out <same-bucket>` appends a
1323
+ sibling `Y/` subdirectory without touching `X/`. Re-running with
1324
+ the same `-w` is sha256-idempotent: only changed task files are
1325
+ rewritten (mtime preserved on identical files); tasks added since
1326
+ the previous export get fresh files; tasks deleted from the DB
1327
+ STAY on disk with a `> **Deleted from DB on <ts>**` banner so you
1328
+ never lose context that may already be git-blamed.
1329
+
1330
+ ```bash
1331
+ # One-shot dump (bucket happens to contain just one source-ws)
1332
+ mu workstream export -w auth-refactor # → ./auth-refactor/
1333
+ mu workstream export -w auth-refactor --out ~/notes/auth/ # explicit dir
1334
+
1335
+ # Additive accumulation across multiple workstreams in one bucket
1336
+ mu workstream export -w mufeedback --out exports/mu # creates exports/mu/mufeedback/
1337
+ mu workstream export -w roadmap-v0-2 --out exports/mu # adds exports/mu/roadmap-v0-2/
1338
+ mu workstream export -w mufeedback-v03 --out exports/mu # adds exports/mu/mufeedback-v03/
1339
+ ```
1340
+
1341
+ The same renderer powers `mu archive export <label> --out <bucket>`,
1342
+ which (re)builds every source-ws subdirectory from the named
1343
+ archive in one shot — see `Archives` below.
1344
+
1345
+ `mu workstream destroy --yes` auto-runs an export to
1346
+ `<state-dir>/exports/<workstream>-<timestamp>/` BEFORE killing the
1347
+ tmux session and dropping the rows, so the conversation survives
1348
+ even if you forgot. Pass `--no-export` to opt out.
1349
+
1350
+ ```bash
1351
+ (cd ~/notes/auth && git init && git add . && git commit -m 'auth-refactor snapshot')
1352
+ ```
1353
+
1354
+ **Pre-0.3 export layouts are not migrated in place.** If `--out`
1355
+ points at a directory whose `manifest.json` was written by an
1356
+ older mu (no `bucketVersion`, top-level `workstream` field), the
1357
+ export refuses with a helpful error: `rm -rf <dir>` and re-run, or
1358
+ pick a different `--out`.
1359
+
1360
+ Markdown only by design — no HTML/PDF, no embedded VCS, no
1361
+ cross-workstream merge. Operators can pandoc / `git init`
1362
+ themselves.
1363
+
1364
+ ### Cross-machine + collab — `mu workstream import`
1365
+
1366
+ The export above plus `mu workstream import <bucket-dir>` is the
1367
+ cross-machine + collaboration story. Push the bucket directory to
1368
+ git on machine A; pull it on machine B (or share it with a
1369
+ teammate); `mu workstream import` rebuilds the workstream + every
1370
+ task + edge + note locally.
1371
+
1372
+ ```bash
1373
+ # Machine A — author
1374
+ mu workstream export -w auth-refactor --out exports/auth
1375
+ (cd exports/auth && git init && git add . && git commit -m 'auth snapshot')
1376
+ git push origin main
1377
+
1378
+ # Machine B — pull + rehydrate
1379
+ git pull
1380
+ mu workstream import exports/auth # → workstream `auth-refactor`
1381
+ mu workstream import exports/auth --workstream auth-v2 # rename on import
1382
+ mu workstream import exports/auth --dry-run # walk + parse + report; no DB writes
1383
+ mu workstream import exports/auth --json # machine-readable per-source-ws result
1384
+
1385
+ # Partial bucket import — multi-source bucket, but you only want
1386
+ # one (or a subset) restored. Two equivalent forms:
1387
+ mu workstream import exports/mu/roadmap-v0-2 # Form 1 — per-source-ws subdir path
1388
+ mu workstream import exports/mu --source-ws roadmap-v0-2 # Form 2 — bucket + filter
1389
+ mu workstream import exports/mu --source-ws auth,ui # Form 2 — X+Y, leave Z behind
1390
+ mu workstream import exports/mu --source-ws auth --source-ws ui # repeat OR comma-separate; or both
1391
+ ```
1392
+
1393
+ Key properties:
1394
+
1395
+ - **Markdown-only.** `.db` files are never imported (binary +
1396
+ machine-specific). `mu undo` + snapshot files cover the
1397
+ same-machine case; this verb covers cross-machine + collab.
1398
+ - **Per-source-ws transactional.** Each source-ws subdirectory is
1399
+ imported in its own SQLite transaction. A failure in source A
1400
+ rolls back A; sibling source B is unaffected.
1401
+ - **Refuses silent merges.** If the target workstream already
1402
+ exists in the DB with tasks, the import errors with
1403
+ `WorkstreamAlreadyExistsError`. Recourse:
1404
+ `--workstream <new-name>` (single-source buckets only) or
1405
+ destroy the existing workstream first.
1406
+ - **Owners reset.** Agents aren't exported, so the imported tasks
1407
+ are unowned. The original owner name survives in the markdown
1408
+ frontmatter — that's the audit trail.
1409
+ - **Tombstones skipped.** Files starting with the
1410
+ `> **Deleted from DB on …**` banner (preserved by re-export of
1411
+ a deleted task) are counted as `tombstones_skipped` and not
1412
+ re-inserted.
1413
+ - **Forward edge refs are deferred.** `blocked_by` / `blocks`
1414
+ arrays are validated against the bucket's id-set up front, then
1415
+ inserted after every task in the source-ws is created.
1416
+ - **Pre-0.3 layouts refuse.** Buckets without a `bucketVersion: 2`
1417
+ manifest throw `ImportLegacyLayoutError` with a re-export hint.
1418
+ - **Partial import.** Multi-source buckets accept either a
1419
+ per-source-ws subdir path (auto-detected via
1420
+ `README.md` + `INDEX.md` + `tasks/` + a parent
1421
+ `manifest.json` listing the subdir as a source) OR a
1422
+ `--source-ws <names...>` filter on the bucket root
1423
+ (variadic per `cli_audit_plurality_uniformity`: repeat,
1424
+ comma-separate, or both). The two forms are equivalent for
1425
+ single-source restores. `--workstream <new-name>` is allowed
1426
+ whenever the resolved source-ws list has exactly one entry
1427
+ (Form 1; or Form 2 with a single name); rejected for
1428
+ multi-source filters. Passing `--source-ws` against a Form 1
1429
+ per-source-ws subdir is refused (the subdir already implies one
1430
+ source). A `--source-ws` name not in the bucket manifest raises
1431
+ `ImportSourceNotInBucketError` (exit 4) and lists the valid
1432
+ names. `--source-ws ',,'` (canonicalises to zero names) is a
1433
+ `UsageError` (exit 2) so a typo doesn't silently fall back to
1434
+ importing the entire bucket.
1435
+
1436
+ ---
1437
+
1438
+ ## 15.5 Archives — cross-workstream preservation of task graphs
1439
+
1440
+ A `mu workstream destroy` blows away the live task graph (a
1441
+ snapshot is taken, but it's a binary `.db` only readable through
1442
+ `mu undo`). The markdown export above keeps the conversation
1443
+ human-readable on disk, but it's not queryable in-DB. The
1444
+ **archive** verb is the third option: a structured, queryable
1445
+ snapshot of a workstream's task graph (tasks + edges + notes +
1446
+ events) that lives in the same `mu.db` indefinitely and can
1447
+ accumulate snapshots from MANY workstreams under the same
1448
+ operator-named label.
1449
+
1450
+ ```bash
1451
+ mu archive create v0-3-wave --description "v0.3 release wave"
1452
+ mu archive add v0-3-wave -w mufeedback-v03
1453
+ mu archive add v0-3-wave -w roadmap-v0-3 --destroy # cascade: archive THEN destroy
1454
+ mu archive list # label | tasks | sources | created | last_added
1455
+ mu archive show v0-3-wave # detail card + per-source-workstream summary
1456
+ mu archive search 'oauth' [--label v0-3-wave] # LIKE-search archived titles + note content (--limit N, --json)
1457
+ mu archive export v0-3-wave --out exports/v0-3-wave # render every source-ws to a bucket directory (markdown)
1458
+ ```
1459
+
1460
+ Key properties:
1461
+
1462
+ - **Globally-unique labels.** Archive labels live in their own
1463
+ namespace (separate from workstream names). Pick once, reuse
1464
+ across years.
1465
+ - **Additive accumulation.** `mu archive add <label> -w <ws>` is
1466
+ idempotent at the (archive, source workstream) granularity.
1467
+ Re-running on the same workstream is a no-op; adding a new task
1468
+ to the source workstream and re-running picks up only the
1469
+ delta. Two different workstreams under the same label coexist
1470
+ as separate `(source_workstream, original_local_id)` rows.
1471
+ - **Outlives the source.** `archived_tasks.source_workstream` is
1472
+ TEXT (not an FK), so the source workstream can be destroyed and
1473
+ the archive's snapshot of it stays queryable forever.
1474
+ - **Reversible.** `mu archive delete <label> --yes` captures a
1475
+ snapshot first; `mu undo --yes` brings the whole archive back.
1476
+ `mu archive remove <label> -w <ws>` is the surgical version
1477
+ (one source workstream's contribution, without touching
1478
+ siblings).
1479
+
1480
+ ### Three lifecycle patterns
1481
+
1482
+ The verb shape supports all three; pick per-call.
1483
+
1484
+ **Pattern A — single bucket per project family** (single growing
1485
+ archive, easy cross-time queries):
1486
+
1487
+ ```bash
1488
+ mu archive create mu --description "every mu-self-development workstream"
1489
+ mu archive add mu -w mufeedback --destroy # initial v0.2 wave
1490
+ mu archive add mu -w roadmap-v0-2 --destroy
1491
+ # weeks later, after v0.3 ships:
1492
+ mu archive add mu -w mufeedback-v03 --destroy
1493
+ mu archive add mu -w roadmap-v0-3 --destroy
1494
+ # months later: same single 'mu' bucket grows.
1495
+ ```
1496
+
1497
+ **Pattern B — per-release buckets** (easier to compare "what
1498
+ shipped in v0.2 vs v0.3"):
1499
+
1500
+ ```bash
1501
+ mu archive create mu-v0-2 ; mu archive add mu-v0-2 -w mufeedback --destroy
1502
+ mu archive create mu-v0-3 ; mu archive add mu-v0-3 -w mufeedback-v03 --destroy
1503
+ ```
1504
+
1505
+ **Pattern C — hybrid** (a workstream lives in BOTH archives;
1506
+ independent rows under each label):
1507
+
1508
+ ```bash
1509
+ mu archive add mu -w mufeedback-v03
1510
+ mu archive add mu-v0-3 -w mufeedback-v03 --destroy
1511
+ ```
1512
+
1513
+ ### Anti-features (intentional)
1514
+
1515
+ - **No "default" / auto-archive.** `mu workstream destroy` does
1516
+ NOT auto-add to a fallback bucket. Either you picked a label
1517
+ deliberately or you didn't want one.
1518
+ - **No re-import.** The archive IS the workstream's afterlife.
1519
+ If you need an archived task back as live work, copy it via
1520
+ `mu sql` into a fresh workstream + `mu task add`.
1521
+ - **No archive→archive merge / rename.** Operator-managed via
1522
+ `mu sql` if it ever matters.
1523
+ - **Snapshots vs archives are separate concerns.** Snapshots are
1524
+ whole-DB binary backups for one-shot recovery (`mu undo`).
1525
+ Archives are first-class queryable structured data with their
1526
+ own lifecycle. Don't confuse them.
1527
+
1528
+ ---
1529
+
1530
+ ## 16. One-shot demo script
1531
+
1532
+ Copy-pasteable, end-to-end. Wipes any prior `~/.local/state/mu/mu.db`.
1533
+
1534
+ ```bash
1535
+ # Clean start
1536
+ tmux kill-session -t mu-demo 2>/dev/null
1537
+ rm -f ~/.local/state/mu/mu.db
1538
+
1539
+ # Plan
1540
+ mu workstream init demo
1541
+ mu task add design --title "Design" --impact 80 --effort-days 2
1542
+ mu task add build --title "Build" --impact 80 --effort-days 5 --blocked-by design
1543
+ mu task add ship --title "Ship" --impact 90 --effort-days 1 --blocked-by build
1544
+
1545
+ # Crew
1546
+ mu agent spawn worker-1 --workstream demo --cli sh
1547
+ mu agent spawn worker-2 --workstream demo --cli sh
1548
+
1549
+ # Assign + observe
1550
+ mu sql "UPDATE tasks SET owner='worker-1', status='IN_PROGRESS' WHERE local_id='design'"
1551
+ mu --workstream demo
1552
+
1553
+ # Watch live (Ctrl+b d to detach)
1554
+ tmux attach -t mu-demo
1555
+
1556
+ # Cleanup
1557
+ mu workstream destroy --workstream demo --yes
1558
+ rm -f ~/.local/state/mu/mu.db
1559
+ ```
1560
+
1561
+ ---
1562
+
1563
+ ## Mental model in three sentences
1564
+
1565
+ 1. **One workstream is one tmux session full of agent panes.** Mu
1566
+ manages the lifecycle; tmux is the substrate. Workstreams on the
1567
+ same machine are isolated by `session_id` in the SQLite registry.
1568
+
1569
+ 2. **The task DAG decides what's actionable; the LLM doesn't gamble.**
1570
+ Mission control + the `Ready` table + parallel-tracks union-find
1571
+ give deterministic answers to "what's next?" and "what can I
1572
+ parallelize?" Diamond patterns (two goals sharing a prerequisite)
1573
+ collapse into one merged track so two agents never collide on
1574
+ shared deps.
1575
+
1576
+ 3. **Agents claim tasks via their pane title — zero config.**
1577
+ `mu task claim foo` from inside `worker-1`'s pane sets `tasks.owner='worker-1'`
1578
+ atomically. mu reads the pane title via
1579
+ `tmux display-message -t $TMUX_PANE -p '#{pane_title}'`, set on
1580
+ spawn. Two agents cannot claim the same task.
1581
+
1582
+ Everything else (`mu sql`, send/read, the bracketed-paste protocol,
1583
+ ghost reconciliation) is plumbing in
1584
+ service of those three.
1585
+
1586
+ ---
1587
+
1588
+ ## What's NOT in 0.3.0 (and how to work around it)
1589
+
1590
+ <a id="whats-not-in-030-and-how-to-work-around-it"></a>
1591
+
1592
+ The full roadmap with promotion criteria lives in
1593
+ [ROADMAP.md](ROADMAP.md). The short list of gaps you might hit
1594
+ in real use:
1595
+
1596
+ | Want | Workaround | Status |
1597
+ | --------------------------------------------- | ----------------------------------------------------------------------- | ------------- |
1598
+ | Multi-CLI status detection (per-CLI prompts) | Braille spinner fallback (`f68838f`) covers pi/pi-meta + every TUI wrapper using standard spinner glyphs. Per-CLI permission-prompt patterns still pi-only. | partially shipped |
1599
+ | Pi extension (typed tools, HUD, wakeups) | `mu state --hud` covers the HUD use-case (run via `watch` / `tmux display-popup` / `status-right`). Other extension tools deferred. | partially shipped |
1600
+ | Markdown agent-definition discovery | Spawn accepts `--cli` and `--command` directly; no template registry | dropped |
1601
+ | `mu run script.ts` (JS DSL) | Use `--json` + bash + jq | rejected |
1602
+ | Sync to GitHub Issues / Linear / Asana | Not in scope; explicitly rejected | — |
1603
+ | ~~`mu task blocked`~~ (removed; the `blocked` SQL view is the abstraction) | `mu sql "SELECT local_id, status, title FROM blocked WHERE workstream='X'"` | removed-with-recipe |
1604
+ | ~~`mu task goals`~~ (removed; same shape as `blocked` — view is the abstraction) | `mu sql "SELECT local_id, status, title FROM goals WHERE workstream='X'"` | removed-with-recipe |
1605
+ | ~~`mu task search <pat>`~~ (removed; case-insensitive LIKE is one SQL line) | `mu sql "SELECT local_id, status, title FROM tasks WHERE workstream='X' AND LOWER(title) LIKE '%pat%'"` (add `LEFT JOIN task_notes` for the old `--in-notes`; drop the `WHERE workstream=` clause for the old `--all`) | removed-with-recipe |
1606
+
1607
+ Anything in this table that bites you in real use is a candidate
1608
+ for **promotion**. Criteria: proven friction in ≥2 real workflows +
1609
+ fits in <300 LOC + no major refactor of the load-bearing pillars.
1610
+ The most useful feedback is "I tried to do X and had to fall back
1611
+ to `mu sql`, twice in one session" — that's exactly the signal we
1612
+ want. File it in [ROADMAP.md](ROADMAP.md).
1613
+
1614
+ ---
1615
+
1616
+ ## Where to go from here
1617
+
1618
+ | Doc | What's in it |
1619
+ | -------------------------------------------- | ------------------------------------------------------- |
1620
+ | [README.md](../README.md) | Project overview, install, comparison vs `pi-subagents` |
1621
+ | [CHANGELOG.md](../CHANGELOG.md) | Release notes |
1622
+ | [ROADMAP.md](ROADMAP.md) | What's next, with promotion criteria + rejected ideas |
1623
+ | [VOCABULARY.md](VOCABULARY.md) | Canonical terms — source of truth for every word |
1624
+ | [VISION.md](VISION.md) | The eight load-bearing pillars + design principles |
1625
+ | [ARCHITECTURE.md](ARCHITECTURE.md) | Module map, reconciliation algorithm, layered design |
1626
+ | [skills/mu/SKILL.md](../skills/mu/SKILL.md) | What an LLM running inside an agent pane sees |
1627
+
1628
+ If you're trying mu and something doesn't work as documented, file an
1629
+ issue with: the exact `mu` command, the full output (set
1630
+ `MU_DB_PATH=/tmp/mu-debug.db` to isolate from your real registry),
1631
+ your tmux version (`tmux -V`), and your platform.