@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/docs/VISION.md CHANGED
@@ -316,10 +316,11 @@ speaking for another.
316
316
  tests. Verification is the caller's job. (mu may grow optional
317
317
  verifying-runners later if friction surfaces; today it's an
318
318
  audit-trail discipline, not enforcement.)
319
- - **Not undoable.** No snapshots, no `mu undo`. `mu workstream
320
- destroy --yes` is irreversible. Recovery is restoring `mu.db` from
321
- a backup. Snapshots are deferred past 1.0 the SQL escape hatch
322
- + FK CASCADE behaviour cover most repair scenarios.
319
+ - **DB-undoable, not substrate-undoable.** Destructive verbs
320
+ auto-capture whole-DB snapshots and `mu undo --yes` restores the
321
+ registry. That does not replay killed tmux panes or recreate freed
322
+ workspace directories; after a restore, reconciliation reports
323
+ ghosts/orphans and the caller decides what to re-spawn or adopt.
323
324
 
324
325
  ---
325
326
 
@@ -354,6 +355,49 @@ speaking for another.
354
355
  (SQLite hooks, fs.watch) are a future ask if anyone hits the
355
356
  latency cliff.
356
357
 
358
+ 7. **Every invocation is short-lived — except for two named
359
+ interactive readers.** mu is a CLI: each verb starts, mutates
360
+ or reads, prints, and exits. There is no daemon, no resident
361
+ state outside SQLite, no background process. Two verbs are
362
+ deliberately and narrowly exempt because they are interactive
363
+ *readers*, not background workers:
364
+
365
+ - `mu log --tail` — polls SQLite once per second; emits NDJSON
366
+ until SIGINT or the parent closes stdin.
367
+ - the TUI dashboard — rendered by `mu state --tui` explicitly or
368
+ by bare `mu` when stdout is attached to a TTY, until the user
369
+ quits with `q`/`Ctrl-C`. Plain `mu state` keeps the static-card
370
+ behaviour; non-TTY bare `mu` prints help instead of entering Ink.
371
+
372
+ Both share the same shape, and the shape is the predicate that
373
+ bounds the exception:
374
+
375
+ - **Interactive, not a daemon.** The process is owned by a
376
+ human (or a parent script) and dies the moment that owner
377
+ ends it. Nothing keeps it alive across sessions; nothing
378
+ restarts it.
379
+ - **Read-only against SQLite.** Neither verb writes. The TUI's
380
+ act-intents (claim, close, send, ...) yank the canonical
381
+ `mu <verb>` command into the clipboard and exit the
382
+ dashboard; every mutation still lands through a fresh
383
+ short-lived CLI invocation.
384
+ - **No resources beyond stdout, stdin, and a poll timer.** No
385
+ sockets, no file watches, no subscriptions to external
386
+ services, no spawned subprocesses, no inter-process state
387
+ beyond the SQLite reads any other CLI invocation already
388
+ does.
389
+ - **Human-TTY gated, with static/script fallbacks.** The TUI mode
390
+ activates when `--tui` is passed to `mu state` or when bare `mu`
391
+ sees `process.stdout.isTTY === true`. Default `mu state` prints
392
+ the static card. Non-interactive callers (pipes, CI, `--json`, or
393
+ `MU_NO_TUI=1`) never enter the TUI.
394
+
395
+ This is not a precedent for any other long-lived process. The
396
+ anti-feature pledges in [ROADMAP.md](ROADMAP.md) ("no daemon,
397
+ watcher, or background process beyond what tmux / SQLite give
398
+ us") remain in force; a third member of this exception class
399
+ would need its own promotion.
400
+
357
401
  ---
358
402
 
359
403
  ## What looking at a prior multi-agent runtime taught us
@@ -49,15 +49,18 @@ defined here, fix the doc. If you need a new term, add it here first.
49
49
  | **one-shot** | Agent that exists for a single task and then terminates | "ephemeral", "transient" |
50
50
  | **workspace** | A VCS-isolated checkout (jj workspace / sl worktree / git worktree / cp) | "branch" (it has one but isn't one), "checkout" (only for `none` backend) |
51
51
  | **workspace orphan** | A directory under `<state-dir>/workspaces/<workstream>/` with no row in `vcs_workspaces`. Blocks subsequent `--workspace` spawns. Surfaced by `mu workspace orphans -w X` and `mu state -w X`. | "stray dir", "leftover workspace" |
52
- | **stale workspace** | A workspace whose `parent_ref` is N commits behind the project's default branch HEAD (per the workspace's local refs cache). Rendered as a color-coded `behind` column (green ≤2, yellow 3–9, red ≥10) in `mu workspace list` and `mu state`; ≥10 triggers a one-line warn in `mu state`. Pure observation — mu never auto-fetches. | "out of date", "drifting" |
52
+ | **stale workspace** | A workspace whose `parent_ref` is N commits behind the project's default branch HEAD (per the workspace's local refs cache). Rendered as a color-coded `behind` column (green ≤2, yellow 3–9, red ≥10) in `mu workspace list` and `mu state`; ≥10 triggers a one-line warn in `mu state`, `mu task claim --for`, and `mu agent send` (or refusal on the two dispatch verbs with `--strict-staleness`). Pure observation — mu never auto-fetches. | "out of date", "drifting" |
53
53
  | **refresh** | `mu workspace refresh <agent>` — rebase the agent's workspace onto a fresh base (default = backend's tracked main; `--from <ref>` overrides) WITHOUT touching the agent or pane. Refuses on dirty WC; surfaces conflicts as exit 5 with a resolve-in-place hint. The `none` backend errors (no VCS to rebase). | "recycle", "reset" (overloaded) |
54
54
  | **recreate** | `mu workspace recreate <agent>` — free + create the agent's workspace in one shot. The between-wave "prep this worker for the next dispatch" verb. Reuses the previous backend unless `--backend` overrides; bases on current main unless `--from <ref>` overrides. Refuses on dirty WC the same way `free` does; `--force` discards the dirty edits (lossy). Sibling of **refresh**: refresh PRESERVES the worker's commits (rebases them onto fresh main); recreate THROWS THEM AWAY. | "recycle", "reset" (overloaded), "free+create" (only in commit messages) |
55
55
  | **backend** | Implementation of `AgentBackend` or `VcsBackend` | "driver", "provider" |
56
56
  | **detector** | Per-CLI pattern matcher for busy/permission/ready. Today mu has one (`detectPiStatus` in `src/detect.ts`); covers vanilla pi + any TUI wrapper that uses Braille spinner glyphs. Other CLIs spawned via `--cli <other>` may misclassify; trust scrollback over the emoji. | "matcher", "parser" |
57
57
  | **snapshot** | A whole-DB backup (`<state-dir>/snapshots/<id>.db`) auto-captured before each destructive verb (workstream destroy, agent close, task close/reject/defer/release/delete, workspace free). Indexed by the `snapshots` table; restore via `mu undo`. | "checkpoint", "backup" |
58
58
  | **prune** | Verb: bulk-drop rows from the snapshots collection per a policy (`mu snapshot prune` with `--keep-last`, `--older-than <D>d`, `--stale-version`, `--all`, or the bare GC-policy form). Sibling of the auto-GC that runs on every capture; the explicit verb is for the dogfood case where the auto-GC's count + age caps need an operator-driven supplement (e.g. "drop every snapshot whose schema_version is now stale"). Surgical single-row removal is `mu snapshot delete <id>`. | "reap", "sweep" (overloaded by workstream-destroy --empty) |
59
- | **export** | A directory of plain markdown files produced by `mu workstream export` (one `.md` per task + `INDEX.md` + `README.md` + `manifest.json`). Survives `mu workstream destroy` (auto-run pre-destroy to `<state-dir>/exports/<ws>-<ts>/` unless `--no-export`). Idempotent: re-export against the same dir rewrites only changed files; deleted tasks are preserved with a banner. Markdown-only by design — no HTML/PDF, no embedded VCS. The inverse is **import** (markdown only; never `.db`). | "dump", "snapshot" (snapshot is the binary `.db`) |
60
- | **import** | The inverse of **export**: `mu workstream import <bucket-dir>` walks a v0.3 bucket directory and rebuilds every source-ws subdir as live tasks + edges + notes in the DB. Markdown-only by design (cross-machine `.db` is `mu undo` + snapshots). Per-source-ws transactional; refuses to merge silently into an existing workstream (use `--workstream <name>` for single-source rename, or destroy the existing one first). Owners reset to NULL on import (agents aren't restored); the original owner name survives in the markdown frontmatter. | "rehydrate", "restore" (restore = `mu undo`) |
59
+ | **machine_id** | Per-state-directory uuid seeded on first `openDb` and stored in `machine_identity`. Identifies one mu DB across `mu db export` / `mu db import`; users do not configure it. | "device id", "host id" |
60
+ | **db sync** | The `mu db {export, import, replay}` cluster of verbs: whole-DB SQLite copy with manifest, per-workstream drift-detecting import, and manual replay from parked divergence sidecars. Explicit file handoff only; not live synchronization. | "live sync", "replication", "collab" |
61
+ | **divergence sidecar** | SQLite file at `<state-dir>/divergence/<ws>-<ts>.db` parked by `mu db import --force-source` before clobbering local divergent state. Later inspected or cherry-picked via `mu db replay`. | "conflict backup", "loser DB" |
62
+ | **export** | A directory of plain markdown files produced by `mu workstream export` (one `.md` per task + `INDEX.md` + `README.md` + `manifest.json`). Survives `mu workstream destroy` (auto-run pre-destroy to `<state-dir>/exports/<ws>-<ts>/` unless `--no-export`). Idempotent: re-export against the same dir rewrites only changed files; deleted tasks are preserved with a banner. Markdown-only by design — no HTML/PDF, no embedded VCS. Exports are now read-only artifacts for humans / git / docs; the lossless movement paths are **db sync** and `mu archive restore`. | "dump", "snapshot" (snapshot is the binary `.db`) |
63
+ | **import** | Avoid as a generic noun unless naming `mu db import`. The removed `mu workstream import` bucket→DB round-trip was replaced by **db sync** for cross-machine handoff and `mu archive restore` for un-archive. | "rehydrate", "restore" (restore has specific meanings) |
61
64
  | **archive** | An operator-named bucket of preserved task graphs (rows in `archives` + `archived_tasks` + `archived_edges` + `archived_notes` + `archived_events`). Cross-workstream and additive: one archive may accumulate snapshots from many workstreams under the same label. Outlives every source workstream; `archived_tasks.source_workstream` is intentionally TEXT (not an FK) so destroyed-workstream attribution survives. Distinct from a **snapshot** (binary whole-DB backup for `mu undo`) and an **export** (markdown files on disk). | "backup", "vault" |
62
65
  | **archived task** | A row in `archived_tasks`: a snapshot of a `tasks` row at archive time. Pins `status`, `impact`, `effort_days`, `owner_name`, and the original `created_at`/`updated_at` for retrospect ordering. The `(archive_id, source_workstream, original_local_id)` composite UNIQUE makes `mu archive add` idempotent at the (archive, workstream) granularity. | "closed task" (status-orthogonal) |
63
66
  | **archive label** | The operator-facing TEXT name of an **archive**. Globally unique across the machine (NOT per-workstream — archives outlive workstreams). Shape: `/^[a-z][a-z0-9_-]{0,63}$/` (wider than workstream names because labels often encode workstream + date + purpose, e.g. `auth-2026-q1`). | "archive name" (in code; `label` only) |
@@ -72,6 +75,19 @@ defined here, fix the doc. If you need a new term, add it here first.
72
75
  | **reconcile** | Verb: re-derive registry rows from substrate reality (tmux). Always runs in `mu agent list` and `mu doctor`. | "sync", "refresh" |
73
76
  | **adopt** | Verb (`mu agent adopt`): register an existing tmux pane as a managed **agent**. The inverse of `mu agent list`'s 'orphan' state. Pane must be in the workstream's tmux session. | "import", "absorb" |
74
77
  | **pi-subagents** | A different package by Nico Bailon for in-pi focused delegation. Mu and pi-subagents are complementary, not competing. | conflating with mu |
78
+ | **TUI** | The interactive ink-based dashboard launched by bare `mu` in a TTY or explicitly by `mu state --tui`. Lives in `src/cli/tui/`. Read-only against SQLite (yanks, never executes). | "GUI", "interactive mode" |
79
+ | **dashboard** | The TUI's main screen — the grid of cards above the status bar. | "home screen", "main view" |
80
+ | **card** | A glanceable summary tile on the dashboard, identified by its toggle digit (0-9). Wrapped in a TitledBox. | "panel", "section" (overloaded) |
81
+ | **popup** | A fullscreen drill-down opened with `Shift+0`-`Shift+9` or a keybind-only shortcut such as `g` for DAG; single-popup invariant. Closed with `Esc`/`q`. | "modal", "dialog", "detail view" |
82
+ | **TitledBox** | The `<TitledBox>` component (`src/cli/tui/titled-box.tsx`) that renders a rounded border with the section header inset into the top border line. The visual primitive used by every card / popup / help overlay. | "header box", "box" (alone) |
83
+ | **tick** | The TUI's periodic data refresh (default 1s; `+/-/=` adjusts). Owned by a single `setInterval` in `<App>`. | "poll", "refresh" (verb sense), "frame" |
84
+ | **yank** | Copy the canonical `mu` command for the focused row to the clipboard. Bound to `y` in every popup. | "copy", "export command" |
85
+ | **footer** | The persistent bottom line on the dashboard showing the last yank. Cleared with `c`. | "status line" (reserved for status bar), "toast" |
86
+ | **toast** | Transient in-popup message (e.g. "tick floor 100ms" when `+` hits floor). | "notification", "banner" |
87
+ | **act-intent** | The conceptual action a `y` keypress would trigger. **Never executed by the TUI** — the user runs the yanked command in their shell. The R1 read-only contract: model drives the CLI. | "command intent", "action proposal" |
88
+ | **help overlay** | The `?` / `F1` modal showing the global + per-popup keymap. Same TitledBox family as cards/popups. | "keys", "cheat sheet" |
89
+ | **glanceable** | Design property of cards: readable at a glance, no cursor, no row interaction. The contract is "never exhaustive" — long lists clip with `+M more` hint pointing at the popup. | "compact", "summary" (use the noun form for the data, the adjective for the property) |
90
+ | **drill-down** | Design property of popups: full-screen, focused, scrollable, filterable. The exhaustive view a card promises in its `+M more` hint. | "detail view", "expansion" |
75
91
 
76
92
  ---
77
93
 
@@ -137,10 +153,10 @@ cache; `mu agent list` reconciles on every call.
137
153
  | Verb | Effect |
138
154
  | --------------------- | --------------------------------------------------------------------------- |
139
155
  | `mu agent free alice` | Sets `alice.status = 'free'`. Agent stays alive. Means "I'm done with you for now; you're available." |
140
- | `mu release feature_a`| Clears `tasks.owner` for `feature_a`. The agent who claimed it is unaffected. |
156
+ | `mu task release feature_a`| Clears the task owner for `feature_a`. The agent who claimed it is unaffected. |
141
157
  | `mu agent close alice` | Terminates alice's pane and removes from registry. Destructive. |
142
158
  | `mu agent kick alice` | Signals (default SIGINT) the foreground process group of alice's pane TTY. For wedged tool subprocesses (`find /`, busy-wait); the wrapping CLI itself is untouched. Refuses when the foreground IS the wrapping CLI. |
143
- | `mu detach alice` | (Future) Tmux-detaches alice's pane without killing the process. Not in v1. |
159
+ | *(none)* | There is no detach verb. Use tmux detach to leave a workstream attached session without killing panes. |
144
160
 
145
161
  **Don't conflate `free` and `release`.** Free is about the *agent*;
146
162
  release is about the *task*.
@@ -152,7 +168,7 @@ release is about the *task*.
152
168
  | `mu task add <id> ...` | Creates a new OPEN task |
153
169
  | `mu task close/open/reject/defer <id>` | Lifecycle transition |
154
170
  | `mu task claim <task> [--for <agent>]` | Atomic: sets `owner`, flips status to `IN_PROGRESS` |
155
- | `mu release <task>` | Clears `owner`. Auto-flips `IN_PROGRESS` → `OPEN` (so the task re-enters the ready set); other statuses preserved. `--reopen` forces `OPEN` from `CLOSED`/`REJECTED`/`DEFERRED` |
171
+ | `mu task release <task>` | Clears `owner`. Auto-flips `IN_PROGRESS` → `OPEN` (so the task re-enters the ready set); other statuses preserved. `--reopen` forces `OPEN` from `CLOSED`/`REJECTED`/`DEFERRED` |
156
172
  | `mu task note <task> "..."` | Appends to `task_notes`. Never edits prior notes. |
157
173
  | `mu task notes <task> [--tail N \| --since <iso> \| --since-claim]` | List notes (oldest first). `--tail N` (alias `--last N`) prints last N; `--since <iso>` filters by `created_at`; `--since-claim` auto-resolves to the most recent `task claim` event timestamp. `--since` and `--since-claim` are mutually exclusive. |
158
174
 
@@ -222,7 +238,19 @@ For worked examples of each verb, see
222
238
  [USAGE_GUIDE.md](USAGE_GUIDE.md).
223
239
 
224
240
  This document is a *vocabulary* doc; it doesn't try to be a verb
225
- reference too.
241
+ reference too. Rows here exist to keep names canonical, not to replace
242
+ `--help`.
243
+
244
+ | Operation | Canonical meaning |
245
+ | --------- | ----------------- |
246
+ | `mu db export <file>` | Whole-DB SQLite copy via `VACUUM INTO` plus `<file>.manifest.json` (`machineId`, `schemaVersion`, per-workstream `latestSeq`). |
247
+ | `mu db import <file>` | Drift-detecting per-workstream import from an exported DB. Dry-run by default; `--apply` commits; five case branches: `IDENTICAL` / `FAST_FORWARD` / `LOCAL_AHEAD` / `CONFLICT` / `IMPORT`. |
248
+ | `mu db replay <sidecar>` | Manual cherry-pick of tasks, notes, and eligible edges from a divergence sidecar parked by `mu db import --force-source`. |
249
+ | `mu archive restore <label> --as <new-ws> [--source <orig-ws>]` | Lossless un-archive from `archived_*` rows into a fresh workstream. |
250
+
251
+ Removed operation: `mu workstream import`. Use `mu db import` for
252
+ cross-machine sync and `mu archive restore` for un-archive. Bucket
253
+ exports remain read-only artifacts.
226
254
 
227
255
  ---
228
256
 
@@ -295,6 +323,9 @@ XDG-Base-Directory-Spec compliant. The state directory resolves as:
295
323
  `mu snapshot show <id>`). Default colocation: snapshots live
296
324
  next to the live DB, so per-test isolation works without env
297
325
  gymnastics.
326
+ - `<state-dir>/divergence/<workstream>-<timestamp>-<suffix>.db` —
327
+ divergence sidecars parked by `mu db import --force-source` before
328
+ clobbering local state. Replay selected rows with `mu db replay`.
298
329
  - mu does NOT consult any agent-template directory. If pi-subagents
299
330
  is installed, its `~/.pi/agent/agents/` and `.pi/agents/` paths
300
331
  are pi-subagents' concern — not mu's.
@@ -348,5 +379,5 @@ documented as "workstream id" in column comments.
348
379
  it duplicated the canonical-terms table at the top of this file,
349
380
  drifted out of sync, and carried entries for rejected features
350
381
  (capability, agent-frontmatter `persistent: false`, the JS DSL,
351
- the `defineOperation` registry). The table is the single source.
382
+ the operation-registry idea). The table is the single source.
352
383
  For deeper background, follow the links the table rows carry. -->
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@martintrojer/mu",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "A persistent, observable crew of pi agents running in one tmux session per workstream, coordinated through a built-in task DAG.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -26,7 +26,7 @@
26
26
  "pi-package"
27
27
  ],
28
28
  "engines": {
29
- "node": ">=20 <24"
29
+ "node": ">=20 <=24"
30
30
  },
31
31
  "main": "./dist/index.js",
32
32
  "types": "./dist/index.d.ts",
@@ -39,36 +39,36 @@
39
39
  "bin": {
40
40
  "mu": "./dist/cli.js"
41
41
  },
42
- "files": [
43
- "dist",
44
- "skills",
45
- "docs",
46
- "README.md",
47
- "AGENTS.md"
48
- ],
42
+ "files": ["dist", "skills", "docs", "README.md", "AGENTS.md"],
49
43
  "scripts": {
50
44
  "build": "tsup",
51
45
  "dev": "tsup --watch",
52
46
  "test": "vitest run",
47
+ "test:fast": "vitest run --config vitest.fast.config.ts",
53
48
  "test:watch": "vitest",
49
+ "test:watch:fast": "vitest --config vitest.fast.config.ts",
54
50
  "lint": "biome check src test",
55
51
  "lint:fix": "biome check --write src test",
56
52
  "format": "biome format --write src test",
57
- "typecheck": "tsc --noEmit",
58
- "prepare": "npm run build"
53
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit",
54
+ "prepare": "npm run build",
55
+ "test:stress": "tools/test-stress.sh"
59
56
  },
60
57
  "dependencies": {
61
58
  "better-sqlite3": "^11.8.0",
62
59
  "cli-table3": "^0.6.5",
63
60
  "commander": "^14.0.0",
64
61
  "execa": "^9.5.2",
62
+ "ink": "^5.0.0",
65
63
  "picocolors": "^1.1.1",
66
- "zod": "^3.24.1"
64
+ "react": "^18.0.0",
65
+ "string-width": "^4.2.3"
67
66
  },
68
67
  "devDependencies": {
69
68
  "@biomejs/biome": "^1.9.4",
70
69
  "@types/better-sqlite3": "^7.6.12",
71
70
  "@types/node": "^22.10.7",
71
+ "@types/react": "^18.0.0",
72
72
  "tsup": "^8.3.5",
73
73
  "typescript": "^5.7.3",
74
74
  "vitest": "^2.1.8"