@mu-crew/mu 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,611 @@
1
+ # Repo guide for AI coding agents
2
+
3
+ You're an AI coding agent working on the `mu` repo, which builds
4
+ **mu** — a CLI that manages a persistent crew of AI agents in
5
+ multiplexer panes (tmux or herdr) coordinated through a built-in
6
+ task DAG.
7
+
8
+ Read the linked docs before you write code. Follow the conventions
9
+ below.
10
+
11
+ ---
12
+
13
+ ## Read these first (in this order)
14
+
15
+ 1. **[docs/USAGE_GUIDE.md](docs/USAGE_GUIDE.md)** — what mu does
16
+ from a user's perspective. ~10 minutes.
17
+ 2. **[CHANGELOG.md](CHANGELOG.md)** — the upcoming version's entry.
18
+ Single source of truth for the verb list, schema, env vars.
19
+ 3. **[docs/VISION.md](docs/VISION.md)** — the design principles you
20
+ must not violate.
21
+ 4. **[docs/ROADMAP.md](docs/ROADMAP.md)** — what's next, with
22
+ promotion criteria. **Read the "Anti-feature pledges" section
23
+ before adding any new dep, abstraction, or surface.**
24
+ 5. **[docs/VOCABULARY.md](docs/VOCABULARY.md)** — canonical terms.
25
+ **Source of truth for every word** in code, docs, and error
26
+ messages. If you use a term not defined there, fix the docs first.
27
+ 6. **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)** — module layout,
28
+ reconciliation algorithm, TUI architecture, key seams.
29
+
30
+ Design rationale for rejected and unbuilt features lives in
31
+ [docs/ROADMAP.md](docs/ROADMAP.md) per item, alongside its promotion
32
+ criteria.
33
+
34
+ > **If you are an orchestrator** — a coding agent driving a crew of
35
+ > pi worker agents on this repo via `mu` — read
36
+ > **[docs/HANDOVER.md](docs/HANDOVER.md)** instead of the order
37
+ > above: onboarding steps, the 8-phase dispatch loop,
38
+ > conflict-resolution playbook, known gotchas, end-of-session
39
+ > checklist. AGENTS.md is for workers and humans editing the repo
40
+ > directly.
41
+
42
+ ---
43
+
44
+ ## Repo layout
45
+
46
+ ```
47
+ mu/
48
+ ├── README.md # human-user entry point
49
+ ├── AGENTS.md # this file
50
+ ├── CHANGELOG.md # release notes
51
+ ├── docs/ # everything else
52
+ │ ├── USAGE_GUIDE.md # user-facing tour (§ 5b is the TUI reference)
53
+ │ ├── HANDOVER.md # orchestrator goto reset doc (8-phase loop + gotchas)
54
+ │ ├── ROADMAP.md # what's next; promotion criteria; anti-feature pledges
55
+ │ ├── VISION.md # design pillars
56
+ │ ├── VOCABULARY.md # canonical terms (single source of truth)
57
+ │ └── ARCHITECTURE.md # module layout, TUI architecture, key seams
58
+ ├── src/ # all source (root files: SDK + shared infra; one
59
+ │ # level of subdirs OK for cohesive clusters — see
60
+ │ # `src/cli/`, `src/agents/`, `src/tasks/`, `src/mux/`)
61
+ │ ├── db.ts # SQLite schema + openDb (single CREATE-IF-NOT-EXISTS block; v10)
62
+ │ │ # also owns SYNCED_ENTITIES / PORTABLE_TABLES / MACHINE_LOCAL_TABLES
63
+ │ ├── mux.ts # multiplexer backend hub (re-exports src/mux/*)
64
+ │ ├── mux/ # cohesive cluster: one file per multiplexer
65
+ │ │ ├── index.ts # re-export hub consumed by src/mux.ts
66
+ │ │ ├── types.ts # MuxBackend interface + MuxError / PaneNotFoundError / NoMultiplexerError
67
+ │ │ ├── detect.ts # MU_MUX → HERDR_ENV → $TMUX → availability ladder; activeMux()
68
+ │ │ ├── tmux.ts # tmux impl: wrapper, 6-step send protocol, pane validation
69
+ │ │ └── herdr.ts # herdr impl: JSON socket API, atomic send, native pane status
70
+ │ ├── tmux.ts # back-compat re-export of the tmux backend (new code imports mux.js)
71
+ │ ├── detect.ts # pi status detector + Braille-spinner fallback; used only when
72
+ │ │ # the backend has no paneStatus() of its own (i.e. tmux)
73
+ │ ├── reconcile.ts # ghost prune + status detect + orphan surface
74
+ │ ├── agents.ts # CRUD + send/read/list/close + liveness + reaper hub (re-exports src/agents/*)
75
+ │ ├── agents/ # cohesive cluster of agent-lifecycle internals
76
+ │ │ ├── spawn.ts # spawnAgent + resolveCliCommand / awaitSpawnLiveness / pane create-or-reuse / prestage / rollback
77
+ │ │ ├── spawn-lock.ts # cross-process advisory lock around a spawn's topology critical section
78
+ │ │ ├── kick.ts # reaper events + cleanup of dead agent rows
79
+ │ │ ├── adopt.ts # adoptAgent: register an existing pane as a managed agent
80
+ │ │ ├── wait.ts # waitForAgents: block on task-less agents finishing (busy → any other state)
81
+ │ │ └── errors.ts # typed agent error classes (AgentNotFoundError, AgentDiedOnSpawnError, …)
82
+ │ ├── tasks.ts # task SDK hub (re-exports src/tasks/*)
83
+ │ ├── tasks/ # cohesive cluster of task-graph internals
84
+ │ │ ├── status.ts # TaskStatus enum + helpers
85
+ │ │ ├── core.ts # core SDK funcs reused across cluster files
86
+ │ │ ├── id.ts # tryResolveTaskId / qualified-id helpers
87
+ │ │ ├── queries.ts # listTasks / nextTasks / owned-by
88
+ │ │ ├── edit.ts # addTask / setTaskTitle / etc (no edges)
89
+ │ │ ├── edges.ts # block / unblock / reparent / delete + dedupe
90
+ │ │ ├── claim.ts # claim / release + resolveActorIdentity (atomic CAS)
91
+ │ │ ├── lifecycle.ts # setTaskStatus / closeTask / openTask + cascade
92
+ │ │ ├── wait.ts # waitForTasks: block until tasks reach a target status
93
+ │ │ ├── sort.ts # sortTasks (roi / recency / age / id)
94
+ │ │ └── errors.ts # typed task error classes (TaskAlreadyOwnedError, CycleError, …)
95
+ │ ├── tracks.ts # parallel-tracks union-find with diamond merge
96
+ │ ├── workstream.ts # ensureWorkstream / list / summarize / destroy
97
+ │ ├── hlc.ts # hybrid logical clock: the monotonic ordering key on every op
98
+ │ ├── capture.ts # SQLite triggers recording every portable mutation as an op, in-transaction
99
+ │ ├── op-context.ts # withOpContext (intent/actor/group) + withCaptureSuppressed echo guard
100
+ │ ├── apply.ts # the apply path: per-field LWW merge + tombstones + deferred reprojection
101
+ │ ├── undo.ts # inverse ops for one group (granular undo; supersede refusal)
102
+ │ ├── rebuild.ts # replay the whole ops log into a NEW DB file (disaster recovery)
103
+ │ ├── drift.ts # ops-log-vs-live-tables drift check (cheap default tier + --deep rebuild-diff)
104
+ │ ├── segments.ts # JSONL segment transport: flush own ops / ingest peers from a watermark
105
+ │ ├── sync.ts # peer status + the ambient flush/ingest hook + --from reader + --repair
106
+ │ ├── fleet-hazards.ts # mixed-fleet doctor checks (DB inside MU_SYNC_DIR, network mount, case collisions)
107
+ │ ├── file-lock.ts # generic cross-process advisory lock via atomic fs.mkdir
108
+ │ ├── logs.ts # typed READER over `ops` + appendLog / emitEvent (the one write path triggers can't cover)
109
+ │ ├── log-render.ts # the ONE op → prose formatter (renderOp); shared by CLI + TUI
110
+ │ ├── vcs.ts # VcsBackend hub (re-exports src/vcs/*: jj/sl/git/none impls)
111
+ │ ├── workspace.ts # per-agent VCS workspaces hub (re-exports src/workspace/*)
112
+ │ ├── glyphs.ts # THE glyph vocabulary: agent status + state glyphs + card digits
113
+ │ ├── shell-quote.ts # POSIX single-quote helper for copy-pasteable Next: hints
114
+ │ ├── dag.ts # full-DAG forest builder (loadFullDag for `mu task tree` + DAG popup)
115
+ │ ├── state.ts # SDK seam for `mu state` (fast SQL tier + slow subprocess tier + merge)
116
+ │ ├── staleness.ts # WORKSPACE_STALE_THRESHOLD + isStaleWorkspace
117
+ │ ├── project-root.ts # detectProjectRoot for the TUI launch cwd ladder
118
+ │ ├── doctor-summary.ts # TUI-friendly slice of `mu doctor` checks + remediation helpers
119
+ │ ├── output.ts # NextStep type + printNextSteps / errorNextSteps
120
+ │ ├── cli.ts # commander wiring (buildProgram); re-exports format/handle for back-compat
121
+ │ ├── cli/ # one file per verb-namespace; thin wrappers over the SDK
122
+ │ │ ├── workstream.ts # workstream init / list / destroy
123
+ │ │ ├── agents.ts # agent spawn / send / read / list / show / close / kick / wait / adopt
124
+ │ │ ├── tasks.ts # `mu task` hub (re-exports wireTaskCommands / cmdMyNext / cmdMyTasks / unescapeNoteText)
125
+ │ │ ├── tasks/ # sub-cluster of the `mu task` namespace
126
+ │ │ │ ├── queries.ts # list / next / owned-by + cmdMyTasks / cmdMyNext (back `mu me tasks` / `mu me next`)
127
+ │ │ │ ├── lifecycle.ts # close / open + cascade preview
128
+ │ │ │ ├── edit.ts # add / show / notes / note / update + helpers
129
+ │ │ │ ├── edges.ts # block / unblock / reparent / delete
130
+ │ │ │ ├── claim.ts # claim / release / wait
131
+ │ │ │ ├── tree.ts # tree rendering
132
+ │ │ │ └── wire.ts # Commander glue
133
+ │ │ ├── workspace.ts # workspace list / free / path / orphans / refresh / commits
134
+ │ │ ├── log.ts # log read / write / tail
135
+ │ │ ├── undo.ts # mu undo [group] (list / preview / --yes apply)
136
+ │ │ ├── sync.ts # mu sync (peer status; --from / --repair)
137
+ │ │ ├── rebuild.ts # mu rebuild <file>
138
+ │ │ ├── state.ts # `mu state` (canonical state card); --tui dispatches to src/cli/tui/
139
+ │ │ ├── staleness.ts # shared workspace-staleness CLI helpers + warn formatter
140
+ │ │ ├── tui-launch-focus.ts # initial-tab focus ladder for bare `mu` and `mu state --tui`
141
+ │ │ ├── tui/ # interactive ink-based TUI cluster; ONLY place ink/react are imported
142
+ │ │ │ ├── index.ts # runTui entrypoint; alt-screen + mouse-mode lifecycle
143
+ │ │ │ ├── escapes.ts # pure ANSI escape constants (ALT_SCREEN_*, mouse-mode bytes) — no ink imports
144
+ │ │ │ ├── app.tsx # <App> root (popup state machine + global keymap + footer + tick + tabs)
145
+ │ │ │ ├── state.ts # poll-loop hook (useDashboardSnapshot; fast/slow tick split)
146
+ │ │ │ ├── keys.ts # pure dispatchGlobalKey + dispatchPopupKey + shouldSwallowGlobalKey
147
+ │ │ │ ├── keymap-spec.ts # canonical keymap source-of-truth (drives help overlay + dispatch)
148
+ │ │ │ ├── yank.ts # clipboard probe + write (pbcopy/wl-copy/xclip/xsel/clip.exe + OSC-52)
149
+ │ │ │ ├── mouse.ts # vendored SGR mouse layer (parser + double-click + useMouse hook)
150
+ │ │ │ ├── layout.ts # responsive multi-column dashboard + per-card row budgets
151
+ │ │ │ ├── columns.ts # column-aligned row layout with protect/clip clipping
152
+ │ │ │ ├── wrap-ansi.ts # ANSI-aware visual-width line wrapper + SGR close-on-end
153
+ │ │ │ ├── format-helpers.ts # shared TUI formatters (relTime, sinceClaim, ROI, etc.)
154
+ │ │ │ ├── titled-box.tsx # rounded border with section header inset into top border + bottomLabel
155
+ │ │ │ ├── popup-shell.tsx # popup outer chrome (cyan TitledBox)
156
+ │ │ │ ├── list-row.tsx # centralised non-selected row primitive (width pin + gutter + truncate)
157
+ │ │ │ ├── padded-rows.tsx # per-card body padder
158
+ │ │ │ ├── help.tsx # ?/F1 keymap overlay (scrollable on short panes)
159
+ │ │ │ ├── status-bar.tsx # bottom status bar (mode + active ws + tick + footer flash)
160
+ │ │ │ ├── tab-strip.tsx # multi-workstream tab switcher (N≥2)
161
+ │ │ │ ├── tab-strip-layout.ts # pure window-around-active layout helper for the tab strip
162
+ │ │ │ ├── tuicr.ts # `t` shortcut: alt-screen handoff to tuicr -r <sha>
163
+ │ │ │ ├── use-popup-filter.tsx # shared '/' substring filter hook + applyFilter + FilterPrompt
164
+ │ │ │ ├── use-status-filter.tsx # task-status toggles for task-list popups (o/i/c)
165
+ │ │ │ ├── use-notes-drill.ts # shared notes-drill memo (5 task popups consume it)
166
+ │ │ │ ├── use-popup-action-queue.ts # consume mouse PopupAction queue once per render
167
+ │ │ │ ├── use-terminal-size.ts # shared reactive terminal-size hook (ink doesn't re-render on resize)
168
+ │ │ │ ├── agent-display.ts # shared agent-row display helpers for cards/popups
169
+ │ │ │ ├── lazygit.ts # `l` in the Commits popup: alt-screen handoff to lazygit
170
+ │ │ │ ├── tmux-attach.ts # `a` in the Agents popup: alt-screen handoff to the agent's mux pane
171
+ │ │ │ ├── cards/{agents,tracks,ready,log,workspaces,inprogress,blocked,recent,commits,doctor}.tsx + _placeholder.tsx
172
+ │ │ │ └── popups/{agents,tracks,ready,log,workspaces,inprogress,blocked,recent,commits,doctor,dag,all-tasks}.tsx
173
+ │ │ │ # plus drill.tsx (DrillScrollView), task-detail.tsx (TaskDetailDrill),
174
+ │ │ │ # cursor-row.tsx, scroll.ts (applyCursor/applyScroll), viewport.ts,
175
+ │ │ │ # show-loader.ts (shared subprocess-preserving loader)
176
+ │ │ ├── db.ts # db backup (VACUUM INTO)
177
+ │ │ ├── sql.ts # sql escape hatch
178
+ │ │ ├── doctor.ts # doctor diagnostic
179
+ │ │ ├── format.ts # pure rendering helpers (table renderers, status colourers, truncate/relTime)
180
+ │ │ └── handle.ts # typed-error → exit-code map + handle() wrapper
181
+ │ └── index.ts # SDK entrypoint (re-exports)
182
+ ├── test/ # ~200 *.test.ts files / ~2900 tests (`npm run test`); many use real tmux/git/jj/sl
183
+ ├── skills/mu/SKILL.md # what the LLM running inside an agent pane sees
184
+ ├── package.json # bin: { mu: ./dist/cli.js }, type: module
185
+ ├── tsconfig.json # strict + noUncheckedIndexedAccess + verbatimModuleSyntax
186
+ ├── tsup.config.ts # bundles src/ → dist/ (cli + index entries)
187
+ ├── biome.json # lint + format
188
+ └── vitest.config.ts # tests
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Working conventions
194
+
195
+ ### Build / test / lint
196
+
197
+ ```bash
198
+ npm install
199
+ npm run build # tsup → dist/
200
+ npm run typecheck # source + test TypeScript checks
201
+ npm run lint # biome check src test
202
+ npm run test:fast # fast unit/dev-loop tier (excludes *.integration.test.ts / *.smoke.test.ts)
203
+ npm run test # full vitest suite, including integration tests
204
+ npm run test:watch # vitest in watch mode
205
+ npm run test:watch:fast # fast-tier watch mode
206
+ ```
207
+
208
+ #### Two TypeScript compilers, on purpose
209
+
210
+ `tsc` is **TypeScript 7** (the Go-native compiler) and `tsc6` is the
211
+ TypeScript 6 JS compiler. `package.json` aliases them:
212
+
213
+ ```json
214
+ "@typescript/native": "npm:typescript@^7.0.2",
215
+ "typescript": "npm:@typescript/typescript6@^6.0.2"
216
+ ```
217
+
218
+ This looks backwards and is not. TS 7.0 ships **no programmatic
219
+ compiler API** (`ts.createProgram` and friends return undefined; the
220
+ package exports exactly two keys). It is deferred to 7.1. `tsup --dts`
221
+ goes through `rollup-plugin-dts`, which needs that API, so a plain
222
+ `typescript@7` install builds JS fine and then dies with
223
+ `Cannot read properties of undefined (reading 'useCaseSensitiveFileNames')`.
224
+
225
+ The alias resolves both needs at once: anything that `require`s
226
+ `typescript` as a LIBRARY gets the 6.x JS API, while the `tsc` BINARY
227
+ is the fast Go compiler (typecheck went ~9s → ~0.6s). The two bins do
228
+ not collide, so no shim is needed.
229
+
230
+ Revisit when 7.1 lands its new API and `rollup-plugin-dts` adopts it;
231
+ at that point `typescript` can point at 7.x directly and
232
+ `@typescript/native` disappears.
233
+
234
+ Use `npm run test:fast` for the inner dev loop and concurrent worker
235
+ checks; it is the concurrency-safe tier that avoids real tmux/VCS
236
+ subprocess fixtures. The green gates still include `npm run test`
237
+ before any commit. Real-tmux integration tests need `$TMUX` set and
238
+ skip themselves otherwise; CI runs inside tmux.
239
+
240
+ ### Commits
241
+
242
+ - Conventional but not strict: prefix with the scope when helpful
243
+ (e.g. `R4 + R9: ...`, `schema: ...`). One logical change per
244
+ commit.
245
+ - Body explains **what changed and why**, not just what. Reference
246
+ the [VISION.md](docs/VISION.md) pillar / [ROADMAP.md](docs/ROADMAP.md)
247
+ item / promotion criterion that motivated the change.
248
+ - Verify typecheck + lint + tests + build clean before committing.
249
+ Say so in the commit message.
250
+
251
+ ### Code style
252
+
253
+ - TypeScript, strict mode. `noUncheckedIndexedAccess` is on — don't
254
+ trust array indices to be defined; use early returns.
255
+ - ESM only (`"type": "module"`). NodeNext module resolution.
256
+ - No `any`. Use `unknown` and narrow.
257
+ - No non-null assertions (`!`). Use early returns or `if (!x) throw`.
258
+ - Errors are typed classes (e.g. `AgentNotFoundError`,
259
+ `TaskAlreadyOwnedError`, `CycleError`, `MuxError` and its per-backend
260
+ subclasses `TmuxError` / `HerdrError`,
261
+ `AgentDiedOnSpawnError`) so the CLI's `handle()` wrapper can map
262
+ them to specific exit codes.
263
+ - Imports stay sorted (Biome's `organizeImports` enforces this).
264
+ - Run `npx biome check --write src test` to auto-fix sort + format.
265
+ **Do not** run `--write --unsafe`: it rewrites `delete
266
+ process.env.X` to `process.env.X = undefined`, which silently
267
+ produces the literal string `"undefined"`. The env-deletion pattern
268
+ here is `const key = "FOO"; delete process.env[key];`.
269
+ - Hard cap: 1500 LOC per file. Refactor signal at 800.
270
+ - **Layout: flat at the root; one level of subdirs is allowed when a
271
+ cluster of files is naturally cohesive** (e.g. `src/cli/` for the
272
+ thin commander wrappers, one file per verb-namespace). Each subdir
273
+ cluster needs:
274
+ (1) a clear theme (every file does the same kind of thing),
275
+ (2) imports go from cluster-files → root-files (no upward imports),
276
+ (3) ARCHITECTURE.md's module table has a row covering it.
277
+
278
+ ### Tests
279
+
280
+ - Unit tests: real SQLite (in-temp-dir), mocked tmux executor via
281
+ `setTmuxExecutor()`. Fast, deterministic. For anything touching the
282
+ multiplexer, use `installMux()` from `test/_mux.ts` rather than the
283
+ raw setters — it installs the backend AND its executor together and
284
+ hands back one `restore()`, which removes the ordering hazard of
285
+ stubbing one backend while asserting against another.
286
+ - TUI popup/card behaviour tests should follow `test/README.md`:
287
+ prefer the `test/_ink-render.ts` CaptureStream seam over
288
+ `readFileSync` source-greps except for narrow structural guards.
289
+ - Fast tier: `npm run test:fast` runs `test/**/*.test.ts` while
290
+ excluding `*.integration.test.ts` and `*.smoke.test.ts`. Keep this
291
+ tier pure/in-process: mocked tmux/VCS, real SQLite only in per-test
292
+ temp DBs, no real tmux/git/jj/sl subprocess fixtures, no
293
+ filesystem-heavy export/import/snapshot paths, and no fixed sleeps
294
+ above 50ms.
295
+ - Integration tests: full-only tests use the `.integration.test.ts`
296
+ suffix (e.g. `tmux.integration.test.ts`). They may touch real tmux
297
+ or herdr servers, git/jj/sl fixture repos, subprocess-backed smoke
298
+ paths, filesystem-heavy export/import/snapshot flows, or
299
+ intentionally slower in-process CLI flows. Real-tmux tests are
300
+ skipped when `$TMUX` is unset; real-herdr tests skip unless a
301
+ running, protocol-compatible server is present. These tests typically opt out of the spawn
302
+ liveness check via `process.env.MU_SPAWN_LIVENESS_MS = "0"` in
303
+ `beforeEach`, since the sh subprocesses they spawn are intentionally
304
+ long-lived.
305
+ - Each test gets its own temp DB and (for integration) a unique
306
+ tmux session like `mu-test-<pid>-<ts>-<rand>` to avoid colliding
307
+ with the user's panes or with parallel test runs.
308
+ - **herdr isolation is a guard, not a socket.** tmux gets a private
309
+ server via `MU_TMUX_SOCKET`, so a stray test is contained
310
+ structurally. herdr has no equivalent: its only isolation is a named
311
+ session (`MU_HERDR_SESSION`), so `test/_mux.ts` enforces it at
312
+ runtime — `assertHerdrIsolated()` refuses to run when the var is
313
+ unset or names `default`, and the fatal verbs (`server stop`,
314
+ `server restart`, `server kill`) are refused even inside a correct
315
+ session, because a stopped server is shared-fate. **Never stop the
316
+ herdr server or kill its main process from a test.** That destroys
317
+ the user's real panes and their unsaved work, with no undo. Clean up
318
+ per-entity (close what you created), never by nuking the server.
319
+ - Dogfood reality: multiple pi worker agents often run `npm run test`
320
+ concurrently on the same machine from different workspaces. Treat
321
+ flakes that pass in isolation but fail under load as concurrency
322
+ bugs first (shared `/tmp` cleanup, tmux socket/session collisions,
323
+ leaked subprocesses, VCS background file activity). Use
324
+ `npm run test:stress` for the pre-release/stability gate; it runs
325
+ the suite repeatedly with a per-run timeout and can simulate
326
+ parallel full-suite runs via
327
+ `MU_TEST_STRESS_MODE=parallel MU_TEST_STRESS_PARALLEL=2`.
328
+ - The acceptance test in `test/acceptance.integration.test.ts` is the
329
+ "everything works" gate. Keep it passing.
330
+ - **DB baseline**: `openDb()` refuses to open the user's REAL
331
+ default DB (`<HOME or XDG_STATE_HOME>/mu/mu.db`) when
332
+ `process.env.VITEST` is set or `NODE_ENV === "test"`. Tests
333
+ MUST use a per-test temp DB — either via MU_DB_PATH (which
334
+ `test/_runCli.ts` sets automatically) or an explicit `{ path }`
335
+ argument to `openDb`. A regression that forgets either one
336
+ fails loudly at the offending openDb() call site instead of
337
+ silently writing to the dev box's live state. Production never
338
+ sets VITEST, so the guard is a no-op outside the test runner.
339
+ - **Env baseline**: `test/_setup.ts` (vitest `setupFiles`) clears
340
+ every `MU_*` env var inherited from the parent shell at the start
341
+ of each fork, so SDK-level overrides (`MU_PI_COMMAND`,
342
+ `MU_IDLE_THRESHOLD_MS`, `MU_SEND_DELAY_MS`, …) can't silently
343
+ change behaviour underneath tests. Allowlist: `MU_TMUX_SOCKET`
344
+ (set by `_global-teardown.ts` at MODULE LOAD time — BEFORE vitest
345
+ spawns the worker pool — for Layer-3 isolation; see round-3
346
+ Part A in the file's header comment for why module-load not
347
+ setup()). Tests that need a specific value opt IN per-test via
348
+ `process.env.X = "..."` or `withEnv()` from `test/_env.ts`.
349
+ - **Default-socket sweep philosophy**: `_global-teardown.ts` runs
350
+ an ALLOWLIST sweep of `mu-*` sessions on the user's default tmux
351
+ socket at suite setup AND teardown. The allowlist is DB-rooted:
352
+ (1) `mu-<name>` for every workstream in the user's REAL DB
353
+ (read-only via better-sqlite3, bypassing the `openDb()` test
354
+ guard) and (2) `mu-$MU_SESSION` if the orchestrator runs the suite
355
+ inside a tmux pane. Anything else is, by elimination, test residue
356
+ and is killed. No session is grandfathered by being present at
357
+ module load — that defeats the self-healing intent. Cost: an ad-hoc
358
+ `tmux new-session -t mu-foo` with no DB row gets killed; run
359
+ `mu workstream init foo` first. Tests can hardcode any workstream
360
+ name — if they accidentally bypass the private socket, the sweep
361
+ catches them.
362
+
363
+ ### When you change behaviour, update VOCABULARY first
364
+
365
+ Vocabulary is canonical. If you introduce a new concept, name, or
366
+ verb, **add it to docs/VOCABULARY.md before the code lands**. If you
367
+ rename something, update VOCABULARY.md in the same commit.
368
+
369
+ ### Deferred features — don't smuggle them in
370
+
371
+ [docs/ROADMAP.md](docs/ROADMAP.md) lists what's next, by version,
372
+ with **promotion criteria**:
373
+
374
+ > 1. A real user hits the missing feature in real workflows ≥2
375
+ > times.
376
+ > 2. The current substrate makes the addition straightforward (no
377
+ > major pillar refactor).
378
+ > 3. The addition fits in <300 LOC or has a clear smaller subset.
379
+
380
+ If you find yourself adding something not on the roadmap and not
381
+ meeting these criteria, **stop**. Add an entry to
382
+ [docs/ROADMAP.md](docs/ROADMAP.md) (or open an issue) and move on.
383
+
384
+ The "anti-feature pledges" in ROADMAP.md are firm:
385
+
386
+ - No config file
387
+ - No daemon / background process beyond what the multiplexer +
388
+ SQLite give us
389
+ - No anticipatory abstractions (no traits with zero implementors)
390
+ - No wrappers around wrappers
391
+ - No codegen / embedded JS engine / workflow DSL
392
+ - No template/discovery system for agent roles (spawn flags + first
393
+ message ARE the definition)
394
+ - No render layer beyond `cli-table3` + `picocolors`, EXCEPT `ink`
395
+ confined to `src/cli/tui/`. NO second TUI stack alongside `ink`
396
+ (no `blessed` / `terminal-kit` etc.); if `ink` ever stops paying
397
+ off, REPLACE it, don't stack stacks.
398
+ - No plugin runtime, web UI, RPC, chat/docs integrations, memory
399
+ system, workflow engine
400
+ - Don't bundle pi (it's a peer dep)
401
+
402
+ ### When in doubt: be small
403
+
404
+ Ship the smallest thing that works, then layer on as real friction
405
+ proves itself.
406
+
407
+ ---
408
+
409
+ ## Common tasks
410
+
411
+ ### "Add a new CLI verb"
412
+
413
+ 1. Find or write the programmatic function in `src/agents.ts`,
414
+ `src/tasks.ts`, `src/workstream.ts`, etc. Test it with mocked
415
+ tmux. Return a typed result object (`{ changed: boolean,
416
+ previousStatus, ... }`) so callers can log lifecycle
417
+ transitions.
418
+ 2. Wire the verb in `src/cli.ts` using `commander`. Use
419
+ `handle(...)` so typed errors map to exit codes. If the verb
420
+ takes `--workstream`, use `command.optsWithGlobals()` (via
421
+ `this`) so the top-level option doesn't swallow it (commander
422
+ gotcha).
423
+ 3. Update [docs/USAGE_GUIDE.md](docs/USAGE_GUIDE.md) with the new
424
+ verb in the right section.
425
+ 4. Update [docs/VOCABULARY.md](docs/VOCABULARY.md) operations
426
+ table.
427
+ 5. Update [skills/mu/SKILL.md](skills/mu/SKILL.md) **only if the verb has a
428
+ gotcha `--help` cannot state.** The skill is not a verb list — see
429
+ § Skill files are context, not documentation, below.
430
+ 6. Update [CHANGELOG.md](CHANGELOG.md) under the upcoming version.
431
+ 7. If this verb promotes a `mu sql` workaround, remove the
432
+ workaround entry from the `docs/USAGE_GUIDE.md` gaps table.
433
+ 8. Smoke-test: `MU_SYNC_DIR= MU_DB_PATH=/tmp/mu-smoke.db node
434
+ dist/cli.js <verb> ...` to verify it works against real tmux. If
435
+ the verb touches the agent layer, smoke it on herdr too —
436
+ `MU_MUX=herdr` plus a private `MU_HERDR_SESSION` (never the default
437
+ session).
438
+
439
+ **Blank `MU_SYNC_DIR` as well as `MU_DB_PATH`.** Overriding the DB
440
+ alone does not contain a smoke test: sync is ambient, so the first
441
+ flush stamps the throwaway DB's fresh `machine_id` onto a segment
442
+ in your REAL sync folder. Nothing prunes it, and absence of a
443
+ segment is the only way a peer disappears, so `mu sync` then lists
444
+ a phantom machine forever. Eight of them accumulated in `~/mu`
445
+ before anyone noticed. `src/segments.ts` now refuses a non-temp
446
+ sync dir under vitest, but a hand-run `node dist/cli.js` is not
447
+ under vitest — blank the var yourself.
448
+
449
+ ### "Update the schema"
450
+
451
+ 1. Current schema version is **v10** (`CURRENT_SCHEMA_VERSION` in
452
+ `src/db.ts`): 10 tables + 3 views. The schema is the
453
+ `applySchema(db)` block — idempotent CREATE-IF-NOT-EXISTS plus
454
+ targeted `DROP TABLE IF EXISTS`. `openDb` REFUSES any pre-v10 DB
455
+ with `SchemaTooOldError` (exit 4); there is no in-process
456
+ migration ladder.
457
+ 2. Bump `CURRENT_SCHEMA_VERSION` in `src/db.ts` and mirror the new
458
+ shape in `CURRENT_SCHEMA`. Prefer script-free bumps: additive
459
+ CREATE-TABLE-IF-NOT-EXISTS, or an idempotent `DROP TABLE` block.
460
+ Reach for a one-shot migration script only when the change can't
461
+ be expressed that way. **If you add a table, classify it** in
462
+ `PORTABLE_TABLES` or `MACHINE_LOCAL_TABLES` —
463
+ `test/entities.test.ts` fails loudly otherwise.
464
+ 3. Update tests that exercise the schema (`test/db.test.ts`).
465
+ 4. Update [CHANGELOG.md](CHANGELOG.md) under the upcoming version's
466
+ `### Changed` section.
467
+
468
+ ### "Add a new multiplexer operation"
469
+
470
+ mu drives **two** multiplexers, tmux and herdr, behind the
471
+ `MuxBackend` interface in `src/mux/types.ts`. Call sites resolve one
472
+ with `await activeMux()` and never name a backend.
473
+
474
+ 1. **Add the method to `MuxBackend`** with a doc comment saying what
475
+ it means, not how tmux does it.
476
+ 2. **Implement it in BOTH** `src/mux/tmux.ts` and `src/mux/herdr.ts`.
477
+ A backend with no equivalent no-ops (see
478
+ `enableMuPaneBorders*`, `selectLayout`) — it does not throw.
479
+ 3. **If only one backend can do it**, make it an OPTIONAL method
480
+ (`paneStatus?()`, `startAgentInPane?()`) and have the caller
481
+ branch on the CAPABILITY being present, never on `mux.name`.
482
+ 4. **Decide load-bearing vs best-effort.** Load-bearing calls (spawn,
483
+ send, kill) let `NoMultiplexerError` propagate to exit 5.
484
+ Best-effort calls (identity, decoration, orphan surfacing) wrap in
485
+ try/catch so a missing mux degrades instead of failing the verb —
486
+ `resolveWorkerIdentity` in `src/tasks/claim.ts` is the shape.
487
+
488
+ All tmux invocations go through `src/mux/tmux.ts` `tmux(args)`; all
489
+ herdr invocations through `src/mux/herdr.ts` `herdr(args)`. **No raw
490
+ `execa("tmux", …)` / `execa("herdr", …)` anywhere else.** Each wrapper
491
+ produces a typed error (`TmuxError` / `HerdrError`, both extending
492
+ `MuxError`) and each has an executor seam the tests mock — use
493
+ `installMux()` from `test/_mux.ts` rather than the raw setters.
494
+
495
+ For send-style operations the two backends differ sharply, and that
496
+ difference is the point: tmux needs the canonical bracketed-paste
497
+ sequence in `sendToPane` (naive `tmux send-keys "<text>"` is broken —
498
+ `/`, `?`, `f` get eaten by the agent's TUI), while herdr's
499
+ `agent prompt` is one atomic call. Do not port the tmux workaround
500
+ to herdr.
501
+
502
+ ### "Fix a flaky integration test"
503
+
504
+ Integration tests against real tmux can be slow because tmux/sh
505
+ processes need time to settle. Use:
506
+
507
+ - Unique session names (`mu-test-<pid>-<ts>-<random>`) so parallel
508
+ runs never collide.
509
+ - Polling loops (50ms × 10 attempts) when waiting for state to
510
+ propagate, not fixed sleeps.
511
+ - `try { ... } catch {}` cleanup in `afterEach` for tmux session
512
+ kills and DB closes — a failure mid-test should never block the
513
+ next.
514
+ - `setSleepForTests(async () => {})` in unit tests so the real
515
+ `MU_SEND_DELAY_MS` doesn't slow them.
516
+ - `process.env.MU_SPAWN_LIVENESS_MS = "0"` in integration-test
517
+ `beforeEach` to skip the 1500ms post-spawn check (the unit-test
518
+ suite covers it).
519
+
520
+ ---
521
+
522
+ ## Skill files are context, not documentation
523
+
524
+ `skills/mu/SKILL.md` and `skills/mu/REMOTE_WORKERS.md` are loaded into an
525
+ orchestrator's context window, SKILL.md on every invocation, where every word
526
+ competes with the user's actual work for attention. **Keep them ruthlessly
527
+ terse.**
528
+
529
+ Read the `writing-for-agents` skill before editing either one. Work its levers
530
+ in this order:
531
+
532
+ - **Cache test.** `mu <verb> --help` is a one-command lookup that cannot go
533
+ stale. A skill that restates it is a cache that rots. Verb lists, flag
534
+ enumerations and option tables belong in `--help`; the skill carries only
535
+ what `--help` cannot say — the gotcha, the reason, the trap.
536
+ - **Single source of truth.** One meaning, one place. Scattered advice does not
537
+ get assembled by the reader: it gets missed. Prefer a consolidating edit that
538
+ deletes as much as it adds.
539
+ - **Progressive disclosure.** SKILL.md gets the trigger and the rule.
540
+ REMOTE_WORKERS.md gets the detail. Anything only some branches need goes
541
+ behind the pointer, never in the always-loaded file.
542
+ - **No-op hunt.** Delete any sentence the model already obeys by default. The
543
+ test is behavioural: does it change what an agent does?
544
+ - **Positive instruction.** State the target behaviour. A prohibition drags the
545
+ banned shape into context and makes it more available, so name an
546
+ anti-pattern only where a reader needs to recognise their own instinct.
547
+
548
+ Measure before and after. An edit that grows SKILL.md needs a reason in the
549
+ commit message, and "the new verb needed documenting" is not one.
550
+
551
+ Two failures already paid for this rule. A 910-word section titled "CLI
552
+ overview (only gotchas; use `--help` for full syntax)" was half verb list, and
553
+ a DO/DON'T section was the orchestrator loop written twice — 675 words removed
554
+ with nothing lost. And a warning about wedging a host existed in the file while
555
+ the agent that wedged the host never saw it, because it sat 350 lines from the
556
+ section it applied to. **A skill that sprawls stops being read**, which makes
557
+ sprawl a correctness problem, not a tidiness one.
558
+
559
+ ---
560
+
561
+ ## What NOT to do
562
+
563
+ - **Don't pad the skill files.** They are context, not docs, and
564
+ `--help` is the verb list. See § Skill files are context, not
565
+ documentation.
566
+ - **Don't add a config file.** mu is CLI flags + env vars.
567
+ - **Don't add a daemon, watcher, or background process.** Every
568
+ invocation is short-lived.
569
+ - **Don't add abstractions for hypothetical future flexibility.**
570
+ Two real impls today, or use a concrete type. (Cautionary tale: a
571
+ prior internal runtime's `RunContext` trait with zero implementors.)
572
+ - **Don't grow stream wrappers around stream wrappers.**
573
+ (`TextStream` / `TextState` / `StreamResult`.)
574
+ - **Don't generate JS strings as a "typed protocol."**
575
+ - **Don't put state-snapshot/handle layering on top of SQLite.**
576
+ SQLite is the canonical state. Read it directly; don't introduce
577
+ a `MuStateHandle` facade.
578
+ - **Don't add a template/discovery system for agent roles** until
579
+ pattern promotion criteria are met.
580
+ - **Don't bundle pi.** It's a peer dep, optional.
581
+ - **Don't write to files outside `~/.local/state/mu/` or the
582
+ project repo** without documenting why.
583
+ - **Don't promote a roadmap item to "shipped"** unless its
584
+ promotion criteria in [docs/ROADMAP.md](docs/ROADMAP.md) are met.
585
+
586
+ ---
587
+
588
+ ## When you're done
589
+
590
+ Before opening a PR or marking a task complete:
591
+
592
+ ```bash
593
+ npm run typecheck && npm run lint && npm run test:fast && npm run test && npm run build
594
+ ```
595
+
596
+ The acceptance test (`test/acceptance.integration.test.ts`) must
597
+ pass — it's the "end-to-end works" gate.
598
+
599
+ Checklist for any non-trivial change:
600
+
601
+ - [ ] If you added a typed verb, the corresponding `mu sql`
602
+ workaround row was removed from `docs/USAGE_GUIDE.md`.
603
+ - [ ] If you added vocabulary, `docs/VOCABULARY.md` has the new
604
+ entry.
605
+ - [ ] If you changed an architectural seam,
606
+ `docs/ARCHITECTURE.md` is updated.
607
+ - [ ] [CHANGELOG.md](CHANGELOG.md) has an entry under the upcoming
608
+ version.
609
+
610
+ That's it. Be small, be typed, follow the conventions, ship clean
611
+ green builds.