@mu-crew/mu 3.13.3 → 4.1.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.
Files changed (119) hide show
  1. package/AGENTS.md +73 -35
  2. package/README.md +71 -57
  3. package/dist/chunk-4ULY3ZKO.js +4482 -0
  4. package/dist/chunk-4ULY3ZKO.js.map +1 -0
  5. package/dist/chunk-7M634JMD.js +375 -0
  6. package/dist/chunk-7M634JMD.js.map +1 -0
  7. package/dist/{chunk-3BARPJLY.js → chunk-PZ5GOCWH.js} +16 -2
  8. package/dist/chunk-PZ5GOCWH.js.map +1 -0
  9. package/dist/chunk-SS7NQSSZ.js +3660 -0
  10. package/dist/chunk-SS7NQSSZ.js.map +1 -0
  11. package/dist/chunk-UCZPJX7K.js +14253 -0
  12. package/dist/chunk-UCZPJX7K.js.map +1 -0
  13. package/dist/{cli-CKOMHBXV.js → cli-TV3N47AZ.js} +7317 -4593
  14. package/dist/cli-TV3N47AZ.js.map +1 -0
  15. package/dist/cli.js +2 -3
  16. package/dist/cli.js.map +1 -1
  17. package/dist/errors-GRONKWFS.js +65 -0
  18. package/dist/extension/mu-pi.js +1632 -837
  19. package/dist/extension/mu-pi.js.map +1 -1
  20. package/dist/index.d.ts +4067 -2561
  21. package/dist/index.js +614 -297
  22. package/dist/index.js.map +1 -1
  23. package/dist/tui-FUBQ326D.js +1848 -0
  24. package/dist/tui-FUBQ326D.js.map +1 -0
  25. package/docs/ARCHITECTURE.md +118 -66
  26. package/docs/VOCABULARY.md +113 -49
  27. package/docs/guide/README.md +3 -3
  28. package/docs/guide/backends.md +8 -4
  29. package/docs/guide/cleanup.md +69 -33
  30. package/docs/guide/delegate.md +149 -66
  31. package/docs/guide/dispatch.md +148 -53
  32. package/docs/guide/getting-started.md +37 -44
  33. package/docs/guide/other-agent-clis.md +102 -0
  34. package/docs/guide/recovery.md +31 -50
  35. package/docs/guide/remote.md +122 -45
  36. package/docs/guide/sql.md +104 -24
  37. package/docs/guide/stop-a-worker.md +30 -37
  38. package/docs/guide/sync.md +11 -3
  39. package/docs/guide/upgrade.md +93 -43
  40. package/docs/reference/env.md +24 -32
  41. package/docs/reference/naming.md +14 -7
  42. package/docs/reference/surface-contract.md +153 -0
  43. package/package.json +33 -11
  44. package/prompts/mu-debug.md +3 -1
  45. package/prompts/mu-refute.md +2 -1
  46. package/prompts/mu-research.md +3 -1
  47. package/prompts/mu-review.md +2 -1
  48. package/prompts/mu-rules-audit.md +5 -4
  49. package/prompts/mu-sweep.md +2 -1
  50. package/prompts/mu-tournament.md +4 -2
  51. package/prompts/mu-until.md +3 -1
  52. package/prompts/ultrathink.md +1 -1
  53. package/skills/mu/SKILL.md +60 -291
  54. package/skills/mu/recipes/adversarial-review.md +12 -103
  55. package/skills/mu/recipes/brief.md +14 -61
  56. package/skills/mu/recipes/course-correct.md +23 -0
  57. package/skills/mu/recipes/findings.md +17 -111
  58. package/skills/mu/recipes/models.md +17 -92
  59. package/skills/mu/recipes/move.md +14 -0
  60. package/skills/mu/recipes/orchestrator-loop.md +20 -119
  61. package/skills/mu/recipes/recovery.md +8 -29
  62. package/skills/mu/recipes/remote-workers.md +17 -318
  63. package/skills/mu/recipes/tasks-or-calls.md +15 -78
  64. package/skills/mu/recipes/test-health.md +19 -77
  65. package/skills/mu/recipes/worker.md +17 -24
  66. package/dist/chunk-45KQLMEL.js +0 -2908
  67. package/dist/chunk-45KQLMEL.js.map +0 -1
  68. package/dist/chunk-G434JKV2.js +0 -641
  69. package/dist/chunk-G434JKV2.js.map +0 -1
  70. package/dist/chunk-KUGUFBWH.js +0 -12527
  71. package/dist/chunk-KUGUFBWH.js.map +0 -1
  72. package/dist/chunk-QITLNZEM.js +0 -87
  73. package/dist/chunk-QITLNZEM.js.map +0 -1
  74. package/dist/chunk-VIEYQMQA.js +0 -393
  75. package/dist/chunk-VIEYQMQA.js.map +0 -1
  76. package/dist/chunk-WI6AWCMK.js +0 -19
  77. package/dist/chunk-WI6AWCMK.js.map +0 -1
  78. package/dist/cli-CKOMHBXV.js.map +0 -1
  79. package/dist/devtools-LS4ZEPWM.js +0 -3772
  80. package/dist/devtools-LS4ZEPWM.js.map +0 -1
  81. package/dist/tui-LFJ5P4Y2.js +0 -40956
  82. package/dist/tui-LFJ5P4Y2.js.map +0 -1
  83. package/dist/tui-sync-worker.js +0 -17
  84. package/dist/tui-sync-worker.js.map +0 -1
  85. package/dist/yank-XDOONQSL.js +0 -13
  86. package/dist/yank-XDOONQSL.js.map +0 -1
  87. package/docs/ROADMAP.md +0 -123
  88. package/docs/VISION.md +0 -298
  89. package/docs/architecture/control-socket.md +0 -142
  90. package/docs/architecture/dag.md +0 -117
  91. package/docs/architecture/mux.md +0 -167
  92. package/docs/architecture/ops-log.md +0 -160
  93. package/docs/architecture/sdk.md +0 -187
  94. package/docs/architecture/sync.md +0 -159
  95. package/docs/architecture/tui.md +0 -165
  96. package/docs/guide/tui.md +0 -98
  97. package/docs/history/CHANGELOG-pre-3.md +0 -4866
  98. package/docs/img/tui-dashboard.png +0 -0
  99. package/docs/specs/2026-09-28-agent-state-from-murmur.md +0 -333
  100. package/docs/specs/2026-09-29-task-substates.md +0 -247
  101. package/docs/specs/2026-10-03-keep-driving-nudge.md +0 -163
  102. package/docs/specs/2026-10-21-fast-and-slow-gates.md +0 -229
  103. package/skills/mu/recipes/backlog-triage.md +0 -45
  104. package/skills/mu/recipes/codemode-driver.md +0 -118
  105. package/skills/mu/recipes/deep-research.md +0 -69
  106. package/skills/mu/recipes/drift-audit.md +0 -75
  107. package/skills/mu/recipes/fan-out.md +0 -42
  108. package/skills/mu/recipes/hypothesis-panel.md +0 -48
  109. package/skills/mu/recipes/long-run.md +0 -32
  110. package/skills/mu/recipes/loop-until-done.md +0 -45
  111. package/skills/mu/recipes/plan.md +0 -67
  112. package/skills/mu/recipes/refute.md +0 -55
  113. package/skills/mu/recipes/review-panel.md +0 -65
  114. package/skills/mu/recipes/rules-audit.md +0 -89
  115. package/skills/mu/recipes/tournament.md +0 -44
  116. package/skills/mu/recipes/ultrathink.md +0 -86
  117. package/skills/mu/recipes/watcher.md +0 -28
  118. package/skills/mu/recipes/waves.md +0 -21
  119. /package/dist/{chunk-3BARPJLY.js.map → errors-GRONKWFS.js.map} +0 -0
@@ -1,117 +0,0 @@
1
- # The task DAG
2
-
3
- mu coordinates work through a directed acyclic graph of tasks. Without
4
- it mu is only an agent runner. Overview:
5
- [ARCHITECTURE.md](../ARCHITECTURE.md). Terms:
6
- [VOCABULARY.md](../VOCABULARY.md).
7
-
8
- ## Model
9
-
10
- - **Tasks** are nodes with mandatory `impact` (1-100) and
11
- `effort_days`. `ROI = impact / effort` drives prioritization.
12
- - **One edge type: `blocks`.** `A → B` means A must close before B can
13
- start. More edge types would make "can this start?" ambiguous.
14
- - **No hidden edges.** Every edge comes from a verb the caller ran (`block`,
15
- `add --blocked-by`, `reparent --blocked-by`); no write adds edges
16
- on its own or wires a task to a top goal. A hidden edge makes phantom
17
- blockers and false cycles, and `unblock` appears to do nothing.
18
- - **Status lifecycle:** `OPEN → IN_PROGRESS → CLOSED`. Status alone
19
- decides edge satisfaction: any `CLOSED` task satisfies its blockers.
20
- - **Substate** qualifies status and never touches edges:
21
- `OPEN/todo|parked|triage`, `IN_PROGRESS/active`,
22
- `CLOSED/done|rejected|wontfix|duplicate|superseded`. The rule is
23
- "store intent, derive graph facts": ready and blocked stay derived
24
- from edges. `OPEN/parked` and `OPEN/triage` are excluded from `ready`
25
- but stay in `goals`; `claim` refuses both without `--force`.
26
- - **Notes** are append-only per task. They survive LLM context loss
27
- and agent restarts, which fixes context loss at the task level
28
- rather than the agent level.
29
-
30
- ## Substate integrity
31
-
32
- The legal pairs live in the `task_substates` lookup table, seeded from
33
- code on every open (machine-local, never synced). `tasks` has a
34
- composite FK `(status, substate) → task_substates`,
35
- `DEFERRABLE INITIALLY DEFERRED`. That FK is the only guard on the pair.
36
-
37
- SQLite checks it at COMMIT, so apply's one-field-at-a-time UPDATEs
38
- pass as long as the pair is legal when the transaction ends. That is
39
- why `applyOp` wraps each task put in `db.transaction`. In autocommit
40
- the first per-field UPDATE would fail the FK.
41
-
42
- Every lifecycle write sets `status` and `substate` in one UPDATE, so
43
- one op carries both under one HLC. A pair can still split across ops:
44
- a status-only op from a v10 peer, an unknown substate from a newer
45
- peer, or a concurrent close and park on two machines. Before commit,
46
- `repairTaskPair` (`src/apply.ts`) sets the substate to the one implied
47
- by the newest op in the log that writes a substate, resolved against
48
- the row's current status. If that pair is illegal, it falls back to
49
- the status default.
50
-
51
- Substate is therefore not per-field LWW. Reading the log instead of
52
- the row makes the result a pure function of the op set plus the row's
53
- status, so every peer converges whatever the arrival order. For the
54
- same reason the repair records no op: each peer computes the identical
55
- repair from the identical log.
56
-
57
- ## Built-in views
58
-
59
- | View | Returns |
60
- | --- | --- |
61
- | `ready` | `OPEN/todo` tasks with no unresolved blockers: work that can start now |
62
- | `blocked` | OPEN tasks waiting on something |
63
- | `goals` | tasks with no dependents: graph endpoints |
64
-
65
- Agents and humans query these views directly through `mu sql`. There
66
- is no separate query layer.
67
-
68
- ## Parallel tracks
69
-
70
- The Tracks section of `mu state` and bare `mu` runs union-find
71
- (`src/tracks.ts`) to find independent subtrees that different agents
72
- can work in parallel.
73
-
74
- Diamonds merge. If two roots share a prerequisite, they collapse into
75
- one track, so two agents never collide on the shared dependency:
76
-
77
- ```
78
- Independent (2 tracks): Diamond (1 merged track):
79
-
80
- goal_a goal_b goal_a goal_b ← Spawn 2 agents
81
- | | \ /
82
- task_a task_b shared ← Spawn 1 (would
83
- | | | collide otherwise)
84
- leaf_a leaf_b leaf
85
- ```
86
-
87
- The graph algorithm decides, not the LLM.
88
-
89
- ## Claim protocol
90
-
91
- `mu task claim <task>` resolves the claimer, then atomically sets
92
- `tasks.owner`, flips `status` to `IN_PROGRESS`, and records a
93
- `task.claim` op carrying the resolved `actor`. The claim is a CAS in
94
- SQLite, so two agents cannot claim the same task.
95
-
96
- Identity resolution (`resolveWorkerIdentity`, `src/tasks/claim.ts`) is
97
- a two-rung ladder:
98
-
99
- 1. **`$MU_AGENT_NAME`**, injected into the pane's environment at spawn.
100
- It is backend-independent.
101
- 2. **`backend.currentAgentName()`**, the mux fallback. On tmux it reads
102
- the pane title (`display-message -p '#{pane_title}'`), never the
103
- window name, because one window can hold several agents. On herdr
104
- the name is registered with the mux and looked up by pane id.
105
-
106
- The fallback exists for **adopted** panes (`mu agent adopt`), which
107
- predate the env injection and carry only a title. The agent does not
108
- need to know its own name.
109
-
110
- ## Scoped subtree views
111
-
112
- `mu task tree <id>` and the task queries show the part of the graph
113
- reachable from a task. Scoping is a `WHERE` clause, which makes
114
- recursive delegation work: a sub-orchestrator inspects its slice
115
- without an LLM inferring the scope. `src/dag.ts` (`loadFullDag`,
116
- `renderForest`, `renderTaskTree`) serves both `mu task tree` and the
117
- TUI DAG popup.
@@ -1,167 +0,0 @@
1
- # Mux topology, backends and reconciliation
2
-
3
- mu runs agents in multiplexer panes, tmux or herdr, behind one
4
- `MuxBackend` interface. Overview: [ARCHITECTURE.md](../ARCHITECTURE.md).
5
-
6
- ## One mux session per workstream
7
-
8
- One workstream is one mux session and one `workstream_id` partition in
9
- `~/.local/state/mu/mu.db`. Workstreams on one machine are independent
10
- mux sessions.
11
-
12
- | mu term | tmux | herdr |
13
- | --- | --- | --- |
14
- | **mux session** | session | workspace |
15
- | **window** | window | tab |
16
- | **pane** | pane (`%15`) | pane (`w1:p1`) |
17
-
18
- herdr's own "session" is server-level (one socket), which is the wrong
19
- granularity, so a herdr workspace plays the mux-session role.
20
-
21
- ```
22
- tmux session: mu-auth-refactor (one mu workstream)
23
- ┌───────────────────────────────────────────────────────────┐
24
- │ Window: Backend Window: Review │
25
- │ ┌──────────┐ ┌──────────┐ ┌────────────────────────┐ │
26
- │ │ worker-1 │ │ worker-2 │ │ reviewer-1 │ │
27
- │ │ (pi) │ │ (pi) │ │ (pi, role=read-only) │ │
28
- │ └──────────┘ └──────────┘ └────────────────────────┘ │
29
- │ Window: mu-orchestrator │
30
- │ ┌────────────────────────────────────────────────────┐ │
31
- │ │ pi (you, with mu extension loaded) │ │
32
- │ └────────────────────────────────────────────────────┘ │
33
- └───────────────────────────────────────────────────────────┘
34
- ```
35
-
36
- - The first `mu agent spawn` creates the session if needed. Name it
37
- with `mu workstream init <name>` or `MU_SESSION=<name>`.
38
- - `tmux attach -t mu-<workstream>` shows the whole crew (herdr:
39
- `herdr workspace focus <id>`). Killing the session kills the
40
- workstream, with no leaked panes, and the crew survives a closed
41
- laptop.
42
- - `mu agent list` is scoped to one workstream; `-w` picks another.
43
- - `mu doctor` warns about orphan panes, ghost rows, and agents whose
44
- mux session is gone.
45
-
46
- **Window vs pane.** Each agent gets its own window, named by `--tab`
47
- (default: the agent name). Agents sharing a `--tab` value share a
48
- window. Identity never depends on the window: each pane gets
49
- `$MU_AGENT_NAME` plus a **pane title** fallback. The title carries
50
- durable context only (agent name and owned tasks), re-pushed by
51
- `composeAgentTitle` after each relevant change. mu enables the top
52
- border and leaves `pane-border-format` to the user's tmux config. The
53
- canonical tmux protocol is the comment block at the top of
54
- `src/mux/tmux.ts`.
55
-
56
- ## The `activeMux()` seam
57
-
58
- Every call site reaches its multiplexer through
59
- `(await activeMux()).<method>()`. Nothing outside `src/mux/` names a
60
- backend. `src/mux/detect.ts` picks one: `MU_MUX` → `HERDR_ENV` →
61
- `$TMUX` → `PATH`. An unknown `MU_MUX` value fails the invocation
62
- instead of falling back to tmux.
63
-
64
- Each call site has decided what "no reachable multiplexer" means:
65
-
66
- | | On `NoMultiplexerError` | Examples |
67
- | --- | --- | --- |
68
- | **Load-bearing** | propagate; `handle()` maps it to exit 5 | spawn, send, read, kill, adopt, kick, reconcile, session create and destroy |
69
- | **Best-effort** | `try`/`catch`, degrade | actor identity, pane titles, pane borders, workstream listings, liveness polls, TUI attach |
70
-
71
- `resolveWorkerIdentity()` in `src/tasks/claim.ts` is the canonical
72
- best-effort shape. `reconcile()` is load-bearing on purpose: treating
73
- an unreachable mux as "zero panes" would prune every agent as a ghost
74
- and reap its in-progress tasks.
75
-
76
- Three concerns belong to the backend, because each would otherwise
77
- hardcode a tmux string a herdr user sees:
78
-
79
- - `attachHint()` and `attachCommands()`: the printed recipe and the
80
- TUI's executed argv.
81
- - `healthCheck()`: version and env facts as data; `mu doctor` renders.
82
- - `paneNotFoundNextSteps()`: borrowed by `PaneNotFoundError` from the
83
- backend that raised it.
84
-
85
- `src/tmux.ts` survives as a re-export for tmux-only concerns: the
86
- `MU_TMUX_SOCKET` test-isolation seam and the shared `sleep` /
87
- `setSleepForTests` poll seam.
88
-
89
- ## Spawn: create-and-run vs create-then-start
90
-
91
- tmux creates a pane and runs a command in one call. herdr has no
92
- create-and-run form: its creation verbs start a plain shell, and
93
- `agent start` needs an existing pane at its prompt. mu expresses this
94
- as a capability, not a name check:
95
-
96
- | | tmux | herdr |
97
- | --- | --- | --- |
98
- | `NewWindowOptions.command` | carries the command | refused if non-empty |
99
- | `startAgentInPane()` | absent | implemented |
100
- | after start | mu checks pane life and startup errors | the mux returns once the agent is ready |
101
-
102
- `spawnAgent` branches on `mux.startAgentInPane !== undefined`.
103
-
104
- - **tmux.** After `MU_SPAWN_LIVENESS_MS` (default 1500, `0` skips) mu
105
- verifies the pane survived and scans its tail for provider or auth
106
- startup errors. mu does not wait for murmur to claim the pane: state
107
- reads before the first claim report `unknown`. A pi agent skips the
108
- fixed wait: mu checks the pane on every handshake tick (a dead pane
109
- rolls back at once) and scans for startup errors when the handshake
110
- ends. After an ok handshake the scan skips shell exec-failure lines
111
- (`command not found`, `No such file or directory`): pi demonstrably
112
- started, and a resumed session can show such lines as old tool output.
113
- - **herdr.** mu creates a bare pane and calls `startAgentInPane`, which
114
- subsumes both checks.
115
- - **pi, either backend.** After the agent starts, mu polls its control
116
- socket until the extension answers (`MU_SPAWN_CTL_MS`, default 30s;
117
- `--no-ctl` or `0` skips). A timeout is reported but not rolled back:
118
- the pane and pi are fine, only control is missing. See
119
- [control-socket.md](control-socket.md).
120
-
121
- A creation verb refuses a command it cannot honour. A dropped command
122
- would leave an empty shell that mu believes hosts an agent. The
123
- refusal and every failure before the handshake route through
124
- `rollbackSpawn`, so no agent row survives for an empty pane.
125
-
126
- Spawns serialize per mux session (`src/agents/spawn-lock.ts`, over
127
- `src/file-lock.ts`) only around the topology check-then-act and the
128
- row insert. Agent start and the readiness waits sit outside the lock,
129
- so a parallel fan-out still runs in parallel.
130
-
131
- ## Reconciliation
132
-
133
- `mu agent list` reconciles the registry against the mux before
134
- returning. `src/reconcile.ts` is the only implementation, and
135
- `mu doctor` calls the same routine.
136
-
137
- 1. **Prune ghosts.** Delete each `agents` row whose `pane_id` no longer
138
- exists.
139
- 2. **Surface orphans.** List panes in the workstream's session that
140
- have no row but whose title looks like an agent name. mu never
141
- adopts them; the user runs `mu agent adopt <pane> [--name X]`.
142
-
143
- The mux is the source of truth for which panes exist.
144
-
145
- ## Agent state
146
-
147
- Runtime state is read on demand and never persisted.
148
- `src/agent-state.ts` picks one source per agent:
149
-
150
- | Agent | Source |
151
- | --- | --- |
152
- | pi (`expectsCtl`) | its control socket, probed in parallel for all pi agents |
153
- | other CLI on herdr | herdr's native `paneStatus()` |
154
- | other CLI on tmux | murmur: one `list-panes -a` reads `@murmur_pane_state` / `@murmur_pane_since`; agents without a local option fall back to `murmur status --json`, cached 10s |
155
-
156
- - ctl `idle` maps to `needs_input`. murmur maps `working` → `busy`,
157
- `blocked` → `needs_permission`, and `idle` / `done` / `crashed` →
158
- `needs_input`. herdr supplies mu's states directly. See murmur's
159
- [stable contract](https://github.com/mu-crew/murmur/blob/main/ARCHITECTURE.md#contract).
160
- - An absent reading is `unknown` with a reason: `ctl missing`,
161
- `ctl refused`, `murmur not installed`, `murmur pi extension not
162
- linked`, `murmur has no row`, `remote snapshot stale`, `herdr reports
163
- no state`, or `pane gone`.
164
- - `src/mux/input-timing.ts` reads pane text only to decide when tmux
165
- input can be submitted on the paste path. It does not report state.
166
- - `agents.status` is deprecated. Inserts write `spawning` to satisfy the
167
- schema; nothing reads it as runtime state.
@@ -1,160 +0,0 @@
1
- # The ops log
2
-
3
- mu keeps one append-only table, `ops`. History, undo, sync, rebuild and
4
- drift detection are all queries or replays over it. Overview:
5
- [ARCHITECTURE.md](../ARCHITECTURE.md).
6
-
7
- ## Capture is a trigger
8
-
9
- Every INSERT, UPDATE and DELETE on a portable table (`workstreams`,
10
- `tasks`, `task_edges`, `task_notes`) is recorded as an **op** by a
11
- SQLite trigger, in the same transaction as the mutation. No SDK
12
- function decides to record history, so no call site can forget to, and
13
- the log cannot drift from the data, even on power loss.
14
-
15
- The tables are the materialized view; the log is the record. Reads hit
16
- the tables as ordinary indexed SQL. Nothing replays the log to answer
17
- a query. The log is what the tables are derived from and can be
18
- rebuilt from.
19
-
20
- ```
21
- mu task close t3
22
- │
23
- ▼
24
- UPDATE tasks SET status=... ──┬──► tables (what reads see)
25
- └──► ops (what everything else reads)
26
- one transaction
27
- ```
28
-
29
- | Feature | Over the log |
30
- | --- | --- |
31
- | history | `mu log` is a typed reader over `ops` (`src/logs.ts`) |
32
- | undo | `mu undo <group>` emits the inverse ops for one group |
33
- | sync | each machine appends its own ops to a JSONL segment and applies each peer's from a watermark ([sync.md](sync.md)) |
34
- | rebuild | `mu rebuild <file>` replays the whole log into a new DB |
35
- | drift | `mu doctor --deep` rebuilds into a temp DB and diffs it against the live tables |
36
-
37
- **Ops are semantic partial updates.** An UPDATE op carries only the
38
- columns that changed. That makes per-field merge free: two machines
39
- editing different fields of one task both keep their edit. A full-row
40
- payload would silently regress this to row-level last-writer-wins.
41
-
42
- **The cost:** one capture bug breaks history, undo and sync at once,
43
- silently. Hence the two drift tiers below.
44
-
45
- **Ordering** across machines comes from the **HLC** (`src/hlc.ts`) on
46
- every op: a hybrid logical clock serialized as sortable TEXT
47
- `<wall_ms:15>.<counter:6>.<machine_id>`, so bytewise `ORDER BY hlc` is
48
- causal order. Clock state lives in `machine_identity`.
49
-
50
- **Machine-local tables** (`agents`, `vcs_workspaces`) have no capture
51
- triggers. Their contents are pane ids and absolute paths, which mean
52
- nothing on another machine. Their changes are recorded as log-only
53
- events through `emitEvent`. `appendLog` writes operator prose
54
- (`mu log "text"`).
55
-
56
- ## Trigger mechanics
57
-
58
- - **Capture triggers are TEMP triggers**, reinstalled per connection
59
- by `src/capture.ts`. SQLite refuses a main-schema trigger that
60
- references the temp `_op_ctx` table. Two consequences follow:
61
- - DELETE keys are captured inline, with the parent stashing its
62
- natural key in `_op_dying` first. FK CASCADE fires child triggers
63
- after the parent is gone.
64
- - The HLC is minted in SQL, because a trigger cannot call into JS.
65
- - **Op context** (`src/op-context.ts`): `withOpContext(db, {intent,
66
- actor, group}, fn)` labels every op in a scope and restores in a
67
- `finally`. Nested scopes inherit the group, which puts a cascade
68
- under one `mu undo`. `withCaptureSuppressed` is the echo guard used
69
- by the apply path.
70
- - An importer must synthesize **ops, not rows**. A direct INSERT is
71
- invisible to sync and reported as drift.
72
-
73
- ## Apply and merge
74
-
75
- `src/apply.ts` is capture's counterpart: given one op, local or from a
76
- peer, it makes the tables reflect it.
77
-
78
- - **Merge is per entity.** Notes are grow-only sets. Tasks and
79
- workstreams merge per field by HLC. Edges are an LWW-element-set.
80
- - A **tombstone** is an ordinary op with an HLC, so out-of-order
81
- arrival is a comparison and resurrection falls out.
82
- - Never use `json_patch`: RFC 7396 reads a null member as
83
- delete-the-key.
84
- - Task `status` and `substate` are repaired as a pair, not merged per
85
- field. See [dag.md](dag.md#substate-integrity).
86
-
87
- ## Undo
88
-
89
- `src/undo.ts` derives the inverse ops for one `group_id` from log
90
- provenance. It refuses a superseded group (exit 4; `--force`
91
- overrides). Bare `mu undo` lists undoable groups (`-n` widens), a group
92
- prefix previews, and `--yes` applies.
93
-
94
- - **Undo and restore write through the tables**, so capture records
95
- them like any other write.
96
- - `restore` records and applies under the **same** HLC. `applyOp`
97
- excludes an op's own HLC from provenance. A fresher HLC would make
98
- the row outrank the op and lose every field to an insert default.
99
- - **Note tombstones are self-describing only when they must be.** Every
100
- other `del` carries `'{}'`, because its key plus earlier puts describe
101
- the row. A note's key embeds its rowid (`<ws>/<task>#<id>`), and a
102
- rebuild or reprojection reassigns rowids, so the puts can sit under an
103
- old key (drift-641). The `task_notes` delete trigger records `OLD.*`
104
- only when no put under the current key is in the log, and `'{}'`
105
- otherwise; `planUndo` folds the puts first and falls back to the
106
- tombstone payload, and `applyOp` deletes by the payload's content
107
- identity, falling back to the put under the same key. For the same reason `src/drift.ts` matches notes on
108
- the task-key prefix.
109
-
110
- ## Rewriting history (local only)
111
-
112
- The log is append-only except for two operator verbs in `src/compact.ts`,
113
- both dry-run by default, both backing up beside the DB and running the
114
- drift check after:
115
-
116
- - **`mu db compact`** blanks note tombstones a put under the same key
117
- already explains: the slim form capture writes now, applied to older
118
- rows. Undo and sync unaffected.
119
- - **`mu db forget <workstream...>`** deletes every op of named
120
- torn-down workstreams (keys `<ws>` and `<ws>/...`). Their rows are
121
- already gone from the tables, so rebuild and drift stay consistent;
122
- `mu undo` of those teardowns is gone on purpose.
123
-
124
- Neither touches sync segments. A peer is not affected: watermarks only
125
- read forward, so a peer never re-sends forgotten history.
126
-
127
- ## Rebuild
128
-
129
- `src/rebuild.ts` (`rebuildInto`) replays the whole log into a new DB
130
- file through `applyOp`. The verb prints counts, supports `--json`,
131
- suggests an `mv` swap, and warns that `agents` and `vcs_workspaces`
132
- cannot be reconstructed.
133
-
134
- Rebuild is not ingest:
135
-
136
- - Ingest filters to `SYNCED_ENTITIES`. Rebuild replays everything, so
137
- log-only entities are copied verbatim, or `mu log` comes back empty.
138
- - `src/legacy-ops.ts` classifies historical log-only intents whose
139
- entity looks projectable. Rebuild copies them without projection;
140
- flush never emits them.
141
- - `machine_identity` carries across whole: id, hostname and HLC clock.
142
- Otherwise the rebuilt DB is a different peer minting HLCs below
143
- every replayed op.
144
-
145
- ## Drift
146
-
147
- `src/drift.ts` has two tiers:
148
-
149
- | Tier | Runs | Checks |
150
- | --- | --- | --- |
151
- | cheap (`checkCheapDriftInvariant`) | default `mu doctor` | every live row has at least one op naming its key |
152
- | deep (`checkDrift`) | `mu doctor --deep` | rebuilds into a temp DB and diffs field by field |
153
-
154
- The deep diff matches rows by natural key and excludes `owner_id`,
155
- because a rebuild always has NULL owners. `driftRemediation` prints the
156
- fix.
157
-
158
- For disaster recovery, `mu db backup <file>` writes one scp-able file
159
- through `VACUUM INTO`. It never overwrites; it is the copy
160
- `SchemaTooOldError` tells you to take.
@@ -1,187 +0,0 @@
1
- # CLI and SDK surface
2
-
3
- Every user-visible operation is a typed SDK function plus a thin
4
- Commander wrapper. This page covers the boundary rules, errors, tests
5
- and packaging. Overview: [ARCHITECTURE.md](../ARCHITECTURE.md).
6
-
7
- ## One surface, two callers
8
-
9
- `src/cli.ts` (`buildProgram`) and the namespace files under `src/cli/`
10
- are the verb surface: no generated registry, no DSL, no separate
11
- operation schema. Programmatic callers import the same functions from
12
- `src/index.ts`. Agents and scripts compose verbs with `--json`.
13
-
14
- A verb's path:
15
-
16
- 1. The Commander block calls a `cmd<Verb>` wrapped in `handle()`.
17
- 2. `handle()` opens the DB, runs ambient sync ingest
18
- ([sync.md](sync.md#the-ambient-hook)), and calls the SDK function.
19
- 3. The SDK function opens `db.transaction(fn)()` for multi-statement
20
- writes. Agent ops call the mux (and jj, sl or git for workspaces);
21
- task ops are pure SQL. Read paths reconcile against the mux.
22
- 4. Capture triggers record ops in the same transaction
23
- ([ops-log.md](ops-log.md)). Machine-local changes go through
24
- `emitEvent`. `mu log --tail` sees them on its next 1-second poll.
25
- 5. On error the transaction rolls back and the typed error reaches
26
- `handle()`, which maps it to an exit code. Flush runs on both paths.
27
-
28
- Output is self-documenting: `src/output.ts` (`printNextSteps`,
29
- `errorNextSteps`) prints `Next:` hints, quoted by `src/shell-quote.ts`
30
- so they paste cleanly.
31
-
32
- ## Surrogate-PK and SDK-boundary discipline
33
-
34
- Every entity table has this shape:
35
-
36
- ```
37
- (
38
- id INTEGER PRIMARY KEY AUTOINCREMENT, -- surrogate; internal
39
- <scope_id> INTEGER NOT NULL REFERENCES <parent>(id) ON DELETE CASCADE,
40
- <name> TEXT NOT NULL, -- operator-facing; mutable
41
- -- ... domain attributes
42
- UNIQUE (<scope_id>, <name>) -- per-scope unique
43
- )
44
- ```
45
-
46
- FKs reference `<parent>_id` (INTEGER), never the TEXT name. The name is
47
- an attribute: searchable, displayable, cheap to rename. The surrogate
48
- id is the identity.
49
-
50
- **TEXT by design:**
51
-
52
- - the workstream's own `name` (it is the mux session name; globally
53
- unique);
54
- - `task_notes.author` and `ops.actor` (free-text labels such as
55
- `"orchestrator"`);
56
- - `ops.entity` (open enum; a new entity needs no migration);
57
- - `agents.cli` (a new CLI needs no schema change);
58
- - every column of `ops`. The log is FK-free and addresses rows by
59
- natural key, so an op outlives its row and its workstream.
60
-
61
- **The boundary rule** matches REST: the external API uses business
62
- identifiers, the internal layer uses primary keys.
63
-
64
- > **Public SDK functions take operator-facing names.**
65
- > **Internal helpers take surrogate ids.**
66
- > **Resolution happens at the public-function entry, exactly once.**
67
-
68
- ```ts
69
- // PUBLIC: takes operator-facing names
70
- export function claimTask(
71
- db: Db,
72
- workstream: string,
73
- localId: string,
74
- opts?: ClaimOptions,
75
- ): ClaimResult {
76
- const wsId = resolveWorkstreamId(db, workstream);
77
- const taskId = tryResolveTaskId(db, wsId, localId);
78
- if (taskId === null) throw new TaskNotFoundError(localId);
79
- const agentId = resolveCurrentAgentId(db, wsId);
80
- return claimTaskById(db, taskId, agentId, opts);
81
- }
82
-
83
- // INTERNAL: takes surrogate ids; never re-resolves
84
- function claimTaskById(db, taskId, agentId, opts): ClaimResult { ... }
85
- ```
86
-
87
- Resolving once gives three things: no double resolution; no
88
- workstream context needed below the boundary, because the FKs make
89
- scope implicit; and one place for error mapping.
90
- `WorkstreamNotFoundError` comes from `resolveWorkstreamId` in
91
- `src/db.ts`. `TaskNotFoundError` and `AgentNotFoundError` are raised by
92
- callers wrapping a `tryResolve*` null, so exit 3 holds whichever lookup
93
- missed.
94
-
95
- **Workstream scoping is mandatory at the CLI boundary.** TEXT names
96
- (`tasks.local_id`, `agents.name`) are unique per workstream, so the
97
- same name can exist in two. Every public function taking such a name
98
- also takes the workstream; internal SQL filters by
99
- `(workstream_id, name)`.
100
-
101
- **`--json` keeps operator-facing names.** Surrogate ids never appear in
102
- `--json`, error payloads, log lines or exports. Exposing them would
103
- bring back a global namespace.
104
-
105
- ## State of truth
106
-
107
- - The DB (`~/.local/state/mu/mu.db`, or `MU_DB_PATH`) is canonical.
108
- Everything else is a cache, including pane titles.
109
- - Writes go through the typed SDK functions, which validate, transact
110
- and reconcile. Op capture is the triggers' job.
111
- - In-memory state lives only for one command's connection.
112
- - Multiple `mu` processes share the file safely through SQLite WAL.
113
-
114
- ## Errors
115
-
116
- Errors are typed classes per layer (`src/agents/errors.ts`,
117
- `src/tasks/errors.ts`, `MuxError` with `TmuxError` / `HerdrError`, and
118
- so on). Nothing swallows them. `src/cli/handle.ts` maps them to exit
119
- codes:
120
-
121
- | Code | Meaning |
122
- | --- | --- |
123
- | 0 | success |
124
- | 1 | generic error |
125
- | 2 | usage error (Commander's default) |
126
- | 3 | not found (agent, task, workspace) |
127
- | 4 | conflict (name collision, double claim, dirty tree, schema too old or new, outdated extension) |
128
- | 5 | substrate unavailable or unsafe (no multiplexer, mux or VCS failure, DB locked, ops-log drift, a `mu doctor` FAIL row, timeout) |
129
- | 6 | `mu task wait`: the reaper reopened a watched task (its pane died) |
130
- | 7 | `mu task wait`: a watched task stalled (`--on-stall exit`) |
131
-
132
- Errors carry structured context (operation, target, attempted action)
133
- so `mu doctor` can show them readably.
134
-
135
- ## Testing layers
136
-
137
- Fast tier (`npm run test:fast`) excludes `*.integration.test.ts` and
138
- `*.smoke.test.ts`. It uses real SQLite in per-test temp DBs and a
139
- mocked mux (`installMux()` from `test/_mux.ts`). Integration tests use
140
- real tmux or herdr, and real git, jj or sl in `os.tmpdir()`; jj and sl
141
- tests skip when the binary is missing. Conventions live in
142
- [AGENTS.md](../../AGENTS.md) and `test/README.md`.
143
-
144
- | Area | What the tests pin |
145
- | --- | --- |
146
- | `src/capture.ts`, `src/op-context.ts` | payloads hold only changed columns (asserted by key count); echo suppression; cascade grouping; FK-CASCADE tombstone keys; the null-intent fail-safe |
147
- | `src/apply.ts` | injected HLCs, no sleeps: field convergence in both orders; same-field LWW determinism; all four put and del orderings; resurrection; grow-only notes; the set-to-NULL trap; `owner_id` stripping; replay idempotence |
148
- | `src/rebuild.ts` | round trip row by row per portable table (a count-only check passes on a garbage rebuild); tombstones; byte-identical ops; `machine_id` and clock carried |
149
- | `src/drift.ts`, `src/fleet-hazards.ts` | drift planted the way a capture bug would (`withCaptureSuppressed` plus a direct write), with equal weight on false positives |
150
- | `src/dormant.ts` | `updated_at` written directly; weighted to the boundary between `finished` and `abandoned` |
151
- | `src/disk-recon.ts` | a per-test `MU_STATE_DIR` with the on-disk shape built by hand, both directions |
152
- | `src/segments.ts`, `src/sync.ts` | two temp DBs and one shared dir; each robustness layer; machine-local ops never reaching a file |
153
- | `src/hlc.ts` | an injected clock: backward jumps, stalled milliseconds, concurrent minting |
154
- | `sync-session.integration` | 8 rounds of two machines: byte-identical portable content, no `--deep` drift, a quiet round adds zero ops |
155
- | `test/acceptance.integration.test.ts` | the end-to-end gate: 10 tasks, 3 agents |
156
-
157
- `npm run test:stress` repeats the suite with per-run timeouts,
158
- optionally in parallel (`MU_TEST_STRESS_MODE=parallel`).
159
-
160
- Two fixture traps:
161
-
162
- - A convergence fixture must share one creation op. Two independent
163
- `task add` calls for the same id make the later creation win every
164
- field.
165
- - An ordering-sensitive sync test must rename segments to pin ingest
166
- order, since peers are discovered by `localeCompare` over random-UUID
167
- filenames.
168
-
169
- Treat a test that passes alone and fails under load as a concurrency
170
- bug first. Drive wait and reaper tests from poll-loop seams, not fixed
171
- timers, and wait for stable Ink output instead of sleeping.
172
-
173
- ## Distribution
174
-
175
- One npm package, `mu`. `tsup` bundles three entries:
176
-
177
- | Output | Role |
178
- | --- | --- |
179
- | `dist/cli.js` | `bin: { mu: ./dist/cli.js }` |
180
- | `dist/index.js`, `dist/index.d.ts` | SDK and types |
181
- | `dist/extension/mu-pi.js` | the pi extension `mu link pi` points at |
182
-
183
- `skills/` and `docs/` ship alongside. There is no build step on the user's
184
- machine. pi stays a peer dependency: the extension types pi's API
185
- structurally. Per-role agent guidance lives in the user's repo, not in
186
- mu. The rule for adding a dependency is in
187
- [ROADMAP § Anti-feature pledges](../ROADMAP.md#anti-feature-pledges).