@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.
- package/AGENTS.md +203 -599
- package/README.md +76 -327
- package/dist/cli.js.map +1 -1
- package/dist/extension/mu-pi.js +178 -45
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +175 -1072
- package/docs/ROADMAP.md +88 -169
- package/docs/VISION.md +205 -415
- package/docs/VOCABULARY.md +202 -466
- package/docs/architecture/control-socket.md +119 -0
- package/docs/architecture/dag.md +113 -0
- package/docs/architecture/mux.md +163 -0
- package/docs/architecture/ops-log.md +140 -0
- package/docs/architecture/sdk.md +185 -0
- package/docs/architecture/sync.md +129 -0
- package/docs/architecture/tui.md +146 -0
- package/docs/guide/README.md +25 -0
- package/docs/guide/backends.md +61 -0
- package/docs/guide/cleanup.md +74 -0
- package/docs/guide/delegate.md +87 -0
- package/docs/guide/dispatch.md +137 -0
- package/docs/guide/getting-started.md +177 -0
- package/docs/guide/recovery.md +133 -0
- package/docs/guide/remote.md +74 -0
- package/docs/guide/sql.md +76 -0
- package/docs/guide/stop-a-worker.md +64 -0
- package/docs/guide/sync.md +78 -0
- package/docs/guide/tui.md +98 -0
- package/docs/guide/upgrade.md +76 -0
- package/docs/history/CHANGELOG-pre-3.md +4866 -0
- package/docs/reference/env.md +87 -0
- package/docs/reference/naming.md +82 -0
- package/docs/specs/2026-09-28-agent-state-from-murmur.md +1 -1
- package/docs/specs/2026-09-29-task-substates.md +1 -1
- package/package.json +1 -1
- package/skills/mu/REMOTE_WORKERS.md +179 -504
- package/skills/mu/SKILL.md +116 -185
- package/docs/HANDOVER.md +0 -486
- package/docs/USAGE_GUIDE.md +0 -2738
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,1090 +1,193 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
3
|
mu is one SQLite file, a crew of agents in multiplexer panes, a task
|
|
4
|
-
DAG, and one append-only op log everything else
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
DAG, and one append-only op log that everything else derives from.
|
|
5
|
+
Callers sit on top, a shared TypeScript core in the middle, and SQLite,
|
|
6
|
+
the multiplexer and the VCS at the base. The CLI verbs and the SDK are
|
|
7
|
+
thin facades over the same core.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- For design rationale, rejected alternatives, and what's on the
|
|
13
|
-
roadmap, see [ROADMAP.md](ROADMAP.md).
|
|
14
|
-
- For principles, see [VISION.md](VISION.md).
|
|
9
|
+
Terms: [VOCABULARY.md](VOCABULARY.md) (source of truth). Principles:
|
|
10
|
+
[VISION.md](VISION.md). Rationale, rejected alternatives and pledges:
|
|
11
|
+
[ROADMAP.md](ROADMAP.md).
|
|
15
12
|
|
|
16
13
|
```
|
|
17
14
|
┌─────────────────────────────────────────────────────────────────┐
|
|
18
|
-
│ Callers
|
|
19
|
-
|
|
20
|
-
│
|
|
21
|
-
|
|
22
|
-
│ └────┬─────┘ └────┬─────┘ └──────┬───────┘ └──────┬──────┘ │
|
|
23
|
-
│ │ │ │ │ │
|
|
24
|
-
└───────┼─────────────┼───────────────┼─────────────────┼─────────┘
|
|
25
|
-
│ in-proc │ subprocess │ subprocess │ in-proc
|
|
26
|
-
▼ ▼ ▼ ▼
|
|
15
|
+
│ Callers: pi shell · bash + jq · pi sub-agent · mu log --tail │
|
|
16
|
+
└───────────────┬─────────────────────────────────┬───────────────┘
|
|
17
|
+
│ subprocess (CLI) │ in-proc (SDK)
|
|
18
|
+
▼ ▼
|
|
27
19
|
┌─────────────────────────────────────────────────────────────────┐
|
|
28
20
|
│ mu core (shared TS modules) │
|
|
29
|
-
│
|
|
30
|
-
│
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
│ │ sources │ │ │ │ │ │ │ │
|
|
36
|
-
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────────┬────────┘ │
|
|
37
|
-
└───────┼─────────────┼─────────────┼─────────────────┼───────────┘
|
|
38
|
-
▼ ▼ ▼ ▼
|
|
39
|
-
┌─────────────────────────────────────────────────────────────────┐
|
|
40
|
-
│ Substrates │
|
|
41
|
-
│ SQLite (~/.local/state/mu/mu.db) · tmux/herdr panes · jj/sl/git│
|
|
42
|
-
└─────────────────────────────────────────────────────────────────┘
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## The ops log (read this first)
|
|
46
|
-
|
|
47
|
-
Everything below rests on one append-only table. Read this section
|
|
48
|
-
before the rest of the file.
|
|
49
|
-
|
|
50
|
-
**Capture is a trigger, not a call site.** Every INSERT / UPDATE /
|
|
51
|
-
DELETE on a portable table (`workstreams`, `tasks`, `task_edges`,
|
|
52
|
-
`task_notes`) is recorded as an **op** by a SQLite trigger, in the
|
|
53
|
-
SAME transaction as the mutation. No SDK function decides to record
|
|
54
|
-
history, so no call site can forget to, and the log cannot drift from
|
|
55
|
-
the data — not even on power loss.
|
|
56
|
-
|
|
57
|
-
**The log is the record; the tables are the materialized view.**
|
|
58
|
-
Reads hit the tables, as ordinary indexed SQL. This is not
|
|
59
|
-
event-sourcing-on-read: nothing replays the log to answer a query.
|
|
60
|
-
The log is what the tables are *derived from* and what they can be
|
|
61
|
-
rebuilt from.
|
|
62
|
-
|
|
63
|
-
```
|
|
64
|
-
mu task close t3
|
|
65
|
-
│
|
|
66
|
-
▼
|
|
67
|
-
UPDATE tasks SET status=... ──┬──► tables (what reads see)
|
|
68
|
-
└──► ops (what everything else reads)
|
|
69
|
-
one transaction
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
**Four features are queries or replays over that one log:**
|
|
73
|
-
|
|
74
|
-
- **history** — `mu log` is a typed reader over `ops`.
|
|
75
|
-
- **undo** — `mu undo <group>` emits the inverse ops for one group.
|
|
76
|
-
- **sync** — a machine appends its own ops to a JSONL segment in a
|
|
77
|
-
shared folder and applies each peer's from a watermark.
|
|
78
|
-
|
|
79
|
-
**Ops are semantic partial updates.** An UPDATE op carries only the
|
|
80
|
-
columns that actually changed. That is what makes per-field merge
|
|
81
|
-
free: two machines editing different fields of one task both keep
|
|
82
|
-
their edit, with no extra bookkeeping. A full-row payload would
|
|
83
|
-
silently regress this to row-level last-writer-wins.
|
|
84
|
-
|
|
85
|
-
**The cost, plainly:** one mechanism means one capture bug breaks
|
|
86
|
-
history, undo and sync at once, silently. That is why
|
|
87
|
-
`mu doctor --deep` rebuilds the log into a temp DB and diffs it
|
|
88
|
-
field-by-field against the live tables, and why the cheap tier
|
|
89
|
-
(every live row has at least one op naming it) runs by default.
|
|
90
|
-
|
|
91
|
-
Ordering across machines comes from the **HLC** on every op — a
|
|
92
|
-
hybrid logical clock serialized so that bytewise `ORDER BY hlc` is
|
|
93
|
-
causal order. Machine-local tables (`agents`, `vcs_workspaces`) have
|
|
94
|
-
no capture triggers: their contents are pane ids and absolute paths,
|
|
95
|
-
which mean nothing on another machine. Their changes are recorded as
|
|
96
|
-
log-only events via `emitEvent`.
|
|
97
|
-
|
|
98
|
-
Per-module detail is in [§ Modules](#modules-actual-src-layout).
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## The task DAG
|
|
103
|
-
|
|
104
|
-
mu's coordination model is a **directed acyclic graph of tasks**. It
|
|
105
|
-
is the central organizing primitive, not a sidecar feature: without
|
|
106
|
-
it mu is just an agent runner.
|
|
107
|
-
|
|
108
|
-
### Model
|
|
109
|
-
|
|
110
|
-
- **Tasks** are nodes with mandatory `impact (1-100)` and `effort_days`.
|
|
111
|
-
`ROI = impact / effort` drives prioritization.
|
|
112
|
-
- **One edge type**: `blocks`. `A → B` means A must close before B can
|
|
113
|
-
start. Multiple edge types create ambiguity that defeats the purpose.
|
|
114
|
-
- **Status lifecycle**: `OPEN → IN_PROGRESS → CLOSED`. Status alone
|
|
115
|
-
decides edge satisfaction: any `CLOSED` task satisfies its blockers.
|
|
116
|
-
- **Substate** qualifies status and never touches edges:
|
|
117
|
-
`OPEN/todo|parked`, `IN_PROGRESS/active`,
|
|
118
|
-
`CLOSED/done|rejected|wontfix|duplicate|superseded`. Rule: **store intent,
|
|
119
|
-
derive graph facts** — ready and blocked stay derived from edges.
|
|
120
|
-
`OPEN/parked` is excluded from `ready` but stays in `goals`.
|
|
121
|
-
|
|
122
|
-
#### Substate integrity: deferred FK + apply-time pair repair
|
|
123
|
-
|
|
124
|
-
The legal pairs live in the `task_substates` lookup table, seeded from
|
|
125
|
-
code on every open (machine-local, never synced). `tasks` has a
|
|
126
|
-
composite FK `(status, substate) → task_substates`,
|
|
127
|
-
`DEFERRABLE INITIALLY DEFERRED`, which is the only guard on the pair.
|
|
128
|
-
It is checked at COMMIT, so apply's one-field-at-a-time UPDATEs pass as
|
|
129
|
-
long as the pair is legal when the transaction ends. That is why
|
|
130
|
-
`applyOp` wraps each task put in `db.transaction` (and undo already runs
|
|
131
|
-
in one): in autocommit the first per-field UPDATE would fail the FK.
|
|
132
|
-
|
|
133
|
-
Every lifecycle write sets `status` and `substate` in one UPDATE, so
|
|
134
|
-
one op carries both under one HLC. A pair can still split across ops:
|
|
135
|
-
a status-only op from a v10 peer, an unknown substate from a newer
|
|
136
|
-
peer, or a concurrent close and park on two machines. Before commit,
|
|
137
|
-
`repairTaskPair` (`src/apply.ts`) sets the substate to the one implied
|
|
138
|
-
by the newest op in the LOG that writes a substate, resolved against
|
|
139
|
-
the row's current status, and falls back to the status default when
|
|
140
|
-
that pair is illegal. Substate is therefore NOT per-field LWW.
|
|
141
|
-
Reading the log instead of the row makes the result a pure function of
|
|
142
|
-
the op set plus the row's status, so every peer converges on the same
|
|
143
|
-
row whatever the arrival order. For the same reason the repair records
|
|
144
|
-
**no op**: each peer computes the identical repair from the identical
|
|
145
|
-
log. Recording one would only add a second writer racing the first.
|
|
146
|
-
- **Notes** are append-only per task; survive across LLM sessions and
|
|
147
|
-
agent restarts. The fix for context loss at the *task* level rather
|
|
148
|
-
than the agent level.
|
|
149
|
-
|
|
150
|
-
### Built-in queries (SQL views)
|
|
151
|
-
|
|
152
|
-
| View | Returns |
|
|
153
|
-
| --------- | ---------------------------------------------------------------------- |
|
|
154
|
-
| `ready` | `OPEN/todo` tasks with no unresolved blockers — work that can start *now* |
|
|
155
|
-
| `blocked` | OPEN tasks waiting on something |
|
|
156
|
-
| `goals` | Tasks with no dependents — graph endpoints |
|
|
157
|
-
|
|
158
|
-
Agents and humans both query these views directly via `mu sql`. No
|
|
159
|
-
separate query layer.
|
|
160
|
-
|
|
161
|
-
### Parallel-track detection
|
|
162
|
-
|
|
163
|
-
The Tracks section in `mu state` / bare `mu` runs union-find on the
|
|
164
|
-
graph to identify independent subtrees that can be assigned to
|
|
165
|
-
different agents in parallel.
|
|
166
|
-
|
|
167
|
-
**Diamond patterns get merged automatically.** If two roots share a
|
|
168
|
-
prerequisite, they collapse into one track — preventing two agents
|
|
169
|
-
from colliding on the shared dependency:
|
|
170
|
-
|
|
171
|
-
```
|
|
172
|
-
Independent (2 tracks): Diamond (1 merged track):
|
|
173
|
-
|
|
174
|
-
goal_a goal_b goal_a goal_b ← Spawn 2 agents
|
|
175
|
-
| | \ /
|
|
176
|
-
task_a task_b shared ← Spawn 1 (would
|
|
177
|
-
| | | collide otherwise)
|
|
178
|
-
leaf_a leaf_b leaf
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
The graph algorithm decides, not the LLM.
|
|
182
|
-
|
|
183
|
-
### Claim protocol via ambient identity
|
|
184
|
-
|
|
185
|
-
`mu task claim <task>` resolves who is claiming, then atomically:
|
|
186
|
-
|
|
187
|
-
1. Sets `tasks.owner = <agent_name>`
|
|
188
|
-
2. Flips `tasks.status = IN_PROGRESS`
|
|
189
|
-
3. Emits a `task.claim` op carrying the resolved `actor`
|
|
190
|
-
|
|
191
|
-
Identity resolution is a two-rung ladder:
|
|
192
|
-
|
|
193
|
-
1. **`$MU_AGENT_NAME`** — injected into the pane's environment at
|
|
194
|
-
spawn. Backend-independent, and more reliable than scraping even on
|
|
195
|
-
tmux.
|
|
196
|
-
2. **`backend.currentAgentName()`** — the mux backend's fallback. On
|
|
197
|
-
tmux that is `display-message -p '#{pane_title}'`; on herdr the
|
|
198
|
-
agent name is registered with the mux and looked up by pane id.
|
|
199
|
-
|
|
200
|
-
The fallback earns its place because **adopted** panes
|
|
201
|
-
(`mu agent adopt`) predate the env injection and carry only a title.
|
|
202
|
-
Because it lives behind `MuxBackend`, the call site has no idea which
|
|
203
|
-
multiplexer it is running under.
|
|
204
|
-
|
|
205
|
-
When the tmux fallback runs, it reads pane title, **not** `#W` (window
|
|
206
|
-
name). Window names come from the `tab:` frontmatter and may group
|
|
207
|
-
multiple agents in one window.
|
|
208
|
-
|
|
209
|
-
Two agents can't claim the same task — atomic CAS in SQLite. The
|
|
210
|
-
agent doesn't have to know its own name.
|
|
211
|
-
|
|
212
|
-
### Scoped subtree views
|
|
213
|
-
|
|
214
|
-
`mu task tree <id>` and task queries show the portion of the graph
|
|
215
|
-
reachable from a task. Subtree scoping is a `WHERE` clause, which is
|
|
216
|
-
what makes recursive delegation work: a sub-orchestrator inspects its
|
|
217
|
-
slice without an LLM inferring the scope.
|
|
218
|
-
|
|
219
|
-
---
|
|
220
|
-
|
|
221
|
-
## Mux session topology
|
|
222
|
-
|
|
223
|
-
mu organizes agents into **one mux session per workstream**. One mu
|
|
224
|
-
workstream = one mux session = one `session_id` partition in
|
|
225
|
-
`~/.local/state/mu/mu.db`. Multiple workstreams on one machine coexist as
|
|
226
|
-
independent mux sessions, fully isolated.
|
|
227
|
-
|
|
228
|
-
A **mux session** is a tmux session on the tmux backend and a herdr
|
|
229
|
-
*workspace* on the herdr backend (herdr's own "session" is
|
|
230
|
-
server-level — one socket — and is the wrong granularity). The
|
|
231
|
-
three-level shape is identical either way:
|
|
232
|
-
|
|
233
|
-
| mu term | tmux | herdr |
|
|
234
|
-
| ---------------- | ------------ | ----------------- |
|
|
235
|
-
| **mux session** | session | workspace |
|
|
236
|
-
| **window** | window | tab |
|
|
237
|
-
| **pane** | pane (`%15`) | pane (`w1:p1`) |
|
|
238
|
-
|
|
239
|
-
The diagram below uses the tmux spelling.
|
|
240
|
-
|
|
241
|
-
```
|
|
242
|
-
tmux session: mu-auth-refactor (one mu workstream)
|
|
243
|
-
┌───────────────────────────────────────────────────────────┐
|
|
244
|
-
│ Window: Backend Window: Review │
|
|
245
|
-
│ ┌──────────┐ ┌──────────┐ ┌────────────────────────┐ │
|
|
246
|
-
│ │ worker-1 │ │ worker-2 │ │ reviewer-1 │ │
|
|
247
|
-
│ │ (pi) │ │ (pi) │ │ (pi, role=read-only) │ │
|
|
248
|
-
│ └──────────┘ └──────────┘ └────────────────────────┘ │
|
|
249
|
-
│ │
|
|
250
|
-
│ Window: mu-orchestrator │
|
|
251
|
-
│ ┌────────────────────────────────────────────────────┐ │
|
|
252
|
-
│ │ pi (you, with mu extension loaded) │ │
|
|
253
|
-
│ └────────────────────────────────────────────────────┘ │
|
|
254
|
-
└───────────────────────────────────────────────────────────┘
|
|
255
|
-
|
|
256
|
-
tmux session: mu-migration-2024q4 (different workstream)
|
|
257
|
-
┌───────────────────────────────────────────────────────────┐
|
|
258
|
-
│ ...different agents, different graph, no overlap │
|
|
259
|
-
└───────────────────────────────────────────────────────────┘
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
### Concretely
|
|
263
|
-
|
|
264
|
-
- **First `mu agent spawn` creates the tmux session** if you're not already
|
|
265
|
-
in one. Default name `mu-<auto>`. Override with `mu workstream init <name>` or
|
|
266
|
-
`MU_SESSION=<name>`.
|
|
267
|
-
- **Subsequent operations** in the same shell (or any child shell with
|
|
268
|
-
`MU_SESSION_ID` set) target the same session.
|
|
269
|
-
- **`tmux attach -t mu-<workstream>`** → attach to the whole
|
|
270
|
-
workstream's mux session (herdr: `herdr workspace focus <id>`)
|
|
271
|
-
- **`mu agent list`** is scoped to one workstream — the current one by
|
|
272
|
-
default; `mu agent list -w <workstream>` for another
|
|
273
|
-
- **`session_id`** is the partition key on the `agents` table
|
|
274
|
-
- **`mu doctor`** warns about cross-session pollution (orphan panes,
|
|
275
|
-
ghost rows, agents whose mux session no longer exists)
|
|
276
|
-
|
|
277
|
-
### The `activeMux()` seam
|
|
278
|
-
|
|
279
|
-
Every call site reaches its multiplexer through
|
|
280
|
-
`(await activeMux()).<method>()`. Nothing outside `src/mux/` names a
|
|
281
|
-
backend, and `MU_MUX` is therefore load-bearing: an unknown value
|
|
282
|
-
fails the invocation rather than quietly running on tmux.
|
|
283
|
-
|
|
284
|
-
Call sites are classified, and the classification is the seam's real
|
|
285
|
-
content. With a second backend in play, "no reachable multiplexer"
|
|
286
|
-
stops being a broken-box edge case and becomes routine, so each site
|
|
287
|
-
has to have already decided what it means:
|
|
288
|
-
|
|
289
|
-
| | Behaviour on `NoMultiplexerError` | Examples |
|
|
290
|
-
| --- | --- | --- |
|
|
291
|
-
| **Load-bearing** | propagate; `handle()` maps it to exit 5 | spawn, send, read, kill, adopt, kick, reconcile, session create/destroy |
|
|
292
|
-
| **Best-effort** | `try`/`catch`, degrade | actor identity, pane titles, pane borders, workstream listings, liveness polls, TUI attach |
|
|
293
|
-
|
|
294
|
-
`resolveWorkerIdentity()` in `src/tasks/claim.ts` is the canonical
|
|
295
|
-
best-effort shape. `reconcile()` is pointedly load-bearing: treating
|
|
296
|
-
an unreachable mux as "zero panes exist" would prune every registered
|
|
297
|
-
agent as a ghost and reap its in-progress tasks.
|
|
298
|
-
|
|
299
|
-
Three things that look like caller concerns belong to the backend,
|
|
300
|
-
because each would otherwise hardcode a tmux string in a place a herdr
|
|
301
|
-
user can see: `attachHint()` / `attachCommands()` (the printed recipe
|
|
302
|
-
and the TUI's executed argv), `healthCheck()` (version + ambient env
|
|
303
|
-
facts as DATA — `mu doctor` owns rendering), and
|
|
304
|
-
`paneNotFoundNextSteps()` (borrowed by `PaneNotFoundError` from the
|
|
305
|
-
backend that raised it).
|
|
306
|
-
|
|
307
|
-
`src/tmux.ts` survives as a re-export for genuinely tmux-only
|
|
308
|
-
concerns: the `MU_TMUX_SOCKET` test-isolation seam and the shared
|
|
309
|
-
`sleep` / `setSleepForTests` poll seam.
|
|
310
|
-
|
|
311
|
-
### The spawn seam: create-and-run vs create-then-start
|
|
312
|
-
|
|
313
|
-
Backends disagree on something structural: tmux creates a pane **and**
|
|
314
|
-
runs a command in one atomic call, while herdr has no create-and-run
|
|
315
|
-
form at all. Its creation verbs always start a plain shell, and `agent
|
|
316
|
-
start` requires an already-existing pane at its interactive prompt.
|
|
317
|
-
|
|
318
|
-
That difference is expressed as a **capability**, not a name check:
|
|
319
|
-
|
|
320
|
-
| | tmux | herdr |
|
|
321
|
-
| --- | --- | --- |
|
|
322
|
-
| `NewWindowOptions.command` etc. | carries the command | **refused** if non-empty |
|
|
323
|
-
| `startAgentInPane()` | not implemented | implemented |
|
|
324
|
-
| liveness / readiness wait | mu checks pane life, startup errors, then murmur readiness when available | the mux blocks until ready |
|
|
325
|
-
|
|
326
|
-
`spawnAgent` branches on `mux.startAgentInPane !== undefined`. When it
|
|
327
|
-
is absent (tmux), the command rides along on the creation verb. The
|
|
328
|
-
liveness check verifies that the pane survived and scans for startup
|
|
329
|
-
errors; when murmur is available, readiness also waits for the pane's
|
|
330
|
-
`@murmur_pane_state`. Without murmur, readiness is skipped and the first
|
|
331
|
-
send uses its input-timing checks. When `startAgentInPane` is present,
|
|
332
|
-
mu creates the pane bare, calls it, and skips the tmux checks because
|
|
333
|
-
the backend returns only after the agent is ready.
|
|
334
|
-
|
|
335
|
-
The creation verbs **refuse** a non-empty command on a backend that
|
|
336
|
-
cannot honour it rather than dropping it silently: a dropped command
|
|
337
|
-
leaves an empty shell mu believes hosts an agent, which is the worst
|
|
338
|
-
available failure mode. Both the refusal and every step-2 failure route
|
|
339
|
-
through `rollbackSpawn`, so no path records an agent row for a pane with
|
|
340
|
-
nothing running in it.
|
|
341
|
-
|
|
342
|
-
Both the pane-creation call and the agent start sit **outside** the
|
|
343
|
-
per-session spawn lock's slow half, so a parallel fan-out still
|
|
344
|
-
parallelises.
|
|
345
|
-
|
|
346
|
-
### Window vs pane
|
|
347
|
-
|
|
348
|
-
By default each agent gets its own **window** (a tmux window, a herdr
|
|
349
|
-
tab — what most terminals call a "tab"), named after the agent's
|
|
350
|
-
`tab:` value; default, the agent name. Agents sharing a `tab:` value
|
|
351
|
-
share a window with multiple panes.
|
|
352
|
-
|
|
353
|
-
Claim/identity depends on the agent's name, not the window name:
|
|
354
|
-
every agent pane gets `$MU_AGENT_NAME` in its environment on spawn,
|
|
355
|
-
plus a **pane title** as fallback, regardless of how panes are grouped.
|
|
356
|
-
The title carries durable mu context only (agent identity plus owned tasks),
|
|
357
|
-
not sampled runtime status. Mu enables the top border but leaves
|
|
358
|
-
`pane-border-format` to the user's tmux configuration. The canonical tmux
|
|
359
|
-
protocol is in the comment block at the top of `src/mux/tmux.ts`.
|
|
360
|
-
|
|
361
|
-
### Why one session per workstream
|
|
362
|
-
|
|
363
|
-
- **Visual co-location.** `tmux a -t mu-auth-refactor` shows the whole
|
|
364
|
-
crew at once.
|
|
365
|
-
- **Isolation.** Kill the tmux session = kill the workstream. No
|
|
366
|
-
leaked panes.
|
|
367
|
-
- **Detach and reattach freely.** The crew survives a closed laptop.
|
|
368
|
-
- **The claim protocol falls out.** Pane title = agent name =
|
|
369
|
-
ownership identity, with no configuration.
|
|
370
|
-
- **Multiple workstreams coexist.** `session_id` partitioning keeps
|
|
371
|
-
the auth-refactor crew out of the migration crew.
|
|
372
|
-
|
|
373
|
-
---
|
|
374
|
-
|
|
375
|
-
## Dual-audience CLI contract
|
|
376
|
-
|
|
377
|
-
The top-level `mu` binary serves two audiences without creating a
|
|
378
|
-
second namespace.
|
|
379
|
-
|
|
380
|
-
- **Human entrypoint:** bare `mu` launches the read-only TUI when
|
|
381
|
-
`process.stdout.isTTY === true`. It loads every workstream on the
|
|
382
|
-
machine and chooses the initial active tab with the shared focus
|
|
383
|
-
ladder (`$MU_SESSION` → tmux session name → cwd inside a workspace
|
|
384
|
-
→ cwd equal to a workspace's VCS-derived project root, with latest
|
|
385
|
-
activity breaking project-root ties → tab 0). If no
|
|
386
|
-
workstreams exist, it prints `mu --help` plus the one-paste
|
|
387
|
-
`Get started: mu workstream init <name>` hint and exits 0.
|
|
388
|
-
- **Agent / script entrypoint:** typed verbs remain the API, with
|
|
389
|
-
`--json` on reads and structured errors. Bare `mu` on non-TTY
|
|
390
|
-
stdout (pipes, redirects, CI, most harnessed agent calls) prints
|
|
391
|
-
help instead of entering Ink. `MU_NO_TUI=1` forces that same path
|
|
392
|
-
for scripted use inside an otherwise-interactive terminal.
|
|
393
|
-
- **`mu state`** is the static state card; `mu state --tui` is the
|
|
394
|
-
explicit TUI selector. The split is stdout-is-TTY plus the opt-out
|
|
395
|
-
env var, not a second command namespace.
|
|
396
|
-
|
|
397
|
-
The TUI import stays dynamic (`await import("./cli/tui/index.js")`).
|
|
398
|
-
No module outside `src/cli/tui/` may statically import ink/react, or
|
|
399
|
-
the static CLI bundle pulls the TUI graph into help/version/json
|
|
400
|
-
paths.
|
|
401
|
-
|
|
402
|
-
---
|
|
403
|
-
|
|
404
|
-
## TUI architecture
|
|
405
|
-
|
|
406
|
-
The TUI is a 10-card live-updating dashboard on `ink`. It is
|
|
407
|
-
**read-only** and lives entirely under `src/cli/tui/` — CLI verbs
|
|
408
|
-
remain the canonical mutation API. The TUI yanks `mu` commands; the
|
|
409
|
-
operator runs them.
|
|
410
|
-
|
|
411
|
-
### Cluster shape
|
|
412
|
-
|
|
413
|
-
`src/cli/tui/` is the only place ink/react are imported.
|
|
414
|
-
|
|
415
|
-
```
|
|
416
|
-
src/cli/tui/
|
|
417
|
-
├── index.ts # runTui entrypoint; alt-screen + mouse-mode lifecycle
|
|
418
|
-
├── escapes.ts # pure ANSI byte sequences (alt-screen, SGR mouse mode)
|
|
419
|
-
├── app.tsx # <App> root: popup state machine, global keymap, tabs
|
|
420
|
-
├── state.ts # useDashboardSnapshot poll-loop hook (fast/slow tier split)
|
|
421
|
-
├── keys.ts # pure dispatchGlobalKey + dispatchPopupKey + shouldSwallowGlobalKey
|
|
422
|
-
├── keymap-spec.ts # canonical keymap source-of-truth (drives help overlay + dispatch)
|
|
423
|
-
├── mouse.ts # vendored SGR mouse parser + double-click + useMouse hook
|
|
424
|
-
├── yank.ts # clipboard probe + write (pbcopy/wl-copy/xclip/xsel/clip.exe + OSC-52)
|
|
425
|
-
├── tuicr.ts # `t` shortcut: alt-screen handoff to tuicr -r <sha>
|
|
426
|
-
├── layout.ts # responsive multi-column dashboard + per-card row budgets
|
|
427
|
-
├── columns.ts # column-aligned row layout with protect/clip clipping
|
|
428
|
-
├── wrap-ansi.ts # ANSI-aware visual-width line wrapper + SGR close-on-end
|
|
429
|
-
├── format-helpers.ts # shared TUI formatters (relTime, sinceClaim, ROI)
|
|
430
|
-
├── titled-box.tsx # rounded border with section-header / bottomLabel inset
|
|
431
|
-
├── popup-shell.tsx # popup outer chrome (cyan TitledBox)
|
|
432
|
-
├── list-row.tsx # centralised non-selected row primitive
|
|
433
|
-
├── padded-rows.tsx # per-card body padder
|
|
434
|
-
├── status-bar.tsx # bottom status bar (mode + active ws + tick + footer flash)
|
|
435
|
-
├── tab-strip.tsx # multi-workstream tab switcher (N≥2)
|
|
436
|
-
├── tab-strip-layout.ts # pure window-around-active layout helper
|
|
437
|
-
├── help.tsx # ?/F1 keymap overlay (scrollable on short panes)
|
|
438
|
-
├── use-popup-filter.tsx # shared '/' substring filter hook + applyFilter + FilterPrompt
|
|
439
|
-
├── use-status-filter.tsx # status toggles (o/i/c) + substate toggles (p/w) for task-list popups
|
|
440
|
-
├── use-notes-drill.ts # shared notes-drill memo (5 task popups consume it)
|
|
441
|
-
├── use-popup-action-queue.ts # consume mouse PopupAction queue once per render
|
|
442
|
-
├── cards/ # 10 dashboard glance cards (one slot each)
|
|
443
|
-
│ ├── _placeholder.tsx # shared loading/empty body wrapper
|
|
444
|
-
│ └── {agents,tracks,ready,log,workspaces,inprogress,blocked,recent,commits,doctor}.tsx
|
|
445
|
-
└── popups/ # fullscreen drill-down popups
|
|
446
|
-
├── {agents,tracks,ready,log,workspaces,inprogress,blocked,recent,commits,doctor}.tsx
|
|
447
|
-
├── dag.tsx # keybind-only on `g`: full task DAG forest
|
|
448
|
-
├── all-tasks.tsx # keybind-only on `t`: sortable / filterable list of every task
|
|
449
|
-
├── task-list-popup.tsx # shared TaskListPopup scaffold (ready/inprogress/recent/blocked config-only)
|
|
450
|
-
├── drill.tsx # DrillScrollView + useDrillKeymap (shared scrollable-text leaf)
|
|
451
|
-
├── task-detail.tsx # TaskDetailDrill (notes timeline; the recursion sink)
|
|
452
|
-
├── cursor-row.tsx # selected-row primitive (delegated to from list-row)
|
|
453
|
-
├── scroll.ts # pure applyCursor / applyScroll / clampScrollTop / isNavAction
|
|
454
|
-
├── viewport.ts # popupViewport + POPUP_CHROME_ROWS + POPUP_VIEWPORT_FLOOR
|
|
455
|
-
└── show-loader.ts # subprocess-preserving show loader (avoids blank-flash mid-refetch)
|
|
21
|
+
│ agents/ mux/ ctl/ · tasks/ tracks · vcs/ workspace/ · ops log │
|
|
22
|
+
│ (capture, apply, undo, sync, drift) │
|
|
23
|
+
└───────────────┬──────────────────┬──────────────────┬───────────┘
|
|
24
|
+
▼ ▼ ▼
|
|
25
|
+
SQLite (~/.local/state/mu/mu.db) · tmux/herdr panes · jj/sl/git
|
|
26
|
+
pi agents: control socket per agent
|
|
456
27
|
```
|
|
457
28
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
The
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
`
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
Text rendering is ANSI-aware: `wrap-ansi.ts` wraps by visual width
|
|
521
|
-
(`string-width`) and closes open SGR state on both the early-return
|
|
522
|
-
and end-of-loop paths, so coloured fragments without a trailing
|
|
523
|
-
`\x1b[0m` can't bleed into adjacent ink chrome. Drill bodies are
|
|
524
|
-
space-padded to exact box width, or ink's `wrap="truncate"` ANSI
|
|
525
|
-
miscount eats the right-border glyph.
|
|
526
|
-
|
|
527
|
-
### Read-only invariant + the `tuicr` escape
|
|
528
|
-
|
|
529
|
-
Every popup row exposes one canonical `mu` command via `y`. `yank.ts`
|
|
530
|
-
probes for a clipboard backend (pbcopy / wl-copy / xclip / xsel /
|
|
531
|
-
clip.exe) and falls back to OSC-52 over stderr if none is found.
|
|
532
|
-
The command goes to the clipboard; the operator runs it.
|
|
533
|
-
|
|
534
|
-
The one escape is `t` inside any `git show` drill: `tuicr.ts` writes
|
|
535
|
-
`ALT_SCREEN_EXIT` + the mouse-mode disable bytes, exec's
|
|
536
|
-
`tuicr -r <sha>` in the project root / workspace cwd as a foreground
|
|
537
|
-
subprocess, then restores both on exit. A handoff, not an in-process
|
|
538
|
-
mutation.
|
|
539
|
-
|
|
540
|
-
The read-only pledge is in `docs/ROADMAP.md`'s anti-feature list; a
|
|
541
|
-
TUI gesture that wants to mutate state needs a roadmap entry first.
|
|
542
|
-
|
|
543
|
-
### Mouse + keyboard
|
|
544
|
-
|
|
545
|
-
Mouse support is opt-in via SGR mouse mode (`escapes.ts` provides
|
|
546
|
-
the enable/disable bytes). `mouse.ts` parses `ESC[<button;x;y;M/m`
|
|
547
|
-
from stdin, detects double-clicks, and exposes a `useMouse()` hook.
|
|
548
|
-
|
|
549
|
-
Keyboard dispatch flows through pure helpers in `keys.ts`:
|
|
550
|
-
`dispatchGlobalKey` (dashboard mode), `dispatchPopupKey` (popup
|
|
551
|
-
mode), and `shouldSwallowGlobalKey` (which keys popups consume
|
|
552
|
-
and do not bubble to the global dispatcher). The keymap source-of-
|
|
553
|
-
truth lives in `keymap-spec.ts` so the help overlay and the
|
|
554
|
-
dispatcher can never drift apart.
|
|
555
|
-
|
|
556
|
-
Double-click on a card emits `{kind: "setCursor", index}` followed
|
|
557
|
-
by `{kind: "drill"}` through `use-popup-action-queue.ts`, which
|
|
558
|
-
consumes one action per render (so the cursor update lands before
|
|
559
|
-
the drill resolves the focused row).
|
|
560
|
-
|
|
561
|
-
### Drill recursion
|
|
562
|
-
|
|
563
|
-
List popups drill via `Enter` into entity-specific leaves. The
|
|
564
|
-
central primitive is `popups/drill.tsx`'s `DrillScrollView` (a
|
|
565
|
-
scrollable text leaf shared by Workspaces' git-show, Agents'
|
|
566
|
-
scrollback, the Activity log payload drill, and the Doctor
|
|
567
|
-
remediation drill). Task popups drill into
|
|
568
|
-
`popups/task-detail.tsx`'s `TaskDetailDrill` (the notes timeline);
|
|
569
|
-
the Tracks popup chains track → task list → TaskDetailDrill via
|
|
570
|
-
the same leaf.
|
|
571
|
-
|
|
572
|
-
`useDrillKeymap` owns the scroll state, takes an optional `resetKey`
|
|
573
|
-
(identity change resets scroll; a tick-driven body refresh preserves
|
|
574
|
-
it) and an optional `onScrollChange` (the DAG popup's focused-root
|
|
575
|
-
tracking), and shares ANSI-aware wrapped body metadata so the
|
|
576
|
-
scroll-clamp math and the painter can't desync.
|
|
577
|
-
|
|
578
|
-
Subprocess-backed drills (Workspaces git-show, Agents scrollback,
|
|
579
|
-
Commits show) use `popups/show-loader.ts`, which preserves the prior
|
|
580
|
-
body during a refetch — no blank flash on the slow tick.
|
|
581
|
-
|
|
582
|
-
### Test seam
|
|
583
|
-
|
|
584
|
-
See `test/README.md`. The seam is `test/_ink-render.ts`'s
|
|
585
|
-
`createInkInputStream` + `createInkCaptureStream` + `simulateInput` +
|
|
586
|
-
`latestRenderedFrame`: mount a popup or `<App>` into a CaptureStream,
|
|
587
|
-
drive keystrokes, assert against the visible frame and spy callbacks.
|
|
588
|
-
Source-greps are for narrow structural guards only (App ↔ keys ↔
|
|
589
|
-
layout wiring; slot ↔ keymap glue), never behaviour.
|
|
590
|
-
|
|
591
|
-
---
|
|
592
|
-
|
|
593
|
-
## CLI / SDK surface
|
|
594
|
-
|
|
595
|
-
Every user-visible operation is a typed SDK function plus a thin
|
|
596
|
-
Commander wrapper. `src/cli.ts` and the verb-namespace files under
|
|
597
|
-
`src/cli/` are the canonical verb surface — no generated registry, no
|
|
598
|
-
DSL, no separate operation schema. Programmatic callers import the
|
|
599
|
-
same SDK functions from `src/index.ts`; agents and scripts compose
|
|
600
|
-
CLI verbs with `--json`.
|
|
601
|
-
|
|
602
|
-
The boundary rule: external surfaces accept operator-facing names
|
|
603
|
-
(workstream, task id, agent name); internal helpers resolve those to
|
|
604
|
-
surrogate INTEGER ids once and then stay on ids. See
|
|
605
|
-
[§ Surrogate-PK + SDK-boundary discipline](#surrogate-pk--sdk-boundary-discipline).
|
|
606
|
-
|
|
607
|
-
---
|
|
608
|
-
|
|
609
|
-
## Reconciliation
|
|
610
|
-
|
|
611
|
-
`mu agent list` always reconciles the registry against mux reality before
|
|
612
|
-
returning. Two steps, in order:
|
|
613
|
-
|
|
614
|
-
1. **Prune ghosts.** For each `agents` row, if its `pane_id` no longer
|
|
615
|
-
exists in the mux, delete the row.
|
|
616
|
-
2. **Surface orphans.** For each pane in the workstream's mux session
|
|
617
|
-
that has no matching `agents` row but whose pane title looks like
|
|
618
|
-
an agent name, add it to the orphans list. **Do not auto-adopt** —
|
|
619
|
-
`mu agent list` shows orphans under a separate "(orphans)" section and
|
|
620
|
-
the user runs `mu agent adopt %15 [--name X]` to claim them.
|
|
621
|
-
|
|
622
|
-
`src/reconcile.ts` is the only implementation. Key properties:
|
|
623
|
-
|
|
624
|
-
- **Reality wins**: the mux is the source of truth for what panes
|
|
625
|
-
exist. Reconciliation closes the gap on every `mu agent list`.
|
|
626
|
-
- **Agent state is reported, not scraped or persisted.** `src/agent-state.ts`
|
|
627
|
-
probes a pi agent's control socket ([§ Control socket](#control-socket)),
|
|
628
|
-
in parallel for all pi agents. For other CLIs it reads herdr's
|
|
629
|
-
`paneStatus()` on herdr. On tmux, one `list-panes -a`
|
|
630
|
-
reads `@murmur_pane_state` and `@murmur_pane_since` for every local
|
|
631
|
-
agent. Only agents without a local option use `murmur status --json`;
|
|
632
|
-
that result is cached for 10 seconds. See murmur's
|
|
633
|
-
[stable contract](https://github.com/mu-crew/murmur/blob/main/ARCHITECTURE.md#contract).
|
|
634
|
-
- **Mapping:** murmur `working` → `busy`, `blocked` →
|
|
635
|
-
`needs_permission`, and `idle` / `done` / `crashed` → `needs_input`.
|
|
636
|
-
herdr supplies mu's runtime states directly.
|
|
637
|
-
- **Absence is `unknown`.** Readings carry reasons such as `murmur not
|
|
638
|
-
installed`, `murmur pi extension not linked`, `murmur has no row`,
|
|
639
|
-
`ambiguous: N hosts`, `remote snapshot stale`, and `pane gone`.
|
|
640
|
-
- **`agents.status` is deprecated.** Inserts write `spawning` to satisfy
|
|
641
|
-
the schema. No code updates or reads the column as runtime state.
|
|
642
|
-
- **No silent adoption**: orphans are reported, never claimed.
|
|
643
|
-
- **`mu doctor` calls the same routine** and reports counts.
|
|
644
|
-
|
|
645
|
-
---
|
|
646
|
-
|
|
647
|
-
## Control socket
|
|
648
|
-
|
|
649
|
-
mu drives a pi agent through a unix socket served by mu's own pi
|
|
650
|
-
extension inside the agent's interactive pi. The pane stays pi's TUI;
|
|
651
|
-
a human attached to it shares the same session. No `pi --mode rpc`, no
|
|
652
|
-
screen scraping, no daemon: the server lives and dies with the pi
|
|
653
|
-
process.
|
|
654
|
-
|
|
655
|
-
**Path derivation** (`src/ctl/path.ts`). `ctlSocketPath(ws, agent)` is
|
|
656
|
-
`<state dir>/sock/<ws>/<agent>.sock`, where the state dir is
|
|
657
|
-
`dirname($MU_DB_PATH)` or the default. Past 103 bytes (macOS
|
|
658
|
-
`sun_path`) it becomes `<state dir>/sock/h/<sha1(ws/agent)[:16]>.sock`.
|
|
659
|
-
Nothing is stored: spawn injects the path as `MU_CTL_SOCK`, and every
|
|
660
|
-
later verb derives it again.
|
|
661
|
-
|
|
662
|
-
**Protocol v1** (`src/ctl/protocol.ts`, imported standalone by the
|
|
663
|
-
extension). JSON lines, one request per connection. Every reply carries
|
|
664
|
-
`v: 1`; the client throws `CtlVersionError` on anything else.
|
|
665
|
-
|
|
666
|
-
| op | does |
|
|
29
|
+
## Subsystems
|
|
30
|
+
|
|
31
|
+
**The ops log.** Every write to a portable table (`workstreams`,
|
|
32
|
+
`tasks`, `task_edges`, `task_notes`) is recorded as an op by a SQLite
|
|
33
|
+
trigger in the same transaction. The tables are the materialized view
|
|
34
|
+
reads hit; the log is the record. History (`mu log`), undo, sync,
|
|
35
|
+
rebuild and drift checks are all queries or replays over it. Ops carry
|
|
36
|
+
only changed columns and an HLC, which makes per-field merge across
|
|
37
|
+
machines free. Read this first:
|
|
38
|
+
[architecture/ops-log.md](architecture/ops-log.md).
|
|
39
|
+
|
|
40
|
+
**The task DAG.** Tasks are nodes with impact and effort; the one edge
|
|
41
|
+
type is `blocks`. Status decides edge satisfaction, and substate
|
|
42
|
+
qualifies status without touching edges. The `ready`, `blocked` and
|
|
43
|
+
`goals` views, union-find tracks with diamond merge, and an atomic
|
|
44
|
+
claim keyed on `$MU_AGENT_NAME` coordinate the crew:
|
|
45
|
+
[architecture/dag.md](architecture/dag.md).
|
|
46
|
+
|
|
47
|
+
**Multiplexer and reconciliation.** One workstream is one mux session
|
|
48
|
+
(a tmux session or a herdr workspace). Call sites reach the backend
|
|
49
|
+
only through `activeMux()` and branch on capabilities, never on the
|
|
50
|
+
backend name. `mu agent list` prunes ghost rows and surfaces orphan
|
|
51
|
+
panes against mux reality. Runtime agent state is read on demand, never
|
|
52
|
+
stored: the control socket for pi agents, herdr's native status on
|
|
53
|
+
herdr, and murmur for other CLIs on tmux:
|
|
54
|
+
[architecture/mux.md](architecture/mux.md).
|
|
55
|
+
|
|
56
|
+
**The control socket.** mu's pi extension serves a unix socket inside
|
|
57
|
+
each spawned pi. Send, `--fresh`, wait, abort and state for pi agents
|
|
58
|
+
go through it, exactly, with no screen reading and no fallback to
|
|
59
|
+
pasting. Remote agents use an ssh socket forward printed by
|
|
60
|
+
`mu agent remote-env`. The tmux bracketed-paste path serves the
|
|
61
|
+
[cases listed there](architecture/control-socket.md#no-silent-fallback).
|
|
62
|
+
|
|
63
|
+
**Sync.** With `MU_SYNC_DIR` set, every `mu` invocation ingests peer
|
|
64
|
+
JSONL segments before the verb and flushes its own ops after it,
|
|
65
|
+
through `handle()`. There is no daemon and no network code:
|
|
66
|
+
[architecture/sync.md](architecture/sync.md).
|
|
67
|
+
|
|
68
|
+
**The TUI.** Bare `mu` on a TTY opens a read-only ink dashboard of 10
|
|
69
|
+
cards with drill-down popups. It polls SQL every second and
|
|
70
|
+
subprocesses every 10 seconds, yanks `mu` commands instead of running
|
|
71
|
+
them, and is the only code that imports ink:
|
|
72
|
+
[architecture/tui.md](architecture/tui.md).
|
|
73
|
+
|
|
74
|
+
**CLI and SDK surface.** Each operation is a typed SDK function plus a
|
|
75
|
+
thin Commander wrapper. Public functions take operator-facing names and
|
|
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:
|
|
78
|
+
[architecture/sdk.md](architecture/sdk.md).
|
|
79
|
+
|
|
80
|
+
## Module map
|
|
81
|
+
|
|
82
|
+
`src/` is flat at the root, with one level of cohesive clusters.
|
|
83
|
+
Cluster files import from neighbours and root modules, never from the
|
|
84
|
+
hub that re-exports them. `src/tasks.ts`, `src/agents.ts`, `src/mux.ts`,
|
|
85
|
+
`src/vcs.ts` and `src/workspace.ts` are re-export hubs.
|
|
86
|
+
|
|
87
|
+
### Storage and the ops log
|
|
88
|
+
|
|
89
|
+
| Module | Responsibility |
|
|
667
90
|
| --- | --- |
|
|
668
|
-
| `
|
|
669
|
-
| `
|
|
670
|
-
| `
|
|
671
|
-
| `
|
|
672
|
-
| `
|
|
673
|
-
| `
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
`
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
`
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
**No silent fallback** (`src/agents/transport.ts`). A pi agent
|
|
693
|
-
(`expectsCtl`) whose socket does not answer is `AgentCtlUnreachableError`
|
|
694
|
-
for send and abort, and `unknown` with reason `ctl missing` /
|
|
695
|
-
`ctl refused` for state. mu never pastes into it or asks murmur instead.
|
|
696
|
-
The mux paste path serves non-pi CLIs, adopted panes without ctl, slash
|
|
697
|
-
commands (`sendUserMessage` does not run them), and explicit `--via mux`.
|
|
698
|
-
|
|
699
|
-
**Remote agents.** The user's `ssh` command carries
|
|
700
|
-
`-L <local derived path>:<remote path>` plus `MU_CTL_SOCK=<remote path>`
|
|
701
|
-
in the remote env; `mu agent remote-env` prints both and runs nothing.
|
|
702
|
-
mu always connects to the local path, so local and remote share one
|
|
703
|
-
transport. ssh leaves the local socket file behind when it dies; it
|
|
704
|
-
probes `refused`, and close or the reaper deletes it.
|
|
705
|
-
|
|
706
|
-
---
|
|
707
|
-
|
|
708
|
-
## Modules (actual src/ layout)
|
|
709
|
-
|
|
710
|
-
Mostly-flat `src/`: root `.ts` modules plus cohesive subclusters
|
|
711
|
-
(`src/agents/`, `src/tasks/`, `src/mux/`, and `src/cli/` wrappers with
|
|
712
|
-
their own `src/cli/tasks/` and `src/cli/tui/` sub-clusters). Cluster
|
|
713
|
-
files import from neighbours and root substrate modules, never from
|
|
714
|
-
the hub they're re-exported through. `src/tasks.ts`, `src/vcs.ts`,
|
|
715
|
-
`src/mux.ts` and `src/workspace.ts` are pure re-export hubs, kept so
|
|
716
|
-
external imports keep working; they hold no logic and are not listed
|
|
717
|
-
separately below.
|
|
718
|
-
|
|
719
|
-
| Module | Responsibility |
|
|
720
|
-
| --------------------- | ----------------------------------------------------------------------------------------- |
|
|
721
|
-
| `src/db.ts` | Connection (better-sqlite3, WAL), **schema v11** (11 tables incl. the `task_substates` lookup + 3 views), `resolveWorkstreamId`. Installs capture on every writable open. Owns `SYNCED_ENTITIES` / `MACHINE_LOCAL_ENTITIES` / `PORTABLE_TABLES` / `MACHINE_LOCAL_TABLES`. Refuses a pre-v11 DB (`SchemaTooOldError`) or a newer one (`SchemaTooNewError`), both exit 4. |
|
|
722
|
-
| `src/hlc.ts` | The **HLC** (VOCABULARY § HLC), serialized as sortable TEXT `<wall_ms:15>.<counter:6>.<machine_id>`. `nextHlc` / `receiveHlc` / `compareHlc` / `parseHlc` / `formatHlc`. Clock state lives in `machine_identity`. |
|
|
723
|
-
| `src/capture.ts` | **Op capture**: builds the triggers that record every write to a portable table as an op in the same transaction. |
|
|
724
|
-
| `src/apply.ts` | **The apply path** — capture's counterpart: given one op, local or from a peer, make the tables reflect it. Also owns `reprojectDeferredOps` ([§ ambient sync hook](#the-ambient-sync-hook)). |
|
|
725
|
-
| `src/undo.ts` + `src/cli/undo.ts` | **Undo as inverse ops**: inverses for one `group_id`, derived from log provenance, refusing a superseded group (exit 4; `--force` overrides). Bare form lists undoable groups (`-n` widens), a prefix previews, `--yes` applies. |
|
|
726
|
-
| `src/project-root.ts` | `detectProjectRoot` — the launch-cwd ladder bare `mu` uses to guess which workstream to focus. Pure filesystem walk; no DB. |
|
|
727
|
-
| `extension/mu-pi.ts` + `extension/delegate.ts` | **The mu pi extension**, built to `dist/extension/mu-pi.js`. Child side (`serveCtl`): serves `$MU_CTL_SOCK` in a pi mu spawned. Parent side (`registerDelegate`, hidden when `MU_MANAGED_AGENT` is set or `MU_DELEGATE=0`): the `mu_delegate` / `mu_delegate_cancel` tools, which shell out to the `mu` CLI (spawn, send, `wait --json`, read, abort, close) and only format the CLI's `outcome` into a follow-up message. Imports nothing from pi; only the protocol from mu. |
|
|
728
|
-
| `src/ctl/*.ts` | **Control socket** client side ([§ Control socket](#control-socket)): `path.ts` (`ctlSocketPath`, `remoteCtlSocketPath`, `MU_CTL_SOCK`), `protocol.ts` (v1 request/reply types, `CTL_OPS`, line framing; imports nothing from mu so the extension can load it), `client.ts` (`ctlRequest`, `ctlProbe`, typed version / unknown-op errors). |
|
|
729
|
-
| `src/link.ts` + `src/cli/link.ts` | **Link** — `mu link pi`: `linkPi` writes the extension shim (or `--copy`), `linkSkill` symlinks the skill, `inspectLinks` reports `ok` / `missing` / `stale-copy` / `foreign` / `dangling` for doctor. Filesystem only: the verb runs outside `handle()` (no DB). |
|
|
730
|
-
| `src/rebuild.ts` + `src/cli/rebuild.ts` | **Rebuild** — disaster recovery: `rebuildInto` replays the whole log into a NEW DB file via `applyOp`. The verb renders the report: counts, `--json`, an `mv`-swap `Next:` step, a warning that `agents` / `vcs_workspaces` are not reconstructible. |
|
|
731
|
-
| `src/legacy-ops.ts` | Permanent compatibility classifier for historical log-only intents whose entity looks projectable. Rebuild copies them without projection; segment flush never emits them. |
|
|
732
|
-
| `src/drift.ts` | **Drift detection**, two tiers: `checkDrift` (`mu doctor --deep`) rebuilds into a temp DB and diffs field-by-field; `checkCheapDriftInvariant` (the default) asserts every live row has ≥1 op naming its key. Plus `driftRemediation`. |
|
|
733
|
-
| `src/segments.ts` | **Segments** — the transport. `flushSegment` appends this machine's unflushed ops to `<MU_SYNC_DIR>/<machine_id>.jsonl`; `ingestSegment` reads a peer segment from its watermark into `applyOp`. Peer discovery is implicit: every non-self `*.jsonl`. |
|
|
734
|
-
| `src/sync.ts` + `src/cli/sync.ts` | **The sync seam**: `peerStatuses`, the ambient hook (`ambientIngest` / `ambientFlush` / `ambientSyncPass`), `ingestFromDb`, `repairPeer`. The verb is a peer status report; `--from <path>`, `--repair <peer>` (unique prefix, ambiguity is exit 4). |
|
|
735
|
-
| `src/cli/db.ts` | `mu db backup <file>` — one scp-able file via `VACUUM INTO`. Never overwrites: it is the pre-migration copy `SchemaTooOldError` tells you to take. |
|
|
736
|
-
| `src/file-lock.ts` | Cross-process advisory lock via atomic `fs.mkdir`; `src/agents/spawn-lock.ts` wraps it per tmux session. Flush takes it so two local `mu` processes cannot interleave partial lines. Best-effort: a non-contention failure runs the body unlocked. |
|
|
737
|
-
| `src/fleet-hazards.ts`| **Mixed-fleet hazards** — three environment checks in the default doctor: `MU_DB_PATH` inside `MU_SYNC_DIR` (fail), DB on a network mount (warn), two workstream names differing only by case (warn). All no-op when `MU_SYNC_DIR` is unset. |
|
|
738
|
-
| `src/dormant.ts` | **Dormant workstreams** — the `housekeeping` doctor section: which workstreams are **teardown** candidates. Two buckets with different remediation (`finished` = all closed + idle ≥14d, safe; `abandoned` = idle ≥60d with unclosed tasks, look first), because one merged list would invite sweeping away open work. Emits `FleetHazard` at `ok` severity, so it never makes doctor claim a fault. One indexed `GROUP BY` over `tasks`; idle is DERIVED from `MAX(tasks.updated_at)` rather than a new `workstreams.last_activity` column, which would be a second source of truth that can disagree with the rows it summarises. |
|
|
739
|
-
| `src/disk-recon.ts` | **Disk↔DB reconciliation** — the only module that reads the STATE DIR and compares it to the DB, in both directions: a `vcs_workspaces` row whose path is gone (`ws-rows`), a dir with no row (`ws-dirs`, reusing `listAllOrphanWorkspaces`), plus residue nothing references (`ws-empty` / `db-copies` / `exports` / `locks`). Emits `FleetHazard` so doctor's renderer, `--json` and the TUI card need no new shape. Default tier is `readdir` + `stat` at depth 2 (~1ms); `measureWorkspaceUsage` recurses for bytes and is `--disk` only, because its cost scales with the checkouts. **Report-only by construction** — every finding carries its cleanup command and the module runs none of them. |
|
|
740
|
-
| `src/op-context.ts` | The **op context** seam: `withOpContext(db, {intent, actor, group}, fn)` labels every op in a scope, restored in a `finally`. Nested scopes inherit the group, which puts a cascade under one `mu undo`. `withCaptureSuppressed` is the echo guard. |
|
|
741
|
-
| `src/mux.ts` | **Mux backend hub** — re-exports `src/mux/*`, same shape as `src/vcs.ts`. The public `MuxBackend` surface every call site imports. |
|
|
742
|
-
| `src/mux/*.ts` | **Multiplexer backends**: `types.ts` (the `MuxBackend` interface + `MuxError` / `PaneNotFoundError`), `detect.ts` (`MU_MUX` → `HERDR_ENV` → `$TMUX` → `PATH` ladder + test seam), `tmux.ts`, `herdr.ts`, and `input-timing.ts`. The backend owns topology, capture, **pane id** validation (tmux `%15` vs herdr `w1:p1`), identity fallback, and optional native agent state (`paneStatus?()`) and agent start (`startAgentInPane?()`). `input-timing.ts` reads pane text only to decide when tmux input can be submitted; it does not report agent state. |
|
|
743
|
-
| `src/tmux.ts` | Re-export of the tmux backend, kept for genuinely tmux-only concerns: the `MU_TMUX_SOCKET` test seam and the shared `sleep` / `setSleepForTests` poll seam. Everything else imports `src/mux.ts` and goes through `activeMux()`. |
|
|
744
|
-
| `src/agent-state.ts` | Runtime agent-state resolver. Reads herdr's native state or murmur's local pane options and remote JSON; maps source states, returns `unknown` with a reason, and caches remote murmur reads for 10 seconds. |
|
|
745
|
-
| `src/reconcile.ts` | Ghost prune + orphan surface; "reality wins" |
|
|
746
|
-
| `src/agents.ts` | Hub: CRUD + send / read / list / close + liveness + reaper. Re-exports `src/agents/*`; pane-title composition (`composeAgentTitle`) lives here. |
|
|
747
|
-
| `src/agents/*.ts` | Agent-lifecycle internals: `spawn.ts` (spawn, CLI resolution, liveness wait *or* backend `startAgentInPane`, pane create-or-reuse, rollback), `spawn-lock.ts` (per-session lock around topology+finalize), `wait.ts`, `adopt.ts`, `kick.ts` (signal a wedged pane's pgid), `abort.ts` (stop a pi turn via the control socket), `transport.ts` (send routing: ctl for pi agents, mux paste otherwise, no fallback), `delegate.ts` (`delegateOutcome`: one wait result → `done` / `empty` / `died` / `timeout` / `pending`, surfaced by `mu agent wait --json`), `errors.ts`. |
|
|
748
|
-
| `src/dag.ts` | Shared DAG read/render helpers: `loadFullDag` plus pure `renderForest` / `renderTaskTree`, reused by `mu task tree` and the TUI DAG popup. |
|
|
749
|
-
| `src/tasks/*.ts` | Task-graph internals: `core.ts` (row shapes, id resolution), `id.ts`, `queries.ts` (reads), `edit.ts` (+ delete cascade preview), `edges.ts` (+ cycle check), `status.ts` (TaskStatus), `sort.ts`, `claim.ts` (atomic CAS), `lifecycle.ts` (close/open), `wait.ts`, `errors.ts`. |
|
|
750
|
-
| `src/tracks.ts` | Parallel-tracks union-find with diamond merge |
|
|
751
|
-
| `src/staleness.ts` | `WORKSPACE_STALE_THRESHOLD = 10` and the pure `isWorkspaceStale` predicate, shared by static state, the TUI Workspaces card, and dispatch-time checks. |
|
|
752
|
-
| `src/workstream.ts` | ensureWorkstream / list / summarize / destroy |
|
|
753
|
-
| `src/logs.ts` | Typed READER over `ops` (`listLogs` / `latestSeq`), plus the write paths triggers cannot cover: `appendLog` (operator prose) and `emitEvent` (changes mutating no portable table — `agent.*`, `workspace.*`), under a typed `LocalIntent`. |
|
|
754
|
-
| `src/log-render.ts` | **The ONE op → prose formatter.** `renderOp` maps an intent (+ key + payload fields) to `{verb, subject, detail}`; plus `renderOpLine`, `opSubject`, `parseOpKey`. Pure and colour-free, so CLI and TUI share one phrasing. |
|
|
755
|
-
| `src/vcs/*.ts` | One backend per file (`git.ts`, `jj.ts`, `sl.ts`, `none.ts`) plus `types.ts` (the `VcsBackend` interface), `helpers.ts`, and `index.ts` (detection precedence `jj` → `sl` → `git` → none; `backendByName`). |
|
|
756
|
-
| `src/workspace/*.ts` | Per-agent VCS workspaces, a registry on top of `vcs.ts`: `core.ts` (row shapes, paths, errors), `crud.ts`, `decorate.ts` (staleness + dirty), `orphans.ts`. |
|
|
757
|
-
| `src/glyphs.ts` | **The ONE glyph vocabulary.** `AGENT_STATE_GLYPH` + `agentStateGlyph()` for runtime agent state, the `GLYPH` record for every other state symbol (blocked, dirty, stale, ok/warn/fail, filter toggles), and `superscriptDigit` for TUI card-header keys. All classic Nerd Font `nf-fa-*` — single-codepoint and one cell wide, so `cli-table3` columns line up. |
|
|
758
|
-
| `src/output.ts` | NextStep type + `printNextSteps` + `errorNextSteps` plumbing for self-documenting output |
|
|
759
|
-
| `src/shell-quote.ts` | `shellQuote` — POSIX single-quoting for tokens interpolated into copy-pasteable next-step hints. |
|
|
760
|
-
| `src/state.ts` | SDK seam for `mu state`: `loadWorkstreamSnapshotFast` (pure SQL, TUI 1s tick), `loadWorkstreamSnapshotSlow` (subprocesses), `mergeSnapshotFastSlow`, `loadWorkstreamSnapshot`. Also parses exact `REMOTE:` task-note inventory and `REMOTE_BASE:` dispatch metadata. Opt-in: `withDirty`, `withDoctor`, `withRecentCommits`, `withAllTasks`. |
|
|
761
|
-
| `src/doctor-summary.ts` | The dashboard's doctor seam: `loadDoctorSummary` (pragmas and COUNT-shape selects only — cheap enough for the poll loop), `loadDoctorChecks` for the popup, plus `yankCommandForCheck` / `remediationParagraph`. |
|
|
762
|
-
| `src/cli.ts` | commander entry; `buildProgram()` (re-exports `format`/`handle` symbols for back-compat with existing import sites). |
|
|
763
|
-
| `src/cli/*.ts` | One file per verb-namespace; thin wrappers over the SDK; `--json` on every read verb. Two non-verb cluster-mates: `format.ts` (table renderers, status colourers) and `handle.ts` (typed-error → exit-code map). |
|
|
764
|
-
| `src/cli/tui/*.tsx` | The interactive ink TUI, lazy-imported by `src/cli/state.ts`. **The only place ink/react are imported** — a ROADMAP pledge. Per-file roles: [§ TUI architecture](#tui-architecture). |
|
|
765
|
-
| `src/cli/tasks/*.ts` | Sub-cluster of the `mu task` namespace: `queries.ts` (list/next/owned-by + `mu me tasks` / `mu me next`), `lifecycle.ts` (close/open), `edit.ts`, `edges.ts` (+ delete cascade preview), `claim.ts`, `tree.ts`, `wire.ts` (Commander glue). |
|
|
766
|
-
| `src/index.ts` | SDK entrypoint (re-exports) |
|
|
767
|
-
| `skills/mu/SKILL.md` | Bundled skill teaching the LLM the orchestration model and operational traps; `mu --help` owns the verb reference. |
|
|
768
|
-
|
|
769
|
-
### What the rows above leave out
|
|
770
|
-
|
|
771
|
-
- **Capture triggers are TEMP triggers**, reinstalled per connection —
|
|
772
|
-
SQLite refuses a main-schema trigger referencing the temp `_op_ctx`.
|
|
773
|
-
Hence two oddities: DELETE keys are captured inline, with the parent
|
|
774
|
-
stashing its natural key in `_op_dying` first (FK CASCADE fires child
|
|
775
|
-
triggers after the parent is gone), and the HLC is minted in SQL,
|
|
776
|
-
because a trigger cannot call into JS.
|
|
777
|
-
- **Merge is per entity**: notes and messages are grow-only sets, tasks
|
|
778
|
-
and workstreams merge per field by HLC, edges are an LWW-element-set.
|
|
779
|
-
A tombstone is an ordinary op carrying an HLC, so out-of-order
|
|
780
|
-
arrival is a comparison and resurrection falls out. Never
|
|
781
|
-
`json_patch` — RFC 7396 reads a null member as delete-the-key.
|
|
782
|
-
- **Note tombstones are the one SELF-DESCRIBING tombstone.** Every
|
|
783
|
-
other `del` carries `'{}'`, because its key plus the puts before it
|
|
784
|
-
describe the row. A note's key embeds its rowid
|
|
785
|
-
(`<ws>/<task>#<id>`), which is NOT stable — a rebuild or
|
|
786
|
-
reprojection reassigns it, leaving the puts under the old key and a
|
|
787
|
-
later `del` under the new one. Undo folds the puts for a key to
|
|
788
|
-
reconstruct a tombstoned row, so such a note folded to nothing and
|
|
789
|
-
was silently never restored (drift-641). The `task_notes` delete
|
|
790
|
-
trigger therefore records `OLD.*`, and `planUndo` falls back to the
|
|
791
|
-
tombstone's payload when the fold is empty. This is also why
|
|
792
|
-
`src/drift.ts` matches notes on the task-key PREFIX rather than the
|
|
793
|
-
exact key.
|
|
794
|
-
- **Undo and restore write through the tables**, so neither can be the
|
|
795
|
-
write path capture misses. `restore` records then applies under the
|
|
796
|
-
SAME HLC: `applyOp` excludes an op's own HLC from provenance, so a
|
|
797
|
-
fresher one makes the row outrank the op and lose every field to an
|
|
798
|
-
insert default.
|
|
799
|
-
- **Segments are single-writer per file** — a machine appends only to
|
|
800
|
-
its own, so nothing is contended and any folder-syncer is adequate
|
|
801
|
-
transport. Regenerable, so no fsync. Four layers stop at the first
|
|
802
|
-
bad record and advance the watermark only that far: `JSON.parse`
|
|
803
|
-
(torn write when the line lacks its trailing `,"crc"…}` framing,
|
|
804
|
-
malformed-shape when it has it — the same failure, but a transfer cut
|
|
805
|
-
short and a writer bug have different remediations), crc32 per line
|
|
806
|
-
(bit rot), monotonic HLC (reorder, duplicate, truncation), a
|
|
807
|
-
`.manifest` sidecar (truncation exactly on a line boundary).
|
|
808
|
-
`ops.payload` is TEXT that is usually but not always JSON (`mu log
|
|
809
|
-
"text"` writes prose), so the encoder passes valid JSON through
|
|
810
|
-
verbatim — the crc covers those exact bytes — and JSON-encodes
|
|
811
|
-
anything else, unwrapping on ingest. Only `SYNCED_ENTITIES` and only THIS machine's ops
|
|
812
|
-
are flushed, so pane ids and absolute paths never leave and peers
|
|
813
|
-
never echo each other's history.
|
|
814
|
-
- **Sync never fails a command**: a truncated segment, a garbage
|
|
815
|
-
segment, a sync dir that is a file, a vanished directory all warn on
|
|
816
|
-
stderr and return.
|
|
817
|
-
- **Damage halts; refusal skips.** Stopping at the first bad record is
|
|
818
|
-
right for DAMAGE, where a gap is indistinguishable from reordering.
|
|
819
|
-
It is wrong for a well-formed line mu merely declines to project: a
|
|
820
|
-
refusal lands nothing, so it leaves no hole, and halting made the
|
|
821
|
-
watermark unrecoverable by any means the CLI offers. One
|
|
822
|
-
`entity:"marker"` line at position 2533 of a 20,305-line peer segment
|
|
823
|
-
froze that peer's watermark permanently, and `mu sync --repair` (which
|
|
824
|
-
only resets the watermark) re-read straight back into the same line.
|
|
825
|
-
So ingest now distinguishes three cases: damage halts, a KNOWN
|
|
826
|
-
machine-local entity is a reported defect that is SKIPPED, and an
|
|
827
|
-
UNRECOGNISED entity applies as a no-op with no defect at all —
|
|
828
|
-
reader-behind-writer vocabulary is a fact of a mixed fleet, not
|
|
829
|
-
corruption. `--repair` is only suggested for defects a re-read can
|
|
830
|
-
clear.
|
|
831
|
-
- **Rebuild is not ingest**: ingest filters to `SYNCED_ENTITIES`, but
|
|
832
|
-
local recovery replays everything, so log-only entities are copied
|
|
833
|
-
verbatim or `mu log` comes back empty. `machine_identity` carries
|
|
834
|
-
across whole — id, hostname, HLC clock — or the rebuilt DB is a
|
|
835
|
-
different peer minting HLCs below every replayed op. Drift diffs by
|
|
836
|
-
natural key and excludes `owner_id`: a rebuild always has NULL
|
|
837
|
-
owners.
|
|
838
|
-
|
|
839
|
-
Why segments and not Litestream / cr-sqlite / a peer list, and why the
|
|
840
|
-
DB must never sit in `MU_SYNC_DIR`:
|
|
841
|
-
[ROADMAP § Rejected sync substrates](ROADMAP.md#rejected-sync-substrates).
|
|
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 |
|
|
92
|
+
| `src/hlc.ts` | hybrid logical clock, serialized as sortable TEXT |
|
|
93
|
+
| `src/capture.ts` | TEMP triggers recording each portable write as an op |
|
|
94
|
+
| `src/op-context.ts` | `withOpContext` (intent, actor, group); `withCaptureSuppressed` echo guard |
|
|
95
|
+
| `src/apply.ts` | apply one op: per-field LWW, tombstones, substate pair repair, `reprojectDeferredOps` |
|
|
96
|
+
| `src/undo.ts` | inverse ops for one group; refuses a superseded group |
|
|
97
|
+
| `src/rebuild.ts` | replay the whole log into a new DB file |
|
|
98
|
+
| `src/legacy-ops.ts` | classifier for historical log-only intents; rebuild copies them unprojected |
|
|
99
|
+
| `src/drift.ts` | cheap invariant (every row has an op) and `--deep` rebuild-diff |
|
|
100
|
+
| `src/logs.ts` | typed reader over `ops`; `appendLog` and `emitEvent`, the writes triggers cannot cover |
|
|
101
|
+
| `src/log-render.ts` | the one op-to-prose formatter (`renderOp`), shared by CLI and TUI |
|
|
102
|
+
|
|
103
|
+
### Sync and doctor checks
|
|
104
|
+
|
|
105
|
+
| Module | Responsibility |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `src/segments.ts` | JSONL segments: flush own ops, ingest peers from a watermark |
|
|
108
|
+
| `src/sync.ts` | peer status, the ambient hook, `--from`, `--repair` |
|
|
109
|
+
| `src/file-lock.ts` | cross-process advisory lock through atomic `fs.mkdir` |
|
|
110
|
+
| `src/fleet-hazards.ts` | mixed-fleet checks: DB inside `MU_SYNC_DIR`, network mount, case collisions |
|
|
111
|
+
| `src/dormant.ts` | doctor housekeeping: `finished` (all closed, idle 14 days) vs `abandoned` (idle 60 days, open tasks) workstreams; idle derived from `MAX(tasks.updated_at)` |
|
|
112
|
+
| `src/disk-recon.ts` | report-only state-dir vs DB reconciliation (rows without dirs, dirs without rows, residue); `--disk` measures bytes |
|
|
113
|
+
| `src/doctor-summary.ts` | cheap doctor slice for the TUI; ctl, extension and skill checks; remediation text |
|
|
842
114
|
|
|
843
|
-
|
|
115
|
+
### Agents, multiplexer and control socket
|
|
844
116
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
117
|
+
| Module | Responsibility |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| `src/agents.ts` | CRUD, send, read, list, close, liveness, reaper; `composeAgentTitle` |
|
|
120
|
+
| `src/agents/spawn.ts` | spawn: CLI resolution, pane create or reuse, liveness, readiness, ctl handshake, rollback |
|
|
121
|
+
| `src/agents/spawn-lock.ts` | per-session lock around topology and row insert |
|
|
122
|
+
| `src/agents/transport.ts` | send routing: ctl for pi agents, mux paste otherwise, no fallback; `expectsCtl` |
|
|
123
|
+
| `src/agents/abort.ts` | stop a pi turn through the control socket |
|
|
124
|
+
| `src/agents/delegate.ts` | `delegateOutcome`: one wait result to `done` / `empty` / `died` / `timeout` / `pending` |
|
|
125
|
+
| `src/agents/wait.ts` | block until task-less agents leave `busy` |
|
|
126
|
+
| `src/agents/adopt.ts` | register an existing pane as an agent |
|
|
127
|
+
| `src/agents/kick.ts` | `mu agent kick`: signal a wedged pane's foreground process group |
|
|
128
|
+
| `src/agents/errors.ts` | typed agent errors |
|
|
129
|
+
| `src/agent-state.ts` | runtime state from ctl, herdr or murmur; `unknown` with a reason |
|
|
130
|
+
| `src/reconcile.ts` | ghost prune and orphan surfacing |
|
|
131
|
+
| `src/mux/types.ts` | `MuxBackend` interface; `MuxError`, `PaneNotFoundError`, `NoMultiplexerError` |
|
|
132
|
+
| `src/mux/detect.ts` | `MU_MUX` → `HERDR_ENV` → `$TMUX` → `PATH` ladder; `activeMux()` |
|
|
133
|
+
| `src/mux/tmux.ts` | tmux backend: the `tmux()` wrapper, bracketed-paste send, pane validation |
|
|
134
|
+
| `src/mux/herdr.ts` | herdr backend: JSON socket API, atomic send, native pane status, `startAgentInPane` |
|
|
135
|
+
| `src/mux/input-timing.ts` | when tmux paste input can be submitted; not a state source |
|
|
136
|
+
| `src/tmux.ts` | tmux-only re-exports: `MU_TMUX_SOCKET` seam, `sleep` / `setSleepForTests` |
|
|
137
|
+
| `src/ctl/path.ts` | `ctlSocketPath`, `remoteCtlSocketPath`, `MU_CTL_SOCK` |
|
|
138
|
+
| `src/ctl/protocol.ts` | protocol v1 types, `CTL_OPS`, line framing; imports nothing from mu |
|
|
139
|
+
| `src/ctl/client.ts` | `ctlRequest`, `ctlProbe`, version and unknown-op errors |
|
|
140
|
+
| `extension/mu-pi.ts` | pi extension, child side: serves `$MU_CTL_SOCK`; built to `dist/extension/mu-pi.js` |
|
|
141
|
+
| `extension/delegate.ts` | pi extension, parent side: `mu_delegate` / `mu_delegate_cancel`, which shell out to `mu` |
|
|
142
|
+
| `src/link.ts` | `mu link pi`: extension shim, skill symlink, `inspectLinks` for doctor |
|
|
143
|
+
|
|
144
|
+
### Tasks, workspaces and workstreams
|
|
145
|
+
|
|
146
|
+
| Module | Responsibility |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `src/tasks/core.ts`, `id.ts` | row shapes, id resolution, qualified ids |
|
|
149
|
+
| `src/tasks/queries.ts`, `sort.ts` | list, next, owned-by; ROI, recency, age and id sorts |
|
|
150
|
+
| `src/tasks/edit.ts`, `edges.ts` | add and edit; block, unblock, reparent, delete with cycle check |
|
|
151
|
+
| `src/tasks/claim.ts` | atomic claim and release; `resolveWorkerIdentity` |
|
|
152
|
+
| `src/tasks/lifecycle.ts`, `status.ts` | close, open and cascade; `TaskStatus` |
|
|
153
|
+
| `src/tasks/wait.ts`, `errors.ts` | `waitForTasks`; typed task errors |
|
|
154
|
+
| `src/tracks.ts` | union-find tracks with diamond merge |
|
|
155
|
+
| `src/dag.ts` | `loadFullDag`, `renderForest`, `renderTaskTree` for `mu task tree` and the DAG popup |
|
|
156
|
+
| `src/workstream.ts` | ensure, list, summarize, destroy |
|
|
157
|
+
| `src/vcs/*.ts` | `VcsBackend` with git, jj, sl and none; detection `jj` → `sl` → `git` → none |
|
|
158
|
+
| `src/workspace/*.ts` | per-agent workspace registry: CRUD, staleness and dirty decoration, orphans |
|
|
159
|
+
| `src/staleness.ts` | `WORKSPACE_STALE_THRESHOLD` and `isWorkspaceStale` |
|
|
160
|
+
| `src/project-root.ts` | `detectProjectRoot` for the TUI launch ladder |
|
|
161
|
+
| `src/state.ts` | `mu state` seam: fast SQL tier, slow subprocess tier, merge; parses `REMOTE:` notes |
|
|
162
|
+
|
|
163
|
+
### CLI, output and packaging
|
|
164
|
+
|
|
165
|
+
| Module | Responsibility |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `src/cli.ts` | Commander wiring (`buildProgram`) |
|
|
168
|
+
| `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 |
|
|
169
|
+
| `src/cli/tasks/*.ts` | the `mu task` namespace, including `mu me tasks` / `mu me next` |
|
|
170
|
+
| `src/cli/dispatch-hints.ts` | dispatch-time `Next:` hints for pi agents (`--fresh`, `--steer`, abort) |
|
|
171
|
+
| `src/cli/agents-remote.ts` | `mu agent remote-env`: prints the ssh forward and env, runs nothing |
|
|
172
|
+
| `src/cli/tui-launch-focus.ts` | initial-tab focus ladder for bare `mu` and `mu state --tui` |
|
|
173
|
+
| `src/cli/tui/` | the ink TUI; the only place ink and react are imported |
|
|
174
|
+
| `src/output.ts`, `src/shell-quote.ts` | `printNextSteps` / `errorNextSteps`; POSIX quoting for hints |
|
|
175
|
+
| `src/glyphs.ts` | the one glyph vocabulary (single-cell Nerd Font codepoints) |
|
|
176
|
+
| `src/index.ts` | SDK entry point |
|
|
177
|
+
| `skills/mu/` | the skill loaded into an agent's context; `mu --help` owns the verb reference |
|
|
178
|
+
| `scripts/migrate.ts` | retained migration sidecar ([scripts/README.md](../scripts/README.md)) |
|
|
179
|
+
| `scripts/check-doc-links.mjs` | relative-link and anchor checker for every markdown file, run by `test/doc-links.test.ts` |
|
|
180
|
+
| `test/` | vitest suites; `*.integration.test.ts` touch real tmux, herdr or VCS ([sdk.md § Testing layers](architecture/sdk.md#testing-layers)) |
|
|
861
181
|
|
|
862
182
|
## Key seams
|
|
863
183
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
| Seam | Add a new impl by... |
|
|
867
|
-
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
868
|
-
| `VcsBackend` | Implementing `detect / createWorkspace / freeWorkspace / isClean / commitsBehind / rebaseTo / commitsSinceBase / recentCommits / showCommit` (~80–150 LOC; jj/sl/git/none are working examples) |
|
|
869
|
-
| New typed verb | An SDK function in the relevant `src/*.ts`; a `cmd<Verb>` in the matching `src/cli/<namespace>.ts`; one commander block in `buildProgram()`, wrapped in `handle()` and routed through `printNextSteps` |
|
|
870
|
-
| New schema migration| Bump `CURRENT_SCHEMA_VERSION` in `src/db.ts` and mirror the shape in `CURRENT_SCHEMA`. Keep startup migration-free; extend the retained fresh-target sidecar only when an old released schema needs a bridge. Recipe: [scripts/README.md](../scripts/README.md) |
|
|
871
|
-
| New syncable field | Add the column to a portable table, extend the capture trigger's changed-column comparison, confirm the apply path writes it. The UPDATE trigger MUST emit only changed columns — a full-row payload silently regresses field merge to row-level LWW |
|
|
872
|
-
| Cross-machine sync | Nothing: every invocation already flushes local ops and ingests each peer segment from its watermark. No daemon, no network code, no membership config |
|
|
873
|
-
|
|
874
|
-
An importer must synthesize **ops, not rows**: the tables are a
|
|
875
|
-
projection of the log, so a direct INSERT is invisible to sync and
|
|
876
|
-
reported as drift.
|
|
877
|
-
|
|
878
|
-
## The ambient sync hook
|
|
879
|
-
|
|
880
|
-
Sync is **ambient, not a daemon**: no watcher, no background process,
|
|
881
|
-
no polling loop outliving a command. It happens because you already
|
|
882
|
-
run `mu` constantly.
|
|
883
|
-
|
|
884
|
-
**The seam is `handle()`** (`src/cli/handle.ts`), the one place every
|
|
885
|
-
verb passes through, and the only seam that is already async —
|
|
886
|
-
`syncPass` is async (it takes the file lock) while most verb bodies
|
|
887
|
-
are synchronous better-sqlite3 code. One `await` before `fn(db)` and
|
|
888
|
-
one after covers all ~60 verbs; no verb learns that sync exists.
|
|
889
|
-
|
|
890
|
-
**Order matters**: ingest BEFORE the body, so the verb reads the
|
|
891
|
-
freshest state transport has delivered; flush AFTER, so this
|
|
892
|
-
invocation's ops reach the segment now rather than next invocation.
|
|
893
|
-
Flush also runs on the ERROR path — a verb may have committed ops
|
|
894
|
-
before throwing, and those ops are already canonical, so withholding
|
|
895
|
-
them would make this machine's history depend on the exit code.
|
|
896
|
-
|
|
897
|
-
Four carve-outs:
|
|
898
|
-
|
|
899
|
-
| Surface | Behaviour | Why |
|
|
900
|
-
| ------- | --------- | --- |
|
|
901
|
-
| `MU_SYNC_DIR` unset | One `if` on one env var. No promise, no filesystem touch. | The single-machine case must pay nothing. Measured indistinguishable from baseline; ~3ms with one peer. |
|
|
902
|
-
| `mu sql` | Opts out (`handle(..., { ambientSync: false })`). | It is the verb whose result an operator diffs against their own expectation; an ambient ingest changing a row count mid-inspection reads as a mu bug. |
|
|
903
|
-
| `mu sync` | Opts out and runs the pass itself. | Otherwise the report prints 'ingested 0' right after ingesting. |
|
|
904
|
-
| The TUI | On the SLOW tick (10s, `SLOW_TICK_MS`) only, guarded so N tabs run one pass per beat, `quiet: true`. | The fast tick can be wound down to 100ms — 10 passes/second on a shared folder. Quiet because the TUI owns the alternate screen; sync problems surface via the Doctor card. |
|
|
905
|
-
|
|
906
|
-
### Out-of-order arrival: `reprojectDeferredOps`
|
|
907
|
-
|
|
908
|
-
An `edge` or `note` put whose task has not been ingested yet skips as
|
|
909
|
-
`absent` — correctly. But ingest advances the watermark past it
|
|
910
|
-
anyway, so without a second pass it never lands: a permanent silent
|
|
911
|
-
divergence, and a coin flip per fleet, since peers are discovered by
|
|
912
|
-
`localeCompare` over random-UUID filenames.
|
|
913
|
-
|
|
914
|
-
`reprojectDeferredOps` re-queries the log for note/edge puts that are
|
|
915
|
-
resolvable NOW but unprojected. No retry queue — that would be a
|
|
916
|
-
second source of truth, and it would not survive a short-lived mu
|
|
917
|
-
process when the parent often arrives days later. It excludes ops
|
|
918
|
-
whose parent task is gone and keys carrying a newer `del`,
|
|
919
|
-
so a deleted edge is never resurrected and an orphan is not retried
|
|
920
|
-
forever. On a healthy DB it is two indexed queries returning zero
|
|
921
|
-
rows. Called once per ingest pass, never per peer: an edge in one
|
|
922
|
-
segment may name a task in another.
|
|
923
|
-
|
|
924
|
-
## Surrogate-PK + SDK-boundary discipline
|
|
925
|
-
|
|
926
|
-
A substrate-wide invariant; every entity table follows it.
|
|
927
|
-
|
|
928
|
-
**Schema shape — every entity table:**
|
|
929
|
-
|
|
930
|
-
```
|
|
931
|
-
(
|
|
932
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT, -- surrogate; internal
|
|
933
|
-
<scope_id> INTEGER NOT NULL REFERENCES <parent>(id) ON DELETE CASCADE,
|
|
934
|
-
<name> TEXT NOT NULL, -- operator-facing; mutable
|
|
935
|
-
-- ... domain attributes
|
|
936
|
-
UNIQUE (<scope_id>, <name>) -- per-scope unique
|
|
937
|
-
)
|
|
938
|
-
```
|
|
939
|
-
|
|
940
|
-
FKs reference `<child>.<parent>_id` (INTEGER), never the TEXT name.
|
|
941
|
-
The TEXT name is an operator-facing attribute — searchable,
|
|
942
|
-
displayable, cheap to rename. The surrogate id is the identity.
|
|
184
|
+
A new implementation of each is small.
|
|
943
185
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
column
|
|
951
|
-
|
|
952
|
-
workstream — it records).
|
|
953
|
-
|
|
954
|
-
**SDK boundary discipline** — same shape as REST: external API
|
|
955
|
-
uses business identifiers, internal layer uses primary keys.
|
|
956
|
-
|
|
957
|
-
> **Public SDK functions take operator-facing names.**
|
|
958
|
-
> **Internal helpers take surrogate ids.**
|
|
959
|
-
> **Resolution happens at the public-function entry, exactly once.**
|
|
960
|
-
|
|
961
|
-
```ts
|
|
962
|
-
// PUBLIC: takes operator-facing names
|
|
963
|
-
export function claimTask(
|
|
964
|
-
db: Db,
|
|
965
|
-
workstream: string,
|
|
966
|
-
localId: string,
|
|
967
|
-
opts?: ClaimOptions,
|
|
968
|
-
): ClaimResult {
|
|
969
|
-
const wsId = resolveWorkstreamId(db, workstream);
|
|
970
|
-
const taskId = tryResolveTaskId(db, wsId, localId);
|
|
971
|
-
if (taskId === null) throw new TaskNotFoundError(localId);
|
|
972
|
-
const agentId = resolveCurrentAgentId(db, wsId);
|
|
973
|
-
return claimTaskById(db, taskId, agentId, opts);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// INTERNAL: takes surrogate ids; never re-resolves
|
|
977
|
-
function claimTaskById(db, taskId, agentId, opts): ClaimResult { ... }
|
|
978
|
-
```
|
|
979
|
-
|
|
980
|
-
Resolving exactly once at the boundary buys three things: no
|
|
981
|
-
double-resolution; no mid-function ambiguity (with surrogate ids in
|
|
982
|
-
hand, internal helpers need no workstream context — the FKs make
|
|
983
|
-
scope implicit); and one place for error mapping
|
|
984
|
-
(`WorkstreamNotFoundError` originates at resolve-time in
|
|
985
|
-
`src/db.ts`; `TaskNotFoundError` / `AgentNotFoundError` are raised by
|
|
986
|
-
SDK callers wrapping the `tryResolve*` null-return, so the typed
|
|
987
|
-
class and its exit-code 3 mapping hold whichever leg missed).
|
|
988
|
-
|
|
989
|
-
**`--json` output preserves operator-facing names.** Surrogate ids
|
|
990
|
-
never leak into `--json`, error payloads, log lines, or markdown
|
|
991
|
-
exports. Promoting them to the public shape would re-introduce a
|
|
992
|
-
global namespace by the back door (anti-feature pledge).
|
|
993
|
-
|
|
994
|
-
## State of truth
|
|
995
|
-
|
|
996
|
-
- **`~/.local/state/mu/mu.db` is canonical.** Everything else is a
|
|
997
|
-
cache, including tmux pane titles (mu re-pushes their durable agent/task
|
|
998
|
-
context via `composeAgentTitle` after every relevant state change).
|
|
999
|
-
- **Reads are cheap** via SQLite views (`ready`, `blocked`, `goals`).
|
|
1000
|
-
- **Writes go through the typed SDK functions** (`src/agents.ts`,
|
|
1001
|
-
`src/tasks.ts`, …) which validate, transact, and reconcile. Op
|
|
1002
|
-
capture is not their job — the triggers handle it.
|
|
1003
|
-
- **Workstream scoping is mandatory at the CLI boundary.** TEXT names
|
|
1004
|
-
(`tasks.local_id`, `agents.name`) are per-workstream unique — the
|
|
1005
|
-
same name may legitimately exist in two workstreams. Every public
|
|
1006
|
-
SDK function taking such a name also takes (or threads) the
|
|
1007
|
-
workstream; internal SQL filters by `(workstream_id, name)`. Test
|
|
1008
|
-
fixtures and `mu sql` read paths may omit it and fall back to
|
|
1009
|
-
first-match-by-name. The schema enforces it structurally
|
|
1010
|
-
(per-workstream UNIQUE on name + INTEGER FKs).
|
|
1011
|
-
- **The ops log is the insurance and it IS version history** — see
|
|
1012
|
-
[§ The ops log](#the-ops-log-read-this-first). `mu undo <group>`
|
|
1013
|
-
reverts one action; `mu rebuild <file>` replays the whole log into
|
|
1014
|
-
a fresh DB.
|
|
1015
|
-
- **In-memory state is short-lived** — the CLI's per-command
|
|
1016
|
-
connection. Gone on process exit.
|
|
1017
|
-
- **Cross-process coordination** is via SQLite WAL — multiple `mu`
|
|
1018
|
-
processes share the file safely.
|
|
1019
|
-
|
|
1020
|
-
## Errors
|
|
1021
|
-
|
|
1022
|
-
Curated error classes per layer; no try/catch swallowing. CLI exit
|
|
1023
|
-
codes:
|
|
1024
|
-
|
|
1025
|
-
| Code | Meaning |
|
|
1026
|
-
| ---- | -------------------------------------------------------- |
|
|
1027
|
-
| 0 | success |
|
|
1028
|
-
| 1 | generic error |
|
|
1029
|
-
| 2 | usage error (commander's default) |
|
|
1030
|
-
| 3 | not found (no such agent / task / workspace) |
|
|
1031
|
-
| 4 | conflict (name collision, double-claim, dirty tree) |
|
|
1032
|
-
| 5 | substrate unavailable (`tmux` not running, DB locked) |
|
|
1033
|
-
|
|
1034
|
-
Errors carry structured context (operation name, target, attempted
|
|
1035
|
-
action) so `mu doctor` can surface them readably.
|
|
1036
|
-
|
|
1037
|
-
## Testing layers
|
|
1038
|
-
|
|
1039
|
-
| Layer | Test approach |
|
|
1040
|
-
| ---------------------------------- | ------------------------------------------------------------------------------ |
|
|
1041
|
-
| `src/db.ts` | Real SQLite in temp dir; schema/table-count assertions |
|
|
1042
|
-
| `src/tasks.ts` | Real SQLite in temp dir; pure functions over fixture data |
|
|
1043
|
-
| `src/tracks.ts` | Pure functions; union-find + diamond-merge properties |
|
|
1044
|
-
| `src/agents.ts` | Mocked tmux executor via `setTmuxExecutor()`; reaper integration tests |
|
|
1045
|
-
| `src/logs.ts` | Real SQLite; cursor semantics, AUTOINCREMENT durability, ops-outlive-workstream |
|
|
1046
|
-
| `src/capture.ts` + `src/op-context.ts` | Real SQLite, no tmux: payloads hold ONLY changed columns (asserted by key COUNT), echo suppression, cascade grouping, FK-CASCADE tombstone keys, the null-intent fail-safe |
|
|
1047
|
-
| `src/apply.ts` | Real SQLite, INJECTED HLCs, no sleeps: field-level convergence in both orders, same-field LWW determinism, all four put/del orderings, resurrection, grow-only notes, edge add/remove, the set-to-NULL trap, `owner_id` stripping, replay idempotence |
|
|
1048
|
-
| `src/rebuild.ts` | Round trip asserted row-BY-ROW per portable table (a count-only check passes on a garbage rebuild), plus idempotence, tombstones, byte-identical ops, log-only entities copied, `machine_id` + clock carried. `cli-rebuild.integration` covers the verb |
|
|
1049
|
-
| `src/drift.ts` + `src/fleet-hazards.ts` | Drift planted the way a capture bug would (`withCaptureSuppressed` + direct mutation), with equal weight on FALSE positives (cascade delete, resurrection, set-to-NULL, no-op update, claimed task). `classifyFsType` against synthetic magic numbers |
|
|
1050
|
-
| `src/dormant.ts` | Task `updated_at` written directly in SQL (the verbs stamp `datetime('now')`, and the age IS the input under test). Weighted toward the BOUNDARY between the two buckets rather than detection: at an age between the thresholds, a closed-out workstream is reported and one with open tasks is not |
|
|
1051
|
-
| `src/disk-recon.ts` | Per-test `MU_STATE_DIR` temp dir with the on-disk shape built by hand — the disagreements it detects cannot be produced through the normal verbs, since those keep disk and DB in step. Both directions covered (row without dir, dir without row), plus the live-WAL-triple exclusion and `stranded` marking |
|
|
1052
|
-
| `src/segments.ts` | TWO temp DBs and one shared dir — the real deployment shape. Round trip; two machines editing different fields both surviving; idempotence; each robustness layer; machine-local ops never reaching a file; Syncthing conflict copies |
|
|
1053
|
-
| `src/sync.ts` + `src/cli/sync.ts` | Fast tier covers decision logic in-process (staleness arithmetic, peer-prefix ambiguity, MU_SYNC_DIR unset, the never-throw contract against four breakages, hook order). `cli-sync.integration` re-checks it THROUGH THE VERB with two DBs and one dir |
|
|
1054
|
-
| the whole sync path, as a SESSION | `sync-session.integration` simulates days of laptop↔devserver work: 8 rounds, two machines, no coordination beyond the folder. Asserts byte-identical portable content, no `--deep` drift, and a quiet round adding exactly zero ops |
|
|
1055
|
-
| `src/hlc.ts` | Real SQLite + an INJECTED clock (no sleeps): backwards jumps, stalled milliseconds, sort order, durability, concurrent minting |
|
|
1056
|
-
| `src/vcs.ts` + `src/workspace.ts` | `*.integration.test.ts` files use real git in `os.tmpdir()`; jj/sl tests feature-detect (skip if binary missing) |
|
|
1057
|
-
| `src/cli.ts` / verb integration | `*.integration.test.ts` files; real tmux server, unique session per test |
|
|
1058
|
-
| Fast unit/dev-loop tier | `npm run test:fast`; excludes `*.integration.test.ts` / `*.smoke.test.ts`, uses mocked tmux/VCS and per-test temp DBs |
|
|
1059
|
-
| Stress / flake audit | `npm run test:stress`; repeats the full suite with per-run logs/timeouts, optionally in parallel waves (`MU_TEST_STRESS_MODE=parallel`) |
|
|
1060
|
-
| End-to-end | `test/acceptance.integration.test.ts` — the canonical 10-task / 3-agent demo |
|
|
1061
|
-
|
|
1062
|
-
Two fixture traps: a convergence fixture must share ONE creation op
|
|
1063
|
-
(two independent `task add`s for the same id make the later creation
|
|
1064
|
-
legitimately win every field), and an ordering-sensitive test must
|
|
1065
|
-
RENAME segments to pin ingest order, since peers are discovered by
|
|
1066
|
-
`localeCompare` over random-UUID filenames.
|
|
1067
|
-
|
|
1068
|
-
Flake lessons: treat pass-alone/fail-under-load cases as
|
|
1069
|
-
concurrency bugs first; use retrying temp-dir cleanup for VCS fixtures
|
|
1070
|
-
whose subprocesses keep files alive briefly; drive wait/reaper
|
|
1071
|
-
integration tests from poll-loop seams, not fixed timers; wait for
|
|
1072
|
-
stable Ink output instead of sleeping.
|
|
1073
|
-
|
|
1074
|
-
## Distribution
|
|
1075
|
-
|
|
1076
|
-
Single npm package `mu` (see `package.json`):
|
|
1077
|
-
|
|
1078
|
-
- `dist/cli.js` — CLI entry, executable (`bin: { mu: ./dist/cli.js }`; shebang preserved by `tsup`)
|
|
1079
|
-
- `dist/index.js` + `dist/index.d.ts` — programmatic API + types for SDK callers
|
|
1080
|
-
- `skills/mu/SKILL.md` — bundled skill (the only non-`dist` asset shipped)
|
|
1081
|
-
|
|
1082
|
-
`tsup` bundles three entries: `index` and `cli` from `src/`, and
|
|
1083
|
-
`extension/mu-pi` (the pi extension `mu link pi` points at). No runtime
|
|
1084
|
-
build step on the user's machine. pi stays a peer dep — the extension
|
|
1085
|
-
types pi's API structurally — and there is no bundled `agents/*.md` or `prompts/*.md`:
|
|
1086
|
-
per-role agent guidance lives in the user's project repo.
|
|
1087
|
-
|
|
1088
|
-
The dependency list lives in `package.json`; the rule for adding
|
|
1089
|
-
new ones is the anti-feature pledge in
|
|
1090
|
-
[ROADMAP.md § Anti-feature pledges](ROADMAP.md#anti-feature-pledges).
|
|
186
|
+
| Seam | Add one by |
|
|
187
|
+
| --- | --- |
|
|
188
|
+
| `MuxBackend` | implementing the interface in `src/mux/`; a backend without an equivalent no-ops, and a backend-only feature is an optional method callers test for |
|
|
189
|
+
| `VcsBackend` | implementing `detect`, `createWorkspace`, `freeWorkspace`, `isClean`, `commitsBehind`, `rebaseTo`, `commitsSinceBase`, `recentCommits`, `showCommit` (about 80-150 LOC) |
|
|
190
|
+
| typed verb | an SDK function; a `cmd<Verb>` in `src/cli/<namespace>.ts`; one Commander block in `buildProgram()` wrapped in `handle()` and routed through `printNextSteps` |
|
|
191
|
+
| schema change | bump `CURRENT_SCHEMA_VERSION` in `src/db.ts` and mirror `CURRENT_SCHEMA`; startup stays migration-free ([scripts/README.md](../scripts/README.md)) |
|
|
192
|
+
| syncable field | add the column to a portable table, extend the UPDATE trigger's changed-column comparison, confirm apply writes it; a full-row payload regresses merge to row-level LWW |
|
|
193
|
+
| cross-machine sync | nothing: every invocation already flushes and ingests |
|