@martintrojer/mu 0.3.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/AGENTS.md +343 -0
- package/README.md +189 -0
- package/dist/cli.js +11260 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3130 -0
- package/dist/index.js +6312 -0
- package/dist/index.js.map +1 -0
- package/docs/ARCHITECTURE.md +481 -0
- package/docs/ROADMAP.md +542 -0
- package/docs/USAGE_GUIDE.md +1631 -0
- package/docs/VISION.md +440 -0
- package/docs/VOCABULARY.md +349 -0
- package/package.json +76 -0
- package/skills/mu/SKILL.md +523 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
mu is layered: callers on top, a shared TypeScript core in the middle,
|
|
4
|
+
SQLite + tmux + VCS substrates at the base. The CLI and the pi
|
|
5
|
+
extension are thin facades over the same core modules.
|
|
6
|
+
|
|
7
|
+
- For canonical terms (*workstream*, *agent*, *task DAG*, *track*,
|
|
8
|
+
*claim*, *free*, *workspace*, *substrate*, ...) see
|
|
9
|
+
[VOCABULARY.md](VOCABULARY.md). It is the source of truth.
|
|
10
|
+
- For design rationale, rejected alternatives, and what's on the
|
|
11
|
+
roadmap, see [ROADMAP.md](ROADMAP.md).
|
|
12
|
+
- For principles, see [VISION.md](VISION.md).
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
┌────────────────────────────────────────────────────────────────┐
|
|
16
|
+
│ Callers │
|
|
17
|
+
│ ┌────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
18
|
+
│ │ Pi │ │ Bash + │ │ Pi sub- │ │ mu log │ │
|
|
19
|
+
│ │ shell │ │ jq │ │ agent │ │ --tail subs │ │
|
|
20
|
+
│ └───┬────┘ └────┬─────┘ └──────┬───────┘ └──────┬───────┘ │
|
|
21
|
+
│ │ │ │ │ │
|
|
22
|
+
└──────┼────────────┼───────────────┼──────────────────┼─────────┘
|
|
23
|
+
│ in-proc │ subprocess │ subprocess │ in-proc
|
|
24
|
+
▼ ▼ ▼ ▼
|
|
25
|
+
┌────────────────────────────────────────────────────────────────┐
|
|
26
|
+
│ mu core (shared TS modules) │
|
|
27
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
|
|
28
|
+
│ │ agents/ │ │ tasks/ │ │ vcs/ │ │ registry/ │ │
|
|
29
|
+
│ │ tmux │ │ schema │ │ jj │ │ snapshot │ │
|
|
30
|
+
│ │ detect │ │ queries │ │ sapling │ │ logs │ │
|
|
31
|
+
│ │ state │ │ tracks │ │ git │ │ doctor │ │
|
|
32
|
+
│ │ │ │ claim │ │ none │ │ │ │
|
|
33
|
+
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │
|
|
34
|
+
└───────┼─────────────┼─────────────┼───────────────┼───────────┘
|
|
35
|
+
▼ ▼ ▼ ▼
|
|
36
|
+
┌────────────────────────────────────────────────────────────────┐
|
|
37
|
+
│ Substrates │
|
|
38
|
+
│ SQLite (~/.local/state/mu/mu.db) · tmux panes · jj/sl/git workspaces │
|
|
39
|
+
└────────────────────────────────────────────────────────────────┘
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## The task DAG
|
|
43
|
+
|
|
44
|
+
mu's coordination model is built around a **directed acyclic graph of
|
|
45
|
+
tasks** (cloned from a prior internal task-graph crate). This is not
|
|
46
|
+
a sidecar feature — it's the central organizing primitive that makes
|
|
47
|
+
deterministic multi-agent orchestration possible. Without it, mu is
|
|
48
|
+
just a fancier agent runner.
|
|
49
|
+
|
|
50
|
+
### Model
|
|
51
|
+
|
|
52
|
+
- **Tasks** are nodes with mandatory `impact (1-100)` and `effort_days`.
|
|
53
|
+
`ROI = impact / effort` drives prioritization.
|
|
54
|
+
- **One edge type**: `blocks`. `A → B` means A must close before B can
|
|
55
|
+
start. Multiple edge types create ambiguity that defeats the purpose.
|
|
56
|
+
- **Status lifecycle**: `OPEN → IN_PROGRESS → CLOSED/RESOLVED`.
|
|
57
|
+
- **Notes** are append-only per task; survive across LLM sessions and
|
|
58
|
+
agent restarts. The fix for context loss at the *task* level rather
|
|
59
|
+
than the agent level.
|
|
60
|
+
|
|
61
|
+
### Built-in queries (SQL views)
|
|
62
|
+
|
|
63
|
+
| View | Returns |
|
|
64
|
+
| --------- | ---------------------------------------------------------------------- |
|
|
65
|
+
| `ready` | OPEN tasks with no unresolved blockers — work that can start *now* |
|
|
66
|
+
| `blocked` | OPEN tasks waiting on something |
|
|
67
|
+
| `goals` | Tasks with no dependents — graph endpoints |
|
|
68
|
+
|
|
69
|
+
Agents and humans both query these views directly via `mu sql`. No
|
|
70
|
+
separate query layer.
|
|
71
|
+
|
|
72
|
+
### Parallel-track detection (the killer feature)
|
|
73
|
+
|
|
74
|
+
`mu task tracks` runs union-find on the graph to identify independent
|
|
75
|
+
subtrees that can be assigned to different agents in parallel.
|
|
76
|
+
|
|
77
|
+
**Diamond patterns get merged automatically.** If two roots share a
|
|
78
|
+
prerequisite, they collapse into one track — preventing two agents
|
|
79
|
+
from colliding on the shared dependency:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
Independent (2 tracks): Diamond (1 merged track):
|
|
83
|
+
|
|
84
|
+
goal_a goal_b goal_a goal_b ← Spawn 2 agents
|
|
85
|
+
| | \ /
|
|
86
|
+
task_a task_b shared ← Spawn 1 (would
|
|
87
|
+
| | | collide otherwise)
|
|
88
|
+
leaf_a leaf_b leaf
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This is **deterministic** — not "the LLM decides whether to
|
|
92
|
+
parallelize." The graph algorithm gives the right answer; the LLM
|
|
93
|
+
follows it.
|
|
94
|
+
|
|
95
|
+
### Claim protocol via tmux pane title
|
|
96
|
+
|
|
97
|
+
`mu task claim <task>` reads the current pane's **pane title** (set on
|
|
98
|
+
spawn via `select-pane -T <agent-name>`) and atomically:
|
|
99
|
+
|
|
100
|
+
1. Sets `tasks.owner = <agent_name>`
|
|
101
|
+
2. Flips `tasks.status = IN_PROGRESS`
|
|
102
|
+
3. Records an `agent_logs` row of kind `claim`
|
|
103
|
+
|
|
104
|
+
Reads via `tmux display-message -p '#{pane_title}'`, **not** `#W`
|
|
105
|
+
(window name). Window names come from the `tab:` frontmatter and may
|
|
106
|
+
group multiple agents in one window.
|
|
107
|
+
|
|
108
|
+
Two agents can't claim the same task — atomic CAS in SQLite. Zero-
|
|
109
|
+
config identity: the agent doesn't have to know its own name.
|
|
110
|
+
|
|
111
|
+
### Scoped subtree views
|
|
112
|
+
|
|
113
|
+
`mu task <id>` shows mission-control output filtered to that task's
|
|
114
|
+
subtree. Enables recursive delegation: a sub-orchestrator agent runs
|
|
115
|
+
`mu --scope feature_a` and sees only its slice of the graph.
|
|
116
|
+
|
|
117
|
+
### Why this is in the core
|
|
118
|
+
|
|
119
|
+
- "What should this agent do next?" becomes a SQL query, not an LLM call
|
|
120
|
+
- Parallelization correctness is structural (union-find + diamond-merge),
|
|
121
|
+
not a prompt
|
|
122
|
+
- Notes give every task a durable knowledge container that outlives any
|
|
123
|
+
LLM session
|
|
124
|
+
- Recursion works because subtree-scoping is just a `WHERE` clause
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Tmux session topology
|
|
129
|
+
|
|
130
|
+
mu organizes agents into **one tmux session per workstream**. One mu
|
|
131
|
+
workstream = one tmux session = one `session_id` partition in
|
|
132
|
+
`~/.local/state/mu/mu.db`. Multiple workstreams on one machine coexist as
|
|
133
|
+
independent tmux sessions, fully isolated.
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
tmux session: mu-auth-refactor (one mu workstream)
|
|
137
|
+
┌────────────────────────────────────────────────┐
|
|
138
|
+
│ Window: Backend Window: Review │
|
|
139
|
+
│ ┌──────────┐ ┌────────┐ ┌─────────────────────┐ │
|
|
140
|
+
│ │ worker-1 │ │ worker-2 │ │ reviewer-1 │ │
|
|
141
|
+
│ │ (pi) │ │ (pi) │ │ (pi, role=read-only) │ │
|
|
142
|
+
│ └──────────┘ └────────┘ └─────────────────────┘ │
|
|
143
|
+
│ │
|
|
144
|
+
│ Window: mu-orchestrator │
|
|
145
|
+
│ ┌────────────────────────────────────────────┐ │
|
|
146
|
+
│ │ pi (you, with mu extension loaded) │ │
|
|
147
|
+
│ └────────────────────────────────────────────┘ │
|
|
148
|
+
└────────────────────────────────────────────────┘
|
|
149
|
+
|
|
150
|
+
tmux session: mu-migration-2024q4 (different workstream)
|
|
151
|
+
┌───────────────────────────────────────────────┐
|
|
152
|
+
│ ...different agents, different graph, no overlap │
|
|
153
|
+
└───────────────────────────────────────────────┘
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Concretely
|
|
157
|
+
|
|
158
|
+
- **First `mu agent spawn` creates the tmux session** if you're not already
|
|
159
|
+
in one. Default name `mu-<auto>`. Override with `mu workstream init <name>` or
|
|
160
|
+
`MU_SESSION=<name>`.
|
|
161
|
+
- **Subsequent operations** in the same shell (or any child shell with
|
|
162
|
+
`MU_SESSION_ID` set) target the same session.
|
|
163
|
+
- **`mu agent attach`** → attach to the whole workstream's tmux session
|
|
164
|
+
- **`mu agent attach <agent>`** → attach and focus that agent's window/pane
|
|
165
|
+
- **`mu agent list`** shows only the current workstream's agents by default
|
|
166
|
+
- **`mu agent list --all`** shows agents across all workstreams on the box
|
|
167
|
+
- **`session_id`** is the partition key on the `agents` table; queries
|
|
168
|
+
filter to the active session unless `--all` is set
|
|
169
|
+
- **`mu doctor`** warns about cross-session pollution (orphan panes,
|
|
170
|
+
ghost rows, agents whose tmux session no longer exists)
|
|
171
|
+
|
|
172
|
+
### Window vs pane
|
|
173
|
+
|
|
174
|
+
By default each agent gets its own **tmux window** (tmux's term for
|
|
175
|
+
what most terminals call a "tab"), with the window name set to the
|
|
176
|
+
agent's `tab:` value (default: the agent name itself, so a single
|
|
177
|
+
agent's window is named after them). Agents that share a `tab:` value
|
|
178
|
+
share a window with multiple panes inside it.
|
|
179
|
+
|
|
180
|
+
The claim/identity logic depends on the **pane title**, not the
|
|
181
|
+
window name — every agent pane has its title set to the agent's name
|
|
182
|
+
via `select-pane -T <name>` on spawn, regardless of how panes are
|
|
183
|
+
grouped into windows. (See [VOCABULARY.md](VOCABULARY.md) and
|
|
184
|
+
the comment block at the top of `src/tmux.ts` for the canonical
|
|
185
|
+
tmux protocol.)
|
|
186
|
+
|
|
187
|
+
### Why one session per workstream
|
|
188
|
+
|
|
189
|
+
- **Visual co-location.** `tmux a -t mu-auth-refactor` shows the whole
|
|
190
|
+
crew at once. No session-switching.
|
|
191
|
+
- **Trivial isolation.** Kill the tmux session = kill the workstream.
|
|
192
|
+
No leaked panes.
|
|
193
|
+
- **Detach and reattach freely.** Close your laptop, open it later,
|
|
194
|
+
`tmux a -t mu-auth-refactor`, the crew is still there.
|
|
195
|
+
- **The claim protocol falls out naturally.** Pane title = agent name
|
|
196
|
+
= ownership identity. Zero-config.
|
|
197
|
+
- **Multiple workstreams coexist.** session_id partitioning (a
|
|
198
|
+
pattern borrowed from a prior internal multi-agent runtime)
|
|
199
|
+
prevents the auth-refactor crew from polluting the migration crew.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Operations registry
|
|
204
|
+
|
|
205
|
+
Every mu action is defined exactly once via `defineOperation(...)`.
|
|
206
|
+
The registry is collected at module import time (no codegen step) and
|
|
207
|
+
from one source produces six surfaces:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
┌─────────────────────────────┐
|
|
211
|
+
│ defineOperation(...) │
|
|
212
|
+
│ name, category, │
|
|
213
|
+
│ caps[], params, │
|
|
214
|
+
│ handler │
|
|
215
|
+
└─────────────┬──────────────┘
|
|
216
|
+
│
|
|
217
|
+
┌────────────┬──────┼──────┬───────────┐
|
|
218
|
+
▼ ▼ ▼ ▼ ▼
|
|
219
|
+
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌───────┐ ┌────────┐
|
|
220
|
+
│ CLI verb│ │ Pi tool │ │ mu.d.ts │ │ skill │ │ doctor │
|
|
221
|
+
└─────────┘ └─────────┘ └─────────┘ └───────┘ └────────┘
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
No operation may exist outside the registry. CLI verbs that are not
|
|
225
|
+
operations (e.g., `mu workstream init`, `mu agent attach`, `mu doctor`) are exceptions
|
|
226
|
+
listed explicitly in the CLI module and motivated.
|
|
227
|
+
|
|
228
|
+
(A capability-tag system on operations was considered and dropped
|
|
229
|
+
as an abstraction with no current consumer; see
|
|
230
|
+
[ROADMAP.md § Open questions](ROADMAP.md#open-questions).)
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Reconciliation
|
|
235
|
+
|
|
236
|
+
`mu agent list` always reconciles the registry against tmux reality before
|
|
237
|
+
returning. Three steps, in order:
|
|
238
|
+
|
|
239
|
+
1. **Prune ghosts.** For each `agents` row, if its `pane_id` no longer
|
|
240
|
+
exists in tmux, delete the row.
|
|
241
|
+
2. **Detect status from scrollback.** For each surviving agent, capture
|
|
242
|
+
the pane and run the per-CLI detector. Update `agents.status` if
|
|
243
|
+
the detected value differs from the stored one.
|
|
244
|
+
3. **Surface orphans.** For each tmux pane in the workstream's session
|
|
245
|
+
that has no matching `agents` row but whose pane title looks like
|
|
246
|
+
an agent name, add it to the orphans list. **Do not auto-adopt** —
|
|
247
|
+
`mu agent list` shows orphans under a separate "(orphans)" section and
|
|
248
|
+
the user runs `mu adopt %15 [--name X]` to formally claim them.
|
|
249
|
+
|
|
250
|
+
Full algorithm lives in `src/reconcile.ts` (the canonical
|
|
251
|
+
implementation).
|
|
252
|
+
|
|
253
|
+
Key properties:
|
|
254
|
+
|
|
255
|
+
- **Reality wins**: tmux is the source of truth for what panes exist.
|
|
256
|
+
The DB records what we last *observed*. Reconciliation closes the
|
|
257
|
+
gap on every `mu agent list`.
|
|
258
|
+
- **Pi-only status detection** (`src/detect.ts`): the `busy` /
|
|
259
|
+
`needs_input` / `idle` / `done` classification works for pi via
|
|
260
|
+
a known marker. Other CLIs would need their own detectors; none
|
|
261
|
+
are built today and none are currently planned.
|
|
262
|
+
- **No silent adoption**: orphans are reported, never claimed without
|
|
263
|
+
user consent. Avoids surprising the user with random panes.
|
|
264
|
+
- **`mu doctor` calls the same routine** and reports counts. The
|
|
265
|
+
algorithm has no other implementation.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Modules (actual src/ layout)
|
|
270
|
+
|
|
271
|
+
Mostly-flat `src/`: 18 root `.ts` files plus two cohesive
|
|
272
|
+
subclusters (`src/agents/`, `src/tasks/`) and the `src/cli/` verb
|
|
273
|
+
wrappers (with their own `src/cli/tasks/` sub-cluster). No
|
|
274
|
+
`core/` subdirectory; no anticipatory layering. Subclusters obey
|
|
275
|
+
the AGENTS.md rule: imports flow cluster → root, never upward.
|
|
276
|
+
Each module is concrete and consumed today.
|
|
277
|
+
|
|
278
|
+
| Module | Responsibility |
|
|
279
|
+
| --------------------- | ----------------------------------------------------------------------------------------- |
|
|
280
|
+
| `src/db.ts` | SQLite (better-sqlite3) connection, WAL mode, schema (14 tables + 3 views, **schema v7** — v5 surrogate-INTEGER-PK substrate, plus v6's 5 additive `archive_*` tables, minus v7's drop of `approvals`), default paths, `resolveWorkstreamId` (the SDK boundary's first leg). Pre-current DBs are upgraded in place on `openDb`: v5 → v6 was additive (CREATE-TABLE-IF-NOT-EXISTS), v6 → v7 is destructive-but-idempotent (`DROP TABLE IF EXISTS approvals` runs before `applySchema`); both happen with no migration script. |
|
|
281
|
+
| `src/tmux.ts` | Single tmux executor wrapper, send protocol (bracketed-paste), pane validation |
|
|
282
|
+
| `src/detect.ts` | Pi-only status detector (`busy` / `needs_input` / `idle` / `done`) |
|
|
283
|
+
| `src/reconcile.ts` | Ghost prune + status detect + orphan surface; "reality wins" |
|
|
284
|
+
| `src/agents.ts` | Hub: CRUD + send / read / list / close / free + liveness + reaper. Re-exports `src/agents/*` (spawn, adopt, errors); pane-title composition (`composeAgentTitle`) lives here. |
|
|
285
|
+
| `src/agents/*.ts` | Cohesive cluster of agent-lifecycle internals: `spawn.ts` (spawnAgent + resolveCliCommand / awaitSpawnLiveness / pane create-or-reuse / prestage / rollback), `adopt.ts` (register an existing tmux pane as a managed agent), `errors.ts` (typed agent error classes — `AgentNotFoundError`, `AgentDiedOnSpawnError`, …). |
|
|
286
|
+
| `src/tasks.ts` | Hub: every read/write verb on the DAG (edit / edges / queries) + cycle check + auto-event emission. Re-exports `src/tasks/*` (status, claim, lifecycle, wait, errors). |
|
|
287
|
+
| `src/tasks/*.ts` | Cohesive cluster of task-graph internals: `status.ts` (TaskStatus enum + helpers — single source of truth), `claim.ts` (claim/release + `resolveActorIdentity`, atomic CAS), `lifecycle.ts` (setTaskStatus / closeTask / openTask / rejectTask / deferTask + cascade), `wait.ts` (waitForTasks: block until tasks reach a target status), `errors.ts` (typed task error classes — `TaskAlreadyOwnedError`, `CycleError`, …). |
|
|
288
|
+
| `src/tracks.ts` | Parallel-tracks union-find with diamond merge |
|
|
289
|
+
| `src/workstream.ts` | ensureWorkstream / list / summarize / destroy / export (thin wrapper around the bucket renderer) |
|
|
290
|
+
| `src/exporting.ts` | Unified bucket renderer for `mu workstream export` and `mu archive export`: per-task markdown + manifest.json (`bucketVersion: 2`); idempotent via per-file sha256; deleted-task preservation banner; refuses pre-0.3 single-source layouts |
|
|
291
|
+
| `src/importing.ts` | Inverse of `src/exporting.ts`: parses a v0.3 bucket directory and rebuilds every source-ws as live tasks + edges + notes. Markdown-only (never reads .db); per-source-ws transactional; refuses silent merges into existing workstreams |
|
|
292
|
+
| `src/archives.ts` | Cross-workstream **archives** — feature complete (SDK + 6 CLI verbs: `mu archive create / list / show / add / remove / delete`, plus `search` and `export` via the unified bucket renderer): `createArchive` / `listArchives` / `getArchive` / `deleteArchive` / `addToArchive` (idempotent at `(archive, source_workstream)`) / `removeFromArchive` / `listArchivedTasks`. Backed by the v6 `archives` + `archived_tasks` + `archived_edges` + `archived_notes` + `archived_events` tables; archives outlive workstreams (TEXT `source_workstream` columns, no FK). |
|
|
293
|
+
| `src/logs.ts` | `agent_logs` SDK: appendLog / listLogs / latestSeq / emitEvent |
|
|
294
|
+
| `src/vcs.ts` | `VcsBackend` interface + jj / sl / git / none impls; detection precedence; `commitsBehind(workspacePath, ref)` for staleness signal (no auto-fetch; pure observation) |
|
|
295
|
+
| `src/workspace.ts` | Per-agent VCS workspaces (registry layer on top of vcs.ts); CRUD + cascade; orphan-dir detection (`listWorkspaceOrphans`); staleness decoration (`decorateWithStaleness` populates `commitsBehindMain` per row) |
|
|
296
|
+
| `src/snapshots.ts` | Whole-DB snapshots (`VACUUM INTO`); auto-captured before destructive verbs; SDK for `mu undo`. The `snapshots` table is schema v4 (carried forward unchanged through v5/v6/v7). |
|
|
297
|
+
| `src/output.ts` | NextStep type + `printNextSteps` + `errorNextSteps` plumbing for self-documenting output |
|
|
298
|
+
| `src/cli.ts` | commander entry; `buildProgram()` (re-exports `format`/`handle` symbols for back-compat with existing import sites). |
|
|
299
|
+
| `src/cli/*.ts` | one file per verb-namespace; thin wrappers over the SDK; `--json` rendering for every read verb. Currently: `workstream.ts`, `agents.ts`, `tasks.ts`, `workspace.ts`, `log.ts`, `archive.ts`, `state.ts` (canonical state card + bare `mu` mission-control / hud render mode), `snapshot.ts`, `sql.ts`, `doctor.ts`. Two non-verb cluster-mates carry the rendering + error-handling primitives that every verb wrapper imports: `format.ts` (table renderers, status colourers, `truncate`/`relTime`) and `handle.ts` (typed-error → exit-code map + the `handle()` wrapper). Imports flow cluster → root (never the other way). |
|
|
300
|
+
| `src/cli/tasks/*.ts` | sub-cluster of the `mu task` namespace; `tasks.ts` at the root re-exports only what callers outside the cluster import (`wireTaskCommands`, `cmdMyNext`/`cmdMyTasks`, `unescapeNoteText`). One file per concern: `queries.ts` (list/next/owned-by + the `cmdMyTasks` / `cmdMyNext` helpers that back `mu me tasks` / `mu me next`), `lifecycle.ts` (close/open/reject/defer + cascade preview), `edit.ts` (add/show/notes/note/update + helpers), `edges.ts` (block/unblock/reparent/delete), `claim.ts` (claim/release/wait), `tree.ts` (tree rendering), `wire.ts` (Commander glue). Each file < 600 LOC; the hub is < 35. |
|
|
301
|
+
| `src/index.ts` | SDK entrypoint (re-exports) |
|
|
302
|
+
| `skills/mu/SKILL.md` | Bundled skill teaching the LLM the model + verb list + jq pipelines |
|
|
303
|
+
|
|
304
|
+
## Data flow
|
|
305
|
+
|
|
306
|
+
1. **A caller invokes a verb** — the CLI subprocess, or in-proc SDK
|
|
307
|
+
use.
|
|
308
|
+
2. **CLI handler dispatches to an SDK function** in `src/agents.ts`
|
|
309
|
+
/ `src/tasks.ts` / etc.
|
|
310
|
+
3. **For multi-statement writes, opens a transaction** via
|
|
311
|
+
better-sqlite3's `db.transaction(fn)()` wrapper.
|
|
312
|
+
4. **Executes the operation** — agent ops shell out to tmux (and to
|
|
313
|
+
jj/sl/git for workspaces); task ops are pure SQL.
|
|
314
|
+
5. **Reconciles with reality** — for read-paths that need accuracy
|
|
315
|
+
(`mu agent list`, mission control), queries tmux for live pane
|
|
316
|
+
state and updates the DB (ghost prune + status detect).
|
|
317
|
+
6. **Auto-emits a `kind='event'` row** to `agent_logs` for any
|
|
318
|
+
state-changing verb, conditional on actual change. `mu log
|
|
319
|
+
--tail` subscribers see it on the next 1-second poll.
|
|
320
|
+
7. **Commits or rolls back** — exception propagates after rollback
|
|
321
|
+
so the caller sees the real error and the typed error class
|
|
322
|
+
maps to a specific exit code in `handle()`.
|
|
323
|
+
|
|
324
|
+
## Key seams
|
|
325
|
+
|
|
326
|
+
These are the abstraction points designed for extension. New impls of
|
|
327
|
+
each are deliberately small.
|
|
328
|
+
|
|
329
|
+
| Seam | Add a new impl by... |
|
|
330
|
+
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
331
|
+
| `VcsBackend` | Implementing `detect / createWorkspace / freeWorkspace / commitsBehind` (~80–150 LOC; jj/sl/git/none are working examples) |
|
|
332
|
+
| Per-CLI `Detector` | Adding patterns to `detectPiStatus` (vanilla pi `to interrupt)`; pi-meta + every TUI wrapper covered by Braille spinner glyph fallback `[\u2800-\u28FF]`) |
|
|
333
|
+
| New typed verb | Add an SDK function in the relevant `src/*.ts`; add a `cmd<Verb>` to the matching `src/cli/<namespace>.ts` (or create a new namespace if the verb doesn't fit existing ones); wire one commander block in `src/cli.ts`'s `buildProgram()` (use `handle()` for the exit-code map; route through `printNextSteps` for self-documenting output) |
|
|
334
|
+
| New schema migration| Bump `CURRENT_SCHEMA_VERSION` in `src/db.ts`; mirror the new shape in `CURRENT_SCHEMA`. Two of the three post-v5 bumps were script-free: v5 → v6 was purely additive (the existing CREATE-TABLE-IF-NOT-EXISTS pass picked up the new `archive_*` tables), and v6 → v7 was a destructive-but-idempotent in-place migration (a `DROP TABLE IF EXISTS approvals` block in `applySchema`). Reach for a one-shot migration script only when the change can't be expressed that way (the v4 → v5 surrogate-PK substrate switch was the canonical example; restore from git history if you need to see the shape). The loud-fail hook in `openDb` rejects pre-current DBs with `SchemaTooOldError` (exit code 4) and a migration instruction. |
|
|
335
|
+
| Snapshot hook | Add `await captureSnapshot(db, 'verb-name', workstream)` at the top of any new destructive verb (one-liner; GC + restore behaviour automatic) |
|
|
336
|
+
|
|
337
|
+
## Surrogate-PK + SDK-boundary discipline (load-bearing)
|
|
338
|
+
|
|
339
|
+
This is the load-bearing pattern v5 turned into a substrate-wide
|
|
340
|
+
invariant; every entity table follows it.
|
|
341
|
+
|
|
342
|
+
**Schema shape — every entity table:**
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
(
|
|
346
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, -- surrogate; internal
|
|
347
|
+
<scope_id> INTEGER NOT NULL REFERENCES <parent>(id) ON DELETE CASCADE,
|
|
348
|
+
<name> TEXT NOT NULL, -- operator-facing; mutable
|
|
349
|
+
-- ... domain attributes
|
|
350
|
+
UNIQUE (<scope_id>, <name>) -- per-scope unique
|
|
351
|
+
)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
FKs reference `<child>.<parent>_id` (INTEGER), never the TEXT name.
|
|
355
|
+
The TEXT name is JUST an operator-facing attribute — searchable,
|
|
356
|
+
displayable, renamable cheaply. The surrogate id is the identity.
|
|
357
|
+
|
|
358
|
+
**TEXT-by-design exceptions** (each one a justified skip): the
|
|
359
|
+
workstream's own `name` (it IS a tmux session name; globally
|
|
360
|
+
unique), `task_notes.author` / `agent_logs.source` (free-text actor
|
|
361
|
+
labels — `"orchestrator"`, `"user"`, `"system"`), `agent_logs.kind`
|
|
362
|
+
(open enum — future kinds need no migration), `agents.cli`
|
|
363
|
+
(adding a new CLI must not require a schema change), and the
|
|
364
|
+
`snapshots.workstream` text column (intentionally NOT an FK so
|
|
365
|
+
the snapshot outlives its workstream).
|
|
366
|
+
|
|
367
|
+
**SDK boundary discipline** — same shape as REST: external API
|
|
368
|
+
uses business identifiers, internal layer uses primary keys.
|
|
369
|
+
|
|
370
|
+
> **Public SDK functions take operator-facing names.**
|
|
371
|
+
> **Internal helpers take surrogate ids.**
|
|
372
|
+
> **Resolution happens at the public-function entry, exactly once.**
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
// PUBLIC: takes operator-facing names
|
|
376
|
+
export function claimTask(
|
|
377
|
+
db: Db,
|
|
378
|
+
workstream: string,
|
|
379
|
+
localId: string,
|
|
380
|
+
opts?: ClaimOptions,
|
|
381
|
+
): ClaimResult {
|
|
382
|
+
const wsId = resolveWorkstreamId(db, workstream);
|
|
383
|
+
const taskId = resolveTaskId(db, wsId, localId);
|
|
384
|
+
const agentId = resolveCurrentAgentId(db, wsId);
|
|
385
|
+
return claimTaskById(db, taskId, agentId, opts);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// INTERNAL: takes surrogate ids; never re-resolves
|
|
389
|
+
function claimTaskById(db, taskId, agentId, opts): ClaimResult { ... }
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Why exactly once at the boundary: no double-resolution; no
|
|
393
|
+
mid-function ambiguity (once surrogate ids exist, internal helpers
|
|
394
|
+
don't need to thread workstream context — the FKs make scope
|
|
395
|
+
implicit); one place to do error mapping
|
|
396
|
+
(`WorkstreamNotFoundError` / `TaskNotFoundError` /
|
|
397
|
+
`AgentNotFoundError` all originate at resolve-time, with the
|
|
398
|
+
operator's input string in the error payload).
|
|
399
|
+
|
|
400
|
+
**`--json` output preserves operator-facing names.** Surrogate ids
|
|
401
|
+
stay strictly internal — they never leak into `--json`, error
|
|
402
|
+
payloads, log lines, or markdown exports. Promoting them to the
|
|
403
|
+
public shape would re-introduce a global namespace through the
|
|
404
|
+
back door (anti-feature pledge).
|
|
405
|
+
|
|
406
|
+
## State of truth
|
|
407
|
+
|
|
408
|
+
- **`~/.local/state/mu/mu.db` is canonical.** Everything else is a
|
|
409
|
+
cache, including tmux pane titles (mu re-pushes them via
|
|
410
|
+
`composeAgentTitle` after every state change).
|
|
411
|
+
- **Reads are cheap** via SQLite views (`ready`, `blocked`, `goals`).
|
|
412
|
+
- **Writes go through the typed SDK functions** (`src/agents.ts`,
|
|
413
|
+
`src/tasks.ts`, etc.) which validate, transact, snapshot (for
|
|
414
|
+
destructive verbs), and reconcile.
|
|
415
|
+
- **Workstream scoping is mandatory at the CLI boundary.** Post-v5,
|
|
416
|
+
TEXT names (`tasks.local_id`, `agents.name`) are
|
|
417
|
+
per-workstream unique — the same name may legitimately exist in two
|
|
418
|
+
workstreams. Every public SDK function that takes such a name also
|
|
419
|
+
takes (or threads from a parent context) the workstream; internal
|
|
420
|
+
SQL filters by `(workstream_id, name)`. Test fixtures and `mu sql`
|
|
421
|
+
read paths can omit the workstream and fall back to the v4
|
|
422
|
+
first-match-by-name contract. The invariant is now structurally
|
|
423
|
+
enforced by the surrogate-id schema (per-workstream UNIQUE on
|
|
424
|
+
name + INTEGER FKs); the previous CI grep guard was retired.
|
|
425
|
+
- **Snapshots are insurance, not version history.** Captured only
|
|
426
|
+
before destructive verbs (workstream destroy, agent close, task
|
|
427
|
+
close/reject/defer/release/delete, workspace free). Status flips and additive ops do NOT snapshot.
|
|
428
|
+
- **In-memory state is short-lived** — the CLI's per-command
|
|
429
|
+
connection. Gone on process exit.
|
|
430
|
+
- **Cross-process coordination** is via SQLite WAL — multiple `mu`
|
|
431
|
+
processes share the file safely.
|
|
432
|
+
|
|
433
|
+
## Errors
|
|
434
|
+
|
|
435
|
+
Curated error classes per layer; no try/catch swallowing. CLI exit
|
|
436
|
+
codes:
|
|
437
|
+
|
|
438
|
+
| Code | Meaning |
|
|
439
|
+
| ---- | -------------------------------------------------------- |
|
|
440
|
+
| 0 | success |
|
|
441
|
+
| 1 | generic error |
|
|
442
|
+
| 2 | usage error (commander's default) |
|
|
443
|
+
| 3 | not found (no such agent / task / workspace) |
|
|
444
|
+
| 4 | conflict (name collision, double-claim, dirty tree) |
|
|
445
|
+
| 5 | substrate unavailable (`tmux` not running, DB locked) |
|
|
446
|
+
|
|
447
|
+
Errors carry structured context (operation name, target, attempted
|
|
448
|
+
action) so `mu doctor` can surface them readably.
|
|
449
|
+
|
|
450
|
+
## Testing layers
|
|
451
|
+
|
|
452
|
+
| Layer | Test approach |
|
|
453
|
+
| ---------------------------------- | ------------------------------------------------------------------------------ |
|
|
454
|
+
| `src/db.ts` | Real SQLite in temp dir; schema/table-count assertions |
|
|
455
|
+
| `src/tasks.ts` | Real SQLite in temp dir; pure functions over fixture data |
|
|
456
|
+
| `src/tracks.ts` | Pure functions; union-find + diamond-merge properties |
|
|
457
|
+
| `src/agents.ts` | Mocked tmux executor via `setTmuxExecutor()`; reaper integration tests |
|
|
458
|
+
| `src/logs.ts` | Real SQLite; cursor semantics, AUTOINCREMENT durability, FK CASCADE |
|
|
459
|
+
| `src/vcs.ts` + `src/workspace.ts` | Real git in `os.tmpdir()`; jj/sl tests feature-detect (skip if binary missing) |
|
|
460
|
+
| `src/cli.ts` / verb integration | `*.integration.test.ts` files; real tmux server, unique session per test |
|
|
461
|
+
| End-to-end | `test/acceptance.test.ts` — the canonical 10-task / 3-agent demo |
|
|
462
|
+
|
|
463
|
+
## Distribution
|
|
464
|
+
|
|
465
|
+
Single npm package `mu` (see `package.json`):
|
|
466
|
+
|
|
467
|
+
- `dist/cli.js` — CLI entry, executable (`bin: { mu: ./dist/cli.js }`; shebang preserved by `tsup`)
|
|
468
|
+
- `dist/index.js` + `dist/index.d.ts` — programmatic API + types for SDK callers
|
|
469
|
+
- `skills/mu/SKILL.md` — bundled skill (the only non-`dist` asset shipped)
|
|
470
|
+
|
|
471
|
+
`tsup` bundles two entries (`index`, `cli`) from `src/`. No
|
|
472
|
+
runtime build step on the user's machine; `npm install` just
|
|
473
|
+
unpacks. There is no pi-extension entry today — pi is a peer dep,
|
|
474
|
+
and the anti-feature pledge in ROADMAP.md keeps it that way.
|
|
475
|
+
Likewise no bundled `agents/*.md` or `prompts/*.md` directory
|
|
476
|
+
exists; per-role agent guidance lives in the user's project repo,
|
|
477
|
+
not in the mu package.
|
|
478
|
+
|
|
479
|
+
The dependency list lives in `package.json`; the rule for adding
|
|
480
|
+
new ones is the anti-feature pledge in
|
|
481
|
+
[ROADMAP.md § Anti-feature pledges](ROADMAP.md#anti-feature-pledges-still-in-force-reinforced-by-an-internal-critique).
|