@martintrojer/mu 0.3.2 → 0.4.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.
@@ -1,26 +1,27 @@
1
1
  # mu — Usage Guide
2
2
 
3
- A practical, copy-pasteable tour of mu (current main; v0.3-track).
3
+ A practical, copy-pasteable tour of mu (current main; v0.4.1).
4
4
  Everything below works against the built CLI. Terms are canonical
5
5
  — see [VOCABULARY.md](VOCABULARY.md) for definitions; the complete
6
6
  current verb list is in `## CLI — complete verb list` of
7
7
  [skills/mu/SKILL.md](../skills/mu/SKILL.md).
8
8
 
9
- > **Status:** v0.3 wave (pre-1.0). ~60 typed verbs across 8
9
+ > **Status:** v0.4.1 (pre-1.0). ~65 typed verbs across 9
10
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).
11
+ > `snapshot`, `archive`, `db`, `me`) plus bare top-level verbs
12
+ > (`state`, `doctor`, `sql`, `undo`). Every verb accepts `--json`
13
+ > (one allow-listed exception, `mu agent attach`), per-agent VCS
14
+ > workspaces (jj/sl/git/none), activity log with `--tail`
15
+ > subscription, bare `mu` TTY dashboard, canonical static state card
16
+ > (`mu state` default / `--tui` render modes), whole-DB snapshots
17
+ > auto-captured before destructive verbs + `mu undo` /
18
+ > `mu snapshot {list,show}`, evidence on lifecycle verbs, schema v8
19
+ > (v5 surrogate INTEGER PKs + per-workstream UNIQUE on
20
+ > operator-facing names; v6 added the `archive_*` family additively;
21
+ > v7 dropped the dead `approvals` table; v8 adds `machine_identity`
22
+ > and `workstream_sync` for db sync).
22
23
  > 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
+ > and [§ Not in 0.4.1](#whats-not-in-041-and-how-to-work-around-it)
24
25
  > at the bottom for the gaps that still need workarounds.
25
26
 
26
27
  *If anything below disagrees with `mu --help`, trust `mu --help`.*
@@ -33,7 +34,8 @@ current verb list is in `## CLI — complete verb list` of
33
34
  2. [Get oriented (`mu doctor`)](#2-get-oriented)
34
35
  3. [Create a workstream (`mu workstream init`)](#3-create-a-workstream)
35
36
  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
+ 5. [See the graph (dashboard + state API)](#5-see-the-graph-dashboard--state-api)
38
+ 5b. [The TUI dashboard (interactive)](#5b-the-tui-dashboard-interactive)
37
39
  6. [Spawn a crew (`mu agent spawn`)](#6-spawn-a-crew)
38
40
  7. [Watch the crew live (`tmux attach`)](#7-watch-the-crew-live)
39
41
  8. [Send work to an agent (`mu agent send`)](#8-send-work-to-an-agent)
@@ -45,9 +47,10 @@ current verb list is in `## CLI — complete verb list` of
45
47
  14. [Recovery scenarios](#14-recovery-scenarios)
46
48
  15. [Cleanup](#15-cleanup)
47
49
  15.5. [Archives — cross-workstream preservation](#155-archives--cross-workstream-preservation-of-task-graphs)
50
+ 15.6. [Multi-machine sync](#156-multi-machine-sync)
48
51
  16. [One-shot demo script](#16-one-shot-demo-script)
49
52
  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)
53
+ 18. [What's NOT in 0.4.1](#whats-not-in-041-and-how-to-work-around-it)
51
54
  19. [Where to go from here](#where-to-go-from-here)
52
55
 
53
56
  ---
@@ -140,6 +143,32 @@ tmux # if you're not already in one
140
143
 
141
144
  ## 2. Get oriented
142
145
 
146
+ For a human at an interactive terminal, bare `mu` is the home base:
147
+ it launches the read-only TUI with every workstream on the machine
148
+ loaded as tabs. Initial tab focus uses this ladder: `$MU_SESSION` when
149
+ it names a loaded workstream; then the current tmux session name when
150
+ it is `mu-<workstream>`; then best-effort cwd detection against
151
+ registered workspace paths; then cwd equal to the VCS-derived project
152
+ root of any loaded workstream's workspaces (ties broken by most-recent
153
+ workstream activity); then tab 0. If no workstream exists yet, it
154
+ prints help plus the one-paste start command:
155
+
156
+ ```bash
157
+ mu
158
+ # Get started: mu workstream init <name>
159
+ ```
160
+
161
+ For scripts, agents, CI, and pipes, bare `mu` deliberately does NOT
162
+ enter the TUI: when stdout is not a TTY it prints `mu --help`. Use
163
+ explicit typed verbs and `--json` for the API surface:
164
+
165
+ ```bash
166
+ mu state -w <workstream> --json
167
+ MU_NO_TUI=1 mu # force the non-TTY/help path even in a terminal
168
+ ```
169
+
170
+ Run the diagnostic once to check tmux + DB health:
171
+
143
172
  ```bash
144
173
  mu doctor
145
174
  ```
@@ -222,9 +251,10 @@ $ mu task list -w foo --json
222
251
  ```
223
252
 
224
253
  `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.
254
+ hop than `jq '.items | length'`. Future siblings layer on without
255
+ breaking the existing two fields. Today `mu workspace commits --json`
256
+ also includes `vcs`, `baseRef`, and `workspacePath` siblings because
257
+ that verb already computes the workspace's fork metadata.
228
258
 
229
259
  Applies to: `mu task list / next / owned-by / notes`,
230
260
  `mu workstream list`, `mu workstream destroy --empty` (dry-run),
@@ -329,8 +359,8 @@ Tasks have **mandatory** `impact` (1–100) and `effort-days` (>0).
329
359
  Edges are blocks-relationships, modelled as **`--blocked-by`** on `mu
330
360
  task add` (and `mu task reparent`): `--blocked-by design` means "this
331
361
  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.
362
+ Tasks are **scoped to a workstream** — the dashboard and state views only show
363
+ tasks for the workstream you're viewing.
334
364
 
335
365
  ```bash
336
366
  # --workstream can be omitted if you're inside the workstream's tmux
@@ -357,112 +387,299 @@ Each task validates its id (`/^[a-z][a-z0-9_-]{0,63}$/`) and rejects
357
387
  duplicates. If you tried `mu task add x --blocked-by y` while `y`
358
388
  already transitively depended on `x`, mu would refuse with a `CycleError`.
359
389
 
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
390
+ **Task ids are per-workstream unique.** The same local id can exist in
391
+ multiple workstreams, so cross-workstream references use the qualified
392
+ form `<workstream>/<id>` when a global scope is needed. Blocks-edges
393
+ are always same-workstream — if a blocker resolves outside the target
363
394
  workstream, mu refuses with a `CrossWorkstreamEdgeError`.
364
395
 
365
396
  ---
366
397
 
367
- ## 5. See the graph (mission control)
398
+ ## 5. See the graph (dashboard + state API)
368
399
 
369
- ```bash
370
- mu --workstream auth-refactor
371
- # or, if your tmux session is mu-auth-refactor:
372
- mu
373
- ```
400
+ `mu` exposes one logical "what's going on?" view with two renderers:
374
401
 
375
- ```
376
- mu-auth-refactor
402
+ | Surface | Use it for |
403
+ | -------------------- | --------------------------------------------------------------- |
404
+ | **bare `mu`** | A human at a terminal — launches the interactive TUI dashboard. |
405
+ | **`mu state --tui`** | Same TUI, explicitly opt-in. Useful in scripts / aliases. |
406
+ | **`mu state`** | Static text card. JSON-friendly; pipeable; `watch`-able. |
407
+ | **`mu state --json`** | The canonical full snapshot. Agents and scripts read this. |
377
408
 
378
- Agents (0)
379
- (no agents)
409
+ The interactive surface is large enough to deserve its own section —
410
+ see [§ 5b. The TUI dashboard](#5b-the-tui-dashboard-interactive)
411
+ immediately below. The rest of this section is the static / JSON
412
+ contract.
380
413
 
381
- Tracks (1)
382
- Track 1: review (3 tasks, 1 ready, track)
414
+ ### Static state card (`mu state`)
383
415
 
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):
416
+ For an agent/script or a static capture, use explicit `mu state`:
417
+
418
+ ```bash
419
+ mu state -w auth-refactor # human-readable card
420
+ mu state -w auth-refactor --json # full snapshot
421
+ mu state --all --json # every workstream on this machine
422
+ ```
423
+
424
+ The static card includes every section the TUI cards summarize:
425
+ agents + orphans + tracks + ready / in-progress / blocked /
426
+ recent-closed tasks + workspaces + recent events. `--json` emits the
427
+ same full snapshot shape regardless of `--tui`.
428
+
429
+ **JSON shapes**
452
430
 
453
431
  - `mu state --json` (single-ws): flat `{ workstreamName, agents,
454
432
  orphans, tracks, ready, blocked, inProgress, recentClosed,
455
433
  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`.
434
+ - `mu state --json` (multi-ws): wrapped `{ workstreams: [{...}, ...] }`.
435
+ - bare `mu --json`: prints `--help` rather than entering the TUI;
436
+ use `mu state --json` for the full snapshot.
437
+ - `--tui` is render-only and incompatible with `--json` (the TUI
438
+ has no JSON shape).
439
+
440
+ **Multi-workstream**: pass `-w` multiple values, or `--all`. See
441
+ [CLI conventions](#cli-conventions-multi-value-flags). In static
442
+ mode N≥2 stacks one per-workstream card after another.
443
+
444
+ > **Migrating from old state surfaces**: `mu state --hud` and
445
+ > `mu state --mission` were removed in v0.4; use `mu state --tui`
446
+ > for the interactive surface and `mu state --json` for the full
447
+ > snapshot. `tmux display-popup -E 'mu state -w X'` keeps working
448
+ > unchanged for popup-card use.
461
449
 
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).
450
+ ---
451
+
452
+ ## 5b. The TUI dashboard (interactive)
453
+
454
+ The interactive TUI is mu's flagship human surface. It is **read-only
455
+ by design** — every act-intent `y`anks the canonical `mu` command to
456
+ your clipboard so you run mutations from your shell, with one
457
+ documented escape (`t` in git-show drills runs `tuicr` in the project
458
+ root, see below).
459
+
460
+ ### Launch
461
+
462
+ ```bash
463
+ mu # bare; opens the TUI when stdout is a TTY
464
+ mu state --tui # explicit; same surface
465
+ mu state --tui -w a,b # restrict to specific workstreams
466
+ mu state --tui --all # all workstreams (default for bare `mu`)
467
+ MU_NO_TUI=1 mu # force the help path even in a TTY
468
+ mu --json # also forces help; pipe `mu state --json`
469
+ ```
470
+
471
+ Quit with `q` or `Ctrl-C`. The dashboard restores your scrollback
472
+ on exit (alt-screen).
473
+
474
+ **Initial active tab** is picked from this ladder:
475
+ `$MU_SESSION` → current tmux session name (`mu-<ws>`) → cwd inside a
476
+ registered workspace → cwd at the VCS-derived project root of any
477
+ workstream (newest activity wins ties) → tab 0.
478
+
479
+ ### Layout: 10 cards, responsive columns
480
+
481
+ The dashboard renders 10 toggleable cards with rounded borders and
482
+ section headers inset into the top border line (lazygit / btop / k9s
483
+ convention):
484
+
485
+ | Slot | Card | Toggle | Popup | Content |
486
+ | ---- | ------------- | ------ | --------- | ---------------------------------------------------- |
487
+ | 0 | Commits | `0` | `Shift+0` | Recent project-root commits (git / jj / sl) |
488
+ | 1 | Agents | `1` | `Shift+1` | Active agents + status + cli + role |
489
+ | 2 | Tracks | `2` | `Shift+2` | Parallel tracks (union-find clusters) |
490
+ | 3 | Ready (Tasks) | `3` | `Shift+3` | Ready-to-claim tasks (no open blockers) |
491
+ | 4 | Activity log | `4` | `Shift+4` | Recent `agent_logs` events |
492
+ | 5 | Workspaces | `5` | `Shift+5` | Per-agent VCS workspaces + behind/dirty status |
493
+ | 6 | In-progress | `6` | `Shift+6` | IN_PROGRESS tasks owned by agents |
494
+ | 7 | Blocked | `7` | `Shift+7` | Tasks with at least one open blocker |
495
+ | 8 | Recent | `8` | `Shift+8` | Recently CLOSED tasks |
496
+ | 9 | Doctor | `9` | `Shift+9` | Cheap health checks (schema, ghosts, orphans, …) |
497
+ | — | DAG | — | `g` | Full task DAG forest (keybind-only) |
498
+ | — | All tasks | — | `t` | Sortable / filterable list of every task |
499
+
500
+ Digit toggles HIDE / SHOW the card on the dashboard; `Shift+digit`
501
+ opens the matching fullscreen popup. **Single-popup invariant**: only
502
+ one popup is visible at a time; `Esc` / `q` returns to the dashboard
503
+ with all toggles + tick rate preserved.
504
+
505
+ **Responsive layout**: cards stack below 120 cols, then reflow into
506
+ pair-aware 2 / 3 / 4-column layouts at 120 / 180 / 240 cols. Each
507
+ visible card gets a dynamic row budget so a noisy list cannot crowd
508
+ out its siblings; overflow shows as `+N more · Shift+N` inset into
509
+ the bottom border. On very short panes, the dashboard culls
510
+ low-priority cards (Doctor → Recent → Workspaces → …) and shows
511
+ `+N cards hidden · resize taller` until the surviving cards fit.
512
+
513
+ Dashboard ordering is slot-stable: within each rendered column,
514
+ non-stream cards are ordered by toggle digit ascending; stream cards
515
+ (Commits, Activity log) sit as natural trailers, with slot 0 trailing
516
+ last.
517
+
518
+ ### Multi-workstream tabs
519
+
520
+ When the TUI is launched with N≥2 workstreams (e.g. bare `mu` on a
521
+ machine with multiple workstreams, or `mu state --tui -w a,b,c`),
522
+ a compact tab strip renders above the cards:
523
+
524
+ ```
525
+ workstreams: ▸ auth-refactor · ui-rewrite · demo (Tab / Shift-Tab)
526
+ ```
527
+
528
+ - `Tab` cycles forward, `Shift-Tab` backward (suppressed inside
529
+ popups so the same key still navigates inside popups that bind
530
+ it locally).
531
+ - The active tab name appears in the status bar's right zone next
532
+ to the tick rate.
533
+ - Cards / popups always operate on the active tab — there's no
534
+ per-row workstream column.
535
+ - For N=1 the strip renders nothing (frame is byte-identical to
536
+ single-ws TUI).
537
+ - When the workstream set is wider than the terminal, the strip
538
+ windows around the active tab and shows `‹N` / `›N` counters
539
+ for hidden workstreams.
540
+
541
+ ### Popup drills
542
+
543
+ `Enter` in any list popup drills into the focused row. Where the
544
+ row is itself an entity (a task), a further `Enter` chains into the
545
+ shared read-only task-detail leaf (notes timeline):
546
+
547
+ - **Tracks popup (`Shift+2`)**: list of tracks → `Enter` opens the
548
+ track's task list → `Enter` opens that task's notes timeline.
549
+ - **Ready / In-progress / Blocked / Recent / All-tasks**:
550
+ list of tasks → `Enter` opens notes; `y` yanks `mu task show <id>`
551
+ (or `mu task claim` / `mu task close` / `mu task tree` depending
552
+ on popup).
553
+ - **Activity log popup (`Shift+4`)**: list of events → `Enter`
554
+ drills into the full untruncated payload of the focused event;
555
+ `y` yanks `mu log --since <seq-1> -n 1 -w <ws>`.
556
+ - **Workspaces popup (`Shift+5`)**: list of workspaces → `Enter`
557
+ opens the commits-since-fork list → `Enter` on a commit opens
558
+ the inline `git show <sha> --stat -p` view; `y` yanks `git show
559
+ <sha>`; `t` launches `tuicr -r <sha>`.
560
+ - **Commits popup (`Shift+0`)**: project-root recent commits →
561
+ `Enter` opens the backend's show view; `y` yanks the show
562
+ command; `t` launches `tuicr`.
563
+ - **Doctor popup (`Shift+9`)**: list of checks → `Enter` opens
564
+ the remediation paragraph for the focused check.
565
+ - **DAG popup (`g`)**: keybind-only; renders the active workstream's
566
+ full task DAG forest (one ASCII subtree per root, diamond-collapse
567
+ marker on repeated nodes).
568
+
569
+ One `Esc` / `q` backs out per recursion level. Drills auto-refresh
570
+ in step with the dashboard tick (fast 1s for SQL-derived bodies
571
+ like notes, slow 10s for subprocess git-show / scrollback). Scroll
572
+ position is preserved across refreshes; subprocess loaders keep the
573
+ prior body visible until the new one arrives so there's no
574
+ blank-flash mid-refetch.
575
+
576
+ ### Search / filter
577
+
578
+ `/` inside any list popup enters an incremental case-insensitive
579
+ substring filter (lazygit / k9s convention):
580
+
581
+ - Every printable character appends to the query; `Backspace` pops
582
+ one.
583
+ - `Esc` cancels (clears the query); `Enter` commits (keeps the
584
+ filter applied while letting `j/k` resume normal navigation).
585
+ - Press `/` again on a committed filter to refine.
586
+ - The filter blob is per-popup: agent name + status + cli + role;
587
+ track head id + title; task name + title + status + owner; log
588
+ verb + payload + source.
589
+ - Filter state is per-popup and dies with the popup.
590
+
591
+ Task-list popups also expose **per-status toggles** (`o` / `i` / `c`
592
+ / `r` / `d` toggle OPEN / IN_PROGRESS / CLOSED / REJECTED / DEFERRED
593
+ visibility; default all-on).
594
+
595
+ The All-tasks popup adds **sort cycle** on `s`: `roi` → `recency`
596
+ → `age` → `id`.
597
+
598
+ ### Mouse
599
+
600
+ Navigation-in only:
601
+
602
+ - Double-click a dashboard card → opens its popup.
603
+ - Scroll-wheel inside a popup list / drill body → moves the focused
604
+ cursor / scrolls the body.
605
+ - Double-click a popup row → drills one level deeper.
606
+
607
+ There is intentionally **no mouse back binding** — use `Esc` / `q`
608
+ to back out predictably.
609
+
610
+ ### Yank contract (`y`) and the `tuicr` escape (`t`)
611
+
612
+ Every popup row exposes one canonical `mu` command via `y`. The
613
+ command goes to your system clipboard (pbcopy / wl-copy / xclip /
614
+ xsel / clip.exe with OSC-52 fallback). You run it in your shell.
615
+ The TUI never executes a mutation.
616
+
617
+ The one user-driven escape from the read-only pledge is **`t`**
618
+ inside any `git show` drill (Workspaces popup or Commits popup):
619
+ mu suspends its alt-screen, runs `tuicr -r <sha>` in the project
620
+ root / workspace cwd, then restores the dashboard when tuicr exits.
621
+ This is a deliberate handoff — the operator drives another TUI
622
+ tool, not mu performing the mutation.
623
+
624
+ Task-list cards and popups colour-code status cells consistently
625
+ with the static CLI tables: OPEN cyan, IN_PROGRESS yellow, CLOSED
626
+ green, REJECTED red, DEFERRED dim/gray.
627
+
628
+ ### Polling tiers
629
+
630
+ The dashboard has two refresh tiers:
631
+
632
+ - **Fast tick** (default 1s, adjustable with `+` / `-` / `=` /
633
+ `0`): SQL-only. Refreshes tasks, tracks, workspace registry rows,
634
+ and the activity log.
635
+ - **Slow tick** (10s, fixed): subprocess-backed. Refreshes
636
+ tmux-derived agent liveness / orphans, workspace dirty flags,
637
+ recent project commits, and the Doctor summary.
638
+
639
+ The last slow-tier result is merged into every fast render so cards
640
+ do not flicker through a loading state. `r` / F5 refreshes both
641
+ tiers immediately. Tab / Shift-Tab triggers an eager slow refresh
642
+ for the newly active workstream.
643
+
644
+ ### Keymap reference
645
+
646
+ | Mode | Keys | Action |
647
+ | --------- | ---------------------------- | -------------------------------------------------------------- |
648
+ | dashboard | `0`-`9` | toggle card visibility |
649
+ | dashboard | `Shift+0`-`Shift+9` | open the matching popup |
650
+ | dashboard | `g` | open DAG popup (keybind-only) |
651
+ | dashboard | `t` | open All-tasks popup (keybind-only) |
652
+ | dashboard | `Tab` / `Shift-Tab` | cycle workstream tabs (N≥2) |
653
+ | dashboard | `+` / `-` / `=` / `0` | adjust fast tick rate (faster / slower / default / pause) |
654
+ | dashboard | `r` / `F5` | force refresh both tiers |
655
+ | dashboard | `?` / `F1` | open help overlay |
656
+ | any | `q` / `Ctrl-C` | quit (or back out of popup; quits at dashboard) |
657
+ | popup | `j` / `k` | move cursor / scroll |
658
+ | popup | `g` / `G` | jump top / bottom |
659
+ | popup | `Ctrl-D` / `Ctrl-U` | half-page down / up |
660
+ | popup | `PgDn` / `PgUp` | full page |
661
+ | popup | `Enter` | drill into focused row |
662
+ | popup | `Esc` / `q` | back out one level |
663
+ | popup | `y` | yank canonical `mu` command for focused row |
664
+ | popup | `/` | enter filter mode |
665
+ | filter | (printable) / `Backspace` | edit query |
666
+ | filter | `Esc` | cancel (clear query) |
667
+ | filter | `Enter` | commit (keep filter, return to nav) |
668
+ | task popup| `o` / `i` / `c` / `r` / `d` | toggle OPEN / IN_PROGRESS / CLOSED / REJECTED / DEFERRED |
669
+ | All-tasks | `s` | cycle sort key (roi → recency → age → id) |
670
+ | git-show | `t` | launch `tuicr -r <sha>` (alt-screen handoff) |
671
+
672
+ `?` shows the same table as a scrollable overlay (j/k/Ctrl-D/U/g/G
673
+ also work inside the overlay).
674
+
675
+ ### Read-only invariant
676
+
677
+ The TUI never executes a mutation. This is not a feature of the
678
+ implementation; it's a load-bearing pledge in `docs/ROADMAP.md`. If
679
+ a future TUI gesture tempts you to call into the SDK to mutate
680
+ state, file a roadmap entry first — the yank-and-run pattern is the
681
+ intentional cost we pay to keep the TUI inspectable, scriptable, and
682
+ recoverable from any shell.
466
683
 
467
684
  ---
468
685
 
@@ -630,6 +847,28 @@ MU_SEND_DELAY_MS=300 mu agent send worker-1 "..." # faster, less safe
630
847
  MU_SEND_DELAY_MS=1000 mu agent send worker-1 "..." # slow remote
631
848
  ```
632
849
 
850
+ If the target agent has a workspace that is **stale** (≥10 commits
851
+ behind main — the same red bucket shown in `mu workspace list` and the
852
+ TUI Workspaces card), `mu agent send` prints a yellow stderr warning
853
+ but still sends by default:
854
+
855
+ ```bash
856
+ WARN: worker-1 workspace is 14 commits behind main (≥10 = stale)
857
+ Next:
858
+ Refresh first : mu workspace refresh worker-1 -w auth-refactor
859
+ ```
860
+
861
+ Use `--strict-staleness` when a wrapper should refuse instead of
862
+ warning:
863
+
864
+ ```bash
865
+ mu agent send worker-1 "..." -w auth-refactor --strict-staleness
866
+ ```
867
+
868
+ Agents without workspaces are skipped (common for read-only roles).
869
+ `--json` output includes `staleness: null` or `{agentName,
870
+ workstreamName, commitsBehindMain, isStale}`.
871
+
633
872
  ---
634
873
 
635
874
  ## 9. Read what an agent did
@@ -714,6 +953,29 @@ workstream prefix, `AgentNotFoundError` (exit 3, message names the
714
953
  workstream) when the named worker doesn't live there. Nothing is
715
954
  written on either failure.
716
955
 
956
+ When `--for` targets an agent with a stale workspace (≥10 commits
957
+ behind main), `mu task claim` warns on stderr and appends a refresh
958
+ hint, but the claim still succeeds by default:
959
+
960
+ ```bash
961
+ mu task claim build -w auth-refactor --for worker-2
962
+ # stderr: WARN: worker-2 workspace is 14 commits behind main (≥10 = stale)
963
+ # Next: Refresh first : mu workspace refresh worker-2 -w auth-refactor
964
+ ```
965
+
966
+ Pass `--strict-staleness` to refuse the claim instead with typed
967
+ `TaskClaimStaleWorkspaceError` (exit 4). This is useful for scripts
968
+ that should never dispatch work onto a stale parent:
969
+
970
+ ```bash
971
+ mu task claim build -w auth-refactor --for worker-2 --strict-staleness
972
+ ```
973
+
974
+ `--json` output includes `staleness: null` or `{agentName,
975
+ workstreamName, commitsBehindMain, isStale}`. Bare in-pane claims and
976
+ `--self` claims do not run this check because they do not assign work
977
+ to a named agent via `--for`.
978
+
717
979
  ### The orchestrator pattern: `--self`
718
980
 
719
981
  Not every action comes from a registered worker pane. Often the
@@ -723,7 +985,7 @@ a worker pane just for a 5-minute job. Two patterns split here:
723
985
 
724
986
  - **Worker** — a pane mu spawned (or you adopted). Has a row in the
725
987
  `agents` table. Identity = pane title. Claims with bare
726
- `mu task claim <id>`. `tasks.owner` is set to the worker's name.
988
+ `mu task claim <id>`. `tasks.owner_id` points at the worker row.
727
989
 
728
990
  - **Actor** — anything that *causes* a state change. Includes
729
991
  workers, but also includes the orchestrator. May or may not have
@@ -745,13 +1007,13 @@ Three actionable next steps. Pick one based on intent:
745
1007
  ```bash
746
1008
  # Orchestrator does the work itself (most common):
747
1009
  mu task claim some-task --self --evidence "trivial 5-line fix"
748
- # -> tasks.owner stays NULL
1010
+ # -> tasks.owner_id stays NULL
749
1011
  # -> agent_logs records 'task claim some-task by pi-mu --self (anonymous)'
750
1012
  # -> mu task show surfaces it as 'owner: (self: pi-mu)'
751
1013
 
752
1014
  # Orchestrator dispatches to a worker:
753
1015
  mu task claim some-task --for worker-1
754
- # -> tasks.owner = 'worker-1'
1016
+ # -> tasks.owner_id points at worker-1
755
1017
 
756
1018
  # Orchestrator wants to BE a registered worker (rare):
757
1019
  mu agent adopt %6441 -w <ws> # only if pane is in mu-<ws> session
@@ -767,7 +1029,7 @@ to pane title, or `$USER`, or `unknown`):
767
1029
  mu task claim deploy --self --actor deploy-bot --evidence "prod release"
768
1030
  ```
769
1031
 
770
- When `tasks.owner IS NULL` because of `--self`, `mu task show` looks
1032
+ When `tasks.owner_id IS NULL` because of `--self`, `mu task show` looks
771
1033
  up the most recent `task claim` event for that task and surfaces it:
772
1034
 
773
1035
  ```
@@ -817,7 +1079,12 @@ verb stays useful on un-claimed tasks).
817
1079
  Or, for ad-hoc shape, the SQL escape hatch:
818
1080
 
819
1081
  ```bash
820
- mu sql "SELECT author, content, created_at FROM task_notes WHERE task_id='design' ORDER BY id"
1082
+ mu sql "SELECT n.author, n.content, n.created_at
1083
+ FROM task_notes n
1084
+ JOIN tasks t ON t.id = n.task_id
1085
+ JOIN workstreams w ON w.id = t.workstream_id
1086
+ WHERE t.local_id='design' AND w.name='auth-refactor'
1087
+ ORDER BY n.id"
821
1088
  ```
822
1089
 
823
1090
  Convention for note content: `KEY: value` lines. Common keys are
@@ -845,6 +1112,18 @@ task re-enters the ready set (the canonical "hand it back to the
845
1112
  pool" workflow). `--reopen` is the escape hatch for forcing `OPEN`
846
1113
  from `CLOSED` / `REJECTED` / `DEFERRED`.
847
1114
 
1115
+ When the closing actor has a per-agent workspace and that workspace
1116
+ has uncommitted edits, a successful close adds one extra `Next:` hint
1117
+ reminding the actor to commit before the next wave:
1118
+
1119
+ ```bash
1120
+ cd $(mu workspace path worker-1 -w auth-refactor) && git commit -am 'Design auth module'
1121
+ ```
1122
+
1123
+ The hint is best-effort: no workspace, a clean workspace, the `none`
1124
+ backend, or a failed VCS dirty check simply omit it. The same
1125
+ `nextSteps` entry is present in `--json` output.
1126
+
848
1127
  `--if-ready` is the umbrella-on-wave-done shape: an orchestrator
849
1128
  fires `mu task close <umbrella> --if-ready` after each wave-task
850
1129
  finishes (or unconditionally as a final action). It's a no-op while
@@ -885,8 +1164,9 @@ verbs don't cover: ad-hoc joins, manual recovery, exploring schema.
885
1164
  The schema is 8 core tables (`workstreams`, `agents`, `tasks`,
886
1165
  `task_edges`, `task_notes`, `agent_logs`, `vcs_workspaces`,
887
1166
  `snapshots`), 5 archive tables (`archives`, `archived_tasks`,
888
- `archived_edges`, `archived_notes`, `archived_events`), 1 meta table
889
- (`schema_version`), plus three views (`ready`, `blocked`, `goals`):
1167
+ `archived_edges`, `archived_notes`, `archived_events`), 2 meta tables
1168
+ (`schema_version`, `machine_identity`), 1 sync table
1169
+ (`workstream_sync`), plus three views (`ready`, `blocked`, `goals`):
890
1170
 
891
1171
  ```bash
892
1172
  mu sql "SELECT name FROM sqlite_master WHERE type IN ('table','view') ORDER BY type, name"
@@ -936,21 +1216,29 @@ one verify, one workspace recycle:
936
1216
  # The dispatch-pipeline recipe: cycle until in_flight is empty.
937
1217
  in_flight=( mufeedback-v03/foo mufeedback-v03/bar roadmap-v0-3/baz )
938
1218
  while (( ${#in_flight[@]} > 0 )); do
939
- closed=$(mu task wait "${in_flight[@]}" --first --timeout 90 --json \
940
- | jq -r '.firing.qualifiedId // empty')
1219
+ res=$(mu task wait "${in_flight[@]}" --first --timeout 90 --json)
1220
+ closed=$(jq -r '.firing.qualifiedId // empty' <<<"$res")
941
1221
  if [[ -z "$closed" ]]; then break; fi # timeout or exit 6 — see below
942
1222
 
943
- # 1. Cherry-pick the worker's HEAD (the worker is named in the
944
- # nextSteps array — or use `mu task show` to look up).
945
- worker=$(mu task show "${closed##*/}" -w "${closed%%/*}" --json | jq -r .ownerName)
1223
+ worker=$(jq -r '.firing.owner // empty' <<<"$res")
946
1224
  ws=${closed%%/*}
947
- # `mu workspace commits --json` knows the workspace's parent_ref
948
- # so this is one verb instead of `cd $(mu workspace path) && git log`.
949
- sha=$(mu workspace commits "$worker" -w "$ws" --json | jq -r '.[-1].sha')
950
- git cherry-pick "$sha"
1225
+
1226
+ # 1. Inspect, then run, the sha-pinned apply hint from nextSteps.
1227
+ # When the worker has commits since its fork point, the command is
1228
+ # `git cherry-pick <sha>` (or `<first>^..<last>` for multiple
1229
+ # commits). When the worker closed without committing, nextSteps
1230
+ # says so and points at manual `git diff` / `git apply` rescue.
1231
+ apply=$(jq -r '.nextSteps[0].command' <<<"$res")
1232
+ printf 'apply hint: %s\n' "$apply"
1233
+ if [[ "$apply" == git\ cherry-pick* ]]; then
1234
+ eval "$apply"
1235
+ else
1236
+ echo "manual rescue required; inspect the worker workspace before continuing"
1237
+ break
1238
+ fi
951
1239
 
952
1240
  # 2. Verify
953
- npm run typecheck && npm run lint && npm run test && npm run build
1241
+ npm run typecheck && npm run lint && npm run test:fast && npm run test && npm run build
954
1242
 
955
1243
  # 3. Refresh the workspace for the next dispatch (rebases onto
956
1244
  # fresh main WITHOUT killing the worker's LLM context). Default
@@ -1058,12 +1346,14 @@ mu sql "UPDATE tasks SET status='IN_PROGRESS'
1058
1346
 
1059
1347
  # What's blocking what (open tasks only) — same data as `mu task tree`
1060
1348
  # but as a flat join when you want a wider report. task_edges is keyed
1061
- # by tasks.id, not local_id.
1349
+ # by tasks.id, not local_id; join workstreams to scope the report.
1062
1350
  mu sql "SELECT b.local_id AS blocked, t.local_id AS by_task
1063
1351
  FROM tasks b
1352
+ JOIN workstreams w ON w.id = b.workstream_id
1064
1353
  JOIN task_edges e ON e.to_task_id = b.id
1065
1354
  JOIN tasks t ON t.id = e.from_task_id
1066
- WHERE t.status != 'CLOSED' AND b.status = 'OPEN'"
1355
+ WHERE w.name='mufeedback-v03'
1356
+ AND t.status != 'CLOSED' AND b.status = 'OPEN'"
1067
1357
 
1068
1358
  # Recursive CTE: every task that transitively blocks `launch` in a
1069
1359
  # given workstream (or use `mu task tree launch --json` for the same
@@ -1348,8 +1638,10 @@ failure leaves the registry intact (you can retry); if you only want
1348
1638
  the DB cleared, use `mu sql` directly:
1349
1639
 
1350
1640
  ```bash
1351
- mu sql "DELETE FROM tasks WHERE workstream='auth-refactor'" # cascades
1352
- mu sql "DELETE FROM agents WHERE workstream='auth-refactor'"
1641
+ mu sql "DELETE FROM tasks
1642
+ WHERE workstream_id=(SELECT id FROM workstreams WHERE name='auth-refactor')" # cascades
1643
+ mu sql "DELETE FROM agents
1644
+ WHERE workstream_id=(SELECT id FROM workstreams WHERE name='auth-refactor')"
1353
1645
  ```
1354
1646
 
1355
1647
  Or nuke the entire DB:
@@ -1376,7 +1668,7 @@ subdirectory per source workstream:
1376
1668
  <bucket>/
1377
1669
  README.md # bucket-level summary (every source-ws + dates + totals)
1378
1670
  INDEX.md # union of all task tables; first column = source-ws
1379
- manifest.json # bucketVersion: 2 + per-source-ws sha256 + per-task sha256
1671
+ manifest.json # bucketVersion: 2, manifest_version: 2, per-source-ws task summaries + sha256s
1380
1672
  <source-ws>/
1381
1673
  README.md # per-source-ws (counts)
1382
1674
  INDEX.md # per-source-ws (table of every task)
@@ -1386,12 +1678,21 @@ subdirectory per source workstream:
1386
1678
  Bucket exports are **additive**: `mu workstream export -w X --out
1387
1679
  <bucket>` creates the bucket scaffolding plus `X/` on first use,
1388
1680
  and a follow-up call with `-w Y --out <same-bucket>` appends a
1389
- sibling `Y/` subdirectory without touching `X/`. Re-running with
1681
+ sibling `Y/` subdirectory without touching `X/`. The top-level
1682
+ `INDEX.md` is always the union from `manifest.sources`, so a later
1683
+ single-workstream refresh does not drop sibling workstreams from the
1684
+ bucket-wide task table. Re-running with
1390
1685
  the same `-w` is sha256-idempotent: only changed task files are
1391
1686
  rewritten (mtime preserved on identical files); tasks added since
1392
1687
  the previous export get fresh files; tasks deleted from the DB
1393
1688
  STAY on disk with a `> **Deleted from DB on <ts>**` banner so you
1394
- never lose context that may already be git-blamed.
1689
+ never lose context that may already be git-blamed. `manifest_version:
1690
+ 2` stores compact task summaries (`name`/`title`/`status`/`impact`/
1691
+ `effortDays`) beside the per-file sha256s; older v1 manifests are
1692
+ accepted on re-export; mu infers the missing summaries from existing
1693
+ per-task markdown when possible, falling back to placeholder values
1694
+ only if a task file is missing or unreadable, so the bucket remains
1695
+ appendable.
1395
1696
 
1396
1697
  ```bash
1397
1698
  # One-shot dump (bucket happens to contain just one source-ws)
@@ -1427,75 +1728,20 @@ Markdown only by design — no HTML/PDF, no embedded VCS, no
1427
1728
  cross-workstream merge. Operators can pandoc / `git init`
1428
1729
  themselves.
1429
1730
 
1430
- ### Cross-machine + collab `mu workstream import`
1731
+ ### Bucket exports are read-only artifacts
1431
1732
 
1432
- The export above plus `mu workstream import <bucket-dir>` is the
1433
- cross-machine + collaboration story. Push the bucket directory to
1434
- git on machine A; pull it on machine B (or share it with a
1435
- teammate); `mu workstream import` rebuilds the workstream + every
1436
- task + edge + note locally.
1733
+ Bucket exports (`mu workstream export` and `mu archive export`) are
1734
+ now **read-only** artifacts for humans / git / docs. They are still
1735
+ excellent for grep, code review, project handoff, and historical
1736
+ write-ups, but they are no longer a load-bearing DB round-trip path.
1437
1737
 
1438
- ```bash
1439
- # Machine A — author
1440
- mu workstream export -w auth-refactor --out exports/auth
1441
- (cd exports/auth && git init && git add . && git commit -m 'auth snapshot')
1442
- git push origin main
1738
+ Use the typed surfaces for recovery and movement:
1443
1739
 
1444
- # Machine B pull + rehydrate
1445
- git pull
1446
- mu workstream import exports/auth # workstream `auth-refactor`
1447
- mu workstream import exports/auth --workstream auth-v2 # rename on import
1448
- mu workstream import exports/auth --dry-run # walk + parse + report; no DB writes
1449
- mu workstream import exports/auth --json # machine-readable per-source-ws result
1450
-
1451
- # Partial bucket import — multi-source bucket, but you only want
1452
- # one (or a subset) restored. Two equivalent forms:
1453
- mu workstream import exports/mu/roadmap-v0-2 # Form 1 — per-source-ws subdir path
1454
- mu workstream import exports/mu --source-ws roadmap-v0-2 # Form 2 — bucket + filter
1455
- mu workstream import exports/mu --source-ws auth,ui # Form 2 — X+Y, leave Z behind
1456
- mu workstream import exports/mu --source-ws auth --source-ws ui # repeat OR comma-separate; or both
1457
- ```
1458
-
1459
- Key properties:
1460
-
1461
- - **Markdown-only.** `.db` files are never imported (binary +
1462
- machine-specific). `mu undo` + snapshot files cover the
1463
- same-machine case; this verb covers cross-machine + collab.
1464
- - **Per-source-ws transactional.** Each source-ws subdirectory is
1465
- imported in its own SQLite transaction. A failure in source A
1466
- rolls back A; sibling source B is unaffected.
1467
- - **Refuses silent merges.** If the target workstream already
1468
- exists in the DB with tasks, the import errors with
1469
- `WorkstreamAlreadyExistsError`. Recourse:
1470
- `--workstream <new-name>` (single-source buckets only) or
1471
- destroy the existing workstream first.
1472
- - **Owners reset.** Agents aren't exported, so the imported tasks
1473
- are unowned. The original owner name survives in the markdown
1474
- frontmatter — that's the audit trail.
1475
- - **Tombstones skipped.** Files starting with the
1476
- `> **Deleted from DB on …**` banner (preserved by re-export of
1477
- a deleted task) are counted as `tombstones_skipped` and not
1478
- re-inserted.
1479
- - **Forward edge refs are deferred.** `blocked_by` / `blocks`
1480
- arrays are validated against the bucket's id-set up front, then
1481
- inserted after every task in the source-ws is created.
1482
- - **Partial import.** Multi-source buckets accept either a
1483
- per-source-ws subdir path (auto-detected via
1484
- `README.md` + `INDEX.md` + `tasks/` + a parent
1485
- `manifest.json` listing the subdir as a source) OR a
1486
- `--source-ws <names...>` filter on the bucket root
1487
- (variadic per `cli_audit_plurality_uniformity`: repeat,
1488
- comma-separate, or both). The two forms are equivalent for
1489
- single-source restores. `--workstream <new-name>` is allowed
1490
- whenever the resolved source-ws list has exactly one entry
1491
- (Form 1; or Form 2 with a single name); rejected for
1492
- multi-source filters. Passing `--source-ws` against a Form 1
1493
- per-source-ws subdir is refused (the subdir already implies one
1494
- source). A `--source-ws` name not in the bucket manifest raises
1495
- `ImportSourceNotInBucketError` (exit 4) and lists the valid
1496
- names. `--source-ws ',,'` (canonicalises to zero names) is a
1497
- `UsageError` (exit 2) so a typo doesn't silently fall back to
1498
- importing the entire bucket.
1740
+ | Need | Verb |
1741
+ | ---- | ---- |
1742
+ | Lossless un-archive | `mu archive restore <label> --as <new-ws> [--source <orig-ws>]` |
1743
+ | Laptop ↔ devserver handoff | `mu db export <file>` + `mu db import <file>` |
1744
+ | Manual recovery from a parked conflict | `mu db replay <sidecar>` |
1499
1745
 
1500
1746
  ---
1501
1747
 
@@ -1518,7 +1764,8 @@ mu archive add v0-3-wave -w roadmap-v0-3 --destroy # cascade: archive THEN des
1518
1764
  mu archive list # label | tasks | sources | created | last_added
1519
1765
  mu archive show v0-3-wave # detail card + per-source-workstream summary
1520
1766
  mu archive search 'oauth' [--label v0-3-wave] # LIKE-search archived titles + note content (--limit N, --json)
1521
- mu archive export v0-3-wave --out exports/v0-3-wave # render every source-ws to a bucket directory (markdown)
1767
+ mu archive restore v0-3-wave --as restored-auth --source auth-refactor
1768
+ mu archive export v0-3-wave --out exports/v0-3-wave # read-only markdown bucket for humans/git/docs
1522
1769
  ```
1523
1770
 
1524
1771
  Key properties:
@@ -1526,15 +1773,25 @@ Key properties:
1526
1773
  - **Globally-unique labels.** Archive labels live in their own
1527
1774
  namespace (separate from workstream names). Pick once, reuse
1528
1775
  across years.
1529
- - **Additive accumulation.** `mu archive add <label> -w <ws>` is
1530
- idempotent at the (archive, source workstream) granularity.
1531
- Re-running on the same workstream is a no-op; adding a new task
1532
- to the source workstream and re-running picks up only the
1533
- delta. Two different workstreams under the same label coexist
1534
- as separate `(source_workstream, original_local_id)` rows.
1776
+ - **Snapshot-only accumulation.** `mu archive add <label> -w <ws>` is
1777
+ idempotent at the (archive, source workstream) granularity and is
1778
+ designed for end-of-milestone snapshot-and-destroy flows. Re-running
1779
+ on the same workstream is task-incremental: newly-created tasks are
1780
+ added, but notes and events for already-archived tasks stay pinned
1781
+ to the original snapshot and are NOT refreshed. If you need a full
1782
+ event-stream refresh for a source workstream, remove that source (or
1783
+ delete/re-create the archive label) and add it again. Two different
1784
+ workstreams under the same label coexist as separate
1785
+ `(source_workstream, original_local_id)` rows.
1535
1786
  - **Outlives the source.** `archived_tasks.source_workstream` is
1536
1787
  TEXT (not an FK), so the source workstream can be destroyed and
1537
1788
  the archive's snapshot of it stays queryable forever.
1789
+ - **Lossless un-archive.** `mu archive restore <label> --as <new-ws>
1790
+ [--source <orig-ws>]` copies tasks, edges, and notes directly from
1791
+ `archived_*` tables into a fresh workstream. It refuses if `--as`
1792
+ collides and snapshots before writing. Archives do not snapshot live
1793
+ panes or the live event log, so agents, workspace paths, and
1794
+ `agent_logs` are not restored.
1538
1795
  - **Reversible.** `mu archive delete <label> --yes` captures a
1539
1796
  snapshot first; `mu undo --yes` brings the whole archive back.
1540
1797
  `mu archive remove <label> -w <ws>` is the surgical version
@@ -1579,9 +1836,9 @@ mu archive add mu-v0-3 -w mufeedback-v03 --destroy
1579
1836
  - **No "default" / auto-archive.** `mu workstream destroy` does
1580
1837
  NOT auto-add to a fallback bucket. Either you picked a label
1581
1838
  deliberately or you didn't want one.
1582
- - **No re-import.** The archive IS the workstream's afterlife.
1583
- If you need an archived task back as live work, copy it via
1584
- `mu sql` into a fresh workstream + `mu task add`.
1839
+ - **No bucket re-import.** The archive IS the workstream's afterlife.
1840
+ If you need an archived source workstream back as live work, use
1841
+ `mu archive restore <label> --as <new-ws> [--source <orig-ws>]`.
1585
1842
  - **No archive→archive merge / rename.** Operator-managed via
1586
1843
  `mu sql` if it ever matters.
1587
1844
  - **Snapshots vs archives are separate concerns.** Snapshots are
@@ -1591,6 +1848,71 @@ mu archive add mu-v0-3 -w mufeedback-v03 --destroy
1591
1848
 
1592
1849
  ---
1593
1850
 
1851
+ ## 15.6 Multi-machine sync
1852
+
1853
+ Use `mu db {export,import,replay}` when one user alternates a
1854
+ workstream between two machines (for example laptop ↔ devserver) over
1855
+ multi-day stretches. You own the transport: `rsync`, `scp`, Dropbox,
1856
+ git-lfs, USB, whatever moves a SQLite file plus its manifest.
1857
+
1858
+ **Hard rule / user contract:** do not edit the same workstream on two
1859
+ machines concurrently. Other workstreams may keep moving locally, but
1860
+ for one workstream, finish or release in-flight claims before export:
1861
+ `mu agent list -w <ws>` shows current owners. `mu db import` does not
1862
+ carry owners because `owner_id` points at the machine-local `agents`
1863
+ table.
1864
+
1865
+ ```bash
1866
+ # Machine A — export the whole DB copy + ~/Dropbox/mu.db.manifest.json
1867
+ mu db export ~/Dropbox/mu.db --force
1868
+ # ship file (rsync / scp / Dropbox / git-lfs / USB)
1869
+
1870
+ # Machine B — preview first, then commit
1871
+ mu db import ~/Dropbox/mu.db # dry-run preview
1872
+ mu db import ~/Dropbox/mu.db --apply # commits FAST_FORWARD / IMPORT rows
1873
+ ```
1874
+
1875
+ Dry-run output is a per-workstream decision table:
1876
+
1877
+ ```
1878
+ workstream decision delta
1879
+ ----------- ------------ -------------------------------
1880
+ auth FAST_FORWARD source 42, local 39, last_synced 39
1881
+ docs IDENTICAL source 12, local 12, last_synced 12
1882
+ local-only LOCAL_AHEAD source 0, local 7, re-export from this machine
1883
+ experiment CONFLICT source 55, local 58, needs --force-source
1884
+ ```
1885
+
1886
+ (The actual CLI also prints the numeric columns separately:
1887
+ `source_seq`, `local_seq`, `last_synced`, and `needs`.)
1888
+
1889
+ Five case branches exist: `IDENTICAL` / `FAST_FORWARD` /
1890
+ `LOCAL_AHEAD` / `CONFLICT` / `IMPORT` (source-only or clean-machine
1891
+ import). `LOCAL_AHEAD` means the incoming file is stale for that
1892
+ workstream; re-export from this machine instead of applying it.
1893
+ `CONFLICT` means both sides advanced since the last sync and mu
1894
+ refuses by default.
1895
+
1896
+ Recovery from an accidental concurrent edit is intentionally sharp:
1897
+
1898
+ ```bash
1899
+ mu db import ~/Dropbox/mu.db --apply --force-source
1900
+ # prints a parked loser like:
1901
+ # <state-dir>/divergence/auth-2026-05-14T10:00:00.000Z-a1b2c3d4.db
1902
+
1903
+ mu db replay <state-dir>/divergence/auth-2026-05-14T10:00:00.000Z-a1b2c3d4.db
1904
+ mu db replay <state-dir>/divergence/auth-2026-05-14T10:00:00.000Z-a1b2c3d4.db --task local_fix --apply
1905
+ mu db replay <state-dir>/divergence/auth-2026-05-14T10:00:00.000Z-a1b2c3d4.db --all --apply
1906
+ ```
1907
+
1908
+ `--force-source` replaces the whole local workstream from the source
1909
+ file, but first parks the local divergent state as a divergence
1910
+ sidecar. `mu db replay` is the manual cherry-pick tool for that
1911
+ sidecar; it is dry-run by default, idempotent, and refuses when the
1912
+ same `local_id` exists locally with diverged content.
1913
+
1914
+ ---
1915
+
1594
1916
  ## 16. One-shot demo script
1595
1917
 
1596
1918
  Copy-pasteable, end-to-end. Wipes any prior `~/.local/state/mu/mu.db`.
@@ -1611,8 +1933,8 @@ mu agent spawn worker-1 --workstream demo --cli sh
1611
1933
  mu agent spawn worker-2 --workstream demo --cli sh
1612
1934
 
1613
1935
  # Assign + observe
1614
- mu sql "UPDATE tasks SET owner='worker-1', status='IN_PROGRESS' WHERE local_id='design'"
1615
- mu --workstream demo
1936
+ mu task claim design -w demo --for worker-1 --evidence "demo assignment"
1937
+ mu state -w demo
1616
1938
 
1617
1939
  # Watch live (Ctrl+b d to detach)
1618
1940
  tmux attach -t mu-demo
@@ -1638,8 +1960,8 @@ rm -f ~/.local/state/mu/mu.db
1638
1960
  shared deps.
1639
1961
 
1640
1962
  3. **Agents claim tasks via their pane title — zero config.**
1641
- `mu task claim foo` from inside `worker-1`'s pane sets `tasks.owner='worker-1'`
1642
- atomically. mu reads the pane title via
1963
+ `mu task claim foo` from inside `worker-1`'s pane sets the task's
1964
+ `owner_id` to the `worker-1` agent row atomically. mu reads the pane title via
1643
1965
  `tmux display-message -t $TMUX_PANE -p '#{pane_title}'`, set on
1644
1966
  spawn. Two agents cannot claim the same task.
1645
1967
 
@@ -1649,9 +1971,9 @@ service of those three.
1649
1971
 
1650
1972
  ---
1651
1973
 
1652
- ## What's NOT in 0.3.0 (and how to work around it)
1974
+ ## What's NOT in 0.4.1 (and how to work around it)
1653
1975
 
1654
- <a id="whats-not-in-030-and-how-to-work-around-it"></a>
1976
+ <a id="whats-not-in-050-and-how-to-work-around-it"></a>
1655
1977
 
1656
1978
  The full roadmap with promotion criteria lives in
1657
1979
  [ROADMAP.md](ROADMAP.md). The short list of gaps you might hit
@@ -1660,13 +1982,13 @@ in real use:
1660
1982
  | Want | Workaround | Status |
1661
1983
  | --------------------------------------------- | ----------------------------------------------------------------------- | ------------- |
1662
1984
  | 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 |
1663
- | 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 |
1985
+ | 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 |
1664
1986
  | Markdown agent-definition discovery | Spawn accepts `--cli` and `--command` directly; no template registry | dropped |
1665
1987
  | `mu run script.ts` (JS DSL) | Use `--json` + bash + jq | rejected |
1666
1988
  | Sync to GitHub Issues / Linear / Asana | Not in scope; explicitly rejected | — |
1667
- | ~~`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 |
1668
- | ~~`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 |
1669
- | ~~`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 |
1989
+ | ~~`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 |
1990
+ | ~~`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 |
1991
+ | ~~`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 |
1670
1992
 
1671
1993
  Anything in this table that bites you in real use is a candidate
1672
1994
  for **promotion**. Criteria: proven friction in ≥2 real workflows +