@mu-crew/mu 3.2.0 → 3.2.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 +203 -599
- package/README.md +76 -327
- package/dist/cli.js.map +1 -1
- package/dist/extension/mu-pi.js +178 -45
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +175 -1072
- package/docs/ROADMAP.md +88 -169
- package/docs/VISION.md +205 -415
- package/docs/VOCABULARY.md +202 -466
- package/docs/architecture/control-socket.md +119 -0
- package/docs/architecture/dag.md +113 -0
- package/docs/architecture/mux.md +163 -0
- package/docs/architecture/ops-log.md +140 -0
- package/docs/architecture/sdk.md +185 -0
- package/docs/architecture/sync.md +129 -0
- package/docs/architecture/tui.md +146 -0
- package/docs/guide/README.md +25 -0
- package/docs/guide/backends.md +61 -0
- package/docs/guide/cleanup.md +74 -0
- package/docs/guide/delegate.md +87 -0
- package/docs/guide/dispatch.md +137 -0
- package/docs/guide/getting-started.md +177 -0
- package/docs/guide/recovery.md +133 -0
- package/docs/guide/remote.md +74 -0
- package/docs/guide/sql.md +76 -0
- package/docs/guide/stop-a-worker.md +64 -0
- package/docs/guide/sync.md +78 -0
- package/docs/guide/tui.md +98 -0
- package/docs/guide/upgrade.md +76 -0
- package/docs/history/CHANGELOG-pre-3.md +4866 -0
- package/docs/reference/env.md +87 -0
- package/docs/reference/naming.md +82 -0
- package/docs/specs/2026-09-28-agent-state-from-murmur.md +1 -1
- package/docs/specs/2026-09-29-task-substates.md +1 -1
- package/package.json +1 -1
- package/skills/mu/REMOTE_WORKERS.md +179 -504
- package/skills/mu/SKILL.md +116 -185
- package/docs/HANDOVER.md +0 -486
- package/docs/USAGE_GUIDE.md +0 -2738
|
@@ -0,0 +1,185 @@
|
|
|
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 (no multiplexer, DB locked) |
|
|
129
|
+
|
|
130
|
+
Errors carry structured context (operation, target, attempted action)
|
|
131
|
+
so `mu doctor` can show them readably.
|
|
132
|
+
|
|
133
|
+
## Testing layers
|
|
134
|
+
|
|
135
|
+
Fast tier (`npm run test:fast`) excludes `*.integration.test.ts` and
|
|
136
|
+
`*.smoke.test.ts`. It uses real SQLite in per-test temp DBs and a
|
|
137
|
+
mocked mux (`installMux()` from `test/_mux.ts`). Integration tests use
|
|
138
|
+
real tmux or herdr, and real git, jj or sl in `os.tmpdir()`; jj and sl
|
|
139
|
+
tests skip when the binary is missing. Conventions live in
|
|
140
|
+
[AGENTS.md](../../AGENTS.md) and `test/README.md`.
|
|
141
|
+
|
|
142
|
+
| Area | What the tests pin |
|
|
143
|
+
| --- | --- |
|
|
144
|
+
| `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 |
|
|
145
|
+
| `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 |
|
|
146
|
+
| `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 |
|
|
147
|
+
| `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 |
|
|
148
|
+
| `src/dormant.ts` | `updated_at` written directly; weighted to the boundary between `finished` and `abandoned` |
|
|
149
|
+
| `src/disk-recon.ts` | a per-test `MU_STATE_DIR` with the on-disk shape built by hand, both directions |
|
|
150
|
+
| `src/segments.ts`, `src/sync.ts` | two temp DBs and one shared dir; each robustness layer; machine-local ops never reaching a file |
|
|
151
|
+
| `src/hlc.ts` | an injected clock: backward jumps, stalled milliseconds, concurrent minting |
|
|
152
|
+
| `sync-session.integration` | 8 rounds of two machines: byte-identical portable content, no `--deep` drift, a quiet round adds zero ops |
|
|
153
|
+
| `test/acceptance.integration.test.ts` | the end-to-end gate: 10 tasks, 3 agents |
|
|
154
|
+
|
|
155
|
+
`npm run test:stress` repeats the suite with per-run timeouts,
|
|
156
|
+
optionally in parallel (`MU_TEST_STRESS_MODE=parallel`).
|
|
157
|
+
|
|
158
|
+
Two fixture traps:
|
|
159
|
+
|
|
160
|
+
- A convergence fixture must share one creation op. Two independent
|
|
161
|
+
`task add` calls for the same id make the later creation win every
|
|
162
|
+
field.
|
|
163
|
+
- An ordering-sensitive sync test must rename segments to pin ingest
|
|
164
|
+
order, since peers are discovered by `localeCompare` over random-UUID
|
|
165
|
+
filenames.
|
|
166
|
+
|
|
167
|
+
Treat a test that passes alone and fails under load as a concurrency
|
|
168
|
+
bug first. Drive wait and reaper tests from poll-loop seams, not fixed
|
|
169
|
+
timers, and wait for stable Ink output instead of sleeping.
|
|
170
|
+
|
|
171
|
+
## Distribution
|
|
172
|
+
|
|
173
|
+
One npm package, `mu`. `tsup` bundles three entries:
|
|
174
|
+
|
|
175
|
+
| Output | Role |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `dist/cli.js` | `bin: { mu: ./dist/cli.js }` |
|
|
178
|
+
| `dist/index.js`, `dist/index.d.ts` | SDK and types |
|
|
179
|
+
| `dist/extension/mu-pi.js` | the pi extension `mu link pi` points at |
|
|
180
|
+
|
|
181
|
+
`skills/` and `docs/` ship alongside. There is no build step on the user's
|
|
182
|
+
machine. pi stays a peer dependency: the extension types pi's API
|
|
183
|
+
structurally. Per-role agent guidance lives in the user's repo, not in
|
|
184
|
+
mu. The rule for adding a dependency is in
|
|
185
|
+
[ROADMAP § Anti-feature pledges](../ROADMAP.md#anti-feature-pledges).
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Sync
|
|
2
|
+
|
|
3
|
+
Machines share a workstream by exchanging ops through a shared folder
|
|
4
|
+
(`MU_SYNC_DIR`) kept in step by any folder syncer, such as Syncthing. The ops
|
|
5
|
+
themselves are described in [ops-log.md](ops-log.md). Overview:
|
|
6
|
+
[ARCHITECTURE.md](../ARCHITECTURE.md). Why segments and not
|
|
7
|
+
Litestream, cr-sqlite or a peer list, and why the DB must never sit in
|
|
8
|
+
`MU_SYNC_DIR`:
|
|
9
|
+
[ROADMAP § Rejected sync substrates](../ROADMAP.md#rejected-sync-substrates).
|
|
10
|
+
|
|
11
|
+
## Segments
|
|
12
|
+
|
|
13
|
+
`src/segments.ts` is the transport.
|
|
14
|
+
|
|
15
|
+
- `flushSegment` appends this machine's unflushed ops to
|
|
16
|
+
`<MU_SYNC_DIR>/<machine_id>.jsonl`.
|
|
17
|
+
- `ingestSegment` reads a peer segment from its watermark into
|
|
18
|
+
`applyOp`.
|
|
19
|
+
- Peer discovery is implicit: every non-self `*.jsonl` is a peer. A
|
|
20
|
+
peer disappears only when its segment is deleted.
|
|
21
|
+
|
|
22
|
+
**Single writer per file.** A machine appends only to its own segment,
|
|
23
|
+
so nothing is contended and any folder syncer is adequate. Segments
|
|
24
|
+
are regenerable, so there is no fsync. `src/file-lock.ts` stops two
|
|
25
|
+
local `mu` processes from interleaving partial lines.
|
|
26
|
+
|
|
27
|
+
**Only `SYNCED_ENTITIES` and only this machine's ops are flushed.**
|
|
28
|
+
Pane ids and absolute paths never leave the machine, and peers never
|
|
29
|
+
echo each other's history.
|
|
30
|
+
|
|
31
|
+
**Payload encoding.** `ops.payload` is TEXT that is usually JSON, but
|
|
32
|
+
`mu log "text"` writes prose. The encoder passes valid JSON through
|
|
33
|
+
verbatim, so the crc covers those exact bytes, and JSON-encodes
|
|
34
|
+
anything else, unwrapping on ingest.
|
|
35
|
+
|
|
36
|
+
## Robustness layers
|
|
37
|
+
|
|
38
|
+
Four checks stop ingest at the first bad record and advance the
|
|
39
|
+
watermark only that far:
|
|
40
|
+
|
|
41
|
+
| Layer | Catches |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `JSON.parse` | a torn write (line lacks its trailing `,"crc"…}` framing) or a malformed shape (framing present). Same failure, different remediation: a cut transfer vs a writer bug |
|
|
44
|
+
| crc32 per line | bit rot |
|
|
45
|
+
| monotonic HLC | reorder, duplicate, truncation |
|
|
46
|
+
| `.manifest` sidecar | truncation exactly on a line boundary |
|
|
47
|
+
|
|
48
|
+
**Damage halts; refusal skips.** Halting is right for damage, where a
|
|
49
|
+
gap looks the same as reordering. It is wrong for a well-formed line
|
|
50
|
+
mu declines to project: a refusal leaves no hole, and halting would
|
|
51
|
+
freeze the peer's watermark for good, since `mu sync --repair` only
|
|
52
|
+
resets the watermark and re-reads into the same line. Ingest
|
|
53
|
+
distinguishes three cases:
|
|
54
|
+
|
|
55
|
+
| Line | Result |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| damaged | halt |
|
|
58
|
+
| a known machine-local entity | reported defect, skipped |
|
|
59
|
+
| an unrecognised entity | applied as a no-op, no defect (a reader behind the writer is normal in a mixed fleet) |
|
|
60
|
+
|
|
61
|
+
`mu sync` suggests `--repair` only for defects a re-read can clear.
|
|
62
|
+
|
|
63
|
+
**Sync never fails a command.** A truncated segment, a garbage segment,
|
|
64
|
+
a sync dir that is a file, or a vanished directory warns on stderr and
|
|
65
|
+
returns.
|
|
66
|
+
|
|
67
|
+
## The ambient hook
|
|
68
|
+
|
|
69
|
+
Sync is ambient, not a daemon: no watcher, no background process, no
|
|
70
|
+
poll outliving a command. It happens because you already run `mu`
|
|
71
|
+
constantly.
|
|
72
|
+
|
|
73
|
+
The seam is `handle()` (`src/cli/handle.ts`), which every verb passes
|
|
74
|
+
through and which is already async. `syncPass` is async because it
|
|
75
|
+
takes the file lock, while most verb bodies are synchronous
|
|
76
|
+
better-sqlite3 code. One `await` before `fn(db)` and one after cover
|
|
77
|
+
every verb.
|
|
78
|
+
|
|
79
|
+
- **Ingest before the body**, so the verb reads the freshest state.
|
|
80
|
+
- **Flush after the body**, so this invocation's ops reach the segment
|
|
81
|
+
now.
|
|
82
|
+
- **Flush on the error path too.** A verb may commit ops before
|
|
83
|
+
throwing. Those ops are canonical, so withholding them would make
|
|
84
|
+
this machine's history depend on the exit code.
|
|
85
|
+
|
|
86
|
+
`src/sync.ts` holds `ambientIngest`, `ambientFlush`, `ambientSyncPass`,
|
|
87
|
+
`peerStatuses`, `ingestFromDb` (`mu sync --from <path>`) and
|
|
88
|
+
`repairPeer` (`mu sync --repair <peer>`, a unique prefix; ambiguity is
|
|
89
|
+
exit 4).
|
|
90
|
+
|
|
91
|
+
### Carve-outs
|
|
92
|
+
|
|
93
|
+
| Surface | Behaviour | Why |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| `MU_SYNC_DIR` unset | one `if` on one env var; no filesystem touch | the single-machine case pays nothing (measured: baseline; about 3ms with one peer) |
|
|
96
|
+
| `mu sql` | opts out (`handle(..., { ambientSync: false })`) | an ingest changing a row count mid-inspection reads as a mu bug |
|
|
97
|
+
| `mu sync` | opts out and runs the pass itself | otherwise the report prints "ingested 0" right after ingesting |
|
|
98
|
+
| the TUI | slow tick only (10s), one pass per beat across tabs, `quiet: true` | the fast tick can go to 100ms; the TUI owns the screen, so problems show on the Doctor card |
|
|
99
|
+
|
|
100
|
+
## Out-of-order arrival
|
|
101
|
+
|
|
102
|
+
An `edge` or `note` put whose task has not arrived yet skips as
|
|
103
|
+
`absent`, correctly. Ingest still advances the watermark past it, so
|
|
104
|
+
without a second pass it would never land. Peers are discovered by
|
|
105
|
+
`localeCompare` over random-UUID filenames, so the arrival order is a
|
|
106
|
+
coin flip per fleet.
|
|
107
|
+
|
|
108
|
+
`reprojectDeferredOps` (`src/apply.ts`) re-queries the log for note and
|
|
109
|
+
edge puts that are resolvable now but unprojected. It skips ops whose
|
|
110
|
+
parent task is gone and keys with a newer `del`, so a deleted edge is
|
|
111
|
+
never resurrected and an orphan is not retried forever. It runs once
|
|
112
|
+
per ingest pass, not per peer, because an edge in one segment may name
|
|
113
|
+
a task in another. On a healthy DB it is two indexed queries returning
|
|
114
|
+
zero rows.
|
|
115
|
+
|
|
116
|
+
There is no retry queue. A queue would be a second source of truth and
|
|
117
|
+
would not survive a short-lived `mu` process when the parent arrives
|
|
118
|
+
days later.
|
|
119
|
+
|
|
120
|
+
## Mixed-fleet hazards
|
|
121
|
+
|
|
122
|
+
`src/fleet-hazards.ts` adds three checks to the default `mu doctor`,
|
|
123
|
+
all no-ops when `MU_SYNC_DIR` is unset:
|
|
124
|
+
|
|
125
|
+
| Check | Severity |
|
|
126
|
+
| --- | --- |
|
|
127
|
+
| `MU_DB_PATH` inside `MU_SYNC_DIR` | fail |
|
|
128
|
+
| DB on a network mount | warn |
|
|
129
|
+
| two workstream names differing only by case | warn |
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# TUI architecture
|
|
2
|
+
|
|
3
|
+
The TUI is a read-only, live-updating dashboard of 10 cards on `ink`.
|
|
4
|
+
It lives entirely under `src/cli/tui/`, the only place ink and react
|
|
5
|
+
are imported (a [ROADMAP](../ROADMAP.md#anti-feature-pledges) pledge).
|
|
6
|
+
CLI verbs stay the mutation API: the TUI yanks `mu` commands and the
|
|
7
|
+
operator runs them. Overview: [ARCHITECTURE.md](../ARCHITECTURE.md).
|
|
8
|
+
Press `?` in the TUI for the keymap.
|
|
9
|
+
|
|
10
|
+
## Entry points
|
|
11
|
+
|
|
12
|
+
- Bare `mu` launches the TUI when `process.stdout.isTTY` is true. It
|
|
13
|
+
loads every workstream and picks the initial tab with the focus
|
|
14
|
+
ladder in `src/cli/tui-launch-focus.ts`: `$MU_SESSION` → tmux session
|
|
15
|
+
name → cwd inside a workspace → cwd equal to a workspace's project
|
|
16
|
+
root (`src/project-root.ts`; latest activity breaks ties) → tab 0.
|
|
17
|
+
With no workstreams it prints `mu --help` plus
|
|
18
|
+
`Get started: mu workstream init <name>` and exits 0.
|
|
19
|
+
- On non-TTY stdout (pipes, CI, most agent calls) bare `mu` prints help.
|
|
20
|
+
`MU_NO_TUI=1` forces that path inside a terminal.
|
|
21
|
+
- `mu state` is the static card; `mu state --tui` selects the TUI.
|
|
22
|
+
- The import stays dynamic (`await import("./cli/tui/index.js")`). A
|
|
23
|
+
static import of ink anywhere else would pull the TUI graph into the
|
|
24
|
+
help, version and `--json` paths.
|
|
25
|
+
|
|
26
|
+
## Cluster shape
|
|
27
|
+
|
|
28
|
+
| Files | Role |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `index.ts`, `escapes.ts` | `runTui` entry; alt-screen and SGR mouse-mode lifecycle; pure escape bytes |
|
|
31
|
+
| `app.tsx` | `<App>` root: popup state machine, global keymap, tabs, footer |
|
|
32
|
+
| `state.ts` | `useDashboardSnapshot` poll loop (fast and slow tiers) |
|
|
33
|
+
| `keys.ts`, `keymap-spec.ts`, `help.tsx` | pure key dispatch; the keymap source of truth that drives both dispatch and the `?`/F1 overlay |
|
|
34
|
+
| `mouse.ts`, `use-popup-action-queue.ts` | vendored SGR mouse parser with double-click; one queued popup action per render |
|
|
35
|
+
| `layout.ts`, `columns.ts`, `wrap-ansi.ts`, `use-terminal-size.ts` | responsive columns and row budgets; aligned clipping; ANSI-aware wrapping; resize hook |
|
|
36
|
+
| `titled-box.tsx`, `popup-shell.tsx`, `list-row.tsx`, `padded-rows.tsx`, `status-bar.tsx`, `tab-strip*.ts(x)` | chrome primitives |
|
|
37
|
+
| `format-helpers.ts`, `agent-display.ts` | shared formatters and agent-row display |
|
|
38
|
+
| `use-popup-filter.tsx`, `use-status-filter.tsx`, `use-notes-drill.ts` | `/` filter; status (`o`/`i`/`c`) and substate (`p`/`w`) toggles; shared notes drill |
|
|
39
|
+
| `yank.ts`, `tuicr.ts`, `lazygit.ts`, `tmux-attach.ts` | clipboard; the alt-screen handoffs |
|
|
40
|
+
| `cards/*.tsx` | the 10 dashboard cards, plus `_placeholder.tsx` |
|
|
41
|
+
| `popups/*.tsx` | one fullscreen popup per card, plus `dag.tsx` (`g`), `all-tasks.tsx` (`t`), the `task-list-popup.tsx` scaffold, `drill.tsx`, `task-detail.tsx`, `cursor-row.tsx`, `scroll.ts`, `viewport.ts`, `show-loader.ts` |
|
|
42
|
+
|
|
43
|
+
## State machine
|
|
44
|
+
|
|
45
|
+
`<App>` owns:
|
|
46
|
+
|
|
47
|
+
- **Popup state:** `null` (dashboard) or one popup id. One popup at a
|
|
48
|
+
time; `Esc` or `q` returns to the dashboard.
|
|
49
|
+
- **Card visibility:** toggled by `0`-`9`.
|
|
50
|
+
- **Tick rate:** fast tick, 1s default, adjusted with `+` `-` `=` `0`.
|
|
51
|
+
- **Active workstream tab:** `Tab` / `Shift-Tab` cycles when there are
|
|
52
|
+
two or more. A torn-down workstream keeps its tab position and
|
|
53
|
+
renders dimmed with strikethrough until the name is recreated. A
|
|
54
|
+
torn-down single workstream shows its normally hidden tab.
|
|
55
|
+
- **Footer flash:** a transient status-bar message.
|
|
56
|
+
|
|
57
|
+
Popups own their local state (cursor, filter, drill mode, local modes
|
|
58
|
+
such as Workspaces' `list` / `commits` / `show`). They never mutate App
|
|
59
|
+
state. They receive a read-only props bag: `snapshot`, `db`,
|
|
60
|
+
`workstream`, `fastTickNonce`, `slowTickNonce`, `yank`, `onClose`,
|
|
61
|
+
`onModeChange`, `onFilterEditingChange`, `onFooter`.
|
|
62
|
+
|
|
63
|
+
## Polling tiers
|
|
64
|
+
|
|
65
|
+
| Tier | Interval | Loader | Reads | Cost |
|
|
66
|
+
| --- | --- | --- | --- | --- |
|
|
67
|
+
| fast | 1s, adjustable | `loadWorkstreamSnapshotFast` | SQL only: tasks, tracks, workspace rows, recent events, workspace orphans | p50 < 1ms |
|
|
68
|
+
| slow | 10s (`SLOW_TICK_MS`) | `loadWorkstreamSnapshotSlow` | subprocesses: agent liveness and state, workspace dirty status, recent commits, doctor summary | p50 hundreds of ms |
|
|
69
|
+
|
|
70
|
+
`mergeSnapshotFastSlow` merges the last slow result into every fast
|
|
71
|
+
render, so cards never flicker through loading. `r` / `F5` runs both
|
|
72
|
+
tiers now. A tab switch clears the slow cache and fetches eagerly.
|
|
73
|
+
`snapshotKey` returns the same `data` reference across no-op ticks, so
|
|
74
|
+
React's diffing short-circuits. The slow tick also runs ambient sync
|
|
75
|
+
([sync.md](sync.md#carve-outs)).
|
|
76
|
+
|
|
77
|
+
## Render geometry
|
|
78
|
+
|
|
79
|
+
`layout.ts`:
|
|
80
|
+
|
|
81
|
+
- **Columns by width:** stacked below 120 columns, then 2 at 120, 3 at
|
|
82
|
+
180, 4 at 240. Stream cards (Commits, Activity log) trail; Commits
|
|
83
|
+
trails last.
|
|
84
|
+
- **Row budgets:** each card has `min` / `max` / `chrome`. The allocator
|
|
85
|
+
distributes rows so a noisy list cannot crowd its siblings. Overflow
|
|
86
|
+
shows as `+N more · Shift+N` in the card's bottom border.
|
|
87
|
+
- **Culling:** when even minimum budgets do not fit, cards drop by
|
|
88
|
+
priority (Doctor → Recent → Workspaces → …) and the screen shows
|
|
89
|
+
`+N cards hidden · resize taller`.
|
|
90
|
+
|
|
91
|
+
`wrap-ansi.ts` wraps by visual width (`string-width`) and closes open
|
|
92
|
+
SGR state on every exit path, so a fragment without a trailing
|
|
93
|
+
`\x1b[0m` cannot bleed into ink chrome. Drill bodies are space-padded
|
|
94
|
+
to the exact box width, or ink's `wrap="truncate"` ANSI miscount eats
|
|
95
|
+
the right border.
|
|
96
|
+
|
|
97
|
+
## Read-only, with handoffs
|
|
98
|
+
|
|
99
|
+
Every popup row exposes one canonical `mu` command through `y`.
|
|
100
|
+
`yank.ts` probes pbcopy, wl-copy, xclip, xsel and clip.exe, then falls
|
|
101
|
+
back to OSC-52 over stderr.
|
|
102
|
+
|
|
103
|
+
The handoffs leave the alt screen, disable mouse mode, run a
|
|
104
|
+
foreground subprocess, and restore both on exit:
|
|
105
|
+
|
|
106
|
+
- `t` in any `git show` drill runs `tuicr -r <sha>` in the project root
|
|
107
|
+
or workspace.
|
|
108
|
+
- `l` in the Commits popup runs lazygit.
|
|
109
|
+
- `a` in the Agents popup attaches to the agent's mux pane.
|
|
110
|
+
|
|
111
|
+
A TUI gesture that mutates state needs a ROADMAP entry first.
|
|
112
|
+
|
|
113
|
+
## Input
|
|
114
|
+
|
|
115
|
+
Mouse support uses SGR mouse mode. `mouse.ts` parses
|
|
116
|
+
`ESC[<button;x;y;M/m`, detects double-clicks, and exposes `useMouse()`.
|
|
117
|
+
A double-click on a card emits `setCursor` then `drill` through the
|
|
118
|
+
action queue, which consumes one action per render, so the cursor lands
|
|
119
|
+
before the drill reads the focused row.
|
|
120
|
+
|
|
121
|
+
`keys.ts` holds `dispatchGlobalKey` (dashboard), `dispatchPopupKey`
|
|
122
|
+
(popup), and `shouldSwallowGlobalKey` (keys a popup consumes).
|
|
123
|
+
|
|
124
|
+
## Drill recursion
|
|
125
|
+
|
|
126
|
+
List popups drill with `Enter`. `DrillScrollView`
|
|
127
|
+
(`popups/drill.tsx`) is the shared scrollable text leaf: Workspaces'
|
|
128
|
+
git show, Agents' scrollback, Activity log payloads, and Doctor
|
|
129
|
+
remediation. Task popups drill into `TaskDetailDrill`
|
|
130
|
+
(`popups/task-detail.tsx`, the notes timeline). Tracks chains track →
|
|
131
|
+
task list → `TaskDetailDrill`.
|
|
132
|
+
|
|
133
|
+
`useDrillKeymap` owns scroll state. An optional `resetKey` resets
|
|
134
|
+
scroll on identity change while a tick refresh keeps it, and
|
|
135
|
+
`onScrollChange` serves the DAG popup's focused-root tracking. Body
|
|
136
|
+
wrap metadata is shared so the scroll clamp and the painter cannot
|
|
137
|
+
disagree. Subprocess-backed drills use `popups/show-loader.ts`, which
|
|
138
|
+
keeps the prior body during a refetch.
|
|
139
|
+
|
|
140
|
+
## Test seam
|
|
141
|
+
|
|
142
|
+
See `test/README.md`. `test/_ink-render.ts` provides
|
|
143
|
+
`createInkInputStream`, `createInkCaptureStream`, `simulateInput` and
|
|
144
|
+
`latestRenderedFrame`: mount a popup or `<App>`, drive keys, assert on
|
|
145
|
+
the visible frame and spy callbacks. Source greps are for narrow
|
|
146
|
+
structural guards only, never behaviour.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# mu guide
|
|
2
|
+
|
|
3
|
+
Start with the tutorial, then open a how-to when you have that job.
|
|
4
|
+
`mu <verb> --help` is the reference: flags, defaults, and exit codes.
|
|
5
|
+
Terms are defined in [VOCABULARY.md](../VOCABULARY.md).
|
|
6
|
+
|
|
7
|
+
1. [Getting started](getting-started.md): install mu, plan two tasks,
|
|
8
|
+
and ship one with a pi worker.
|
|
9
|
+
|
|
10
|
+
How-to guides:
|
|
11
|
+
|
|
12
|
+
- [Dispatch work to a worker](dispatch.md)
|
|
13
|
+
- [Stop a worker](stop-a-worker.md)
|
|
14
|
+
- [Delegate a task (mu_delegate)](delegate.md)
|
|
15
|
+
- [Run a worker on another machine](remote.md)
|
|
16
|
+
- [Recover from a broken state](recovery.md)
|
|
17
|
+
- [Sync between machines](sync.md)
|
|
18
|
+
- [Use the TUI dashboard](tui.md)
|
|
19
|
+
- [Query and script mu](sql.md)
|
|
20
|
+
- [Upgrade mu](upgrade.md)
|
|
21
|
+
- [Choose a multiplexer backend](backends.md)
|
|
22
|
+
- [Clean up](cleanup.md)
|
|
23
|
+
|
|
24
|
+
For features mu does not have on purpose, see
|
|
25
|
+
[ROADMAP.md § Explicitly rejected](../ROADMAP.md#explicitly-rejected).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# How to choose a multiplexer backend
|
|
2
|
+
|
|
3
|
+
mu drives one multiplexer per command: tmux or
|
|
4
|
+
[herdr](https://github.com/herdrdev/herdr). Spawn, send, read, and
|
|
5
|
+
agent state work on both.
|
|
6
|
+
|
|
7
|
+
## See which backend is active
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
mu doctor # the environment block names it
|
|
11
|
+
mu doctor --json # .environment.mux.name
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
mu picks the backend from the first rule that matches:
|
|
15
|
+
|
|
16
|
+
1. `MU_MUX=tmux` or `MU_MUX=herdr`. An unknown value fails with exit 1.
|
|
17
|
+
2. `HERDR_ENV=1`: herdr. This wins over tmux variables, because a herdr
|
|
18
|
+
pane can host a tmux server.
|
|
19
|
+
3. `$TMUX` or `$TMUX_PANE` is set: tmux.
|
|
20
|
+
4. The first binary that runs, tmux before herdr.
|
|
21
|
+
5. Neither: `NoMultiplexerError`, exit 5.
|
|
22
|
+
|
|
23
|
+
## Know what differs
|
|
24
|
+
|
|
25
|
+
| | tmux | herdr |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| Workstream | session `mu-<ws>` | workspace labelled `mu-<ws>` |
|
|
28
|
+
| Window | window | tab |
|
|
29
|
+
| Pane id | `%15` | `w1:p1` |
|
|
30
|
+
| Attach | `tmux attach -t mu-<ws>` | `herdr session attach mu-<ws>` |
|
|
31
|
+
| pi agent state | control socket | control socket |
|
|
32
|
+
| Other agents' state | murmur | herdr |
|
|
33
|
+
| Send to non-pi agents | bracketed paste, then Enter | one atomic `agent prompt` |
|
|
34
|
+
| Spawn | one command | bare pane, then `agent start` |
|
|
35
|
+
| Pane borders, layout | set by mu | left to herdr |
|
|
36
|
+
| Test isolation | `MU_TMUX_SOCKET` | `MU_HERDR_SESSION` |
|
|
37
|
+
|
|
38
|
+
Where each agent's state comes from, per backend:
|
|
39
|
+
[architecture/mux.md § Agent state](../architecture/mux.md#agent-state).
|
|
40
|
+
|
|
41
|
+
## Know herdr's limits
|
|
42
|
+
|
|
43
|
+
- `mu agent kick` works on Linux only. It reads the TTY from
|
|
44
|
+
`/proc/<pid>/fd/0`, which macOS does not have.
|
|
45
|
+
- `mu agent read --lines` cannot recover rows that scrolled off a pane
|
|
46
|
+
on the alternate screen.
|
|
47
|
+
- `MU_<CLI>_COMMAND` overrides apply on tmux only. herdr picks the
|
|
48
|
+
agent binary itself.
|
|
49
|
+
- herdr creates every workspace detached. Run `herdr workspace focus`
|
|
50
|
+
yourself.
|
|
51
|
+
|
|
52
|
+
## Expect loud failures, not silent ones
|
|
53
|
+
|
|
54
|
+
If the multiplexer is down, `mu doctor` still prints a full report.
|
|
55
|
+
Verbs that need the multiplexer, such as spawn, send, close, and
|
|
56
|
+
reconcile, exit 5 with remediation steps. Verbs that only use it for
|
|
57
|
+
decoration, such as identity lookup and `mu workstream list`, carry on.
|
|
58
|
+
|
|
59
|
+
A herdr argument error raises `HerdrSyntaxError`, not a multiplexer
|
|
60
|
+
error. It means herdr's CLI changed and mu needs a fix. The herdr
|
|
61
|
+
server is fine.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# How to clean up
|
|
2
|
+
|
|
3
|
+
## Close one agent
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
mu agent close worker-1 -w auth
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
A clean workspace is freed with the agent. If the workspace has edits
|
|
10
|
+
or commits, close refuses with exit 4. Run
|
|
11
|
+
`mu workspace free worker-1 -w auth --commit` first, or pass
|
|
12
|
+
`--discard-workspace` to lose the changes. See
|
|
13
|
+
[Stop a worker](stop-a-worker.md).
|
|
14
|
+
|
|
15
|
+
## Tear down a workstream
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
mu workstream teardown auth # dry run: prints what goes
|
|
19
|
+
mu workstream teardown auth --yes # kills the session, deletes the rows
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Teardown kills the tmux session first, then deletes the agents, tasks,
|
|
23
|
+
edges, notes, and workspaces. Running it again is a no-op.
|
|
24
|
+
|
|
25
|
+
To sweep every workstream that has no tasks, agents, or workspaces,
|
|
26
|
+
including `mu-*` tmux sessions with no DB row:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
mu workstream teardown --empty
|
|
30
|
+
mu workstream teardown --empty --yes
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Undo a teardown
|
|
34
|
+
|
|
35
|
+
Teardown writes tombstone ops, so the ops log keeps everything. You do
|
|
36
|
+
not need a backup first.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
mu workstream list --torn-down
|
|
40
|
+
mu undo 7a40e6cc --yes
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The list shows one row per teardown, newest first. `← recreated since`
|
|
44
|
+
means the name is in use again, so the undo would change nothing.
|
|
45
|
+
Killed panes and freed workspace directories do not come back.
|
|
46
|
+
|
|
47
|
+
## Find workstreams you can remove
|
|
48
|
+
|
|
49
|
+
The `housekeeping` section of `mu doctor` lists two kinds:
|
|
50
|
+
|
|
51
|
+
| Kind | Rule | Advice |
|
|
52
|
+
| ---- | ---- | ------ |
|
|
53
|
+
| finished | every task `CLOSED`, idle 14 days or more | safe to tear down |
|
|
54
|
+
| abandoned | unclosed tasks, idle 60 days or more | read the open tasks first |
|
|
55
|
+
|
|
56
|
+
It skips your current workstream, `scratch`, and any workstream with a
|
|
57
|
+
live agent or a workspace. Teardown removes the plan, not the code: no
|
|
58
|
+
commit or checkout is touched.
|
|
59
|
+
|
|
60
|
+
## Free disk space
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
mu doctor --disk # bytes per workspace checkout
|
|
64
|
+
mu workspace orphans -w auth # directories with no DB row
|
|
65
|
+
mu workspace free worker-1 -w auth # delete one workspace
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Keep a copy of the graph
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
mu db backup /tmp/mu-backup.db # VACUUM INTO; never overwrites
|
|
72
|
+
mu task list -w auth --json
|
|
73
|
+
mu task notes design -w auth --json
|
|
74
|
+
```
|