@mu-crew/mu 3.2.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AGENTS.md +203 -599
  2. package/README.md +76 -327
  3. package/dist/cli.js.map +1 -1
  4. package/dist/extension/mu-pi.js +178 -45
  5. package/dist/extension/mu-pi.js.map +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/docs/ARCHITECTURE.md +175 -1072
  8. package/docs/ROADMAP.md +88 -169
  9. package/docs/VISION.md +205 -415
  10. package/docs/VOCABULARY.md +202 -466
  11. package/docs/architecture/control-socket.md +119 -0
  12. package/docs/architecture/dag.md +113 -0
  13. package/docs/architecture/mux.md +163 -0
  14. package/docs/architecture/ops-log.md +140 -0
  15. package/docs/architecture/sdk.md +185 -0
  16. package/docs/architecture/sync.md +129 -0
  17. package/docs/architecture/tui.md +146 -0
  18. package/docs/guide/README.md +25 -0
  19. package/docs/guide/backends.md +61 -0
  20. package/docs/guide/cleanup.md +74 -0
  21. package/docs/guide/delegate.md +87 -0
  22. package/docs/guide/dispatch.md +137 -0
  23. package/docs/guide/getting-started.md +177 -0
  24. package/docs/guide/recovery.md +133 -0
  25. package/docs/guide/remote.md +74 -0
  26. package/docs/guide/sql.md +76 -0
  27. package/docs/guide/stop-a-worker.md +64 -0
  28. package/docs/guide/sync.md +78 -0
  29. package/docs/guide/tui.md +98 -0
  30. package/docs/guide/upgrade.md +76 -0
  31. package/docs/history/CHANGELOG-pre-3.md +4866 -0
  32. package/docs/reference/env.md +87 -0
  33. package/docs/reference/naming.md +82 -0
  34. package/docs/specs/2026-09-28-agent-state-from-murmur.md +1 -1
  35. package/docs/specs/2026-09-29-task-substates.md +1 -1
  36. package/package.json +1 -1
  37. package/skills/mu/REMOTE_WORKERS.md +179 -504
  38. package/skills/mu/SKILL.md +116 -185
  39. package/docs/HANDOVER.md +0 -486
  40. package/docs/USAGE_GUIDE.md +0 -2738
@@ -0,0 +1,185 @@
1
+ # CLI and SDK surface
2
+
3
+ Every user-visible operation is a typed SDK function plus a thin
4
+ Commander wrapper. This page covers the boundary rules, errors, tests
5
+ and packaging. Overview: [ARCHITECTURE.md](../ARCHITECTURE.md).
6
+
7
+ ## One surface, two callers
8
+
9
+ `src/cli.ts` (`buildProgram`) and the namespace files under `src/cli/`
10
+ are the verb surface: no generated registry, no DSL, no separate
11
+ operation schema. Programmatic callers import the same functions from
12
+ `src/index.ts`. Agents and scripts compose verbs with `--json`.
13
+
14
+ A verb's path:
15
+
16
+ 1. The Commander block calls a `cmd<Verb>` wrapped in `handle()`.
17
+ 2. `handle()` opens the DB, runs ambient sync ingest
18
+ ([sync.md](sync.md#the-ambient-hook)), and calls the SDK function.
19
+ 3. The SDK function opens `db.transaction(fn)()` for multi-statement
20
+ writes. Agent ops call the mux (and jj, sl or git for workspaces);
21
+ task ops are pure SQL. Read paths reconcile against the mux.
22
+ 4. Capture triggers record ops in the same transaction
23
+ ([ops-log.md](ops-log.md)). Machine-local changes go through
24
+ `emitEvent`. `mu log --tail` sees them on its next 1-second poll.
25
+ 5. On error the transaction rolls back and the typed error reaches
26
+ `handle()`, which maps it to an exit code. Flush runs on both paths.
27
+
28
+ Output is self-documenting: `src/output.ts` (`printNextSteps`,
29
+ `errorNextSteps`) prints `Next:` hints, quoted by `src/shell-quote.ts`
30
+ so they paste cleanly.
31
+
32
+ ## Surrogate-PK and SDK-boundary discipline
33
+
34
+ Every entity table has this shape:
35
+
36
+ ```
37
+ (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT, -- surrogate; internal
39
+ <scope_id> INTEGER NOT NULL REFERENCES <parent>(id) ON DELETE CASCADE,
40
+ <name> TEXT NOT NULL, -- operator-facing; mutable
41
+ -- ... domain attributes
42
+ UNIQUE (<scope_id>, <name>) -- per-scope unique
43
+ )
44
+ ```
45
+
46
+ FKs reference `<parent>_id` (INTEGER), never the TEXT name. The name is
47
+ an attribute: searchable, displayable, cheap to rename. The surrogate
48
+ id is the identity.
49
+
50
+ **TEXT by design:**
51
+
52
+ - the workstream's own `name` (it is the mux session name; globally
53
+ unique);
54
+ - `task_notes.author` and `ops.actor` (free-text labels such as
55
+ `"orchestrator"`);
56
+ - `ops.entity` (open enum; a new entity needs no migration);
57
+ - `agents.cli` (a new CLI needs no schema change);
58
+ - every column of `ops`. The log is FK-free and addresses rows by
59
+ natural key, so an op outlives its row and its workstream.
60
+
61
+ **The boundary rule** matches REST: the external API uses business
62
+ identifiers, the internal layer uses primary keys.
63
+
64
+ > **Public SDK functions take operator-facing names.**
65
+ > **Internal helpers take surrogate ids.**
66
+ > **Resolution happens at the public-function entry, exactly once.**
67
+
68
+ ```ts
69
+ // PUBLIC: takes operator-facing names
70
+ export function claimTask(
71
+ db: Db,
72
+ workstream: string,
73
+ localId: string,
74
+ opts?: ClaimOptions,
75
+ ): ClaimResult {
76
+ const wsId = resolveWorkstreamId(db, workstream);
77
+ const taskId = tryResolveTaskId(db, wsId, localId);
78
+ if (taskId === null) throw new TaskNotFoundError(localId);
79
+ const agentId = resolveCurrentAgentId(db, wsId);
80
+ return claimTaskById(db, taskId, agentId, opts);
81
+ }
82
+
83
+ // INTERNAL: takes surrogate ids; never re-resolves
84
+ function claimTaskById(db, taskId, agentId, opts): ClaimResult { ... }
85
+ ```
86
+
87
+ Resolving once gives three things: no double resolution; no
88
+ workstream context needed below the boundary, because the FKs make
89
+ scope implicit; and one place for error mapping.
90
+ `WorkstreamNotFoundError` comes from `resolveWorkstreamId` in
91
+ `src/db.ts`. `TaskNotFoundError` and `AgentNotFoundError` are raised by
92
+ callers wrapping a `tryResolve*` null, so exit 3 holds whichever lookup
93
+ missed.
94
+
95
+ **Workstream scoping is mandatory at the CLI boundary.** TEXT names
96
+ (`tasks.local_id`, `agents.name`) are unique per workstream, so the
97
+ same name can exist in two. Every public function taking such a name
98
+ also takes the workstream; internal SQL filters by
99
+ `(workstream_id, name)`.
100
+
101
+ **`--json` keeps operator-facing names.** Surrogate ids never appear in
102
+ `--json`, error payloads, log lines or exports. Exposing them would
103
+ bring back a global namespace.
104
+
105
+ ## State of truth
106
+
107
+ - The DB (`~/.local/state/mu/mu.db`, or `MU_DB_PATH`) is canonical.
108
+ Everything else is a cache, including pane titles.
109
+ - Writes go through the typed SDK functions, which validate, transact
110
+ and reconcile. Op capture is the triggers' job.
111
+ - In-memory state lives only for one command's connection.
112
+ - Multiple `mu` processes share the file safely through SQLite WAL.
113
+
114
+ ## Errors
115
+
116
+ Errors are typed classes per layer (`src/agents/errors.ts`,
117
+ `src/tasks/errors.ts`, `MuxError` with `TmuxError` / `HerdrError`, and
118
+ so on). Nothing swallows them. `src/cli/handle.ts` maps them to exit
119
+ codes:
120
+
121
+ | Code | Meaning |
122
+ | --- | --- |
123
+ | 0 | success |
124
+ | 1 | generic error |
125
+ | 2 | usage error (Commander's default) |
126
+ | 3 | not found (agent, task, workspace) |
127
+ | 4 | conflict (name collision, double claim, dirty tree, schema too old or new, outdated extension) |
128
+ | 5 | substrate unavailable (no multiplexer, DB locked) |
129
+
130
+ Errors carry structured context (operation, target, attempted action)
131
+ so `mu doctor` can show them readably.
132
+
133
+ ## Testing layers
134
+
135
+ Fast tier (`npm run test:fast`) excludes `*.integration.test.ts` and
136
+ `*.smoke.test.ts`. It uses real SQLite in per-test temp DBs and a
137
+ mocked mux (`installMux()` from `test/_mux.ts`). Integration tests use
138
+ real tmux or herdr, and real git, jj or sl in `os.tmpdir()`; jj and sl
139
+ tests skip when the binary is missing. Conventions live in
140
+ [AGENTS.md](../../AGENTS.md) and `test/README.md`.
141
+
142
+ | Area | What the tests pin |
143
+ | --- | --- |
144
+ | `src/capture.ts`, `src/op-context.ts` | payloads hold only changed columns (asserted by key count); echo suppression; cascade grouping; FK-CASCADE tombstone keys; the null-intent fail-safe |
145
+ | `src/apply.ts` | injected HLCs, no sleeps: field convergence in both orders; same-field LWW determinism; all four put and del orderings; resurrection; grow-only notes; the set-to-NULL trap; `owner_id` stripping; replay idempotence |
146
+ | `src/rebuild.ts` | round trip row by row per portable table (a count-only check passes on a garbage rebuild); tombstones; byte-identical ops; `machine_id` and clock carried |
147
+ | `src/drift.ts`, `src/fleet-hazards.ts` | drift planted the way a capture bug would (`withCaptureSuppressed` plus a direct write), with equal weight on false positives |
148
+ | `src/dormant.ts` | `updated_at` written directly; weighted to the boundary between `finished` and `abandoned` |
149
+ | `src/disk-recon.ts` | a per-test `MU_STATE_DIR` with the on-disk shape built by hand, both directions |
150
+ | `src/segments.ts`, `src/sync.ts` | two temp DBs and one shared dir; each robustness layer; machine-local ops never reaching a file |
151
+ | `src/hlc.ts` | an injected clock: backward jumps, stalled milliseconds, concurrent minting |
152
+ | `sync-session.integration` | 8 rounds of two machines: byte-identical portable content, no `--deep` drift, a quiet round adds zero ops |
153
+ | `test/acceptance.integration.test.ts` | the end-to-end gate: 10 tasks, 3 agents |
154
+
155
+ `npm run test:stress` repeats the suite with per-run timeouts,
156
+ optionally in parallel (`MU_TEST_STRESS_MODE=parallel`).
157
+
158
+ Two fixture traps:
159
+
160
+ - A convergence fixture must share one creation op. Two independent
161
+ `task add` calls for the same id make the later creation win every
162
+ field.
163
+ - An ordering-sensitive sync test must rename segments to pin ingest
164
+ order, since peers are discovered by `localeCompare` over random-UUID
165
+ filenames.
166
+
167
+ Treat a test that passes alone and fails under load as a concurrency
168
+ bug first. Drive wait and reaper tests from poll-loop seams, not fixed
169
+ timers, and wait for stable Ink output instead of sleeping.
170
+
171
+ ## Distribution
172
+
173
+ One npm package, `mu`. `tsup` bundles three entries:
174
+
175
+ | Output | Role |
176
+ | --- | --- |
177
+ | `dist/cli.js` | `bin: { mu: ./dist/cli.js }` |
178
+ | `dist/index.js`, `dist/index.d.ts` | SDK and types |
179
+ | `dist/extension/mu-pi.js` | the pi extension `mu link pi` points at |
180
+
181
+ `skills/` and `docs/` ship alongside. There is no build step on the user's
182
+ machine. pi stays a peer dependency: the extension types pi's API
183
+ structurally. Per-role agent guidance lives in the user's repo, not in
184
+ mu. The rule for adding a dependency is in
185
+ [ROADMAP § Anti-feature pledges](../ROADMAP.md#anti-feature-pledges).
@@ -0,0 +1,129 @@
1
+ # Sync
2
+
3
+ Machines share a workstream by exchanging ops through a shared folder
4
+ (`MU_SYNC_DIR`) kept in step by any folder syncer, such as Syncthing. The ops
5
+ themselves are described in [ops-log.md](ops-log.md). Overview:
6
+ [ARCHITECTURE.md](../ARCHITECTURE.md). Why segments and not
7
+ Litestream, cr-sqlite or a peer list, and why the DB must never sit in
8
+ `MU_SYNC_DIR`:
9
+ [ROADMAP § Rejected sync substrates](../ROADMAP.md#rejected-sync-substrates).
10
+
11
+ ## Segments
12
+
13
+ `src/segments.ts` is the transport.
14
+
15
+ - `flushSegment` appends this machine's unflushed ops to
16
+ `<MU_SYNC_DIR>/<machine_id>.jsonl`.
17
+ - `ingestSegment` reads a peer segment from its watermark into
18
+ `applyOp`.
19
+ - Peer discovery is implicit: every non-self `*.jsonl` is a peer. A
20
+ peer disappears only when its segment is deleted.
21
+
22
+ **Single writer per file.** A machine appends only to its own segment,
23
+ so nothing is contended and any folder syncer is adequate. Segments
24
+ are regenerable, so there is no fsync. `src/file-lock.ts` stops two
25
+ local `mu` processes from interleaving partial lines.
26
+
27
+ **Only `SYNCED_ENTITIES` and only this machine's ops are flushed.**
28
+ Pane ids and absolute paths never leave the machine, and peers never
29
+ echo each other's history.
30
+
31
+ **Payload encoding.** `ops.payload` is TEXT that is usually JSON, but
32
+ `mu log "text"` writes prose. The encoder passes valid JSON through
33
+ verbatim, so the crc covers those exact bytes, and JSON-encodes
34
+ anything else, unwrapping on ingest.
35
+
36
+ ## Robustness layers
37
+
38
+ Four checks stop ingest at the first bad record and advance the
39
+ watermark only that far:
40
+
41
+ | Layer | Catches |
42
+ | --- | --- |
43
+ | `JSON.parse` | a torn write (line lacks its trailing `,"crc"…}` framing) or a malformed shape (framing present). Same failure, different remediation: a cut transfer vs a writer bug |
44
+ | crc32 per line | bit rot |
45
+ | monotonic HLC | reorder, duplicate, truncation |
46
+ | `.manifest` sidecar | truncation exactly on a line boundary |
47
+
48
+ **Damage halts; refusal skips.** Halting is right for damage, where a
49
+ gap looks the same as reordering. It is wrong for a well-formed line
50
+ mu declines to project: a refusal leaves no hole, and halting would
51
+ freeze the peer's watermark for good, since `mu sync --repair` only
52
+ resets the watermark and re-reads into the same line. Ingest
53
+ distinguishes three cases:
54
+
55
+ | Line | Result |
56
+ | --- | --- |
57
+ | damaged | halt |
58
+ | a known machine-local entity | reported defect, skipped |
59
+ | an unrecognised entity | applied as a no-op, no defect (a reader behind the writer is normal in a mixed fleet) |
60
+
61
+ `mu sync` suggests `--repair` only for defects a re-read can clear.
62
+
63
+ **Sync never fails a command.** A truncated segment, a garbage segment,
64
+ a sync dir that is a file, or a vanished directory warns on stderr and
65
+ returns.
66
+
67
+ ## The ambient hook
68
+
69
+ Sync is ambient, not a daemon: no watcher, no background process, no
70
+ poll outliving a command. It happens because you already run `mu`
71
+ constantly.
72
+
73
+ The seam is `handle()` (`src/cli/handle.ts`), which every verb passes
74
+ through and which is already async. `syncPass` is async because it
75
+ takes the file lock, while most verb bodies are synchronous
76
+ better-sqlite3 code. One `await` before `fn(db)` and one after cover
77
+ every verb.
78
+
79
+ - **Ingest before the body**, so the verb reads the freshest state.
80
+ - **Flush after the body**, so this invocation's ops reach the segment
81
+ now.
82
+ - **Flush on the error path too.** A verb may commit ops before
83
+ throwing. Those ops are canonical, so withholding them would make
84
+ this machine's history depend on the exit code.
85
+
86
+ `src/sync.ts` holds `ambientIngest`, `ambientFlush`, `ambientSyncPass`,
87
+ `peerStatuses`, `ingestFromDb` (`mu sync --from <path>`) and
88
+ `repairPeer` (`mu sync --repair <peer>`, a unique prefix; ambiguity is
89
+ exit 4).
90
+
91
+ ### Carve-outs
92
+
93
+ | Surface | Behaviour | Why |
94
+ | --- | --- | --- |
95
+ | `MU_SYNC_DIR` unset | one `if` on one env var; no filesystem touch | the single-machine case pays nothing (measured: baseline; about 3ms with one peer) |
96
+ | `mu sql` | opts out (`handle(..., { ambientSync: false })`) | an ingest changing a row count mid-inspection reads as a mu bug |
97
+ | `mu sync` | opts out and runs the pass itself | otherwise the report prints "ingested 0" right after ingesting |
98
+ | the TUI | slow tick only (10s), one pass per beat across tabs, `quiet: true` | the fast tick can go to 100ms; the TUI owns the screen, so problems show on the Doctor card |
99
+
100
+ ## Out-of-order arrival
101
+
102
+ An `edge` or `note` put whose task has not arrived yet skips as
103
+ `absent`, correctly. Ingest still advances the watermark past it, so
104
+ without a second pass it would never land. Peers are discovered by
105
+ `localeCompare` over random-UUID filenames, so the arrival order is a
106
+ coin flip per fleet.
107
+
108
+ `reprojectDeferredOps` (`src/apply.ts`) re-queries the log for note and
109
+ edge puts that are resolvable now but unprojected. It skips ops whose
110
+ parent task is gone and keys with a newer `del`, so a deleted edge is
111
+ never resurrected and an orphan is not retried forever. It runs once
112
+ per ingest pass, not per peer, because an edge in one segment may name
113
+ a task in another. On a healthy DB it is two indexed queries returning
114
+ zero rows.
115
+
116
+ There is no retry queue. A queue would be a second source of truth and
117
+ would not survive a short-lived `mu` process when the parent arrives
118
+ days later.
119
+
120
+ ## Mixed-fleet hazards
121
+
122
+ `src/fleet-hazards.ts` adds three checks to the default `mu doctor`,
123
+ all no-ops when `MU_SYNC_DIR` is unset:
124
+
125
+ | Check | Severity |
126
+ | --- | --- |
127
+ | `MU_DB_PATH` inside `MU_SYNC_DIR` | fail |
128
+ | DB on a network mount | warn |
129
+ | two workstream names differing only by case | warn |
@@ -0,0 +1,146 @@
1
+ # TUI architecture
2
+
3
+ The TUI is a read-only, live-updating dashboard of 10 cards on `ink`.
4
+ It lives entirely under `src/cli/tui/`, the only place ink and react
5
+ are imported (a [ROADMAP](../ROADMAP.md#anti-feature-pledges) pledge).
6
+ CLI verbs stay the mutation API: the TUI yanks `mu` commands and the
7
+ operator runs them. Overview: [ARCHITECTURE.md](../ARCHITECTURE.md).
8
+ Press `?` in the TUI for the keymap.
9
+
10
+ ## Entry points
11
+
12
+ - Bare `mu` launches the TUI when `process.stdout.isTTY` is true. It
13
+ loads every workstream and picks the initial tab with the focus
14
+ ladder in `src/cli/tui-launch-focus.ts`: `$MU_SESSION` → tmux session
15
+ name → cwd inside a workspace → cwd equal to a workspace's project
16
+ root (`src/project-root.ts`; latest activity breaks ties) → tab 0.
17
+ With no workstreams it prints `mu --help` plus
18
+ `Get started: mu workstream init <name>` and exits 0.
19
+ - On non-TTY stdout (pipes, CI, most agent calls) bare `mu` prints help.
20
+ `MU_NO_TUI=1` forces that path inside a terminal.
21
+ - `mu state` is the static card; `mu state --tui` selects the TUI.
22
+ - The import stays dynamic (`await import("./cli/tui/index.js")`). A
23
+ static import of ink anywhere else would pull the TUI graph into the
24
+ help, version and `--json` paths.
25
+
26
+ ## Cluster shape
27
+
28
+ | Files | Role |
29
+ | --- | --- |
30
+ | `index.ts`, `escapes.ts` | `runTui` entry; alt-screen and SGR mouse-mode lifecycle; pure escape bytes |
31
+ | `app.tsx` | `<App>` root: popup state machine, global keymap, tabs, footer |
32
+ | `state.ts` | `useDashboardSnapshot` poll loop (fast and slow tiers) |
33
+ | `keys.ts`, `keymap-spec.ts`, `help.tsx` | pure key dispatch; the keymap source of truth that drives both dispatch and the `?`/F1 overlay |
34
+ | `mouse.ts`, `use-popup-action-queue.ts` | vendored SGR mouse parser with double-click; one queued popup action per render |
35
+ | `layout.ts`, `columns.ts`, `wrap-ansi.ts`, `use-terminal-size.ts` | responsive columns and row budgets; aligned clipping; ANSI-aware wrapping; resize hook |
36
+ | `titled-box.tsx`, `popup-shell.tsx`, `list-row.tsx`, `padded-rows.tsx`, `status-bar.tsx`, `tab-strip*.ts(x)` | chrome primitives |
37
+ | `format-helpers.ts`, `agent-display.ts` | shared formatters and agent-row display |
38
+ | `use-popup-filter.tsx`, `use-status-filter.tsx`, `use-notes-drill.ts` | `/` filter; status (`o`/`i`/`c`) and substate (`p`/`w`) toggles; shared notes drill |
39
+ | `yank.ts`, `tuicr.ts`, `lazygit.ts`, `tmux-attach.ts` | clipboard; the alt-screen handoffs |
40
+ | `cards/*.tsx` | the 10 dashboard cards, plus `_placeholder.tsx` |
41
+ | `popups/*.tsx` | one fullscreen popup per card, plus `dag.tsx` (`g`), `all-tasks.tsx` (`t`), the `task-list-popup.tsx` scaffold, `drill.tsx`, `task-detail.tsx`, `cursor-row.tsx`, `scroll.ts`, `viewport.ts`, `show-loader.ts` |
42
+
43
+ ## State machine
44
+
45
+ `<App>` owns:
46
+
47
+ - **Popup state:** `null` (dashboard) or one popup id. One popup at a
48
+ time; `Esc` or `q` returns to the dashboard.
49
+ - **Card visibility:** toggled by `0`-`9`.
50
+ - **Tick rate:** fast tick, 1s default, adjusted with `+` `-` `=` `0`.
51
+ - **Active workstream tab:** `Tab` / `Shift-Tab` cycles when there are
52
+ two or more. A torn-down workstream keeps its tab position and
53
+ renders dimmed with strikethrough until the name is recreated. A
54
+ torn-down single workstream shows its normally hidden tab.
55
+ - **Footer flash:** a transient status-bar message.
56
+
57
+ Popups own their local state (cursor, filter, drill mode, local modes
58
+ such as Workspaces' `list` / `commits` / `show`). They never mutate App
59
+ state. They receive a read-only props bag: `snapshot`, `db`,
60
+ `workstream`, `fastTickNonce`, `slowTickNonce`, `yank`, `onClose`,
61
+ `onModeChange`, `onFilterEditingChange`, `onFooter`.
62
+
63
+ ## Polling tiers
64
+
65
+ | Tier | Interval | Loader | Reads | Cost |
66
+ | --- | --- | --- | --- | --- |
67
+ | fast | 1s, adjustable | `loadWorkstreamSnapshotFast` | SQL only: tasks, tracks, workspace rows, recent events, workspace orphans | p50 < 1ms |
68
+ | slow | 10s (`SLOW_TICK_MS`) | `loadWorkstreamSnapshotSlow` | subprocesses: agent liveness and state, workspace dirty status, recent commits, doctor summary | p50 hundreds of ms |
69
+
70
+ `mergeSnapshotFastSlow` merges the last slow result into every fast
71
+ render, so cards never flicker through loading. `r` / `F5` runs both
72
+ tiers now. A tab switch clears the slow cache and fetches eagerly.
73
+ `snapshotKey` returns the same `data` reference across no-op ticks, so
74
+ React's diffing short-circuits. The slow tick also runs ambient sync
75
+ ([sync.md](sync.md#carve-outs)).
76
+
77
+ ## Render geometry
78
+
79
+ `layout.ts`:
80
+
81
+ - **Columns by width:** stacked below 120 columns, then 2 at 120, 3 at
82
+ 180, 4 at 240. Stream cards (Commits, Activity log) trail; Commits
83
+ trails last.
84
+ - **Row budgets:** each card has `min` / `max` / `chrome`. The allocator
85
+ distributes rows so a noisy list cannot crowd its siblings. Overflow
86
+ shows as `+N more · Shift+N` in the card's bottom border.
87
+ - **Culling:** when even minimum budgets do not fit, cards drop by
88
+ priority (Doctor → Recent → Workspaces → …) and the screen shows
89
+ `+N cards hidden · resize taller`.
90
+
91
+ `wrap-ansi.ts` wraps by visual width (`string-width`) and closes open
92
+ SGR state on every exit path, so a fragment without a trailing
93
+ `\x1b[0m` cannot bleed into ink chrome. Drill bodies are space-padded
94
+ to the exact box width, or ink's `wrap="truncate"` ANSI miscount eats
95
+ the right border.
96
+
97
+ ## Read-only, with handoffs
98
+
99
+ Every popup row exposes one canonical `mu` command through `y`.
100
+ `yank.ts` probes pbcopy, wl-copy, xclip, xsel and clip.exe, then falls
101
+ back to OSC-52 over stderr.
102
+
103
+ The handoffs leave the alt screen, disable mouse mode, run a
104
+ foreground subprocess, and restore both on exit:
105
+
106
+ - `t` in any `git show` drill runs `tuicr -r <sha>` in the project root
107
+ or workspace.
108
+ - `l` in the Commits popup runs lazygit.
109
+ - `a` in the Agents popup attaches to the agent's mux pane.
110
+
111
+ A TUI gesture that mutates state needs a ROADMAP entry first.
112
+
113
+ ## Input
114
+
115
+ Mouse support uses SGR mouse mode. `mouse.ts` parses
116
+ `ESC[<button;x;y;M/m`, detects double-clicks, and exposes `useMouse()`.
117
+ A double-click on a card emits `setCursor` then `drill` through the
118
+ action queue, which consumes one action per render, so the cursor lands
119
+ before the drill reads the focused row.
120
+
121
+ `keys.ts` holds `dispatchGlobalKey` (dashboard), `dispatchPopupKey`
122
+ (popup), and `shouldSwallowGlobalKey` (keys a popup consumes).
123
+
124
+ ## Drill recursion
125
+
126
+ List popups drill with `Enter`. `DrillScrollView`
127
+ (`popups/drill.tsx`) is the shared scrollable text leaf: Workspaces'
128
+ git show, Agents' scrollback, Activity log payloads, and Doctor
129
+ remediation. Task popups drill into `TaskDetailDrill`
130
+ (`popups/task-detail.tsx`, the notes timeline). Tracks chains track →
131
+ task list → `TaskDetailDrill`.
132
+
133
+ `useDrillKeymap` owns scroll state. An optional `resetKey` resets
134
+ scroll on identity change while a tick refresh keeps it, and
135
+ `onScrollChange` serves the DAG popup's focused-root tracking. Body
136
+ wrap metadata is shared so the scroll clamp and the painter cannot
137
+ disagree. Subprocess-backed drills use `popups/show-loader.ts`, which
138
+ keeps the prior body during a refetch.
139
+
140
+ ## Test seam
141
+
142
+ See `test/README.md`. `test/_ink-render.ts` provides
143
+ `createInkInputStream`, `createInkCaptureStream`, `simulateInput` and
144
+ `latestRenderedFrame`: mount a popup or `<App>`, drive keys, assert on
145
+ the visible frame and spy callbacks. Source greps are for narrow
146
+ structural guards only, never behaviour.
@@ -0,0 +1,25 @@
1
+ # mu guide
2
+
3
+ Start with the tutorial, then open a how-to when you have that job.
4
+ `mu <verb> --help` is the reference: flags, defaults, and exit codes.
5
+ Terms are defined in [VOCABULARY.md](../VOCABULARY.md).
6
+
7
+ 1. [Getting started](getting-started.md): install mu, plan two tasks,
8
+ and ship one with a pi worker.
9
+
10
+ How-to guides:
11
+
12
+ - [Dispatch work to a worker](dispatch.md)
13
+ - [Stop a worker](stop-a-worker.md)
14
+ - [Delegate a task (mu_delegate)](delegate.md)
15
+ - [Run a worker on another machine](remote.md)
16
+ - [Recover from a broken state](recovery.md)
17
+ - [Sync between machines](sync.md)
18
+ - [Use the TUI dashboard](tui.md)
19
+ - [Query and script mu](sql.md)
20
+ - [Upgrade mu](upgrade.md)
21
+ - [Choose a multiplexer backend](backends.md)
22
+ - [Clean up](cleanup.md)
23
+
24
+ For features mu does not have on purpose, see
25
+ [ROADMAP.md § Explicitly rejected](../ROADMAP.md#explicitly-rejected).
@@ -0,0 +1,61 @@
1
+ # How to choose a multiplexer backend
2
+
3
+ mu drives one multiplexer per command: tmux or
4
+ [herdr](https://github.com/herdrdev/herdr). Spawn, send, read, and
5
+ agent state work on both.
6
+
7
+ ## See which backend is active
8
+
9
+ ```bash
10
+ mu doctor # the environment block names it
11
+ mu doctor --json # .environment.mux.name
12
+ ```
13
+
14
+ mu picks the backend from the first rule that matches:
15
+
16
+ 1. `MU_MUX=tmux` or `MU_MUX=herdr`. An unknown value fails with exit 1.
17
+ 2. `HERDR_ENV=1`: herdr. This wins over tmux variables, because a herdr
18
+ pane can host a tmux server.
19
+ 3. `$TMUX` or `$TMUX_PANE` is set: tmux.
20
+ 4. The first binary that runs, tmux before herdr.
21
+ 5. Neither: `NoMultiplexerError`, exit 5.
22
+
23
+ ## Know what differs
24
+
25
+ | | tmux | herdr |
26
+ | --- | --- | --- |
27
+ | Workstream | session `mu-<ws>` | workspace labelled `mu-<ws>` |
28
+ | Window | window | tab |
29
+ | Pane id | `%15` | `w1:p1` |
30
+ | Attach | `tmux attach -t mu-<ws>` | `herdr session attach mu-<ws>` |
31
+ | pi agent state | control socket | control socket |
32
+ | Other agents' state | murmur | herdr |
33
+ | Send to non-pi agents | bracketed paste, then Enter | one atomic `agent prompt` |
34
+ | Spawn | one command | bare pane, then `agent start` |
35
+ | Pane borders, layout | set by mu | left to herdr |
36
+ | Test isolation | `MU_TMUX_SOCKET` | `MU_HERDR_SESSION` |
37
+
38
+ Where each agent's state comes from, per backend:
39
+ [architecture/mux.md § Agent state](../architecture/mux.md#agent-state).
40
+
41
+ ## Know herdr's limits
42
+
43
+ - `mu agent kick` works on Linux only. It reads the TTY from
44
+ `/proc/<pid>/fd/0`, which macOS does not have.
45
+ - `mu agent read --lines` cannot recover rows that scrolled off a pane
46
+ on the alternate screen.
47
+ - `MU_<CLI>_COMMAND` overrides apply on tmux only. herdr picks the
48
+ agent binary itself.
49
+ - herdr creates every workspace detached. Run `herdr workspace focus`
50
+ yourself.
51
+
52
+ ## Expect loud failures, not silent ones
53
+
54
+ If the multiplexer is down, `mu doctor` still prints a full report.
55
+ Verbs that need the multiplexer, such as spawn, send, close, and
56
+ reconcile, exit 5 with remediation steps. Verbs that only use it for
57
+ decoration, such as identity lookup and `mu workstream list`, carry on.
58
+
59
+ A herdr argument error raises `HerdrSyntaxError`, not a multiplexer
60
+ error. It means herdr's CLI changed and mu needs a fix. The herdr
61
+ server is fine.
@@ -0,0 +1,74 @@
1
+ # How to clean up
2
+
3
+ ## Close one agent
4
+
5
+ ```bash
6
+ mu agent close worker-1 -w auth
7
+ ```
8
+
9
+ A clean workspace is freed with the agent. If the workspace has edits
10
+ or commits, close refuses with exit 4. Run
11
+ `mu workspace free worker-1 -w auth --commit` first, or pass
12
+ `--discard-workspace` to lose the changes. See
13
+ [Stop a worker](stop-a-worker.md).
14
+
15
+ ## Tear down a workstream
16
+
17
+ ```bash
18
+ mu workstream teardown auth # dry run: prints what goes
19
+ mu workstream teardown auth --yes # kills the session, deletes the rows
20
+ ```
21
+
22
+ Teardown kills the tmux session first, then deletes the agents, tasks,
23
+ edges, notes, and workspaces. Running it again is a no-op.
24
+
25
+ To sweep every workstream that has no tasks, agents, or workspaces,
26
+ including `mu-*` tmux sessions with no DB row:
27
+
28
+ ```bash
29
+ mu workstream teardown --empty
30
+ mu workstream teardown --empty --yes
31
+ ```
32
+
33
+ ## Undo a teardown
34
+
35
+ Teardown writes tombstone ops, so the ops log keeps everything. You do
36
+ not need a backup first.
37
+
38
+ ```bash
39
+ mu workstream list --torn-down
40
+ mu undo 7a40e6cc --yes
41
+ ```
42
+
43
+ The list shows one row per teardown, newest first. `← recreated since`
44
+ means the name is in use again, so the undo would change nothing.
45
+ Killed panes and freed workspace directories do not come back.
46
+
47
+ ## Find workstreams you can remove
48
+
49
+ The `housekeeping` section of `mu doctor` lists two kinds:
50
+
51
+ | Kind | Rule | Advice |
52
+ | ---- | ---- | ------ |
53
+ | finished | every task `CLOSED`, idle 14 days or more | safe to tear down |
54
+ | abandoned | unclosed tasks, idle 60 days or more | read the open tasks first |
55
+
56
+ It skips your current workstream, `scratch`, and any workstream with a
57
+ live agent or a workspace. Teardown removes the plan, not the code: no
58
+ commit or checkout is touched.
59
+
60
+ ## Free disk space
61
+
62
+ ```bash
63
+ mu doctor --disk # bytes per workspace checkout
64
+ mu workspace orphans -w auth # directories with no DB row
65
+ mu workspace free worker-1 -w auth # delete one workspace
66
+ ```
67
+
68
+ ## Keep a copy of the graph
69
+
70
+ ```bash
71
+ mu db backup /tmp/mu-backup.db # VACUUM INTO; never overwrites
72
+ mu task list -w auth --json
73
+ mu task notes design -w auth --json
74
+ ```