@mu-crew/mu 3.8.1 → 3.10.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.
Files changed (54) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +1 -0
  3. package/dist/chunk-4PACTXAB.js +17 -0
  4. package/dist/chunk-4PACTXAB.js.map +1 -0
  5. package/dist/chunk-77GBZ2KR.js +85 -0
  6. package/dist/chunk-77GBZ2KR.js.map +1 -0
  7. package/dist/chunk-7OQNJWBY.js +391 -0
  8. package/dist/chunk-7OQNJWBY.js.map +1 -0
  9. package/dist/chunk-PZ5AY32C.js +10 -0
  10. package/dist/chunk-PZ5AY32C.js.map +1 -0
  11. package/dist/chunk-WT7IN2OD.js +9887 -0
  12. package/dist/chunk-WT7IN2OD.js.map +1 -0
  13. package/dist/chunk-XXK7FR37.js +640 -0
  14. package/dist/chunk-XXK7FR37.js.map +1 -0
  15. package/dist/chunk-YIF3X32A.js +2792 -0
  16. package/dist/chunk-YIF3X32A.js.map +1 -0
  17. package/dist/cli-UZEMPIPW.js +6266 -0
  18. package/dist/cli-UZEMPIPW.js.map +1 -0
  19. package/dist/cli.js +37 -25280
  20. package/dist/cli.js.map +1 -1
  21. package/dist/extension/mu-pi.js +230 -138
  22. package/dist/extension/mu-pi.js.map +1 -1
  23. package/dist/index.d.ts +543 -142
  24. package/dist/index.js +340 -11169
  25. package/dist/index.js.map +1 -1
  26. package/dist/tui-X2XICJTN.js +6452 -0
  27. package/dist/tui-X2XICJTN.js.map +1 -0
  28. package/dist/tui-sync-worker.js +16 -0
  29. package/dist/tui-sync-worker.js.map +1 -0
  30. package/dist/yank-T3AYQTVR.js +12 -0
  31. package/dist/yank-T3AYQTVR.js.map +1 -0
  32. package/docs/ARCHITECTURE.md +14 -6
  33. package/docs/VISION.md +14 -7
  34. package/docs/VOCABULARY.md +5 -6
  35. package/docs/architecture/control-socket.md +6 -2
  36. package/docs/architecture/mux.md +4 -2
  37. package/docs/architecture/ops-log.md +2 -1
  38. package/docs/architecture/sdk.md +3 -1
  39. package/docs/architecture/sync.md +32 -10
  40. package/docs/architecture/tui.md +18 -8
  41. package/docs/guide/backends.md +5 -3
  42. package/docs/guide/cleanup.md +8 -3
  43. package/docs/guide/delegate.md +3 -1
  44. package/docs/guide/dispatch.md +9 -3
  45. package/docs/guide/recovery.md +19 -8
  46. package/docs/guide/sql.md +11 -4
  47. package/docs/guide/stop-a-worker.md +3 -1
  48. package/docs/guide/tui.md +3 -3
  49. package/docs/guide/upgrade.md +8 -2
  50. package/docs/reference/env.md +11 -5
  51. package/docs/reference/naming.md +3 -4
  52. package/package.json +1 -1
  53. package/skills/mu/SKILL.md +2 -2
  54. package/skills/mu/recipes/drift-audit.md +5 -3
@@ -0,0 +1,16 @@
1
+ import {
2
+ ambientSyncPass,
3
+ openDb
4
+ } from "./chunk-YIF3X32A.js";
5
+ import "./chunk-4PACTXAB.js";
6
+ import "./chunk-PZ5AY32C.js";
7
+
8
+ // src/cli/tui/sync-worker.ts
9
+ import { parentPort, workerData } from "worker_threads";
10
+ var port = parentPort;
11
+ if (port === null) throw new Error("tui-sync-worker must run as a worker_thread");
12
+ var db = openDb({ path: workerData.dbPath });
13
+ port.on("message", () => {
14
+ void ambientSyncPass(db, { quiet: true }).catch(() => null).then((result) => port.postMessage(result));
15
+ });
16
+ //# sourceMappingURL=tui-sync-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/tui/sync-worker.ts"],"sourcesContent":["// worker_thread entry for the TUI's ambient sync pass (built to\n// dist/tui-sync-worker.js). ingest and flush are synchronous SQLite and\n// file work (seconds on a first read, hundreds of ms per peer append on\n// a 40 MB segment), so on ink's thread they froze input and repaints.\n// Here they block only this thread. The worker owns its own connection\n// to the same DB file: WAL serves the TUI's reads meanwhile, and the\n// TUI sees the result on its next read.\n//\n// Protocol: the parent posts any message to start one pass, and the\n// worker replies with the pass's result (`null` if it threw) when the\n// pass is over. ambientSyncPass is total, and `quiet` keeps stderr off\n// the alternate screen.\n\nimport { parentPort, workerData } from \"node:worker_threads\";\nimport { openDb } from \"../../db.js\";\nimport { ambientSyncPass } from \"../../sync.js\";\n\nconst port = parentPort;\nif (port === null) throw new Error(\"tui-sync-worker must run as a worker_thread\");\nconst db = openDb({ path: (workerData as { dbPath: string }).dbPath });\nport.on(\"message\", () => {\n void ambientSyncPass(db, { quiet: true })\n .catch(() => null)\n .then((result) => port.postMessage(result));\n});\n"],"mappings":";;;;;;;;AAaA,SAAS,YAAY,kBAAkB;AAIvC,IAAM,OAAO;AACb,IAAI,SAAS,KAAM,OAAM,IAAI,MAAM,6CAA6C;AAChF,IAAM,KAAK,OAAO,EAAE,MAAO,WAAkC,OAAO,CAAC;AACrE,KAAK,GAAG,WAAW,MAAM;AACvB,OAAK,gBAAgB,IAAI,EAAE,OAAO,KAAK,CAAC,EACrC,MAAM,MAAM,IAAI,EAChB,KAAK,CAAC,WAAW,KAAK,YAAY,MAAM,CAAC;AAC9C,CAAC;","names":[]}
@@ -0,0 +1,12 @@
1
+ import {
2
+ osc52Sequence,
3
+ probeClipboardBackend,
4
+ yank
5
+ } from "./chunk-77GBZ2KR.js";
6
+ import "./chunk-PZ5AY32C.js";
7
+ export {
8
+ osc52Sequence,
9
+ probeClipboardBackend,
10
+ yank
11
+ };
12
+ //# sourceMappingURL=yank-T3AYQTVR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -74,7 +74,7 @@ them, and is the only code that imports ink:
74
74
  **CLI and SDK surface.** Each operation is a typed SDK function plus a
75
75
  thin Commander wrapper. Public functions take operator-facing names and
76
76
  resolve them to surrogate ids exactly once. Typed errors map to exit
77
- codes 0-5. The same page covers testing layers and packaging:
77
+ codes 0-7. The same page covers testing layers and packaging:
78
78
  [architecture/sdk.md](architecture/sdk.md).
79
79
 
80
80
  ## Module map
@@ -88,13 +88,14 @@ hub that re-exports them. `src/tasks.ts`, `src/agents.ts`, `src/mux.ts`,
88
88
 
89
89
  | Module | Responsibility |
90
90
  | --- | --- |
91
- | `src/db.ts` | better-sqlite3 connection (WAL); schema v11 (11 tables, 3 views) in `applySchema`; installs capture; owns `SYNCED_ENTITIES`, `PORTABLE_TABLES`, `MACHINE_LOCAL_TABLES`; refuses older (`SchemaTooOldError`) or newer (`SchemaTooNewError`) DBs, exit 4 |
91
+ | `src/db.ts` | better-sqlite3 connection (WAL); schema v11 (12 tables, 3 views) in `applySchema`; installs capture; owns `SYNCED_ENTITIES`, `PORTABLE_TABLES`, `MACHINE_LOCAL_TABLES`; refuses older (`SchemaTooOldError`) or newer (`SchemaTooNewError`) DBs, exit 4 |
92
92
  | `src/hlc.ts` | hybrid logical clock, serialized as sortable TEXT |
93
- | `src/capture.ts` | TEMP triggers recording each portable write as an op |
93
+ | `src/capture.ts` | TEMP triggers recording each portable write as an op, and refusing natural-key changes |
94
94
  | `src/op-context.ts` | `withOpContext` (intent, actor, group); `withCaptureSuppressed` echo guard |
95
95
  | `src/apply.ts` | apply one op: per-field LWW, tombstones, substate pair repair, `reprojectDeferredOps` |
96
96
  | `src/undo.ts` | inverse ops for one group; refuses a superseded group |
97
97
  | `src/rebuild.ts` | replay the whole log into a new DB file |
98
+ | `src/compact.ts` | `mu db compact` (blank redundant note-tombstone payloads) and `mu db forget` (drop a torn-down workstream's ops); this machine only |
98
99
  | `src/legacy-ops.ts` | classifier for historical log-only intents; rebuild copies them unprojected |
99
100
  | `src/drift.ts` | cheap invariant (every row has an op) and `--deep` rebuild-diff |
100
101
  | `src/logs.ts` | typed reader over `ops`; `appendLog` and `emitEvent`, the writes triggers cannot cover |
@@ -105,6 +106,8 @@ hub that re-exports them. `src/tasks.ts`, `src/agents.ts`, `src/mux.ts`,
105
106
  | Module | Responsibility |
106
107
  | --- | --- |
107
108
  | `src/segments.ts` | JSONL segments: flush own ops, ingest peers from a watermark |
109
+ | `src/segment-manifest.ts` | segment `.manifest` sidecar: read, write, seal, and the self-consistency check the owner's flush runs before trusting it |
110
+ | `src/sha256-resumable.ts` | SHA-256 with a serialisable running state, so the segment manifest hashes only appended bytes |
108
111
  | `src/sync.ts` | peer status, the ambient hook, `--from`, `--repair` |
109
112
  | `src/file-lock.ts` | cross-process advisory lock through atomic `fs.mkdir` |
110
113
  | `src/fleet-hazards.ts` | mixed-fleet checks: DB inside `MU_SYNC_DIR`, network mount, case collisions |
@@ -121,13 +124,14 @@ hub that re-exports them. `src/tasks.ts`, `src/agents.ts`, `src/mux.ts`,
121
124
  | `src/agents/spawn-lock.ts` | per-session lock around topology and row insert |
122
125
  | `src/agents/transport.ts` | send routing: ctl for pi agents, mux paste otherwise, no fallback; `expectsCtl` |
123
126
  | `src/agents/abort.ts` | stop a pi turn through the control socket; abort + send (`--interrupt`) |
124
- | `src/agents/delegate.ts` | `delegateOutcome`: one wait result to `done` / `empty` / `died` / `timeout` / `pending` |
127
+ | `src/agents/delegate.ts` | `delegateOutcome`: one wait result to `done` / `empty` / `error` / `died` / `timeout` / `pending` |
125
128
  | `src/agents/wait.ts` | block until task-less agents leave `busy` |
126
129
  | `src/agents/adopt.ts` | register an existing pane as an agent |
127
130
  | `src/agents/kick.ts` | `mu agent kick`: signal a wedged pane's foreground process group |
128
131
  | `src/agents/errors.ts` | typed agent errors |
129
132
  | `src/agent-state.ts` | runtime state from ctl, herdr or murmur; `unknown` with a reason |
130
133
  | `src/reconcile.ts` | ghost prune and orphan surfacing |
134
+ | `src/mux/index.ts` | cluster barrel that `src/mux.ts` re-exports |
131
135
  | `src/mux/types.ts` | `MuxBackend` interface; `MuxError`, `PaneNotFoundError`, `NoMultiplexerError` |
132
136
  | `src/mux/detect.ts` | `MU_MUX` → `HERDR_ENV` → `$TMUX` → `PATH` ladder; `activeMux()` |
133
137
  | `src/mux/tmux.ts` | tmux backend: the `tmux()` wrapper, bracketed-paste send, pane validation |
@@ -139,7 +143,7 @@ hub that re-exports them. `src/tasks.ts`, `src/agents.ts`, `src/mux.ts`,
139
143
  | `src/ctl/client.ts` | `ctlRequest`, `ctlProbe`, version and unknown-op errors |
140
144
  | `extension/mu-pi.ts` | pi extension, child side: serves `$MU_CTL_SOCK`; built to `dist/extension/mu-pi.js` |
141
145
  | `extension/delegate.ts` | pi extension, parent side: `mu_delegate` / `mu_delegate_cancel`, which shell out to `mu` |
142
- | `extension/nudge.ts` | pi extension: the keep-driving nudge. Arms on a `mu` dispatch, checks `mu state --json` at `agent_before_settle`, injects the SKILL.md rule once per prompt, logs `--kind nudge`. Also the close nudge (worker settles owning a task) and the refute nudge (`task claim --for` on a task with no refute decision in `mu task notes --json`; one notice, no continuation) |
146
+ | `extension/nudge.ts` | pi extension: the keep-driving nudge. Arms on a `mu` dispatch, checks `mu state --json` at `agent_before_settle`, injects the SKILL.md rule once per prompt, logs `--kind nudge`. Also the close nudge (worker settles owning a task) and the refute nudge (`task claim --for` on a task with no refute decision in `mu task notes --json`; one notice, no continuation). `parallelSettle` runs the three settle checks in parallel behind one `agent_before_settle` handler and merges their entries; `mu log` breadcrumbs are not awaited |
143
147
  | `src/link.ts` | `mu link pi`: extension shim, skill symlink, `inspectLinks` for doctor |
144
148
 
145
149
  ### Tasks, workspaces and workstreams
@@ -165,14 +169,18 @@ hub that re-exports them. `src/tasks.ts`, `src/agents.ts`, `src/mux.ts`,
165
169
 
166
170
  | Module | Responsibility |
167
171
  | --- | --- |
168
- | `src/cli.ts` | Commander wiring (`buildProgram`) |
172
+ | `src/main.ts` | the `mu` bin (`dist/cli.js`): enables the compile cache, then `import()`s `src/cli.ts` |
173
+ | `src/compile-cache.ts`, `src/state-dir.ts` | best-effort V8 compile cache under `<state>/compile-cache`; builtins-only state-dir resolution |
174
+ | `src/cli.ts` | Commander wiring (`buildProgram`, `runCli`) |
169
175
  | `src/cli/*.ts` | one file per verb namespace, thin wrappers over the SDK; `format.ts` renders tables, `handle.ts` maps errors to exit codes and runs ambient sync |
170
176
  | `src/cli/tasks/*.ts` | the `mu task` namespace, including `mu me tasks` / `mu me next` |
171
177
  | `src/cli/dispatch-hints.ts` | dispatch-time `Next:` hints for pi agents (`--fresh`, `--interrupt`, abort) |
172
178
  | `src/cli/stdin.ts` | `-` as a text argument: reads note / send text from stdin (heredoc prose) |
173
179
  | `src/cli/agents-remote.ts` | `mu agent remote-env`: prints the ssh forward and env, runs nothing |
174
180
  | `src/cli/tui-launch-focus.ts` | initial-tab focus ladder for bare `mu` and `mu state --tui` |
181
+ | `src/cli/tui-load.ts` | lazy `import()` of the TUI with `NODE_ENV` defaulted to `production` (React prod build) |
175
182
  | `src/cli/tui/` | the ink TUI; the only place ink and react are imported |
183
+ | `src/cli/tui/sync-worker.ts` | worker_thread (`dist/tui-sync-worker.js`) running the TUI's slow-tick sync pass off ink's thread |
176
184
  | `src/output.ts`, `src/shell-quote.ts` | `printNextSteps` / `errorNextSteps`; POSIX quoting for hints |
177
185
  | `src/glyphs.ts` | the one glyph vocabulary (single-cell Nerd Font codepoints) |
178
186
  | `src/index.ts` | SDK entry point |
package/docs/VISION.md CHANGED
@@ -109,7 +109,7 @@ which task.
109
109
  A workstream is a mux session: a tmux session on tmux, a herdr workspace
110
110
  on herdr. Its agents are panes in that session, so
111
111
  `tmux a -t mu-<workstream>` shows the whole crew. Workstreams on one
112
- machine are separate sessions, partitioned in the DB by `session_id`.
112
+ machine are separate sessions, partitioned in the DB by `workstream_id`.
113
113
  The crew survives detach and reattach.
114
114
 
115
115
  The multiplexer is a backend chosen by detection
@@ -161,7 +161,7 @@ win32-x64) or a C++ toolchain.
161
161
  - **Read views**: the `ready`, `blocked`, and `goals` views, and
162
162
  `mu state` as the state card.
163
163
  - **Typed verbs** that map to resource transitions (`task claim`,
164
- `task close`, `agent spawn --workspace`, `workstream destroy`).
164
+ `task close`, `agent spawn --workspace`, `workstream teardown`).
165
165
  - **`--json` on every read verb**, so scripts use `jq`, not table parsing.
166
166
  - **`mu sql`** underneath, as the explicit escape hatch.
167
167
 
@@ -249,10 +249,17 @@ mu agent spawn reviewer-1 --cli pi_big
249
249
  The exceptions are `mu log --tail` and the TUI (`mu state --tui`, or
250
250
  bare `mu` on a TTY). Both are:
251
251
  - owned by a human or parent script and gone when it ends;
252
- - read-only: TUI actions copy the `mu <verb>` command to the
253
- clipboard and exit, so every write is a fresh CLI call;
254
- - limited to stdin, stdout, and a poll timer: no sockets, watches, or
255
- subprocesses;
252
+ - free of user actions: TUI keys copy the `mu <verb>` command to
253
+ the clipboard and exit, so every change the operator asks for is a
254
+ fresh CLI call. The TUI's slow tick still does the housekeeping
255
+ any `mu` invocation does: ambient sync (ingest peer ops, flush the
256
+ local segment) and reconcile (prune agents whose pane died, which
257
+ reopens their tasks with a `[reaper]` note, and refresh pane
258
+ titles);
259
+ - limited to stdin, stdout, and a poll timer: no sockets or watches.
260
+ The TUI's slow tick runs short-lived probes (mux liveness, VCS
261
+ status) and ambient sync, and its handoffs (lazygit, tuicr, attach,
262
+ clipboard) run one foreground child each; none outlives the TUI;
256
263
  - TTY-gated: pipes, CI, `--json`, and `MU_NO_TUI=1` never enter the
257
264
  TUI, and plain `mu state` prints the static card.
258
265
 
@@ -276,5 +283,5 @@ the work, pi plus manual tmux is more transparent than mu.
276
283
 
277
284
  Known gaps, none promoted yet: `--evidence` grounds but does not verify,
278
285
  mutations have no declared idempotency keys, most mutations have no dry
279
- run (`workstream destroy`, `task delete`, and `undo` do), and the `role`
286
+ run (`workstream teardown`, `task delete`, and `undo` do), and the `role`
280
287
  field is stored but not enforced.
@@ -15,7 +15,7 @@ agent names, and file paths are in [reference/naming.md](reference/naming.md).
15
15
  | Term | Meaning | Don't use |
16
16
  | --- | --- | --- |
17
17
  | **workstream** | The unit of organization: one **mux session** and one DB partition. | "project", "session" (alone), "context" |
18
- | **scratch workstream** | The reserved workstream `scratch` for helpers outside any task DAG. Auto-created on first spawn with explicit `-w scratch`; `mu workstream init scratch` is rejected. | "throwaway ws", "temp workstream" |
18
+ | **scratch workstream** | The reserved workstream `scratch` for helpers outside any task DAG. Auto-created on first spawn with explicit `-w scratch`; `mu workstream init scratch` is rejected. Its session keeps a `_mu` window, so it stays open after the last agent closes; `teardown --empty` skips it. | "throwaway ws", "temp workstream" |
19
19
  | **multiplexer** / **mux** | The terminal program that owns panes: **tmux** or **herdr**. "mux" in code (`src/mux/`, `MU_MUX`), "multiplexer" in prose; one is active per invocation. | "terminal", "emulator", "backend" (alone) |
20
20
  | **mux backend** | An implementation of `MuxBackend` (`src/mux/tmux.ts`, `src/mux/herdr.ts`). Owns everything backend-specific. | "driver", "provider", "adapter" |
21
21
  | **mux detection** | Picks the mux backend: `MU_MUX` → `HERDR_ENV=1` → `$TMUX` → binary on `PATH` (tmux wins a tie) → `NoMultiplexerError`. | "auto-detect", "probe" |
@@ -105,7 +105,7 @@ agent names, and file paths are in [reference/naming.md](reference/naming.md).
105
105
  | **op** | One `ops` row, written by a trigger in the mutation's transaction. Holds only changed columns. | "event", "delta", "change" |
106
106
  | **ops log** | The `ops` table: the append-only record that sync, undo, and history read. | "event log", "journal", "WAL" |
107
107
  | **log entry** | An op as rendered by `mu log`, through one formatter (`src/log-render.ts`). | "message", "event" |
108
- | **kind** | The operator's channel tag on a log entry (`mu log --kind`), stored as the op's `entity`. | "category", "type" |
108
+ | **kind** | The operator's channel tag on a log entry (`mu log --kind`), stored as the op's `entity`. `workstream`, `task`, `edge`, and `note` are reserved. | "category", "type" |
109
109
  | **log ledger** | A convention: a custom `--kind` used as a watcher loop's durable dedupe record. | "state file" |
110
110
  | **intent** | The semantic label on an op (`task.close`), set once per SDK function via **op context**. | "verb", "action" |
111
111
  | **group** | The ops of one user action (`group_id`), undone as a unit. Any unique id prefix works. | "transaction", "batch" |
@@ -119,7 +119,7 @@ agent names, and file paths are in [reference/naming.md](reference/naming.md).
119
119
  | **resurrection** | A put newer than a seen tombstone, which legitimately recreates the row. | "undelete", "revive" |
120
120
  | **provenance** | Which HLC last wrote a field, derived from the ops log by query, never stored. | "version vector" |
121
121
  | **inverse op** | The op that reverts another; an ordinary op, so undo syncs and is undoable. | "rollback" |
122
- | **superseded** (group) | A later group wrote the same field, so `mu undo` refuses (exit 4) without `--force`. | "stale" |
122
+ | **superseded** (group) | A later group wrote the same field (other than `updated_at`), so `mu undo` refuses (exit 4) without `--force`. | "stale" |
123
123
  | **teardown** | Kill a workstream's mux session and delete its rows. `mu undo` restores rows, not panes or dirs. | "destroy", "nuke", "purge" |
124
124
  | **drift** | The ops log and live tables disagree. Always a bug. | "inconsistency", "corruption" |
125
125
  | **shallow / deep check** | Drift checks in `mu doctor` and `mu doctor --deep` (full rebuild). | "fsck" |
@@ -128,7 +128,7 @@ agent names, and file paths are in [reference/naming.md](reference/naming.md).
128
128
  | **machine_id** | Per-state-dir uuid naming this DB's **peer** and **segment**. | "device id", "host id" |
129
129
  | **segment** | `<sync-dir>/<machine_id>.jsonl`: one machine's ops, single writer. | "replica", "shard" |
130
130
  | **peer** | Another machine, found by its segment; named by `machine_id` prefix. **Stale** after 24h unchanged. | "node", "remote" |
131
- | **watermark** | `sync_peers.last_applied_seq`: how many lines of a peer's segment have been applied. | "offset", "cursor" |
131
+ | **watermark** | `sync_peers.last_applied_seq`: how many lines of a peer's segment file have been applied. A conflict copy has its own, keyed by its file stem. | "offset", "cursor" |
132
132
  | **flush** / **ingest** | Write my ops to my segment; apply peers' from their watermarks. Every invocation, when `MU_SYNC_DIR` is set. | "push", "pull" |
133
133
  | **reprojection** | After ingest, apply deferred ops whose parent arrived from another peer. | "retry queue" |
134
134
  | **mixed fleet** | Machines sharing state across different OSes or filesystems. | "cluster" |
@@ -222,5 +222,4 @@ deprecated, always `spawning`, and never read.
222
222
  | **pi session** | pi's conversation |
223
223
  | **agent session** | Colloquial for an agent's lifetime; avoid in code |
224
224
 
225
- New columns and variables say `workstream_id`, not `session_id`. The
226
- `agents.session_id` column keeps its name for schema stability.
225
+ Columns and variables say `workstream_id`, never `session_id`.
@@ -132,7 +132,11 @@ probes `refused`, and `mu agent close` or the reaper deletes it.
132
132
  when `MU_MANAGED_AGENT` is set (inside a mu-spawned pane) or
133
133
  `MU_DELEGATE=0`. The tools shell out to the `mu` CLI (spawn, send,
134
134
  `wait --json`, read, abort, close) and only format the CLI's
135
- `outcome` into a follow-up message. `delegateOutcome`
135
+ `outcome` into a follow-up message. A call runs one
136
+ `mu agent spawn --next-free --send <task> --json` (mu picks the free
137
+ name under its spawn lock and returns the send's `runs`), then one
138
+ `mu agent wait --after-runs <runs>` in the background; with `record`,
139
+ a `mu task show` comes first. `delegateOutcome`
136
140
  (`src/agents/delegate.ts`) maps one wait result to `done`, `empty`,
137
- `died`, `timeout` or `pending`. The extension imports nothing from pi;
141
+ `error`, `died`, `timeout` or `pending`. The extension imports nothing from pi;
138
142
  it types pi's API structurally.
@@ -5,7 +5,7 @@ mu runs agents in multiplexer panes, tmux or herdr, behind one
5
5
 
6
6
  ## One mux session per workstream
7
7
 
8
- One workstream is one mux session and one `session_id` partition in
8
+ One workstream is one mux session and one `workstream_id` partition in
9
9
  `~/.local/state/mu/mu.db`. Workstreams on one machine are independent
10
10
  mux sessions.
11
11
 
@@ -107,7 +107,9 @@ as a capability, not a name check:
107
107
  reads before the first claim report `unknown`. A pi agent skips the
108
108
  fixed wait: mu checks the pane on every handshake tick (a dead pane
109
109
  rolls back at once) and scans for startup errors when the handshake
110
- ends.
110
+ ends. After an ok handshake the scan skips shell exec-failure lines
111
+ (`command not found`, `No such file or directory`): pi demonstrably
112
+ started, and a resumed session can show such lines as old tool output.
111
113
  - **herdr.** mu creates a bare pane and calls `startAgentInPane`, which
112
114
  subsumes both checks.
113
115
  - **pi, either backend.** After the agent starts, mu polls its control
@@ -103,7 +103,8 @@ prefix previews, and `--yes` applies.
103
103
  old key (drift-641). The `task_notes` delete trigger records `OLD.*`
104
104
  only when no put under the current key is in the log, and `'{}'`
105
105
  otherwise; `planUndo` folds the puts first and falls back to the
106
- tombstone payload. For the same reason `src/drift.ts` matches notes on
106
+ tombstone payload, and `applyOp` deletes by the payload's content
107
+ identity, falling back to the put under the same key. For the same reason `src/drift.ts` matches notes on
107
108
  the task-key prefix.
108
109
 
109
110
  ## Rewriting history (local only)
@@ -125,7 +125,9 @@ codes:
125
125
  | 2 | usage error (Commander's default) |
126
126
  | 3 | not found (agent, task, workspace) |
127
127
  | 4 | conflict (name collision, double claim, dirty tree, schema too old or new, outdated extension) |
128
- | 5 | substrate unavailable (no multiplexer, DB locked) |
128
+ | 5 | substrate unavailable or unsafe (no multiplexer, mux or VCS failure, DB locked, ops-log drift, a `mu doctor` FAIL row, timeout) |
129
+ | 6 | `mu task wait`: the reaper reopened a watched task (its pane died) |
130
+ | 7 | `mu task wait`: a watched task stalled (`--on-stall exit`) |
129
131
 
130
132
  Errors carry structured context (operation, target, attempted action)
131
133
  so `mu doctor` can show them readably.
@@ -15,14 +15,26 @@ Litestream, cr-sqlite or a peer list, and why the DB must never sit in
15
15
  - `flushSegment` appends this machine's unflushed ops to
16
16
  `<MU_SYNC_DIR>/<machine_id>.jsonl`.
17
17
  - `ingestSegment` reads a peer segment from its watermark into
18
- `applyOp`.
18
+ `applyOp`. Ambient ingest skips a peer that is caught up: the manifest
19
+ counts the watermark, and the segment's size, exact mtime and
20
+ manifest sha256 equal what the last clean read to the end recorded
21
+ (in `sync_fingerprints`). Any change, including a rewrite with a
22
+ backdated mtime, fails that check, so the full read hashes the file
23
+ and reports the mismatch. `mu sync` always reads every line.
19
24
  - Peer discovery is implicit: every non-self `*.jsonl` is a peer. A
20
25
  peer disappears only when its segment is deleted.
26
+ - A Syncthing conflict copy (`<machine>.sync-conflict-….jsonl`) is
27
+ ingested too, with its own watermark keyed by the file stem: it can
28
+ diverge from the original, so line counts are not comparable.
21
29
 
22
30
  **Single writer per file.** A machine appends only to its own segment,
23
31
  so nothing is contended and any folder syncer is adequate. Segments
24
32
  are regenerable, so there is no fsync. `src/file-lock.ts` stops two
25
- local `mu` processes from interleaving partial lines.
33
+ local `mu` processes from interleaving partial lines. A flush never
34
+ appends without that lock: if another process still holds it after
35
+ 2 s (15 s for `mu sync`), the flush writes nothing, the ops stay
36
+ pending in `ops`, and the next invocation appends them. A stale lock
37
+ (holder crashed, older than 30 s) is broken as before.
26
38
 
27
39
  **Only `SYNCED_ENTITIES` and only this machine's ops are flushed.**
28
40
  Pane ids and absolute paths never leave the machine, and peers never
@@ -35,8 +47,10 @@ anything else, unwrapping on ingest.
35
47
 
36
48
  ## Robustness layers
37
49
 
38
- Four checks stop ingest at the first bad record and advance the
39
- watermark only that far:
50
+ The first three checks stop ingest at the first bad record and advance
51
+ the watermark only that far. The fourth is whole-file: it reports the
52
+ defect without halting, because every line still present is valid, and
53
+ only a fresh copy of the file clears it:
40
54
 
41
55
  | Layer | Catches |
42
56
  | --- | --- |
@@ -54,8 +68,9 @@ distinguishes three cases:
54
68
 
55
69
  | Line | Result |
56
70
  | --- | --- |
57
- | damaged | halt |
71
+ | damaged (including a blank line) | halt |
58
72
  | a known machine-local entity | reported defect, skipped |
73
+ | a historical log-only intent (`workstream.export` prose from mu < 1.1) | skipped silently, as flush and `--from` skip it |
59
74
  | an unrecognised entity | applied as a no-op, no defect (a reader behind the writer is normal in a mixed fleet) |
60
75
 
61
76
  `mu sync` suggests `--repair` only for defects a re-read can clear.
@@ -71,8 +86,9 @@ poll outliving a command. It happens because you already run `mu`
71
86
  constantly.
72
87
 
73
88
  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
89
+ through and which is already async. It awaits `ambientIngest` and
90
+ `ambientFlush`; the flush is async because it takes the file lock,
91
+ while most verb bodies are synchronous
76
92
  better-sqlite3 code. One `await` before `fn(db)` and one after cover
77
93
  every verb.
78
94
 
@@ -86,7 +102,8 @@ every verb.
86
102
  `src/sync.ts` holds `ambientIngest`, `ambientFlush`, `ambientSyncPass`,
87
103
  `peerStatuses`, `ingestFromDb` (`mu sync --from <path>`) and
88
104
  `repairPeer` (`mu sync --repair <peer>`, a unique prefix; ambiguity is
89
- exit 4).
105
+ exit 4). A prefix names a machine, so it resets the watermark of the
106
+ peer's segment and of every conflict copy of it.
90
107
 
91
108
  ### Carve-outs
92
109
 
@@ -110,8 +127,13 @@ edge puts that are resolvable now but unprojected. It skips ops whose
110
127
  parent task is gone and keys with a newer `del`, so a deleted edge is
111
128
  never resurrected and an orphan is not retried forever. It runs once
112
129
  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.
130
+ a task in another. Ambient ingest runs it only while SQLite's
131
+ `user_version` is 1. `applyIncomingOp` sets that marker in the same
132
+ transaction whenever an op changes a row or defers (`absent`), whether
133
+ it came from a segment, `--from`, or an SDK caller; the repair clears
134
+ it. A process that dies between
135
+ the two leaves the marker set, so the next invocation runs the repair.
136
+ `mu sync` and `--from` always run it.
115
137
 
116
138
  There is no retry queue. A queue would be a second source of truth and
117
139
  would not survive a short-lived `mu` process when the parent arrives
@@ -30,9 +30,10 @@ Press `?` in the TUI for the keymap.
30
30
  | `index.ts`, `escapes.ts` | `runTui` entry; alt-screen and SGR mouse-mode lifecycle; pure escape bytes |
31
31
  | `app.tsx` | `<App>` root: popup state machine, global keymap, tabs, footer |
32
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 |
33
+ | `keys.ts`, `keymap-spec.ts`, `help.tsx` | pure key dispatch; the keymap source of truth that drives both dispatch and the `?` overlay |
34
34
  | `mouse.ts`, `use-popup-action-queue.ts` | vendored SGR mouse parser with double-click; one queued popup action per render |
35
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
+ | `popup-input.ts` | `usePopupInput`: popup keyboard, off while the help overlay covers the popup |
36
37
  | `titled-box.tsx`, `popup-shell.tsx`, `list-row.tsx`, `padded-rows.tsx`, `status-bar.tsx`, `tab-strip*.ts(x)` | chrome primitives |
37
38
  | `format-helpers.ts`, `agent-display.ts` | shared formatters and agent-row display |
38
39
  | `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 |
@@ -45,9 +46,12 @@ Press `?` in the TUI for the keymap.
45
46
  `<App>` owns:
46
47
 
47
48
  - **Popup state:** `null` (dashboard) or one popup id. One popup at a
48
- time; `Esc` or `q` returns to the dashboard.
49
+ time; `Esc` or `q` returns to the dashboard. `?` over a popup hides
50
+ it (`display="none"`) and turns its keyboard off rather than
51
+ unmounting it, so its cursor, filter and drill survive the overlay.
49
52
  - **Card visibility:** toggled by `0`-`9`.
50
- - **Tick rate:** fast tick, 1s default, adjusted with `+` `-` `=` `0`.
53
+ - **Tick rate:** fast tick, 1s default, adjusted with `+` / `=` and
54
+ `-`. There is no reset key: `0` toggles the Commits card.
51
55
  - **Active workstream tab:** `Tab` / `Shift-Tab` cycles when there are
52
56
  two or more. A torn-down workstream keeps its tab position and
53
57
  renders dimmed with strikethrough until the name is recreated. A
@@ -98,7 +102,7 @@ the right border.
98
102
 
99
103
  Every popup row exposes one canonical `mu` command through `y`.
100
104
  `yank.ts` probes pbcopy, wl-copy, xclip, xsel and clip.exe, then falls
101
- back to OSC-52 over stderr.
105
+ back to OSC-52 written to `/dev/tty`, since ink owns stdout.
102
106
 
103
107
  The handoffs leave the alt screen, disable mouse mode, run a
104
108
  foreground subprocess, and restore both on exit:
@@ -114,16 +118,22 @@ A TUI gesture that mutates state needs a ROADMAP entry first.
114
118
 
115
119
  Mouse support uses SGR mouse mode. `mouse.ts` parses
116
120
  `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.
121
+ A double-click on a card opens its popup. A double-click on a popup row
122
+ emits one `clickRow` action (the 0-based body row) through the action
123
+ queue. The popup maps that row through its own scroll window and
124
+ header strips (`clickedItem` in `popups/scroll.ts`), then focuses and
125
+ drills the item. App cannot do the mapping: only the popup knows its
126
+ window start and strip rows.
120
127
 
121
128
  `keys.ts` holds `dispatchGlobalKey` (dashboard), `dispatchPopupKey`
122
129
  (popup), and `shouldSwallowGlobalKey` (keys a popup consumes).
123
130
 
124
131
  ## Drill recursion
125
132
 
126
- List popups drill with `Enter`. `DrillScrollView`
133
+ List popups drill with `Enter`. Each popup pins the drilled row's
134
+ identity at `Enter` (task, agent, check, commit sha, log seq, track
135
+ goals), so a refresh that reorders or extends the list cannot move an
136
+ open drill to another row. `DrillScrollView`
127
137
  (`popups/drill.tsx`) is the shared scrollable text leaf: Workspaces'
128
138
  git show, Agents' scrollback, Activity log payloads, and Doctor
129
139
  remediation. Task popups drill into `TaskDetailDrill`
@@ -27,7 +27,7 @@ mu picks the backend from the first rule that matches:
27
27
  | Workstream | session `mu-<ws>` | workspace labelled `mu-<ws>` |
28
28
  | Window | window | tab |
29
29
  | Pane id | `%15` | `w1:p1` |
30
- | Attach | `tmux attach -t mu-<ws>` | `herdr session attach mu-<ws>` |
30
+ | Attach | `tmux attach -t mu-<ws>` | `herdr workspace focus <id>`, then `herdr` outside a herdr pane |
31
31
  | pi agent state | control socket | control socket |
32
32
  | Other agents' state | murmur | herdr |
33
33
  | Send to non-pi agents | bracketed paste, then Enter | one atomic `agent prompt` |
@@ -44,8 +44,10 @@ Where each agent's state comes from, per backend:
44
44
  `/proc/<pid>/fd/0`, which macOS does not have.
45
45
  - `mu agent read --lines` cannot recover rows that scrolled off a pane
46
46
  on the alternate screen.
47
- - `MU_<CLI>_COMMAND` overrides apply on tmux only. herdr picks the
48
- agent binary itself.
47
+ - herdr starts the agent binary itself (`herdr agent start --kind
48
+ <cli>`), so spawn refuses `--command` and `MU_<CLI>_COMMAND` with
49
+ exit 2 instead of running a different binary. Unset the variable, or
50
+ spawn with `MU_MUX=tmux`.
49
51
  - herdr creates every workspace detached. Run `herdr workspace focus`
50
52
  yourself.
51
53
 
@@ -22,14 +22,18 @@ mu workstream teardown auth --yes # kills the session, deletes the rows
22
22
  Teardown kills the tmux session first, then deletes the agents, tasks,
23
23
  edges, notes, and workspaces. Running it again is a no-op.
24
24
 
25
- To sweep every workstream that has no tasks, agents, or workspaces,
26
- including `mu-*` tmux sessions with no DB row:
25
+ To sweep every workstream that has no tasks, agents, or workspaces:
27
26
 
28
27
  ```bash
29
28
  mu workstream teardown --empty
30
29
  mu workstream teardown --empty --yes
31
30
  ```
32
31
 
32
+ The sweep skips `scratch`, whose idle session is kept open on purpose.
33
+ It never takes a `mu-*` session that has no workstream row in this DB:
34
+ it may be a live workstream from another DB. The output names
35
+ such sessions; tear one down by name once you know it is unused.
36
+
33
37
  ## Undo a teardown
34
38
 
35
39
  Teardown writes tombstone ops, so the ops log keeps everything. You do
@@ -40,7 +44,8 @@ mu workstream list --torn-down
40
44
  mu undo 7a40e6cc --yes
41
45
  ```
42
46
 
43
- The list shows one row per teardown, newest first. `← recreated since`
47
+ The list shows one row per teardown, newest first. A `--empty` sweep
48
+ writes one row per workstream, so undo each one. `← recreated since`
44
49
  means the name is in use again, so the undo would change nothing.
45
50
  Killed panes and freed workspace directories do not come back.
46
51
 
@@ -95,7 +95,9 @@ Exit codes: `0` met, `5` timeout, `6` pane died. Non-pi agents have no
95
95
  ## Keep a helper
96
96
 
97
97
  Pass `keep: true`, or spawn a named agent in `scratch`. It is created
98
- on first spawn, needs no tasks, and cannot be `init`ed.
98
+ on first spawn, needs no tasks, and cannot be `init`ed. Its `mu-scratch`
99
+ session stays open after the last agent closes; remove it with
100
+ `mu workstream teardown scratch --yes`.
99
101
 
100
102
  ```bash
101
103
  mu agent spawn helper -w scratch
@@ -58,6 +58,10 @@ socket. Pick the mode by what the agent is doing:
58
58
  pastes nothing. `--via mux` pastes into the pane on purpose.
59
59
 
60
60
  After a claim, spawn, or wait, the `Next:` block prints the right send.
61
+ `mu agent spawn --send <text>` spawns and sends the first prompt in one
62
+ call; `--next-free` takes the next free name when `<name>` is taken
63
+ (`worker-1` → `worker-2`; the stem is cut to keep the name within 32
64
+ chars, and it gives up with exit 4 when no free name is near).
61
65
 
62
66
  ### Non-pi agents
63
67
 
@@ -103,9 +107,11 @@ mu workspace refresh worker-1 -w auth
103
107
  ```
104
108
 
105
109
  It rebases onto the remote's main (`origin/HEAD`); without a remote,
106
- pass `--from main`. It refuses on a dirty tree. On a conflict it exits 5
107
- with a `cd` hint. To throw the workspace away instead, run
108
- `mu workspace free worker-1 -w auth`.
110
+ pass `--from main`. It refuses on a dirty tree. On a conflict it exits 5:
111
+ git and sl abort the rebase and leave the workspace as it was, while jj
112
+ keeps the rebase with the conflicts committed in place. After you resolve,
113
+ run refresh again so mu records the new base. To throw the workspace away
114
+ instead, run `mu workspace free worker-1 -w auth`.
109
115
 
110
116
  Claim and send warn when the worker's workspace is 10 or more commits
111
117
  behind main. `--strict-staleness` makes that a refusal (exit 4).
@@ -7,7 +7,7 @@ read the same rules in
7
7
  [recipes/recovery.md](../../skills/mu/recipes/recovery.md).
8
8
 
9
9
  ```bash
10
- mu doctor # exit 0 healthy, 5 on drift
10
+ mu doctor # exit 0 healthy (WARN rows too), 5 on drift or a FAIL row
11
11
  mu doctor --deep # rebuild the ops log and diff every field
12
12
  mu doctor --disk # add per-workspace byte usage
13
13
  ```
@@ -30,6 +30,8 @@ mu agent adopt %15 -w auth
30
30
  mu agent adopt %15 -w auth --name investigator
31
31
  ```
32
32
 
33
+ On herdr, pass its pane id instead (`mu agent adopt w1:p2 -w auth`).
34
+
33
35
  The pane title becomes the agent name. The pane must be in the
34
36
  `mu-<workstream>` session. An adopted pi has no control socket until
35
37
  you restart it with the `MU_CTL_SOCK` path that `adopt` prints.
@@ -86,7 +88,8 @@ mu undo 6380dd3d --yes # apply
86
88
  - Any unique prefix of a group id works.
87
89
  - The undo is its own group, so undoing it again is redo.
88
90
  - If later work changed the same fields, undo exits 4 and names the
89
- conflict. `--force --yes` overrides and discards the newer work.
91
+ conflict. A later write to `updated_at` alone, such as `mu task note`,
92
+ does not count. `--force --yes` overrides and discards the newer work.
90
93
  - Undo restores rows only. Killed panes and freed workspace
91
94
  directories do not come back.
92
95
 
@@ -113,21 +116,29 @@ command.
113
116
  | `ws-rows` | a workspace row whose directory is gone; the next send fails in the VCS |
114
117
  | `ws-dirs` | a workspace directory with no row; it blocks the next `--workspace` spawn with that name |
115
118
  | `ws-empty` | a workstream directory with no checkouts |
116
- | `db-copies` | stray `mu.db*` files that nothing reads |
119
+ | `db-copies` | stray `mu.db*` files that nothing reads, including the `pre-compact` / `pre-forget` backups |
117
120
  | `exports` | output left by the removed export verb |
118
121
  | `locks` | lock directories older than one hour, from a spawn or flush that died |
119
122
 
120
123
  To list workspace orphans with cleanup commands, run
121
- `mu workspace orphans -w auth`. Remove a git-backed orphan with
122
- `git worktree remove --force <path>` from the project root.
124
+ `mu workspace orphans -w auth`. An orphan has no row, so
125
+ `mu workspace free` cannot remove it. Remove a git-backed orphan with
126
+ `git worktree remove --force <path>` from the project root, and any
127
+ other with `rm -rf <path>`.
123
128
 
124
129
  ## Rename a workstream
125
130
 
126
- There is no rename verb. Every child table cascades on the name:
131
+ There is no rename verb, and `mu sql` refuses to change
132
+ `workstreams.name`. The name is part of the key that every op is filed
133
+ under, so a raw rename leaves the ops log on the old name: `mu doctor`
134
+ reports drift, and sync or `mu rebuild` brings the old name back.
135
+ Re-create the workstream instead:
127
136
 
128
137
  ```bash
129
- mu sql "UPDATE workstreams SET name='auth-refactor' WHERE name='auth-refator'"
130
- tmux rename-session -t mu-auth-refator mu-auth-refactor
138
+ mu db backup /tmp/mu-before-rename.db
139
+ mu workstream init auth-refactor
140
+ # re-add the tasks you still need, with -w auth-refactor
141
+ mu workstream teardown auth-refator --yes
131
142
  ```
132
143
 
133
144
  Names start with a lowercase letter, use letters, digits, `_`, or
package/docs/guide/sql.md CHANGED
@@ -20,12 +20,12 @@ mu task list -w auth --json | jq '.count'
20
20
  ```
21
21
 
22
22
  Exit codes: `2` usage error, `3` not found, `4` conflict, `5`
23
- timeout or a multiplexer, VCS, or drift failure, `6` reaped pane,
24
- `7` stall.
23
+ timeout, a locked DB, or a multiplexer, VCS, drift, or `mu doctor`
24
+ FAIL-row failure, `6` reaped pane, `7` stall.
25
25
 
26
26
  ## Read the schema
27
27
 
28
- The schema has 11 tables and 3 views (`ready`, `blocked`, `goals`).
28
+ The schema has 12 tables and 3 views (`ready`, `blocked`, `goals`).
29
29
  Four tables travel between machines: `workstreams`, `tasks`,
30
30
  `task_edges`, and `task_notes`. List them all:
31
31
 
@@ -66,7 +66,14 @@ mu sql "WITH RECURSIVE prereqs(id) AS (
66
66
  ```
67
67
 
68
68
  `mu sql` runs writes too and prints the number of rows changed. Writes
69
- are captured as ops, so `mu undo` can revert them.
69
+ are captured as ops under one `sql.write` group per call, so `mu undo`
70
+ reverts the whole call. `--confirm-rows N` counts rows removed by
71
+ `ON DELETE CASCADE` too: deleting a task with one edge counts 2.
72
+
73
+ `mu sql` refuses to change a natural-key column (`workstreams.name`,
74
+ `tasks.workstream_id`, `tasks.local_id`, a note's task, or an edge's
75
+ endpoints). The ops log files every change under that key, so changing
76
+ it would desync sync, undo, and `mu doctor`. Add a new row instead.
70
77
 
71
78
  ## Pass list flags
72
79
 
@@ -55,7 +55,9 @@ freed with it. If the workspace has edits or commits, close refuses
55
55
  with exit 4. Then do one of these:
56
56
 
57
57
  - Run `mu workspace free worker-1 -w auth --commit` to commit pending
58
- changes and free the workspace, then close.
58
+ changes and free the workspace, then close. For git, if the commits
59
+ are on no branch, mu keeps them on a new branch
60
+ `mu/<workstream>/<agent>-<sha>`.
59
61
  - Run `mu agent close worker-1 -w auth --discard-workspace` to free
60
62
  and close in one step. You lose the changes.
61
63