@martintrojer/mu 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +188 -44
- package/README.md +80 -25
- package/dist/cli.js +16743 -7931
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1844 -1677
- package/dist/index.js +4992 -3667
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +302 -59
- package/docs/HANDOVER.md +461 -0
- package/docs/ROADMAP.md +198 -497
- package/docs/USAGE_GUIDE.md +545 -223
- package/docs/VISION.md +48 -4
- package/docs/VOCABULARY.md +39 -8
- package/docs/img/tui-dashboard.png +0 -0
- package/package.json +12 -12
- package/skills/mu/SKILL.md +278 -469
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -53,7 +53,8 @@ just a fancier agent runner.
|
|
|
53
53
|
`ROI = impact / effort` drives prioritization.
|
|
54
54
|
- **One edge type**: `blocks`. `A → B` means A must close before B can
|
|
55
55
|
start. Multiple edge types create ambiguity that defeats the purpose.
|
|
56
|
-
- **Status lifecycle**: `OPEN → IN_PROGRESS → CLOSED
|
|
56
|
+
- **Status lifecycle**: `OPEN → IN_PROGRESS → CLOSED`, with
|
|
57
|
+
`REJECTED` and `DEFERRED` as terminal still-blocking outcomes.
|
|
57
58
|
- **Notes** are append-only per task; survive across LLM sessions and
|
|
58
59
|
agent restarts. The fix for context loss at the *task* level rather
|
|
59
60
|
than the agent level.
|
|
@@ -71,8 +72,9 @@ separate query layer.
|
|
|
71
72
|
|
|
72
73
|
### Parallel-track detection (the killer feature)
|
|
73
74
|
|
|
74
|
-
`mu
|
|
75
|
-
subtrees that can be assigned to
|
|
75
|
+
The Tracks section in `mu state` / bare `mu` runs union-find on the
|
|
76
|
+
graph to identify independent subtrees that can be assigned to
|
|
77
|
+
different agents in parallel.
|
|
76
78
|
|
|
77
79
|
**Diamond patterns get merged automatically.** If two roots share a
|
|
78
80
|
prerequisite, they collapse into one track — preventing two agents
|
|
@@ -110,9 +112,10 @@ config identity: the agent doesn't have to know its own name.
|
|
|
110
112
|
|
|
111
113
|
### Scoped subtree views
|
|
112
114
|
|
|
113
|
-
`mu task <id>`
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
`mu task tree <id>` and task queries show the portion of the graph
|
|
116
|
+
reachable from a task. This enables recursive delegation: a
|
|
117
|
+
sub-orchestrator agent can inspect only its slice of the graph without
|
|
118
|
+
asking an LLM to infer the scope.
|
|
116
119
|
|
|
117
120
|
### Why this is in the core
|
|
118
121
|
|
|
@@ -160,12 +163,15 @@ independent tmux sessions, fully isolated.
|
|
|
160
163
|
`MU_SESSION=<name>`.
|
|
161
164
|
- **Subsequent operations** in the same shell (or any child shell with
|
|
162
165
|
`MU_SESSION_ID` set) target the same session.
|
|
163
|
-
- **`
|
|
164
|
-
|
|
166
|
+
- **`tmux attach -t mu-<workstream>`** → attach to the whole
|
|
167
|
+
workstream's tmux session
|
|
168
|
+
- **`mu agent attach <agent>`** → print the agent's scrollback plus
|
|
169
|
+
the one-paste tmux attach command for that pane
|
|
165
170
|
- **`mu agent list`** shows only the current workstream's agents by default
|
|
166
|
-
- **`mu agent list
|
|
167
|
-
|
|
168
|
-
|
|
171
|
+
- **`mu agent list`** is scoped to one workstream; list workstreams first,
|
|
172
|
+
then run `mu agent list -w <workstream>` for the scope you want
|
|
173
|
+
- **`session_id`** is the partition key on the `agents` table; agent-list
|
|
174
|
+
queries filter to the active workstream
|
|
169
175
|
- **`mu doctor`** warns about cross-session pollution (orphan panes,
|
|
170
176
|
ghost rows, agents whose tmux session no longer exists)
|
|
171
177
|
|
|
@@ -200,34 +206,244 @@ tmux protocol.)
|
|
|
200
206
|
|
|
201
207
|
---
|
|
202
208
|
|
|
203
|
-
##
|
|
209
|
+
## Dual-audience CLI contract
|
|
210
|
+
|
|
211
|
+
The top-level `mu` binary serves two audiences without creating a
|
|
212
|
+
second namespace.
|
|
213
|
+
|
|
214
|
+
- **Human entrypoint:** bare `mu` launches the read-only TUI when
|
|
215
|
+
`process.stdout.isTTY === true`. It loads every workstream on the
|
|
216
|
+
machine and chooses the initial active tab with the shared focus
|
|
217
|
+
ladder (`$MU_SESSION` → tmux session name → cwd inside a workspace
|
|
218
|
+
→ cwd equal to a workspace's VCS-derived project root, with latest
|
|
219
|
+
activity breaking project-root ties → tab 0). If no
|
|
220
|
+
workstreams exist, it prints `mu --help` plus the one-paste
|
|
221
|
+
`Get started: mu workstream init <name>` hint and exits 0.
|
|
222
|
+
- **Agent / script entrypoint:** typed verbs remain the API, with
|
|
223
|
+
`--json` on reads and structured errors. Bare `mu` on non-TTY
|
|
224
|
+
stdout (pipes, redirects, CI, most harnessed agent calls) prints
|
|
225
|
+
help instead of entering Ink. `MU_NO_TUI=1` forces that same path
|
|
226
|
+
for scripted use inside an otherwise-interactive terminal.
|
|
227
|
+
- **Back-compat:** `mu state` remains the static state card, and
|
|
228
|
+
`mu state --tui` remains an explicit TUI selector. The split is
|
|
229
|
+
stdout-is-TTY plus the opt-out env var, not a separate
|
|
230
|
+
human-vs-agent command namespace.
|
|
231
|
+
|
|
232
|
+
The TUI import stays dynamic (`await import("./cli/tui/index.js")` or
|
|
233
|
+
the sibling state-module equivalent). No module outside
|
|
234
|
+
`src/cli/tui/` may statically import ink/react; this prevents the
|
|
235
|
+
static CLI bundle from pulling the TUI graph into help/version/json
|
|
236
|
+
paths and preserves the ROADMAP render-layer pledge.
|
|
204
237
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## TUI architecture
|
|
241
|
+
|
|
242
|
+
The TUI is a 10-card live-updating dashboard built on `ink` (React
|
|
243
|
+
for the terminal). It is mu's flagship human surface, but it is
|
|
244
|
+
**read-only** and lives entirely under `src/cli/tui/` — the static
|
|
245
|
+
CLI verbs remain the canonical mutation API. The TUI yanks `mu`
|
|
246
|
+
commands; the operator runs them.
|
|
247
|
+
|
|
248
|
+
### Cluster shape
|
|
249
|
+
|
|
250
|
+
`src/cli/tui/` is the only place ink/react are imported. The cluster
|
|
251
|
+
role-by-role:
|
|
208
252
|
|
|
209
253
|
```
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
254
|
+
src/cli/tui/
|
|
255
|
+
├── index.ts # runTui entrypoint; alt-screen + mouse-mode lifecycle
|
|
256
|
+
├── escapes.ts # pure ANSI byte sequences (alt-screen, SGR mouse mode)
|
|
257
|
+
├── app.tsx # <App> root: popup state machine, global keymap, tabs
|
|
258
|
+
├── state.ts # useDashboardSnapshot poll-loop hook (fast/slow tier split)
|
|
259
|
+
├── keys.ts # pure dispatchGlobalKey + dispatchPopupKey + shouldSwallowGlobalKey
|
|
260
|
+
├── keymap-spec.ts # canonical keymap source-of-truth (drives help overlay + dispatch)
|
|
261
|
+
├── mouse.ts # vendored SGR mouse parser + double-click + useMouse hook
|
|
262
|
+
├── yank.ts # clipboard probe + write (pbcopy/wl-copy/xclip/xsel/clip.exe + OSC-52)
|
|
263
|
+
├── tuicr.ts # `t` shortcut: alt-screen handoff to tuicr -r <sha>
|
|
264
|
+
├── layout.ts # responsive multi-column dashboard + per-card row budgets
|
|
265
|
+
├── columns.ts # column-aligned row layout with protect/clip clipping
|
|
266
|
+
├── wrap-ansi.ts # ANSI-aware visual-width line wrapper + SGR close-on-end
|
|
267
|
+
├── glyphs.ts # superscript digit + status glyphs
|
|
268
|
+
├── format-helpers.ts # shared TUI formatters (relTime, sinceClaim, ROI)
|
|
269
|
+
├── titled-box.tsx # rounded border with section-header / bottomLabel inset
|
|
270
|
+
├── popup-shell.tsx # popup outer chrome (cyan TitledBox)
|
|
271
|
+
├── list-row.tsx # centralised non-selected row primitive
|
|
272
|
+
├── padded-rows.tsx # per-card body padder
|
|
273
|
+
├── status-bar.tsx # bottom status bar (mode + active ws + tick + footer flash)
|
|
274
|
+
├── tab-strip.tsx # multi-workstream tab switcher (N≥2)
|
|
275
|
+
├── tab-strip-layout.ts # pure window-around-active layout helper
|
|
276
|
+
├── help.tsx # ?/F1 keymap overlay (scrollable on short panes)
|
|
277
|
+
├── use-popup-filter.tsx # shared '/' substring filter hook + applyFilter + FilterPrompt
|
|
278
|
+
├── use-status-filter.tsx # task-status toggles (o/i/c/r/d) for task-list popups
|
|
279
|
+
├── use-notes-drill.ts # shared notes-drill memo (5 task popups consume it)
|
|
280
|
+
├── use-popup-action-queue.ts # consume mouse PopupAction queue once per render
|
|
281
|
+
├── cards/ # 10 dashboard glance cards (one slot each)
|
|
282
|
+
│ ├── _placeholder.tsx # shared loading/empty body wrapper
|
|
283
|
+
│ └── {agents,tracks,ready,log,workspaces,inprogress,blocked,recent,commits,doctor}.tsx
|
|
284
|
+
└── popups/ # fullscreen drill-down popups
|
|
285
|
+
├── {agents,tracks,ready,log,workspaces,inprogress,blocked,recent,commits,doctor}.tsx
|
|
286
|
+
├── dag.tsx # keybind-only on `g`: full task DAG forest
|
|
287
|
+
├── all-tasks.tsx # keybind-only on `t`: sortable / filterable list of every task
|
|
288
|
+
├── drill.tsx # DrillScrollView + useDrillKeymap (shared scrollable-text leaf)
|
|
289
|
+
├── task-detail.tsx # TaskDetailDrill (notes timeline; the recursion sink)
|
|
290
|
+
├── cursor-row.tsx # selected-row primitive (delegated to from list-row)
|
|
291
|
+
├── scroll.ts # pure applyCursor / applyScroll / clampScrollTop / isNavAction
|
|
292
|
+
├── viewport.ts # popupViewport + POPUP_CHROME_ROWS + POPUP_VIEWPORT_FLOOR
|
|
293
|
+
└── show-loader.ts # subprocess-preserving show loader (avoids blank-flash mid-refetch)
|
|
222
294
|
```
|
|
223
295
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
296
|
+
### State machine
|
|
297
|
+
|
|
298
|
+
`<App>` is the root. It owns:
|
|
299
|
+
|
|
300
|
+
- **Popup state** — `null` (dashboard) or one of the popup ids.
|
|
301
|
+
Single-popup invariant; `Esc` / `q` returns to dashboard.
|
|
302
|
+
- **Card visibility** — `Record<CardId, boolean>` toggled by `0`-`9`.
|
|
303
|
+
- **Tick rate** — fast tick interval (1s default; adjustable with
|
|
304
|
+
`+` / `-` / `=` / `0`).
|
|
305
|
+
- **Active workstream tab** — index into the resolved workstream
|
|
306
|
+
set; `Tab` / `Shift-Tab` cycles when N≥2.
|
|
307
|
+
- **Footer flash** — transient status-bar message (yank confirm,
|
|
308
|
+
tuicr exit, etc.).
|
|
309
|
+
|
|
310
|
+
Popups own their own local state (cursor, filter query, drill mode,
|
|
311
|
+
local modes like Workspaces' `list` / `commits` / `show`). Popups
|
|
312
|
+
NEVER mutate App-level state — they receive a read-only props bag
|
|
313
|
+
(`snapshot`, `db`, `workstream`, `fastTickNonce`, `slowTickNonce`,
|
|
314
|
+
`yank`, `onClose`, `onModeChange`, `onFilterEditingChange`,
|
|
315
|
+
`onFooter`).
|
|
316
|
+
|
|
317
|
+
### Polling tiers (fast vs slow)
|
|
318
|
+
|
|
319
|
+
The poll loop in `state.ts` (`useDashboardSnapshot`) splits work
|
|
320
|
+
into two intervals:
|
|
321
|
+
|
|
322
|
+
- **Fast tick** (default 1s, adjustable): SQL-only. `loadWorkstreamSnapshotFast`
|
|
323
|
+
reads tasks, tracks, workspace registry rows, recent events,
|
|
324
|
+
workspace orphans. Cheap (~p50 <1ms).
|
|
325
|
+
- **Slow tick** (10s, hardcoded `SLOW_TICK_MS`): subprocess-backed.
|
|
326
|
+
`loadWorkstreamSnapshotSlow` runs tmux liveness, per-workspace
|
|
327
|
+
dirty status, recent project commits, and the Doctor summary.
|
|
328
|
+
Expensive (~p50 hundreds of ms).
|
|
329
|
+
|
|
330
|
+
The last slow result is merged into every fast render via
|
|
331
|
+
`mergeSnapshotFastSlow` so cards never flicker through a loading
|
|
332
|
+
state. `r` / `F5` triggers both intervals immediately. Workstream
|
|
333
|
+
tab switch clears the slow cache and eager-fetches the new
|
|
334
|
+
workstream so cards are fresh within 1s of switching.
|
|
335
|
+
|
|
336
|
+
A pure `snapshotKey` / `snapshotKeyString` re-render guard returns
|
|
337
|
+
the SAME `data` reference across no-op ticks so React's diffing
|
|
338
|
+
short-circuits cleanly.
|
|
339
|
+
|
|
340
|
+
### Render geometry
|
|
341
|
+
|
|
342
|
+
Responsive layout lives in `layout.ts`:
|
|
343
|
+
|
|
344
|
+
- **Breakpoint-driven columns**: stacked below 120 cols; 2 columns
|
|
345
|
+
at 120; 3 at 180; 4 at 240. Stream cards (Commits, Activity log)
|
|
346
|
+
trail; slot 0 (Commits) trails last.
|
|
347
|
+
- **Per-card row budgets**: each visible card gets a `min` /
|
|
348
|
+
`max` / `chrome` budget; the allocator distributes available
|
|
349
|
+
rows so a noisy list can't crowd siblings. Overflow surfaces as
|
|
350
|
+
`+N more · Shift+N` inset into the card's bottom border.
|
|
351
|
+
- **Cull-on-tight-pane**: when even minimum budgets don't fit,
|
|
352
|
+
cull cards by priority (Doctor → Recent → Workspaces → …) and
|
|
353
|
+
show `+N cards hidden · resize taller` at the bottom. Outer
|
|
354
|
+
height clip is the safety net.
|
|
355
|
+
|
|
356
|
+
Text rendering is ANSI-aware: `wrap-ansi.ts` wraps by visual width
|
|
357
|
+
(via `string-width`) and closes any open SGR state on the early-
|
|
358
|
+
return + end-of-loop paths so coloured fragments without trailing
|
|
359
|
+
`\x1b[0m` can't bleed into adjacent ink chrome cells. Drill bodies
|
|
360
|
+
are also space-padded to exact box width so ink's `wrap="truncate"`
|
|
361
|
+
ANSI miscount can't eat the trailing right-border glyph.
|
|
362
|
+
|
|
363
|
+
### Read-only invariant + the `tuicr` escape
|
|
364
|
+
|
|
365
|
+
Every popup row exposes one canonical `mu` command via `y`. `yank.ts`
|
|
366
|
+
probes for a clipboard backend (pbcopy / wl-copy / xclip / xsel /
|
|
367
|
+
clip.exe) and falls back to OSC-52 over stderr if none is found.
|
|
368
|
+
The command goes to the clipboard; the operator runs it.
|
|
369
|
+
|
|
370
|
+
The one user-driven escape is `t` inside any `git show` drill:
|
|
371
|
+
`tuicr.ts` writes `ALT_SCREEN_EXIT` + the SGR mouse-mode disable
|
|
372
|
+
bytes, exec's `tuicr -r <sha>` in the project root / workspace cwd
|
|
373
|
+
as a foreground subprocess, then on exit writes `ALT_SCREEN_ENTER`
|
|
374
|
+
+ mouse-mode-enable and the dashboard re-renders. This is a
|
|
375
|
+
deliberate handoff, not an in-process mutation.
|
|
376
|
+
|
|
377
|
+
The read-only pledge is in `docs/ROADMAP.md`'s anti-feature list;
|
|
378
|
+
any future TUI gesture that wants to mutate state must file a
|
|
379
|
+
roadmap entry first.
|
|
380
|
+
|
|
381
|
+
### Mouse + keyboard
|
|
382
|
+
|
|
383
|
+
Mouse support is opt-in via SGR mouse mode (`escapes.ts` provides
|
|
384
|
+
the enable/disable bytes). `mouse.ts` parses `ESC[<button;x;y;M/m`
|
|
385
|
+
from stdin, detects double-clicks, and exposes a `useMouse()` hook.
|
|
386
|
+
|
|
387
|
+
Keyboard dispatch flows through pure helpers in `keys.ts`:
|
|
388
|
+
`dispatchGlobalKey` (dashboard mode), `dispatchPopupKey` (popup
|
|
389
|
+
mode), and `shouldSwallowGlobalKey` (which keys popups consume
|
|
390
|
+
and do not bubble to the global dispatcher). The keymap source-of-
|
|
391
|
+
truth lives in `keymap-spec.ts` so the help overlay and the
|
|
392
|
+
dispatcher can never drift apart.
|
|
393
|
+
|
|
394
|
+
Double-click on a card emits `{kind: "setCursor", index}` followed
|
|
395
|
+
by `{kind: "drill"}` through `use-popup-action-queue.ts`, which
|
|
396
|
+
consumes one action per render (so the cursor update lands before
|
|
397
|
+
the drill resolves the focused row).
|
|
398
|
+
|
|
399
|
+
### Drill recursion
|
|
400
|
+
|
|
401
|
+
List popups drill via `Enter` into entity-specific leaves. The
|
|
402
|
+
central primitive is `popups/drill.tsx`'s `DrillScrollView` (a
|
|
403
|
+
scrollable text leaf shared by Workspaces' git-show, Agents'
|
|
404
|
+
scrollback, the Activity log payload drill, and the Doctor
|
|
405
|
+
remediation drill). Task popups drill into
|
|
406
|
+
`popups/task-detail.tsx`'s `TaskDetailDrill` (the notes timeline);
|
|
407
|
+
the Tracks popup chains track → task list → TaskDetailDrill via
|
|
408
|
+
the same leaf.
|
|
409
|
+
|
|
410
|
+
`useDrillKeymap` owns the scroll state, accepts an optional
|
|
411
|
+
`resetKey` (so identity-change resets scroll while tick-driven body
|
|
412
|
+
refreshes preserve it), an optional `onScrollChange` callback (so
|
|
413
|
+
the DAG popup's focused-root tracking stays in lockstep), and
|
|
414
|
+
shares ANSI-aware wrapped body metadata so the scroll-clamp math
|
|
415
|
+
and the painter can't desync.
|
|
416
|
+
|
|
417
|
+
Subprocess-backed drills (Workspaces git-show, Agents scrollback,
|
|
418
|
+
Commits show) use `popups/show-loader.ts` which preserves the
|
|
419
|
+
prior body during a refetch — no blank-flash flicker on the slow
|
|
420
|
+
tick.
|
|
421
|
+
|
|
422
|
+
### Test seam
|
|
423
|
+
|
|
424
|
+
TUI behaviour testing is documented in `test/README.md`. The
|
|
425
|
+
seam is `test/_ink-render.ts`'s `createInkInputStream` +
|
|
426
|
+
`createInkCaptureStream` + `simulateInput` + `latestRenderedFrame`.
|
|
427
|
+
Mount a popup or `<App>` into a CaptureStream, drive keystrokes,
|
|
428
|
+
assert against the visible frame and spy callbacks. Source-greps
|
|
429
|
+
are reserved for narrow structural guards (App ↔ keys ↔ layout
|
|
430
|
+
wiring; slot ↔ keymap glue) — not for behaviour.
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## CLI / SDK surface
|
|
435
|
+
|
|
436
|
+
Every user-visible operation is a typed SDK function plus a thin
|
|
437
|
+
Commander wrapper. The CLI wiring in `src/cli.ts` and the verb
|
|
438
|
+
namespace files under `src/cli/` are the canonical verb surface;
|
|
439
|
+
there is no generated registry layer, DSL, or separate operation
|
|
440
|
+
schema. Programmatic callers import the same SDK functions from
|
|
441
|
+
`src/index.ts`, while agents/scripts compose CLI verbs with `--json`.
|
|
227
442
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
443
|
+
The boundary rule is: external surfaces accept operator-facing names
|
|
444
|
+
(`workstream`, task id, agent name); internal helpers resolve those to
|
|
445
|
+
surrogate INTEGER ids once and then stay on ids. See
|
|
446
|
+
[§ Surrogate-PK + SDK-boundary discipline](#surrogate-pk--sdk-boundary-discipline-load-bearing).
|
|
231
447
|
|
|
232
448
|
---
|
|
233
449
|
|
|
@@ -268,35 +484,48 @@ Key properties:
|
|
|
268
484
|
|
|
269
485
|
## Modules (actual src/ layout)
|
|
270
486
|
|
|
271
|
-
Mostly-flat `src/`:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
487
|
+
Mostly-flat `src/`: root `.ts` modules plus cohesive subclusters
|
|
488
|
+
(`src/agents/`, `src/tasks/`, and `src/cli/` wrappers with their own
|
|
489
|
+
`src/cli/tasks/` and `src/cli/tui/` sub-clusters). No `core/`
|
|
490
|
+
subdirectory; no anticipatory layering. Subclusters obey the
|
|
491
|
+
AGENTS.md rule: cluster files import from neighbours and root
|
|
492
|
+
substrate modules, never from the hub they're re-exported through.
|
|
276
493
|
Each module is concrete and consumed today.
|
|
277
494
|
|
|
278
495
|
| Module | Responsibility |
|
|
279
496
|
| --------------------- | ----------------------------------------------------------------------------------------- |
|
|
280
|
-
| `src/db.ts` | SQLite (better-sqlite3) connection, WAL mode, schema (
|
|
497
|
+
| `src/db.ts` | SQLite (better-sqlite3) connection, WAL mode, schema (16 tables + 3 views, **schema v8** — v5 surrogate-INTEGER-PK substrate, v6's 5 additive `archive_*` tables, v7's drop of `approvals`, v8's additive `machine_identity` + `workstream_sync` sync substrate), default paths, `resolveWorkstreamId` (the SDK boundary's first leg). `openDb` refuses pre-v5 DBs loudly; v5+ DBs are brought to the current idempotent schema shape by `applySchema` (including v7's `DROP TABLE IF EXISTS approvals`) and `openDb` seeds `machine_identity` on open. |
|
|
281
498
|
| `src/tmux.ts` | Single tmux executor wrapper, send protocol (bracketed-paste), pane validation |
|
|
282
499
|
| `src/detect.ts` | Pi-only status detector (`busy` / `needs_input` / `idle` / `done`) |
|
|
283
500
|
| `src/reconcile.ts` | Ghost prune + status detect + orphan surface; "reality wins" |
|
|
284
501
|
| `src/agents.ts` | Hub: CRUD + send / read / list / close / free + liveness + reaper. Re-exports `src/agents/*` (spawn, adopt, errors); pane-title composition (`composeAgentTitle`) lives here. |
|
|
285
502
|
| `src/agents/*.ts` | Cohesive cluster of agent-lifecycle internals: `spawn.ts` (spawnAgent + resolveCliCommand / awaitSpawnLiveness / pane create-or-reuse / prestage / rollback), `adopt.ts` (register an existing tmux pane as a managed agent), `kick.ts` (signal the foreground pgid of an agent pane's TTY — escape hatch for wedged tool subprocesses), `errors.ts` (typed agent error classes — `AgentNotFoundError`, `AgentDiedOnSpawnError`, …). |
|
|
286
|
-
| `src/
|
|
287
|
-
| `src/tasks
|
|
503
|
+
| `src/dag.ts` | Shared DAG read/render helpers: `loadFullDag(db, workstream)` for whole-workstream root+edge forests and pure `renderForest` / `renderTaskTree` ASCII rendering reused by `mu task tree` and the TUI DAG popup. |
|
|
504
|
+
| `src/tasks.ts` | Task SDK hub: re-exports the concrete task-graph cluster so external imports keep using `./tasks.js`; no implementation logic. |
|
|
505
|
+
| `src/tasks/*.ts` | Cohesive cluster of task-graph internals: `core.ts` (row-shape mapping, surrogate-id resolution, `touchTask`), `id.ts` (task-id validation + title slug helpers), `queries.ts` (get/list/ready/blocked/goals/notes/owned/search reads), `edit.ts` (add task/note, update, delete), `edges.ts` (edge reads, cycle check, block/unblock/reparent), `status.ts` (TaskStatus enum + helpers — single source of truth), `sort.ts` (shared task sort keys/comparators for CLI + TUI), `claim.ts` (claim/release + `resolveActorIdentity`, atomic CAS), `lifecycle.ts` (setTaskStatus / closeTask / openTask / rejectTask / deferTask + cascade), `wait.ts` (waitForTasks: block until tasks reach a target status), `errors.ts` (typed task error classes — `TaskAlreadyOwnedError`, `CycleError`, …). Cluster files import neighbours/root substrate modules directly, never the `src/tasks.ts` hub. |
|
|
288
506
|
| `src/tracks.ts` | Parallel-tracks union-find with diamond merge |
|
|
507
|
+
| `src/staleness.ts` | Shared workspace staleness threshold (`WORKSPACE_STALE_THRESHOLD = 10`) and pure `isWorkspaceStale` predicate consumed by static state, the TUI Workspaces card, and dispatch-time warn/refuse checks. |
|
|
289
508
|
| `src/workstream.ts` | ensureWorkstream / list / summarize / destroy / export (thin wrapper around the bucket renderer) |
|
|
290
|
-
| `src/exporting.ts` | Unified bucket renderer for `mu workstream export` and `mu archive export`: per-task markdown + manifest.json (`bucketVersion: 2`); idempotent via per-file sha256; deleted-task preservation banner; refuses pre-0.3 single-source layouts |
|
|
291
|
-
| `src/
|
|
292
|
-
| `src/
|
|
509
|
+
| `src/exporting.ts` | Unified bucket renderer for `mu workstream export` and `mu archive export`: per-task markdown + manifest.json (`bucketVersion: 2`); idempotent via per-file sha256; deleted-task preservation banner; refuses pre-0.3 single-source layouts. Buckets are read-only artifacts for humans / git / docs, not a DB round-trip substrate. |
|
|
510
|
+
| `src/db-sync.ts` | Whole-machine DB sync SDK: `exportDb` (`VACUUM INTO` + manifest), `importDb` (per-workstream drift plan over `machine_identity` + `workstream_sync`; dry-run by default; `--force-source` parks divergence sidecars), manifest/schema validation, workstream copy/replace helpers, typed db-sync errors. |
|
|
511
|
+
| `src/db-sync-replay.ts` | Manual replay planner/applier for divergence sidecars parked by `mu db import --force-source`: selects missing tasks/notes/eligible edges, refuses `local_id` collisions with diverged content, dry-run by default. Re-exported by `src/db-sync.ts` for SDK callers. |
|
|
512
|
+
| `src/archives.ts` | Archive SDK hub: re-exports the concrete `src/archives/` cluster, including restore, so external imports keep using `./archives.js`; no implementation logic. |
|
|
513
|
+
| `src/archives/*.ts` | Cohesive cluster for cross-workstream **archives** — feature complete (SDK + CLI verbs: `mu archive create / list / show / add / restore / remove / delete`, plus `search` and read-only `export` via the unified bucket renderer): `core.ts` (label validation, row types, typed archive errors, id resolution/summarise helpers), `query.ts` (`createArchive`, `listArchives`, `getArchive`, `listArchivedTasks`, `searchArchives`), `addremove.ts` (`addToArchive` idempotent at `(archive, source_workstream)`, `removeFromArchive`), `restore.ts` (`restoreArchive` lossless un-archive into a fresh workstream), `delete.ts` (`deleteArchive`). Backed by the v6 `archives` + `archived_tasks` + `archived_edges` + `archived_notes` + `archived_events` tables; archives outlive workstreams (TEXT `source_workstream` columns, no FK). Cluster files import neighbours/root substrate modules directly, never the `src/archives.ts` hub. |
|
|
514
|
+
| `src/archives/restore.ts` | Lossless un-archive implementation: validates `--source` when an archive has multiple source workstreams, refuses `--as` collisions through workstream creation, snapshots before writing, copies archived tasks/edges/notes directly from `archived_*` rows, and emits an archive-restore event. Does not restore agents, workspace paths, or the live `agent_logs` stream. |
|
|
293
515
|
| `src/logs.ts` | `agent_logs` SDK: appendLog / listLogs / latestSeq / emitEvent |
|
|
294
|
-
| `src/vcs.ts` |
|
|
295
|
-
| `src/
|
|
296
|
-
| `src/
|
|
516
|
+
| `src/vcs.ts` | VCS SDK hub: re-exports the concrete `src/vcs/` cluster so external imports keep using `./vcs.js`; no implementation logic. |
|
|
517
|
+
| `src/vcs/*.ts` | Cohesive cluster of VCS backends: `types.ts` (`VcsBackend` interface, result shapes, typed workspace errors, show-output cap), `helpers.ts` (exec/probe/run/show/commit-summary parsing helpers), `git.ts`, `jj.ts`, `sl.ts`, and `none.ts` (one concrete backend per file), `index.ts` (detection precedence dispatcher: `jj root` → `sl root` → `git rev-parse --show-toplevel` → none; `backendByName`). Backend methods cover `commitsBehind(workspacePath, ref)` for staleness (no auto-fetch; pure observation), `recentCommits(projectRoot, limit)` + `showCommit(projectRoot, sha)` for the TUI Commits card/popup, and `isClean(workspacePath)` for `closeAgent`'s clean-workspace auto-free path. Cluster files import neighbours/root substrate modules directly, never the `src/vcs.ts` hub. |
|
|
518
|
+
| `src/workspace.ts` | Workspace SDK hub: re-exports the concrete `src/workspace/` cluster so external imports keep using `./workspace.js`; no implementation logic. |
|
|
519
|
+
| `src/workspace/*.ts` | Cohesive cluster for per-agent VCS workspaces (registry layer on top of `vcs.ts`): `core.ts` (row shapes, path helpers, typed workspace errors), `crud.ts` (create/get/list/free/refresh/commits/clean checks), `decorate.ts` (staleness + dirty decoration), `orphans.ts` (per-workstream and all-workstream orphan-dir detection), `recreate.ts` (free+create between-wave verb). Cluster files import neighbours/root substrate modules directly, never the `src/workspace.ts` hub. |
|
|
520
|
+
| `src/snapshots.ts` | Snapshot SDK hub: re-exports the concrete `src/snapshots/` cluster so external imports keep using `./snapshots.js`; no implementation logic. |
|
|
521
|
+
| `src/snapshots/*.ts` | Cohesive cluster for whole-DB snapshots (`VACUUM INTO`): `core.ts` (row shapes, typed snapshot/prune errors, GC env readers, paths, size/version helpers), `capture.ts` (capture/list/auto-GC), `restore.ts` (`mu undo` restore file-swap), `prune.ts` (manual prune/delete cleanup verbs). The `snapshots` table is schema v4 (carried forward unchanged through v5/v6/v7/v8). Cluster files import neighbours/root substrate modules directly, never the `src/snapshots.ts` hub. |
|
|
297
522
|
| `src/output.ts` | NextStep type + `printNextSteps` + `errorNextSteps` plumbing for self-documenting output |
|
|
523
|
+
| `src/state.ts` | SDK seam for the `mu state` verb. `loadWorkstreamSnapshotFast(db, ws, opts?)` is the pure-SQL tier used by the TUI's 1s fast tick (tracks, task slices, workspace registry rows, workspace orphans, recent events; subprocess fields empty). `loadWorkstreamSnapshotSlow(db, ws, opts?)` is the subprocess tier (tmux-derived `view`, workspace dirty flags, recent project commits/backend, Doctor summary). `mergeSnapshotFastSlow` overlays the last slow result onto each fast result, and `loadWorkstreamSnapshot(db, ws, opts?)` stays as a back-compat wrapper that composes both tiers for static/non-TUI callers. Opt-in flags: `withDirty` (slow-tier dirty flag), `withDoctor` (Doctor summary), `withRecentCommits` (Commits card/popup), `withAllTasks` (legacy/full-snapshot all-task list; the TUI all-tasks popup can read SQLite directly while open). Plus pure derivation helpers: `agentStatusHistogram(agents)`, `summarizeOwnedTasks(owned)`, `roiBucket(impact, effortDays)`. |
|
|
524
|
+
| `src/doctor-summary.ts` | TUI-friendly slice of `mu doctor`'s checks. `loadDoctorSummary(db, snapshot)` returns a `DoctorSummary` (`{ checks: DoctorCheck[], problemCount }`) using only synchronous DB pragmas + COUNT-shape SELECTs and snapshot-derived counts (ghosts / orphan panes / orphan workspace dirs) — cheap enough for the per-tick poll-loop the TUI's slot-9 Doctor card runs on. `loadDoctorChecks(db, snapshot)` is a thin wrapper that returns the full check array (OK + warn + fail) for the slot-9 Doctor popup, which renders every row rather than just the non-OK subset. Also home to the per-check remediation helpers `yankCommandForCheck(check)` (informational SELECT-shape verb to yank for the focused row, with a `# ...` comment fallback for schema-shape checks) and `remediationParagraph(check)` (multi-line prose explaining the failure shape) — both pure, both re-exported from `src/index.ts`, both consumed by the slot-9 popup's drill view but living next to `DoctorCheck` so adding a new check is a single touchpoint. The textual `mu doctor` verb (`src/cli/doctor.ts`) keeps its own renderer; this is the data seam consumed by the dashboard. |
|
|
298
525
|
| `src/cli.ts` | commander entry; `buildProgram()` (re-exports `format`/`handle` symbols for back-compat with existing import sites). |
|
|
299
|
-
| `src/cli
|
|
526
|
+
| `src/cli/db.ts` | Thin commander/renderer for `mu db export / import / replay`: summary tables, dry-run vs apply Next steps, `--only-ws` repeated-or-comma parsing, and JSON envelopes over the `src/db-sync.ts` SDK. |
|
|
527
|
+
| `src/cli/*.ts` | one file per verb-namespace; thin wrappers over the SDK; `--json` rendering for every read verb. Currently: `workstream.ts`, `agents.ts`, `tasks.ts`, `workspace.ts`, `log.ts`, `archive.ts`, `db.ts` (whole-machine sync), `state.ts` (canonical static state card + explicit `--tui` back-compat dispatch; bare `mu` TTY routing lives in `src/cli.ts` so it can inspect the root argv/TTY seam), `tui-launch-focus.ts` (pure shared initial-tab focus ladder for bare `mu` and `mu state --tui`: `$MU_SESSION`, tmux session, cwd inside workspace, cwd at VCS-derived project root with latest-activity tie-break, tab 0), `snapshot.ts`, `sql.ts`, `doctor.ts`. Two non-verb cluster-mates carry the rendering + error-handling primitives that every verb wrapper imports: `format.ts` (table renderers, status colourers, `truncate`/`relTime`) and `handle.ts` (typed-error → exit-code map + the `handle()` wrapper). Imports flow cluster → root (never the other way). |
|
|
528
|
+
| `src/cli/tui/*.tsx` | Cohesive cluster of the interactive ink-based TUI (`mu state --tui`). Lazy-imported by `src/cli/state.ts` so non-TUI verbs avoid the ink/react cost. Per-file: `index.ts` (runTui entrypoint; writes the alt-screen enter/exit sequences from `escapes.ts` around the ink render and enables/disables mouse mode in the same finally-guarded lifecycle), `escapes.ts` (pure ANSI escape constants `ALT_SCREEN_ENTER`/`ALT_SCREEN_EXIT` plus SGR mouse-mode enter/exit bytes — no ink/react imports so unit tests can assert exact bytes without booting a renderer), `mouse.ts` (tiny vendored SGR mouse layer: enable/disable helpers, stdin parser for `ESC[<button;x;y;M/m`, double-click detector, and `useMouse()` hook), `app.tsx` (root `<App>` with popup state machine + global keymap dispatch + footer + tick state + active-workstream-tab state per feat_tui_multi_workstream), `state.ts` (poll-loop hook `useDashboardSnapshot` split into a fast SQL-only interval controlled by `tickMs` and a hardcoded `SLOW_TICK_MS = 10_000` subprocess interval; cached slow fields are merged into every fast render, `r`/F5 triggers both intervals immediately, and workstream switches clear the slow cache then eager-fetch the new workstream; plus pure `snapshotKey`/`snapshotKeyString` re-render guard so the hook returns the SAME `data` reference across no-op ticks; `lastTickMs` lives in its own useState so its tick-rate display can update without dragging the cards along; plus `clampTick`/`fasterTick`/`slowerTick` constants), `keys.ts` (pure `dispatchGlobalKey` + `dispatchPopupKey` keymap dispatchers), `yank.ts` (clipboard probe + write: pbcopy/wl-copy/xclip/xsel/clip.exe + OSC-52 fallback), `list-row.tsx` (`<ListRow>` — the centralised non-selected row primitive every popup/card consumes per feat_centralize_list_row_render; owns four invariants in one place: outer `<Box width={contentWidth}>` pin, canonical `COL_GUTTER`-spaced cells, `wrap="truncate"` on the outer `<Text>`, and selected→`<CursorRow>` delegation. Per-cell colours pass in declaratively as a `colors` array sibling of `COLUMN_SPECS`. Replaces 18 near-identical hand-rolled row JSX blocks across `popups/*.tsx`+`cards/*.tsx`; the test/tui-card-render-width.test.ts invariant is now "every renderRow consumer routes through ListRow OR CursorRow" — enforced by static-source assertions so a future popup author can't drift the gutter, forget the width pin, or skip wrap=truncate), `titled-box.tsx` (rounded-border primitive with section header inset into the top border; optional `bottomLabel` prop insets a `+M more · Shift+N` truncation hint into the BOTTOM border line per feat_card_footer_inset, suppressing the inner Box's bottom edge — the geometry is shared with the top-border path via the pure `computeBorderRowDashes` helper), `layout.ts` (pure responsive-dashboard helpers: breakpoint-driven pair-aware card columns plus per-card row-budget allocation with min/max/chrome config; columns use slot-stable ordering, slot 0 trails, and the 2-column layout splits stream cards as bottom trailers to keep the all-cards view balanced), `columns.ts` (column-aligned row layout with protect/clip clipping policy; exposes `contentWidthFromCols(cols)` + `termColsForLayout()` helpers — every card/popup feeds the result as `layoutColumns(rows, specs, contentWidth)` so clip cells actually clip instead of overflowing the row to a second line per bug_tui_long_lines_overflow), `help.tsx` (? keymap overlay), `cards/{agents,tracks,ready,log,workspaces,inprogress,blocked,recent,commits,doctor}.tsx` + `cards/_placeholder.tsx` (`<CardPlaceholder>` — shared loading/empty body wrapper invoked as a function so the test walker still sees the underlying TitledBox/PaddedRows; collapses 20 near-identical 10-line `<TitledBox><PaddedRows><Text dimColor>...</Text></PaddedRows></TitledBox>` blocks across the 10 cards per review_tui_card_loading_empty_boilerplate) (10 dashboard glance cards; slot 0 is Commits, slot 5 promoted by feat_card_5_workspaces, slot 6 by feat_card_6_inprogress, slot 7 by feat_card_7_blocked, slot 8 is Recent, slot 9 by feat_card_9_doctor; DAG and all-tasks are keybind-only popup conventions, not cards), `popups/{dag,all-tasks,agents,tracks,ready,log,workspaces,inprogress,blocked,recent,commits,doctor}.tsx` (12 fullscreen drill-down popups; `dag.tsx` is keybind-only on `g` and renders the active workstream's full task-DAG forest; `all-tasks.tsx` is keybind-only on `t`, renders every task as a sortable/filterable list via the shared `use-status-filter.tsx`, and drills into `TaskDetailDrill`; `commits.tsx` is slot-0 via Shift+0 and drills into backend show output; slot-5 popup promoted by feat_popup_5_workspaces, slot-6 by feat_popup_6_inprogress, slot-7 by feat_popup_7_blocked, slot-8 by feat_popup_8_recent (yanks `mu task open <id>`); slot-9 by feat_popup_9_doctor (the Doctor drill is a small ad-hoc detail view via `DrillScrollView`, NOT TaskDetailDrill — rows are doctor checks rather than tasks). All reserved numeric popup slots are now filled), `popups/drill.tsx` (`DrillScrollView` — the scroll-list primitive every popup-drill body shares; re-exports `clampScrollTop` from `popups/scroll.ts` for back-compat), `popups/scroll.ts` (pure `applyCursor` + `applyScroll` + `clampScrollTop` + `isNavAction` — the centralised navigation primitive every popup + drill consumes per feat_centralize_scroll_navigation; replaces ~60 near-duplicate `case "moveDown"/"moveUp"/"jumpTop"/"jumpBottom"/"pageUp"/"pageDown"` switch arms across 9 popups so j/k/g/G/Ctrl-D/U/PgUp/PgDn behave identically in every list-mode AND every drill-mode; pure TS with no ink/react imports, covered by test/tui-scroll.test.ts), `popups/viewport.ts` (pure `popupViewport(rows, chromeOverride?)` + `POPUP_CHROME_ROWS` + `POPUP_VIEWPORT_FLOOR` — each popup reads `useStdout().rows` at render time and calls `popupViewport` to size the body slice; replaces the prior hardcoded `const VIEWPORT = 20` per bug_tui_popup_data_doesnt_fill so the row data inside a `flexGrow={1}` popup Shell actually fills the pane), `popups/task-detail.tsx` (`TaskDetailDrill` — the read-only task-notes leaf consumed by the Tasks popup drill AND by the Tracks-popup `drill → task-detail` chain; future task-list popups under feat_more_cards_umbrella plug in unchanged), `use-popup-filter.tsx` (shared `/` filter state-machine: pure `popupFilterReducer` + `usePopupFilter` hook + `applyFilter<T>(items, query, blobOf)` + `<FilterPrompt>`. Every list popup wires the hook in ~5 LOC and gets the full UX — incremental edit, Enter commit, Esc cancel, status-bar mode flip, no-matches fallback — for free; new card popups under feat_more_cards_umbrella MUST consume it rather than re-implement), `use-status-filter.tsx` (shared task-status toggle hook + `<StatusFilterStrip>` for task-list popups; default all-on, popup-local, mnemonic o/i/c/r/d toggles OPEN / IN_PROGRESS / CLOSED / REJECTED / DEFERRED, no persistence), `use-notes-drill.ts` (shared notes-drill memo — returns the `renderNotes(...)` body string for the focused task only when the popup is in drill mode; per task review_tui_task_popups_duplicated_template the byte-identical useMemo block deduped from all five task-list popups (Tasks/ready, In-progress, Blocked, Recent, All-tasks) so the next task-list popup is a one-line drop-in and the SQL+tick semantics stay in lockstep), `tab-strip.tsx` (`<TabStrip>` — multi-workstream tab switcher rendered above the cards when `<App>` is launched with N≥2 workstreams; bold/cyan + `▸ ` marker for the active tab, dim names + ` · ` separators for the rest, plus a `(Tab / Shift-Tab)` affordance hint; renders nothing for N=1 so the single-ws frame is byte-identical to the pre-multi-ws build; pure presentational — the active index lives in `<App>`, `Tab`/`Shift-Tab` keys come through `dispatchGlobalKey`'s `nextTab`/`prevTab` actions). **The ONLY place ink/react are imported** — enforced by ROADMAP pledge. |
|
|
300
529
|
| `src/cli/tasks/*.ts` | sub-cluster of the `mu task` namespace; `tasks.ts` at the root re-exports only what callers outside the cluster import (`wireTaskCommands`, `cmdMyNext`/`cmdMyTasks`, `unescapeNoteText`). One file per concern: `queries.ts` (list/next/owned-by + the `cmdMyTasks` / `cmdMyNext` helpers that back `mu me tasks` / `mu me next`), `lifecycle.ts` (close/open/reject/defer + cascade preview), `edit.ts` (add/show/notes/note/update + helpers), `edges.ts` (block/unblock/reparent/delete), `claim.ts` (claim/release/wait), `tree.ts` (tree rendering), `wire.ts` (Commander glue). Each file < 600 LOC; the hub is < 35. |
|
|
301
530
|
| `src/index.ts` | SDK entrypoint (re-exports) |
|
|
302
531
|
| `skills/mu/SKILL.md` | Bundled skill teaching the LLM the model + verb list + jq pipelines |
|
|
@@ -312,7 +541,7 @@ Each module is concrete and consumed today.
|
|
|
312
541
|
4. **Executes the operation** — agent ops shell out to tmux (and to
|
|
313
542
|
jj/sl/git for workspaces); task ops are pure SQL.
|
|
314
543
|
5. **Reconciles with reality** — for read-paths that need accuracy
|
|
315
|
-
(`mu agent list`,
|
|
544
|
+
(`mu agent list`, state views), queries tmux for live pane
|
|
316
545
|
state and updates the DB (ghost prune + status detect).
|
|
317
546
|
6. **Auto-emits a `kind='event'` row** to `agent_logs` for any
|
|
318
547
|
state-changing verb, conditional on actual change. `mu log
|
|
@@ -328,11 +557,12 @@ each are deliberately small.
|
|
|
328
557
|
|
|
329
558
|
| Seam | Add a new impl by... |
|
|
330
559
|
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
331
|
-
| `VcsBackend` | Implementing `detect / createWorkspace / freeWorkspace / isClean / commitsBehind / rebaseTo / commitsSinceBase` (~80–150 LOC; jj/sl/git/none are working examples) |
|
|
560
|
+
| `VcsBackend` | Implementing `detect / createWorkspace / freeWorkspace / isClean / commitsBehind / rebaseTo / commitsSinceBase / recentCommits / showCommit` (~80–150 LOC; jj/sl/git/none are working examples) |
|
|
332
561
|
| Per-CLI `Detector` | Adding patterns to `detectPiStatus` (vanilla pi `to interrupt)`; pi-meta + every TUI wrapper covered by Braille spinner glyph fallback `[\u2800-\u28FF]`) |
|
|
333
562
|
| New typed verb | Add an SDK function in the relevant `src/*.ts`; add a `cmd<Verb>` to the matching `src/cli/<namespace>.ts` (or create a new namespace if the verb doesn't fit existing ones); wire one commander block in `src/cli.ts`'s `buildProgram()` (use `handle()` for the exit-code map; route through `printNextSteps` for self-documenting output) |
|
|
334
|
-
| New schema migration| Bump `CURRENT_SCHEMA_VERSION` in `src/db.ts`; mirror the new shape in `CURRENT_SCHEMA`.
|
|
563
|
+
| New schema migration| Bump `CURRENT_SCHEMA_VERSION` in `src/db.ts`; mirror the new shape in `CURRENT_SCHEMA`. Three of the four post-v5 bumps were script-free: v5 → v6 was purely additive (the existing CREATE-TABLE-IF-NOT-EXISTS pass picked up the new `archive_*` tables), v6 → v7 was a destructive-but-idempotent in-place migration (a `DROP TABLE IF EXISTS approvals` block in `applySchema`), and v7 → v8 is additive (`machine_identity`, `workstream_sync`, plus the `openDb` seed for `machine_identity`). Reach for a one-shot migration script only when the change can't be expressed that way (the v4 → v5 surrogate-PK substrate switch was the canonical example; restore from git history if you need to see the shape). The loud-fail hook in `openDb` rejects pre-current DBs with `SchemaTooOldError` (exit code 4) and a migration instruction. |
|
|
335
564
|
| Snapshot hook | Add `await captureSnapshot(db, 'verb-name', workstream)` at the top of any new destructive verb (one-liner; GC + restore behaviour automatic) |
|
|
565
|
+
| Cross-machine sync | `machine_identity` gives each state directory a durable uuid; `workstream_sync.last_known_peer_seqs` records per-workstream peer progress. `mu db import` compares source `latestSeq`, local `latestSeq`, and the last-seen peer seq to classify the five cases: `IDENTICAL` / `FAST_FORWARD` / `LOCAL_AHEAD` / `CONFLICT` / `IMPORT`. Conflicts are sharp: refuse by default, or `--force-source` after parking the whole local workstream into a divergence sidecar for later `mu db replay`. |
|
|
336
566
|
|
|
337
567
|
## Surrogate-PK + SDK-boundary discipline (load-bearing)
|
|
338
568
|
|
|
@@ -380,7 +610,8 @@ export function claimTask(
|
|
|
380
610
|
opts?: ClaimOptions,
|
|
381
611
|
): ClaimResult {
|
|
382
612
|
const wsId = resolveWorkstreamId(db, workstream);
|
|
383
|
-
const taskId =
|
|
613
|
+
const taskId = tryResolveTaskId(db, wsId, localId);
|
|
614
|
+
if (taskId === null) throw new TaskNotFoundError(localId);
|
|
384
615
|
const agentId = resolveCurrentAgentId(db, wsId);
|
|
385
616
|
return claimTaskById(db, taskId, agentId, opts);
|
|
386
617
|
}
|
|
@@ -392,10 +623,12 @@ function claimTaskById(db, taskId, agentId, opts): ClaimResult { ... }
|
|
|
392
623
|
Why exactly once at the boundary: no double-resolution; no
|
|
393
624
|
mid-function ambiguity (once surrogate ids exist, internal helpers
|
|
394
625
|
don't need to thread workstream context — the FKs make scope
|
|
395
|
-
implicit); one place to do error mapping
|
|
396
|
-
|
|
397
|
-
`AgentNotFoundError`
|
|
398
|
-
|
|
626
|
+
implicit); one place to do error mapping (`WorkstreamNotFoundError`
|
|
627
|
+
originates at resolve-time inside `src/db.ts`; `TaskNotFoundError` /
|
|
628
|
+
`AgentNotFoundError` are raised by SDK callers wrapping the
|
|
629
|
+
`tryResolve*` null-return so the typed class — and the CLI's
|
|
630
|
+
exit-code 3 mapping — stays consistent regardless of which leg of
|
|
631
|
+
the resolve missed).
|
|
399
632
|
|
|
400
633
|
**`--json` output preserves operator-facing names.** Surrogate ids
|
|
401
634
|
stay strictly internal — they never leak into `--json`, error
|
|
@@ -456,9 +689,19 @@ action) so `mu doctor` can surface them readably.
|
|
|
456
689
|
| `src/tracks.ts` | Pure functions; union-find + diamond-merge properties |
|
|
457
690
|
| `src/agents.ts` | Mocked tmux executor via `setTmuxExecutor()`; reaper integration tests |
|
|
458
691
|
| `src/logs.ts` | Real SQLite; cursor semantics, AUTOINCREMENT durability, FK CASCADE |
|
|
459
|
-
| `src/vcs.ts` + `src/workspace.ts` |
|
|
692
|
+
| `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) |
|
|
460
693
|
| `src/cli.ts` / verb integration | `*.integration.test.ts` files; real tmux server, unique session per test |
|
|
461
|
-
|
|
|
694
|
+
| 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 |
|
|
695
|
+
| Stress / flake audit | `npm run test:stress`; repeats the full suite with per-run logs/timeouts and can run parallel full-suite waves (`MU_TEST_STRESS_MODE=parallel`) to simulate multiple mu agents testing concurrently |
|
|
696
|
+
| End-to-end | `test/acceptance.integration.test.ts` — the canonical 10-task / 3-agent demo |
|
|
697
|
+
|
|
698
|
+
Historical flake audit summary: the closed
|
|
699
|
+
`bug_test_suite_flakes_audit_and_remediate` task found no separate
|
|
700
|
+
product seam. The durable lessons are: treat pass-alone/fail-under-load
|
|
701
|
+
cases as concurrency bugs first; use retrying temp-dir cleanup for VCS
|
|
702
|
+
fixtures whose subprocesses keep files alive briefly; drive wait/reaper
|
|
703
|
+
integration tests from poll-loop seams instead of fixed timers; and wait
|
|
704
|
+
for stable Ink output instead of sleeping a fixed number of ms.
|
|
462
705
|
|
|
463
706
|
## Distribution
|
|
464
707
|
|
|
@@ -478,4 +721,4 @@ not in the mu package.
|
|
|
478
721
|
|
|
479
722
|
The dependency list lives in `package.json`; the rule for adding
|
|
480
723
|
new ones is the anti-feature pledge in
|
|
481
|
-
[ROADMAP.md § Anti-feature pledges](ROADMAP.md#anti-feature-pledges
|
|
724
|
+
[ROADMAP.md § Anti-feature pledges](ROADMAP.md#anti-feature-pledges).
|