@martintrojer/mu 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,26 +1,26 @@
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-track).
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 wave (pre-1.0). ~60 typed verbs across 8
10
10
  > namespaces (`workstream`, `agent`, `task`, `workspace`, `log`,
11
11
  > `snapshot`, `archive`, `me`) plus bare top-level verbs
12
12
  > (`state`, `doctor`, `sql`, `undo`, `adopt`). Every verb accepts
13
13
  > `--json` (one allow-listed exception, `mu agent attach`),
14
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
15
+ > `--tail` subscription, bare `mu` TTY dashboard, canonical static
16
+ > state card (`mu state` default / `--tui` render modes), whole-DB
17
17
  > snapshots auto-captured before destructive verbs +
18
18
  > `mu undo` / `mu snapshot {list,show}`, evidence on lifecycle
19
19
  > verbs, schema v7 (v5 surrogate INTEGER PKs + per-workstream
20
20
  > UNIQUE on operator-facing names; v6 added the `archive_*`
21
21
  > family additively; v7 dropped the dead `approvals` table).
22
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)
23
+ > and [§ Not in 0.4.0](#whats-not-in-040-and-how-to-work-around-it)
24
24
  > at the bottom for the gaps that still need workarounds.
25
25
 
26
26
  *If anything below disagrees with `mu --help`, trust `mu --help`.*
@@ -33,7 +33,8 @@ current verb list is in `## CLI — complete verb list` of
33
33
  2. [Get oriented (`mu doctor`)](#2-get-oriented)
34
34
  3. [Create a workstream (`mu workstream init`)](#3-create-a-workstream)
35
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)
36
+ 5. [See the graph (dashboard + state API)](#5-see-the-graph-dashboard--state-api)
37
+ 5b. [The TUI dashboard (interactive)](#5b-the-tui-dashboard-interactive)
37
38
  6. [Spawn a crew (`mu agent spawn`)](#6-spawn-a-crew)
38
39
  7. [Watch the crew live (`tmux attach`)](#7-watch-the-crew-live)
39
40
  8. [Send work to an agent (`mu agent send`)](#8-send-work-to-an-agent)
@@ -47,7 +48,7 @@ current verb list is in `## CLI — complete verb list` of
47
48
  15.5. [Archives — cross-workstream preservation](#155-archives--cross-workstream-preservation-of-task-graphs)
48
49
  16. [One-shot demo script](#16-one-shot-demo-script)
49
50
  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
+ 18. [What's NOT in 0.4.0](#whats-not-in-040-and-how-to-work-around-it)
51
52
  19. [Where to go from here](#where-to-go-from-here)
52
53
 
53
54
  ---
@@ -140,6 +141,32 @@ tmux # if you're not already in one
140
141
 
141
142
  ## 2. Get oriented
142
143
 
144
+ For a human at an interactive terminal, bare `mu` is the home base:
145
+ it launches the read-only TUI with every workstream on the machine
146
+ loaded as tabs. Initial tab focus uses this ladder: `$MU_SESSION` when
147
+ it names a loaded workstream; then the current tmux session name when
148
+ it is `mu-<workstream>`; then best-effort cwd detection against
149
+ registered workspace paths; then cwd equal to the VCS-derived project
150
+ root of any loaded workstream's workspaces (ties broken by most-recent
151
+ workstream activity); then tab 0. If no workstream exists yet, it
152
+ prints help plus the one-paste start command:
153
+
154
+ ```bash
155
+ mu
156
+ # Get started: mu workstream init <name>
157
+ ```
158
+
159
+ For scripts, agents, CI, and pipes, bare `mu` deliberately does NOT
160
+ enter the TUI: when stdout is not a TTY it prints `mu --help`. Use
161
+ explicit typed verbs and `--json` for the API surface:
162
+
163
+ ```bash
164
+ mu state -w <workstream> --json
165
+ MU_NO_TUI=1 mu # force the non-TTY/help path even in a terminal
166
+ ```
167
+
168
+ Run the diagnostic once to check tmux + DB health:
169
+
143
170
  ```bash
144
171
  mu doctor
145
172
  ```
@@ -222,9 +249,10 @@ $ mu task list -w foo --json
222
249
  ```
223
250
 
224
251
  `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.
252
+ hop than `jq '.items | length'`. Future siblings layer on without
253
+ breaking the existing two fields. Today `mu workspace commits --json`
254
+ also includes `vcs`, `baseRef`, and `workspacePath` siblings because
255
+ that verb already computes the workspace's fork metadata.
228
256
 
229
257
  Applies to: `mu task list / next / owned-by / notes`,
230
258
  `mu workstream list`, `mu workstream destroy --empty` (dry-run),
@@ -329,8 +357,8 @@ Tasks have **mandatory** `impact` (1–100) and `effort-days` (>0).
329
357
  Edges are blocks-relationships, modelled as **`--blocked-by`** on `mu
330
358
  task add` (and `mu task reparent`): `--blocked-by design` means "this
331
359
  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.
360
+ Tasks are **scoped to a workstream** — the dashboard and state views only show
361
+ tasks for the workstream you're viewing.
334
362
 
335
363
  ```bash
336
364
  # --workstream can be omitted if you're inside the workstream's tmux
@@ -357,112 +385,299 @@ Each task validates its id (`/^[a-z][a-z0-9_-]{0,63}$/`) and rejects
357
385
  duplicates. If you tried `mu task add x --blocked-by y` while `y`
358
386
  already transitively depended on `x`, mu would refuse with a `CycleError`.
359
387
 
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
388
+ **Task ids are per-workstream unique.** The same local id can exist in
389
+ multiple workstreams, so cross-workstream references use the qualified
390
+ form `<workstream>/<id>` when a global scope is needed. Blocks-edges
391
+ are always same-workstream — if a blocker resolves outside the target
363
392
  workstream, mu refuses with a `CrossWorkstreamEdgeError`.
364
393
 
365
394
  ---
366
395
 
367
- ## 5. See the graph (mission control)
396
+ ## 5. See the graph (dashboard + state API)
368
397
 
369
- ```bash
370
- mu --workstream auth-refactor
371
- # or, if your tmux session is mu-auth-refactor:
372
- mu
373
- ```
398
+ `mu` exposes one logical "what's going on?" view with two renderers:
374
399
 
375
- ```
376
- mu-auth-refactor
400
+ | Surface | Use it for |
401
+ | -------------------- | --------------------------------------------------------------- |
402
+ | **bare `mu`** | A human at a terminal — launches the interactive TUI dashboard. |
403
+ | **`mu state --tui`** | Same TUI, explicitly opt-in. Useful in scripts / aliases. |
404
+ | **`mu state`** | Static text card. JSON-friendly; pipeable; `watch`-able. |
405
+ | **`mu state --json`** | The canonical full snapshot. Agents and scripts read this. |
377
406
 
378
- Agents (0)
379
- (no agents)
407
+ The interactive surface is large enough to deserve its own section —
408
+ see [§ 5b. The TUI dashboard](#5b-the-tui-dashboard-interactive)
409
+ immediately below. The rest of this section is the static / JSON
410
+ contract.
380
411
 
381
- Tracks (1)
382
- Track 1: review (3 tasks, 1 ready, track)
412
+ ### Static state card (`mu state`)
383
413
 
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):
414
+ For an agent/script or a static capture, use explicit `mu state`:
415
+
416
+ ```bash
417
+ mu state -w auth-refactor # human-readable card
418
+ mu state -w auth-refactor --json # full snapshot
419
+ mu state --all --json # every workstream on this machine
420
+ ```
421
+
422
+ The static card includes every section the TUI cards summarize:
423
+ agents + orphans + tracks + ready / in-progress / blocked /
424
+ recent-closed tasks + workspaces + recent events. `--json` emits the
425
+ same full snapshot shape regardless of `--tui`.
426
+
427
+ **JSON shapes**
452
428
 
453
429
  - `mu state --json` (single-ws): flat `{ workstreamName, agents,
454
430
  orphans, tracks, ready, blocked, inProgress, recentClosed,
455
431
  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`.
432
+ - `mu state --json` (multi-ws): wrapped `{ workstreams: [{...}, ...] }`.
433
+ - bare `mu --json`: prints `--help` rather than entering the TUI;
434
+ use `mu state --json` for the full snapshot.
435
+ - `--tui` is render-only and incompatible with `--json` (the TUI
436
+ has no JSON shape).
437
+
438
+ **Multi-workstream**: pass `-w` multiple values, or `--all`. See
439
+ [CLI conventions](#cli-conventions-multi-value-flags). In static
440
+ mode N≥2 stacks one per-workstream card after another.
441
+
442
+ > **Migrating from old state surfaces**: `mu state --hud` and
443
+ > `mu state --mission` were removed in v0.4; use `mu state --tui`
444
+ > for the interactive surface and `mu state --json` for the full
445
+ > snapshot. `tmux display-popup -E 'mu state -w X'` keeps working
446
+ > unchanged for popup-card use.
447
+
448
+ ---
449
+
450
+ ## 5b. The TUI dashboard (interactive)
451
+
452
+ The interactive TUI is mu's flagship human surface. It is **read-only
453
+ by design** — every act-intent `y`anks the canonical `mu` command to
454
+ your clipboard so you run mutations from your shell, with one
455
+ documented escape (`t` in git-show drills runs `tuicr` in the project
456
+ root, see below).
457
+
458
+ ### Launch
461
459
 
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).
460
+ ```bash
461
+ mu # bare; opens the TUI when stdout is a TTY
462
+ mu state --tui # explicit; same surface
463
+ mu state --tui -w a,b # restrict to specific workstreams
464
+ mu state --tui --all # all workstreams (default for bare `mu`)
465
+ MU_NO_TUI=1 mu # force the help path even in a TTY
466
+ mu --json # also forces help; pipe `mu state --json`
467
+ ```
468
+
469
+ Quit with `q` or `Ctrl-C`. The dashboard restores your scrollback
470
+ on exit (alt-screen).
471
+
472
+ **Initial active tab** is picked from this ladder:
473
+ `$MU_SESSION` → current tmux session name (`mu-<ws>`) → cwd inside a
474
+ registered workspace → cwd at the VCS-derived project root of any
475
+ workstream (newest activity wins ties) → tab 0.
476
+
477
+ ### Layout: 10 cards, responsive columns
478
+
479
+ The dashboard renders 10 toggleable cards with rounded borders and
480
+ section headers inset into the top border line (lazygit / btop / k9s
481
+ convention):
482
+
483
+ | Slot | Card | Toggle | Popup | Content |
484
+ | ---- | ------------- | ------ | --------- | ---------------------------------------------------- |
485
+ | 0 | Commits | `0` | `Shift+0` | Recent project-root commits (git / jj / sl) |
486
+ | 1 | Agents | `1` | `Shift+1` | Active agents + status + cli + role |
487
+ | 2 | Tracks | `2` | `Shift+2` | Parallel tracks (union-find clusters) |
488
+ | 3 | Ready (Tasks) | `3` | `Shift+3` | Ready-to-claim tasks (no open blockers) |
489
+ | 4 | Activity log | `4` | `Shift+4` | Recent `agent_logs` events |
490
+ | 5 | Workspaces | `5` | `Shift+5` | Per-agent VCS workspaces + behind/dirty status |
491
+ | 6 | In-progress | `6` | `Shift+6` | IN_PROGRESS tasks owned by agents |
492
+ | 7 | Blocked | `7` | `Shift+7` | Tasks with at least one open blocker |
493
+ | 8 | Recent | `8` | `Shift+8` | Recently CLOSED tasks |
494
+ | 9 | Doctor | `9` | `Shift+9` | Cheap health checks (schema, ghosts, orphans, …) |
495
+ | — | DAG | — | `g` | Full task DAG forest (keybind-only) |
496
+ | — | All tasks | — | `t` | Sortable / filterable list of every task |
497
+
498
+ Digit toggles HIDE / SHOW the card on the dashboard; `Shift+digit`
499
+ opens the matching fullscreen popup. **Single-popup invariant**: only
500
+ one popup is visible at a time; `Esc` / `q` returns to the dashboard
501
+ with all toggles + tick rate preserved.
502
+
503
+ **Responsive layout**: cards stack below 120 cols, then reflow into
504
+ pair-aware 2 / 3 / 4-column layouts at 120 / 180 / 240 cols. Each
505
+ visible card gets a dynamic row budget so a noisy list cannot crowd
506
+ out its siblings; overflow shows as `+N more · Shift+N` inset into
507
+ the bottom border. On very short panes, the dashboard culls
508
+ low-priority cards (Doctor → Recent → Workspaces → …) and shows
509
+ `+N cards hidden · resize taller` until the surviving cards fit.
510
+
511
+ Dashboard ordering is slot-stable: within each rendered column,
512
+ non-stream cards are ordered by toggle digit ascending; stream cards
513
+ (Commits, Activity log) sit as natural trailers, with slot 0 trailing
514
+ last.
515
+
516
+ ### Multi-workstream tabs
517
+
518
+ When the TUI is launched with N≥2 workstreams (e.g. bare `mu` on a
519
+ machine with multiple workstreams, or `mu state --tui -w a,b,c`),
520
+ a compact tab strip renders above the cards:
521
+
522
+ ```
523
+ workstreams: ▸ auth-refactor · ui-rewrite · demo (Tab / Shift-Tab)
524
+ ```
525
+
526
+ - `Tab` cycles forward, `Shift-Tab` backward (suppressed inside
527
+ popups so the same key still navigates inside popups that bind
528
+ it locally).
529
+ - The active tab name appears in the status bar's right zone next
530
+ to the tick rate.
531
+ - Cards / popups always operate on the active tab — there's no
532
+ per-row workstream column.
533
+ - For N=1 the strip renders nothing (frame is byte-identical to
534
+ single-ws TUI).
535
+ - When the workstream set is wider than the terminal, the strip
536
+ windows around the active tab and shows `‹N` / `›N` counters
537
+ for hidden workstreams.
538
+
539
+ ### Popup drills
540
+
541
+ `Enter` in any list popup drills into the focused row. Where the
542
+ row is itself an entity (a task), a further `Enter` chains into the
543
+ shared read-only task-detail leaf (notes timeline):
544
+
545
+ - **Tracks popup (`Shift+2`)**: list of tracks → `Enter` opens the
546
+ track's task list → `Enter` opens that task's notes timeline.
547
+ - **Ready / In-progress / Blocked / Recent / All-tasks**:
548
+ list of tasks → `Enter` opens notes; `y` yanks `mu task show <id>`
549
+ (or `mu task claim` / `mu task close` / `mu task tree` depending
550
+ on popup).
551
+ - **Activity log popup (`Shift+4`)**: list of events → `Enter`
552
+ drills into the full untruncated payload of the focused event;
553
+ `y` yanks `mu log --since <seq-1> -n 1 -w <ws>`.
554
+ - **Workspaces popup (`Shift+5`)**: list of workspaces → `Enter`
555
+ opens the commits-since-fork list → `Enter` on a commit opens
556
+ the inline `git show <sha> --stat -p` view; `y` yanks `git show
557
+ <sha>`; `t` launches `tuicr -r <sha>`.
558
+ - **Commits popup (`Shift+0`)**: project-root recent commits →
559
+ `Enter` opens the backend's show view; `y` yanks the show
560
+ command; `t` launches `tuicr`.
561
+ - **Doctor popup (`Shift+9`)**: list of checks → `Enter` opens
562
+ the remediation paragraph for the focused check.
563
+ - **DAG popup (`g`)**: keybind-only; renders the active workstream's
564
+ full task DAG forest (one ASCII subtree per root, diamond-collapse
565
+ marker on repeated nodes).
566
+
567
+ One `Esc` / `q` backs out per recursion level. Drills auto-refresh
568
+ in step with the dashboard tick (fast 1s for SQL-derived bodies
569
+ like notes, slow 10s for subprocess git-show / scrollback). Scroll
570
+ position is preserved across refreshes; subprocess loaders keep the
571
+ prior body visible until the new one arrives so there's no
572
+ blank-flash mid-refetch.
573
+
574
+ ### Search / filter
575
+
576
+ `/` inside any list popup enters an incremental case-insensitive
577
+ substring filter (lazygit / k9s convention):
578
+
579
+ - Every printable character appends to the query; `Backspace` pops
580
+ one.
581
+ - `Esc` cancels (clears the query); `Enter` commits (keeps the
582
+ filter applied while letting `j/k` resume normal navigation).
583
+ - Press `/` again on a committed filter to refine.
584
+ - The filter blob is per-popup: agent name + status + cli + role;
585
+ track head id + title; task name + title + status + owner; log
586
+ verb + payload + source.
587
+ - Filter state is per-popup and dies with the popup.
588
+
589
+ Task-list popups also expose **per-status toggles** (`o` / `i` / `c`
590
+ / `r` / `d` toggle OPEN / IN_PROGRESS / CLOSED / REJECTED / DEFERRED
591
+ visibility; default all-on).
592
+
593
+ The All-tasks popup adds **sort cycle** on `s`: `roi` → `recency`
594
+ → `age` → `id`.
595
+
596
+ ### Mouse
597
+
598
+ Navigation-in only:
599
+
600
+ - Double-click a dashboard card → opens its popup.
601
+ - Scroll-wheel inside a popup list / drill body → moves the focused
602
+ cursor / scrolls the body.
603
+ - Double-click a popup row → drills one level deeper.
604
+
605
+ There is intentionally **no mouse back binding** — use `Esc` / `q`
606
+ to back out predictably.
607
+
608
+ ### Yank contract (`y`) and the `tuicr` escape (`t`)
609
+
610
+ Every popup row exposes one canonical `mu` command via `y`. The
611
+ command goes to your system clipboard (pbcopy / wl-copy / xclip /
612
+ xsel / clip.exe with OSC-52 fallback). You run it in your shell.
613
+ The TUI never executes a mutation.
614
+
615
+ The one user-driven escape from the read-only pledge is **`t`**
616
+ inside any `git show` drill (Workspaces popup or Commits popup):
617
+ mu suspends its alt-screen, runs `tuicr -r <sha>` in the project
618
+ root / workspace cwd, then restores the dashboard when tuicr exits.
619
+ This is a deliberate handoff — the operator drives another TUI
620
+ tool, not mu performing the mutation.
621
+
622
+ Task-list cards and popups colour-code status cells consistently
623
+ with the static CLI tables: OPEN cyan, IN_PROGRESS yellow, CLOSED
624
+ green, REJECTED red, DEFERRED dim/gray.
625
+
626
+ ### Polling tiers
627
+
628
+ The dashboard has two refresh tiers:
629
+
630
+ - **Fast tick** (default 1s, adjustable with `+` / `-` / `=` /
631
+ `0`): SQL-only. Refreshes tasks, tracks, workspace registry rows,
632
+ and the activity log.
633
+ - **Slow tick** (10s, fixed): subprocess-backed. Refreshes
634
+ tmux-derived agent liveness / orphans, workspace dirty flags,
635
+ recent project commits, and the Doctor summary.
636
+
637
+ The last slow-tier result is merged into every fast render so cards
638
+ do not flicker through a loading state. `r` / F5 refreshes both
639
+ tiers immediately. Tab / Shift-Tab triggers an eager slow refresh
640
+ for the newly active workstream.
641
+
642
+ ### Keymap reference
643
+
644
+ | Mode | Keys | Action |
645
+ | --------- | ---------------------------- | -------------------------------------------------------------- |
646
+ | dashboard | `0`-`9` | toggle card visibility |
647
+ | dashboard | `Shift+0`-`Shift+9` | open the matching popup |
648
+ | dashboard | `g` | open DAG popup (keybind-only) |
649
+ | dashboard | `t` | open All-tasks popup (keybind-only) |
650
+ | dashboard | `Tab` / `Shift-Tab` | cycle workstream tabs (N≥2) |
651
+ | dashboard | `+` / `-` / `=` / `0` | adjust fast tick rate (faster / slower / default / pause) |
652
+ | dashboard | `r` / `F5` | force refresh both tiers |
653
+ | dashboard | `?` / `F1` | open help overlay |
654
+ | any | `q` / `Ctrl-C` | quit (or back out of popup; quits at dashboard) |
655
+ | popup | `j` / `k` | move cursor / scroll |
656
+ | popup | `g` / `G` | jump top / bottom |
657
+ | popup | `Ctrl-D` / `Ctrl-U` | half-page down / up |
658
+ | popup | `PgDn` / `PgUp` | full page |
659
+ | popup | `Enter` | drill into focused row |
660
+ | popup | `Esc` / `q` | back out one level |
661
+ | popup | `y` | yank canonical `mu` command for focused row |
662
+ | popup | `/` | enter filter mode |
663
+ | filter | (printable) / `Backspace` | edit query |
664
+ | filter | `Esc` | cancel (clear query) |
665
+ | filter | `Enter` | commit (keep filter, return to nav) |
666
+ | task popup| `o` / `i` / `c` / `r` / `d` | toggle OPEN / IN_PROGRESS / CLOSED / REJECTED / DEFERRED |
667
+ | All-tasks | `s` | cycle sort key (roi → recency → age → id) |
668
+ | git-show | `t` | launch `tuicr -r <sha>` (alt-screen handoff) |
669
+
670
+ `?` shows the same table as a scrollable overlay (j/k/Ctrl-D/U/g/G
671
+ also work inside the overlay).
672
+
673
+ ### Read-only invariant
674
+
675
+ The TUI never executes a mutation. This is not a feature of the
676
+ implementation; it's a load-bearing pledge in `docs/ROADMAP.md`. If
677
+ a future TUI gesture tempts you to call into the SDK to mutate
678
+ state, file a roadmap entry first — the yank-and-run pattern is the
679
+ intentional cost we pay to keep the TUI inspectable, scriptable, and
680
+ recoverable from any shell.
466
681
 
467
682
  ---
468
683
 
@@ -630,6 +845,28 @@ MU_SEND_DELAY_MS=300 mu agent send worker-1 "..." # faster, less safe
630
845
  MU_SEND_DELAY_MS=1000 mu agent send worker-1 "..." # slow remote
631
846
  ```
632
847
 
848
+ If the target agent has a workspace that is **stale** (≥10 commits
849
+ behind main — the same red bucket shown in `mu workspace list` and the
850
+ TUI Workspaces card), `mu agent send` prints a yellow stderr warning
851
+ but still sends by default:
852
+
853
+ ```bash
854
+ WARN: worker-1 workspace is 14 commits behind main (≥10 = stale)
855
+ Next:
856
+ Refresh first : mu workspace refresh worker-1 -w auth-refactor
857
+ ```
858
+
859
+ Use `--strict-staleness` when a wrapper should refuse instead of
860
+ warning:
861
+
862
+ ```bash
863
+ mu agent send worker-1 "..." -w auth-refactor --strict-staleness
864
+ ```
865
+
866
+ Agents without workspaces are skipped (common for read-only roles).
867
+ `--json` output includes `staleness: null` or `{agentName,
868
+ workstreamName, commitsBehindMain, isStale}`.
869
+
633
870
  ---
634
871
 
635
872
  ## 9. Read what an agent did
@@ -714,6 +951,29 @@ workstream prefix, `AgentNotFoundError` (exit 3, message names the
714
951
  workstream) when the named worker doesn't live there. Nothing is
715
952
  written on either failure.
716
953
 
954
+ When `--for` targets an agent with a stale workspace (≥10 commits
955
+ behind main), `mu task claim` warns on stderr and appends a refresh
956
+ hint, but the claim still succeeds by default:
957
+
958
+ ```bash
959
+ mu task claim build -w auth-refactor --for worker-2
960
+ # stderr: WARN: worker-2 workspace is 14 commits behind main (≥10 = stale)
961
+ # Next: Refresh first : mu workspace refresh worker-2 -w auth-refactor
962
+ ```
963
+
964
+ Pass `--strict-staleness` to refuse the claim instead with typed
965
+ `TaskClaimStaleWorkspaceError` (exit 4). This is useful for scripts
966
+ that should never dispatch work onto a stale parent:
967
+
968
+ ```bash
969
+ mu task claim build -w auth-refactor --for worker-2 --strict-staleness
970
+ ```
971
+
972
+ `--json` output includes `staleness: null` or `{agentName,
973
+ workstreamName, commitsBehindMain, isStale}`. Bare in-pane claims and
974
+ `--self` claims do not run this check because they do not assign work
975
+ to a named agent via `--for`.
976
+
717
977
  ### The orchestrator pattern: `--self`
718
978
 
719
979
  Not every action comes from a registered worker pane. Often the
@@ -723,7 +983,7 @@ a worker pane just for a 5-minute job. Two patterns split here:
723
983
 
724
984
  - **Worker** — a pane mu spawned (or you adopted). Has a row in the
725
985
  `agents` table. Identity = pane title. Claims with bare
726
- `mu task claim <id>`. `tasks.owner` is set to the worker's name.
986
+ `mu task claim <id>`. `tasks.owner_id` points at the worker row.
727
987
 
728
988
  - **Actor** — anything that *causes* a state change. Includes
729
989
  workers, but also includes the orchestrator. May or may not have
@@ -745,13 +1005,13 @@ Three actionable next steps. Pick one based on intent:
745
1005
  ```bash
746
1006
  # Orchestrator does the work itself (most common):
747
1007
  mu task claim some-task --self --evidence "trivial 5-line fix"
748
- # -> tasks.owner stays NULL
1008
+ # -> tasks.owner_id stays NULL
749
1009
  # -> agent_logs records 'task claim some-task by pi-mu --self (anonymous)'
750
1010
  # -> mu task show surfaces it as 'owner: (self: pi-mu)'
751
1011
 
752
1012
  # Orchestrator dispatches to a worker:
753
1013
  mu task claim some-task --for worker-1
754
- # -> tasks.owner = 'worker-1'
1014
+ # -> tasks.owner_id points at worker-1
755
1015
 
756
1016
  # Orchestrator wants to BE a registered worker (rare):
757
1017
  mu agent adopt %6441 -w <ws> # only if pane is in mu-<ws> session
@@ -767,7 +1027,7 @@ to pane title, or `$USER`, or `unknown`):
767
1027
  mu task claim deploy --self --actor deploy-bot --evidence "prod release"
768
1028
  ```
769
1029
 
770
- When `tasks.owner IS NULL` because of `--self`, `mu task show` looks
1030
+ When `tasks.owner_id IS NULL` because of `--self`, `mu task show` looks
771
1031
  up the most recent `task claim` event for that task and surfaces it:
772
1032
 
773
1033
  ```
@@ -817,7 +1077,12 @@ verb stays useful on un-claimed tasks).
817
1077
  Or, for ad-hoc shape, the SQL escape hatch:
818
1078
 
819
1079
  ```bash
820
- mu sql "SELECT author, content, created_at FROM task_notes WHERE task_id='design' ORDER BY id"
1080
+ mu sql "SELECT n.author, n.content, n.created_at
1081
+ FROM task_notes n
1082
+ JOIN tasks t ON t.id = n.task_id
1083
+ JOIN workstreams w ON w.id = t.workstream_id
1084
+ WHERE t.local_id='design' AND w.name='auth-refactor'
1085
+ ORDER BY n.id"
821
1086
  ```
822
1087
 
823
1088
  Convention for note content: `KEY: value` lines. Common keys are
@@ -845,6 +1110,18 @@ task re-enters the ready set (the canonical "hand it back to the
845
1110
  pool" workflow). `--reopen` is the escape hatch for forcing `OPEN`
846
1111
  from `CLOSED` / `REJECTED` / `DEFERRED`.
847
1112
 
1113
+ When the closing actor has a per-agent workspace and that workspace
1114
+ has uncommitted edits, a successful close adds one extra `Next:` hint
1115
+ reminding the actor to commit before the next wave:
1116
+
1117
+ ```bash
1118
+ cd $(mu workspace path worker-1 -w auth-refactor) && git commit -am 'Design auth module'
1119
+ ```
1120
+
1121
+ The hint is best-effort: no workspace, a clean workspace, the `none`
1122
+ backend, or a failed VCS dirty check simply omit it. The same
1123
+ `nextSteps` entry is present in `--json` output.
1124
+
848
1125
  `--if-ready` is the umbrella-on-wave-done shape: an orchestrator
849
1126
  fires `mu task close <umbrella> --if-ready` after each wave-task
850
1127
  finishes (or unconditionally as a final action). It's a no-op while
@@ -936,21 +1213,29 @@ one verify, one workspace recycle:
936
1213
  # The dispatch-pipeline recipe: cycle until in_flight is empty.
937
1214
  in_flight=( mufeedback-v03/foo mufeedback-v03/bar roadmap-v0-3/baz )
938
1215
  while (( ${#in_flight[@]} > 0 )); do
939
- closed=$(mu task wait "${in_flight[@]}" --first --timeout 90 --json \
940
- | jq -r '.firing.qualifiedId // empty')
1216
+ res=$(mu task wait "${in_flight[@]}" --first --timeout 90 --json)
1217
+ closed=$(jq -r '.firing.qualifiedId // empty' <<<"$res")
941
1218
  if [[ -z "$closed" ]]; then break; fi # timeout or exit 6 — see below
942
1219
 
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)
1220
+ worker=$(jq -r '.firing.owner // empty' <<<"$res")
946
1221
  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"
1222
+
1223
+ # 1. Inspect, then run, the sha-pinned apply hint from nextSteps.
1224
+ # When the worker has commits since its fork point, the command is
1225
+ # `git cherry-pick <sha>` (or `<first>^..<last>` for multiple
1226
+ # commits). When the worker closed without committing, nextSteps
1227
+ # says so and points at manual `git diff` / `git apply` rescue.
1228
+ apply=$(jq -r '.nextSteps[0].command' <<<"$res")
1229
+ printf 'apply hint: %s\n' "$apply"
1230
+ if [[ "$apply" == git\ cherry-pick* ]]; then
1231
+ eval "$apply"
1232
+ else
1233
+ echo "manual rescue required; inspect the worker workspace before continuing"
1234
+ break
1235
+ fi
951
1236
 
952
1237
  # 2. Verify
953
- npm run typecheck && npm run lint && npm run test && npm run build
1238
+ npm run typecheck && npm run lint && npm run test:fast && npm run test && npm run build
954
1239
 
955
1240
  # 3. Refresh the workspace for the next dispatch (rebases onto
956
1241
  # fresh main WITHOUT killing the worker's LLM context). Default
@@ -1058,12 +1343,14 @@ mu sql "UPDATE tasks SET status='IN_PROGRESS'
1058
1343
 
1059
1344
  # What's blocking what (open tasks only) — same data as `mu task tree`
1060
1345
  # but as a flat join when you want a wider report. task_edges is keyed
1061
- # by tasks.id, not local_id.
1346
+ # by tasks.id, not local_id; join workstreams to scope the report.
1062
1347
  mu sql "SELECT b.local_id AS blocked, t.local_id AS by_task
1063
1348
  FROM tasks b
1349
+ JOIN workstreams w ON w.id = b.workstream_id
1064
1350
  JOIN task_edges e ON e.to_task_id = b.id
1065
1351
  JOIN tasks t ON t.id = e.from_task_id
1066
- WHERE t.status != 'CLOSED' AND b.status = 'OPEN'"
1352
+ WHERE w.name='mufeedback-v03'
1353
+ AND t.status != 'CLOSED' AND b.status = 'OPEN'"
1067
1354
 
1068
1355
  # Recursive CTE: every task that transitively blocks `launch` in a
1069
1356
  # given workstream (or use `mu task tree launch --json` for the same
@@ -1348,8 +1635,10 @@ failure leaves the registry intact (you can retry); if you only want
1348
1635
  the DB cleared, use `mu sql` directly:
1349
1636
 
1350
1637
  ```bash
1351
- mu sql "DELETE FROM tasks WHERE workstream='auth-refactor'" # cascades
1352
- mu sql "DELETE FROM agents WHERE workstream='auth-refactor'"
1638
+ mu sql "DELETE FROM tasks
1639
+ WHERE workstream_id=(SELECT id FROM workstreams WHERE name='auth-refactor')" # cascades
1640
+ mu sql "DELETE FROM agents
1641
+ WHERE workstream_id=(SELECT id FROM workstreams WHERE name='auth-refactor')"
1353
1642
  ```
1354
1643
 
1355
1644
  Or nuke the entire DB:
@@ -1376,7 +1665,7 @@ subdirectory per source workstream:
1376
1665
  <bucket>/
1377
1666
  README.md # bucket-level summary (every source-ws + dates + totals)
1378
1667
  INDEX.md # union of all task tables; first column = source-ws
1379
- manifest.json # bucketVersion: 2 + per-source-ws sha256 + per-task sha256
1668
+ manifest.json # bucketVersion: 2, manifest_version: 2, per-source-ws task summaries + sha256s
1380
1669
  <source-ws>/
1381
1670
  README.md # per-source-ws (counts)
1382
1671
  INDEX.md # per-source-ws (table of every task)
@@ -1386,12 +1675,21 @@ subdirectory per source workstream:
1386
1675
  Bucket exports are **additive**: `mu workstream export -w X --out
1387
1676
  <bucket>` creates the bucket scaffolding plus `X/` on first use,
1388
1677
  and a follow-up call with `-w Y --out <same-bucket>` appends a
1389
- sibling `Y/` subdirectory without touching `X/`. Re-running with
1678
+ sibling `Y/` subdirectory without touching `X/`. The top-level
1679
+ `INDEX.md` is always the union from `manifest.sources`, so a later
1680
+ single-workstream refresh does not drop sibling workstreams from the
1681
+ bucket-wide task table. Re-running with
1390
1682
  the same `-w` is sha256-idempotent: only changed task files are
1391
1683
  rewritten (mtime preserved on identical files); tasks added since
1392
1684
  the previous export get fresh files; tasks deleted from the DB
1393
1685
  STAY on disk with a `> **Deleted from DB on <ts>**` banner so you
1394
- never lose context that may already be git-blamed.
1686
+ never lose context that may already be git-blamed. `manifest_version:
1687
+ 2` stores compact task summaries (`name`/`title`/`status`/`impact`/
1688
+ `effortDays`) beside the per-file sha256s; older v1 manifests are
1689
+ accepted on re-export; mu infers the missing summaries from existing
1690
+ per-task markdown when possible, falling back to placeholder values
1691
+ only if a task file is missing or unreadable, so the bucket remains
1692
+ appendable.
1395
1693
 
1396
1694
  ```bash
1397
1695
  # One-shot dump (bucket happens to contain just one source-ws)
@@ -1465,7 +1763,7 @@ Key properties:
1465
1763
  imported in its own SQLite transaction. A failure in source A
1466
1764
  rolls back A; sibling source B is unaffected.
1467
1765
  - **Refuses silent merges.** If the target workstream already
1468
- exists in the DB with tasks, the import errors with
1766
+ exists in the DB, the import errors with
1469
1767
  `WorkstreamAlreadyExistsError`. Recourse:
1470
1768
  `--workstream <new-name>` (single-source buckets only) or
1471
1769
  destroy the existing workstream first.
@@ -1526,12 +1824,16 @@ Key properties:
1526
1824
  - **Globally-unique labels.** Archive labels live in their own
1527
1825
  namespace (separate from workstream names). Pick once, reuse
1528
1826
  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.
1827
+ - **Snapshot-only accumulation.** `mu archive add <label> -w <ws>` is
1828
+ idempotent at the (archive, source workstream) granularity and is
1829
+ designed for end-of-milestone snapshot-and-destroy flows. Re-running
1830
+ on the same workstream is task-incremental: newly-created tasks are
1831
+ added, but notes and events for already-archived tasks stay pinned
1832
+ to the original snapshot and are NOT refreshed. If you need a full
1833
+ event-stream refresh for a source workstream, remove that source (or
1834
+ delete/re-create the archive label) and add it again. Two different
1835
+ workstreams under the same label coexist as separate
1836
+ `(source_workstream, original_local_id)` rows.
1535
1837
  - **Outlives the source.** `archived_tasks.source_workstream` is
1536
1838
  TEXT (not an FK), so the source workstream can be destroyed and
1537
1839
  the archive's snapshot of it stays queryable forever.
@@ -1611,8 +1913,8 @@ mu agent spawn worker-1 --workstream demo --cli sh
1611
1913
  mu agent spawn worker-2 --workstream demo --cli sh
1612
1914
 
1613
1915
  # Assign + observe
1614
- mu sql "UPDATE tasks SET owner='worker-1', status='IN_PROGRESS' WHERE local_id='design'"
1615
- mu --workstream demo
1916
+ mu task claim design -w demo --for worker-1 --evidence "demo assignment"
1917
+ mu state -w demo
1616
1918
 
1617
1919
  # Watch live (Ctrl+b d to detach)
1618
1920
  tmux attach -t mu-demo
@@ -1638,8 +1940,8 @@ rm -f ~/.local/state/mu/mu.db
1638
1940
  shared deps.
1639
1941
 
1640
1942
  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
1943
+ `mu task claim foo` from inside `worker-1`'s pane sets the task's
1944
+ `owner_id` to the `worker-1` agent row atomically. mu reads the pane title via
1643
1945
  `tmux display-message -t $TMUX_PANE -p '#{pane_title}'`, set on
1644
1946
  spawn. Two agents cannot claim the same task.
1645
1947
 
@@ -1649,9 +1951,9 @@ service of those three.
1649
1951
 
1650
1952
  ---
1651
1953
 
1652
- ## What's NOT in 0.3.0 (and how to work around it)
1954
+ ## What's NOT in 0.4.0 (and how to work around it)
1653
1955
 
1654
- <a id="whats-not-in-030-and-how-to-work-around-it"></a>
1956
+ <a id="whats-not-in-040-and-how-to-work-around-it"></a>
1655
1957
 
1656
1958
  The full roadmap with promotion criteria lives in
1657
1959
  [ROADMAP.md](ROADMAP.md). The short list of gaps you might hit
@@ -1660,13 +1962,13 @@ in real use:
1660
1962
  | Want | Workaround | Status |
1661
1963
  | --------------------------------------------- | ----------------------------------------------------------------------- | ------------- |
1662
1964
  | 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 |
1965
+ | 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
1966
  | Markdown agent-definition discovery | Spawn accepts `--cli` and `--command` directly; no template registry | dropped |
1665
1967
  | `mu run script.ts` (JS DSL) | Use `--json` + bash + jq | rejected |
1666
1968
  | 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 |
1969
+ | ~~`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 |
1970
+ | ~~`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 |
1971
+ | ~~`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
1972
 
1671
1973
  Anything in this table that bites you in real use is a candidate
1672
1974
  for **promotion**. Criteria: proven friction in ≥2 real workflows +