@martintrojer/mu 0.3.1 → 0.4.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/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,8 +49,9 @@ 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
+ | **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) |
54
55
  | **backend** | Implementation of `AgentBackend` or `VcsBackend` | "driver", "provider" |
55
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" |
56
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" |
@@ -69,8 +70,21 @@ defined here, fix the doc. If you need a new term, add it here first.
69
70
  | **substrate** | An external system mu depends on (tmux, jj, sl, git, sqlite) | "dependency" (means npm dep), "service" |
70
71
  | **operation** | A canonical mu verb (e.g. `mu task add`). Each verb is a thin CLI wrapper over a typed function in `src/*.ts` — the SDK and the CLI share one surface. | "command" (overloaded), "action" |
71
72
  | **reconcile** | Verb: re-derive registry rows from substrate reality (tmux). Always runs in `mu agent list` and `mu doctor`. | "sync", "refresh" |
72
- | **adopt** | Verb: 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" |
73
+ | **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" |
73
74
  | **pi-subagents** | A different package by Nico Bailon for in-pi focused delegation. Mu and pi-subagents are complementary, not competing. | conflating with mu |
75
+ | **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" |
76
+ | **dashboard** | The TUI's main screen — the grid of cards above the status bar. | "home screen", "main view" |
77
+ | **card** | A glanceable summary tile on the dashboard, identified by its toggle digit (0-9). Wrapped in a TitledBox. | "panel", "section" (overloaded) |
78
+ | **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" |
79
+ | **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) |
80
+ | **tick** | The TUI's periodic data refresh (default 1s; `+/-/=` adjusts). Owned by a single `setInterval` in `<App>`. | "poll", "refresh" (verb sense), "frame" |
81
+ | **yank** | Copy the canonical `mu` command for the focused row to the clipboard. Bound to `y` in every popup. | "copy", "export command" |
82
+ | **footer** | The persistent bottom line on the dashboard showing the last yank. Cleared with `c`. | "status line" (reserved for status bar), "toast" |
83
+ | **toast** | Transient in-popup message (e.g. "tick floor 100ms" when `+` hits floor). | "notification", "banner" |
84
+ | **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" |
85
+ | **help overlay** | The `?` / `F1` modal showing the global + per-popup keymap. Same TitledBox family as cards/popups. | "keys", "cheat sheet" |
86
+ | **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) |
87
+ | **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" |
74
88
 
75
89
  ---
76
90
 
@@ -136,9 +150,10 @@ cache; `mu agent list` reconciles on every call.
136
150
  | Verb | Effect |
137
151
  | --------------------- | --------------------------------------------------------------------------- |
138
152
  | `mu agent free alice` | Sets `alice.status = 'free'`. Agent stays alive. Means "I'm done with you for now; you're available." |
139
- | `mu release feature_a`| Clears `tasks.owner` for `feature_a`. The agent who claimed it is unaffected. |
153
+ | `mu task release feature_a`| Clears the task owner for `feature_a`. The agent who claimed it is unaffected. |
140
154
  | `mu agent close alice` | Terminates alice's pane and removes from registry. Destructive. |
141
- | `mu detach alice` | (Future) Tmux-detaches alice's pane without killing the process. Not in v1. |
155
+ | `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. |
156
+ | *(none)* | There is no detach verb. Use tmux detach to leave a workstream attached session without killing panes. |
142
157
 
143
158
  **Don't conflate `free` and `release`.** Free is about the *agent*;
144
159
  release is about the *task*.
@@ -150,8 +165,9 @@ release is about the *task*.
150
165
  | `mu task add <id> ...` | Creates a new OPEN task |
151
166
  | `mu task close/open/reject/defer <id>` | Lifecycle transition |
152
167
  | `mu task claim <task> [--for <agent>]` | Atomic: sets `owner`, flips status to `IN_PROGRESS` |
153
- | `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` |
168
+ | `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` |
154
169
  | `mu task note <task> "..."` | Appends to `task_notes`. Never edits prior notes. |
170
+ | `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. |
155
171
 
156
172
  ---
157
173
 
@@ -316,8 +332,8 @@ XDG-Base-Directory-Spec compliant. The state directory resolves as:
316
332
  | `XDG_STATE_HOME` | Inherited; mu uses `<XDG_STATE_HOME>/mu` by default |
317
333
  | `MU_SEND_DELAY_MS` | Delay between bracketed paste and Enter (default `500`) |
318
334
  | `MU_TMUX_SOCKET` | Override tmux socket (`-L <name>`); default uses `$TMUX` |
319
- | `MU_<UPPER_CLI>_COMMAND` | Override the executable launched for `--cli <cli>` (e.g. `MU_PI_COMMAND=pi-alt` makes `--cli pi` exec `pi-alt`). Accepts multi-word strings (`MU_PI_COMMAND="pi-alt --some-flag"`); tmux exec's via a shell. Reconcile also treats the resolved binary as agent-worthy when surfacing orphan panes. |
320
- | `MU_SPAWN_LIVENESS_MS` | After spawn, wait this many ms then verify the pane is still alive. Default 1500. Set to 0 to disable (useful in CI). On detected death, the DB row is rolled back and `AgentDiedOnSpawnError` is thrown with the captured scrollback. |
335
+ | `MU_<UPPER_CLI>_COMMAND` | Override the executable launched for `--cli <cli>` (e.g. `MU_PI_COMMAND=pi-alt` makes `--cli pi` exec `pi-alt`; hyphens in the cli key become underscores in the env var name, so `--cli pi-meta` reads `MU_PI_META_COMMAND`). Accepts multi-word strings (`MU_PI_COMMAND="pi-alt --some-flag"`); tmux exec's via a shell. Reconcile also treats the resolved binary as agent-worthy when surfacing orphan panes. When this env var supplies the override (and `--command` did not), the spawn-success line surfaces the env-var name (`Spawned worker-1 (pi-meta via $MU_PI_META_COMMAND)`) so stale aliases are visible without `mu agent show`. |
336
+ | `MU_SPAWN_LIVENESS_MS` | After spawn, wait this many ms then verify the pane is still alive AND scan the tail of its scrollback for known startup-error patterns (provider auth failures — `No API key found for X`, `401 Unauthorized`, … — plus shell-level `command not found` / `No such file or directory` when the spawned binary vanished post-pre-flight). Default 1500. Set to 0 to disable (useful in CI). On detected death the DB row is rolled back and `AgentDiedOnSpawnError` is thrown with the captured scrollback; on a startup-error match (pane alive but parked at an error prompt) the row is rolled back and `AgentSpawnStartupError` is thrown with the matched line + remediation hints. The complementary pre-flight check (PATH lookup of `--cli`'s resolved binary BEFORE any side effect) is not env-tunable; on miss it throws `AgentSpawnCliNotFoundError` with no orphan workspace / pane / row. |
321
337
 
322
338
  These mirror pi-subagents' `PI_SUBAGENT_*` env vars in spirit but live
323
339
  in a separate namespace so the two can coexist in one pi session.
@@ -345,5 +361,5 @@ documented as "workstream id" in column comments.
345
361
  it duplicated the canonical-terms table at the top of this file,
346
362
  drifted out of sync, and carried entries for rejected features
347
363
  (capability, agent-frontmatter `persistent: false`, the JS DSL,
348
- the `defineOperation` registry). The table is the single source.
364
+ the operation-registry idea). The table is the single source.
349
365
  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.1",
3
+ "version": "0.4.0",
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",
@@ -37,7 +37,7 @@
37
37
  }
38
38
  },
39
39
  "bin": {
40
- "mu": "./dist/cli.js"
40
+ "mu": "dist/cli.js"
41
41
  },
42
42
  "files": [
43
43
  "dist",
@@ -50,25 +50,31 @@
50
50
  "build": "tsup",
51
51
  "dev": "tsup --watch",
52
52
  "test": "vitest run",
53
+ "test:fast": "vitest run --config vitest.fast.config.ts",
53
54
  "test:watch": "vitest",
55
+ "test:watch:fast": "vitest --config vitest.fast.config.ts",
54
56
  "lint": "biome check src test",
55
57
  "lint:fix": "biome check --write src test",
56
58
  "format": "biome format --write src test",
57
- "typecheck": "tsc --noEmit",
58
- "prepare": "npm run build"
59
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit",
60
+ "prepare": "npm run build",
61
+ "test:stress": "tools/test-stress.sh"
59
62
  },
60
63
  "dependencies": {
61
64
  "better-sqlite3": "^11.8.0",
62
65
  "cli-table3": "^0.6.5",
63
66
  "commander": "^14.0.0",
64
67
  "execa": "^9.5.2",
68
+ "ink": "^5.0.0",
65
69
  "picocolors": "^1.1.1",
66
- "zod": "^3.24.1"
70
+ "react": "^18.0.0",
71
+ "string-width": "^4.2.3"
67
72
  },
68
73
  "devDependencies": {
69
74
  "@biomejs/biome": "^1.9.4",
70
75
  "@types/better-sqlite3": "^7.6.12",
71
76
  "@types/node": "^22.10.7",
77
+ "@types/react": "^18.0.0",
72
78
  "tsup": "^8.3.5",
73
79
  "typescript": "^5.7.3",
74
80
  "vitest": "^2.1.8"