@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.
@@ -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/RESOLVED`.
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 task tracks` runs union-find on the graph to identify independent
75
- subtrees that can be assigned to different agents in parallel.
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>` shows mission-control output filtered to that task's
114
- subtree. Enables recursive delegation: a sub-orchestrator agent runs
115
- `mu --scope feature_a` and sees only its slice of the graph.
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
- - **`mu agent attach`** → attach to the whole workstream's tmux session
164
- - **`mu agent attach <agent>`** → attach and focus that agent's window/pane
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 --all`** shows agents across all workstreams on the box
167
- - **`session_id`** is the partition key on the `agents` table; queries
168
- filter to the active session unless `--all` is set
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
- ## Operations registry
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
- Every mu action is defined exactly once via `defineOperation(...)`.
206
- The registry is collected at module import time (no codegen step) and
207
- from one source produces six surfaces:
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
- │ defineOperation(...) │
212
- │ name, category, │
213
- │ caps[], params,
214
- │ handler │
215
- └─────────────┬──────────────┘
216
-
217
- ┌────────────┬──────┼──────┬───────────┐
218
- ▼ ▼ ▼ ▼ ▼
219
- ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌───────┐ ┌────────┐
220
- CLI verb│ │ Pi tool │ │ mu.d.ts │ │ skill │ │ doctor │
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
- No operation may exist outside the registry. CLI verbs that are not
225
- operations (e.g., `mu workstream init`, `mu agent attach`, `mu doctor`) are exceptions
226
- listed explicitly in the CLI module and motivated.
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
- (A capability-tag system on operations was considered and dropped
229
- as an abstraction with no current consumer; see
230
- [ROADMAP.md § Open questions](ROADMAP.md#open-questions).)
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/`: 18 root `.ts` files plus two cohesive
272
- subclusters (`src/agents/`, `src/tasks/`) and the `src/cli/` verb
273
- wrappers (with their own `src/cli/tasks/` sub-cluster). No
274
- `core/` subdirectory; no anticipatory layering. Subclusters obey
275
- the AGENTS.md rule: imports flow cluster root, never upward.
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 (14 tables + 3 views, **schema v7** — v5 surrogate-INTEGER-PK substrate, plus v6's 5 additive `archive_*` tables, minus v7's drop of `approvals`), default paths, `resolveWorkstreamId` (the SDK boundary's first leg). Pre-current DBs are upgraded in place on `openDb`: v5 v6 was additive (CREATE-TABLE-IF-NOT-EXISTS), v6 v7 is destructive-but-idempotent (`DROP TABLE IF EXISTS approvals` runs before `applySchema`); both happen with no migration script. |
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/tasks.ts` | Hub: every read/write verb on the DAG (edit / edges / queries) + cycle check + auto-event emission. Re-exports `src/tasks/*` (status, claim, lifecycle, wait, errors). |
287
- | `src/tasks/*.ts` | Cohesive cluster of task-graph internals: `status.ts` (TaskStatus enum + helpers single source of truth), `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`, …). |
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/importing.ts` | Inverse of `src/exporting.ts`: parses a v0.3 bucket directory and rebuilds every source-ws as live tasks + edges + notes. Markdown-only (never reads .db); per-source-ws transactional; refuses silent merges into existing workstreams |
292
- | `src/archives.ts` | Cross-workstream **archives** feature complete (SDK + 6 CLI verbs: `mu archive create / list / show / add / remove / delete`, plus `search` and `export` via the unified bucket renderer): `createArchive` / `listArchives` / `getArchive` / `deleteArchive` / `addToArchive` (idempotent at `(archive, source_workstream)`) / `removeFromArchive` / `listArchivedTasks`. Backed by the v6 `archives` + `archived_tasks` + `archived_edges` + `archived_notes` + `archived_events` tables; archives outlive workstreams (TEXT `source_workstream` columns, no FK). |
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` | `VcsBackend` interface + jj / sl / git / none impls; detection precedence; `commitsBehind(workspacePath, ref)` for staleness signal (no auto-fetch; pure observation); `isClean(workspacePath)` cheap working-copy probe used by `closeAgent`'s clean-workspace auto-free path |
295
- | `src/workspace.ts` | Per-agent VCS workspaces (registry layer on top of vcs.ts); CRUD + cascade; orphan-dir detection (`listWorkspaceOrphans`); staleness decoration (`decorateWithStaleness` populates `commitsBehindMain` per row) |
296
- | `src/snapshots.ts` | Whole-DB snapshots (`VACUUM INTO`); auto-captured before destructive verbs; SDK for `mu undo`. The `snapshots` table is schema v4 (carried forward unchanged through v5/v6/v7). |
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/*.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`, `state.ts` (canonical state card + bare `mu` mission-control / hud render mode), `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). |
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`, mission control), queries tmux for live pane
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`. Two of the three 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), and v6 → v7 was a destructive-but-idempotent in-place migration (a `DROP TABLE IF EXISTS approvals` block in `applySchema`). 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. |
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 = resolveTaskId(db, wsId, localId);
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
- (`WorkstreamNotFoundError` / `TaskNotFoundError` /
397
- `AgentNotFoundError` all originate at resolve-time, with the
398
- operator's input string in the error payload).
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` | Real git in `os.tmpdir()`; jj/sl tests feature-detect (skip if binary missing) |
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
- | End-to-end | `test/acceptance.test.ts` the canonical 10-task / 3-agent demo |
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-still-in-force-reinforced-by-an-internal-critique).
724
+ [ROADMAP.md § Anti-feature pledges](ROADMAP.md#anti-feature-pledges).