@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.
Files changed (40) hide show
  1. package/AGENTS.md +203 -599
  2. package/README.md +76 -327
  3. package/dist/cli.js.map +1 -1
  4. package/dist/extension/mu-pi.js +178 -45
  5. package/dist/extension/mu-pi.js.map +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/docs/ARCHITECTURE.md +175 -1072
  8. package/docs/ROADMAP.md +88 -169
  9. package/docs/VISION.md +205 -415
  10. package/docs/VOCABULARY.md +202 -466
  11. package/docs/architecture/control-socket.md +119 -0
  12. package/docs/architecture/dag.md +113 -0
  13. package/docs/architecture/mux.md +163 -0
  14. package/docs/architecture/ops-log.md +140 -0
  15. package/docs/architecture/sdk.md +185 -0
  16. package/docs/architecture/sync.md +129 -0
  17. package/docs/architecture/tui.md +146 -0
  18. package/docs/guide/README.md +25 -0
  19. package/docs/guide/backends.md +61 -0
  20. package/docs/guide/cleanup.md +74 -0
  21. package/docs/guide/delegate.md +87 -0
  22. package/docs/guide/dispatch.md +137 -0
  23. package/docs/guide/getting-started.md +177 -0
  24. package/docs/guide/recovery.md +133 -0
  25. package/docs/guide/remote.md +74 -0
  26. package/docs/guide/sql.md +76 -0
  27. package/docs/guide/stop-a-worker.md +64 -0
  28. package/docs/guide/sync.md +78 -0
  29. package/docs/guide/tui.md +98 -0
  30. package/docs/guide/upgrade.md +76 -0
  31. package/docs/history/CHANGELOG-pre-3.md +4866 -0
  32. package/docs/reference/env.md +87 -0
  33. package/docs/reference/naming.md +82 -0
  34. package/docs/specs/2026-09-28-agent-state-from-murmur.md +1 -1
  35. package/docs/specs/2026-09-29-task-substates.md +1 -1
  36. package/package.json +1 -1
  37. package/skills/mu/REMOTE_WORKERS.md +179 -504
  38. package/skills/mu/SKILL.md +116 -185
  39. package/docs/HANDOVER.md +0 -486
  40. package/docs/USAGE_GUIDE.md +0 -2738
@@ -7,7 +7,7 @@ import {
7
7
  readFileSync,
8
8
  realpathSync,
9
9
  rmSync,
10
- statSync
10
+ statSync as statSync2
11
11
  } from "fs";
12
12
  import { connect, createServer } from "net";
13
13
  import { dirname, join as join2 } from "path";
@@ -39,13 +39,17 @@ var LineDecoder = class {
39
39
 
40
40
  // extension/delegate.ts
41
41
  import { execFile } from "child_process";
42
- import { existsSync } from "fs";
42
+ import { existsSync, statSync } from "fs";
43
43
  import { delimiter, join } from "path";
44
44
  import { fileURLToPath } from "url";
45
45
  var DELEGATE_TOOL = "mu_delegate";
46
46
  var DELEGATE_CANCEL_TOOL = "mu_delegate_cancel";
47
47
  var DELEGATE_MESSAGE_TYPE = "mu-delegate";
48
48
  var DELEGATE_WORKSTREAM = "scratch";
49
+ var DELEGATE_STATUS_KEY = "mu-delegate";
50
+ function delegateStatus(n) {
51
+ return n > 0 ? `${n} delegate${n === 1 ? "" : "s"} running` : void 0;
52
+ }
49
53
  var DELEGATE_TIMEOUT_S = 3600;
50
54
  var W = DELEGATE_WORKSTREAM;
51
55
  function delegateEnabled(env = process.env) {
@@ -54,6 +58,13 @@ function delegateEnabled(env = process.env) {
54
58
  if (env.MU_MUX || env.HERDR_ENV === "1" || env.TMUX || env.TMUX_PANE) return true;
55
59
  return onPath("tmux", env) || onPath("herdr", env);
56
60
  }
61
+ function isDir(p) {
62
+ try {
63
+ return statSync(p).isDirectory();
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
57
68
  function onPath(bin, env) {
58
69
  return (env.PATH ?? "").split(delimiter).some((d) => d !== "" && existsSync(join(d, bin)));
59
70
  }
@@ -77,41 +88,71 @@ function defaultRunner() {
77
88
  );
78
89
  });
79
90
  }
80
- function delegateMessage(name, agent, pane, tail, attach) {
91
+ function formatElapsed(ms) {
92
+ const t = Math.max(0, Math.round(ms / 1e3));
93
+ if (t < 60) return `${t}s`;
94
+ const m = Math.floor(t / 60);
95
+ if (m < 60) return `${m}m ${String(t % 60).padStart(2, "0")}s`;
96
+ return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, "0")}m`;
97
+ }
98
+ function delegateMessage(name, agent, pane, x = {}) {
99
+ const timeoutS = x.timeoutS ?? DELEGATE_TIMEOUT_S;
81
100
  const who = `Delegate ${W}/${name}`;
101
+ const took = x.elapsedMs !== void 0 ? ` after ${formatElapsed(x.elapsedMs)}` : "";
82
102
  const fate = pane.closed ? "Pane closed." : `Pane kept (${pane.why}).`;
83
103
  const look = [
84
- attach ? `Attach: ${attach}` : void 0,
104
+ x.attach ? `Attach: ${x.attach}` : void 0,
85
105
  `Read: mu agent read ${name} -n 50 -w ${W}`
86
- ].filter((x) => x !== void 0);
87
- const tailBlock = tail?.trim() ? `
106
+ ].filter((v) => v !== void 0);
107
+ const tailBlock = x.tail?.trim() ? `
88
108
 
89
109
  Pane tail:
90
- ${tail.trimEnd()}` : "";
110
+ ${x.tail.trimEnd()}` : "";
111
+ const ws = x.workspace ? ` Workspace: ${x.workspace}` : "";
91
112
  switch (agent?.outcome) {
92
113
  case "done":
93
- return `${who} finished. ${fate}
114
+ return `${who} finished${took}. ${fate}${ws}
94
115
 
95
116
  ${agent.lastText ?? ""}`;
96
117
  case "empty":
97
- return `${who} finished without a text answer. ${fate}${tailBlock}`;
118
+ return `${who} finished${took} without a text answer. ${fate}${ws}${tailBlock}`;
98
119
  case "died":
99
- return `${who} died before answering. ${fate}${tailBlock}`;
120
+ return `${who} died${took} before answering. ${fate}${ws}${tailBlock}`;
100
121
  case "timeout":
101
122
  case "pending":
102
123
  return [
103
- `${who} is still running after ${DELEGATE_TIMEOUT_S}s; stopped waiting. ${fate}`,
124
+ `${who} is still running after ${timeoutS}s; stopped waiting, so its answer will not arrive here. ${fate}${ws}`,
104
125
  ...look,
105
126
  `Wait again: mu agent wait ${name} -w ${W} --json`
106
127
  ].join("\n");
107
128
  default:
108
- return `${who}: mu agent wait gave no result. ${fate}${tailBlock}`;
129
+ return `${who}: mu agent wait gave no result. ${fate}${ws}${tailBlock}`;
109
130
  }
110
131
  }
132
+ function labelStem(label) {
133
+ if (typeof label !== "string") return void 0;
134
+ const stem = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20).replace(/-+$/, "");
135
+ return stem || void 0;
136
+ }
111
137
  function registerDelegate(pi, run = defaultRunner()) {
112
138
  if (!delegateEnabled()) return;
113
139
  const inflight = /* @__PURE__ */ new Map();
114
140
  const reserved = /* @__PURE__ */ new Set();
141
+ let ui;
142
+ const showStatus = () => {
143
+ try {
144
+ ui?.setStatus?.(DELEGATE_STATUS_KEY, delegateStatus(inflight.size));
145
+ } catch {
146
+ }
147
+ };
148
+ const track = (ctx) => {
149
+ if (ctx?.hasUI !== false && ctx?.ui) ui = ctx.ui;
150
+ };
151
+ const forget = (name) => {
152
+ inflight.delete(name);
153
+ reserved.delete(name);
154
+ showStatus();
155
+ };
115
156
  const mu = run;
116
157
  const json = (r) => {
117
158
  try {
@@ -122,28 +163,39 @@ function registerDelegate(pi, run = defaultRunner()) {
122
163
  }
123
164
  };
124
165
  const failure = (what, r) => new Error(`${what} failed (exit ${r.code}): ${(r.stderr || r.stdout).trim()}`);
125
- async function pickName() {
166
+ async function pickName(stem) {
126
167
  const r = await mu(["agent", "list", "-w", W, "--json"]);
127
168
  const agents = json(r)?.agents;
128
169
  const taken = new Set(
129
170
  Array.isArray(agents) ? agents.map((a) => a.name).filter((n) => n) : []
130
171
  );
131
172
  for (let n = 1; ; n++) {
132
- const name = `delegate-${n}`;
173
+ const name = stem ? n === 1 ? `delegate-${stem}` : `delegate-${stem}-${n}` : `delegate-${n}`;
133
174
  if (!taken.has(name) && !reserved.has(name)) {
134
175
  reserved.add(name);
135
176
  return name;
136
177
  }
137
178
  }
138
179
  }
139
- async function spawn(params) {
180
+ async function spawn(params, cwd, signal) {
181
+ const stem = labelStem(params.label);
140
182
  for (let attempt = 0; ; attempt++) {
141
- const name = await pickName();
183
+ signal?.throwIfAborted();
184
+ const name = await pickName(stem);
142
185
  const args = ["agent", "spawn", name, "-w", W, "--json"];
143
186
  if (params.workspace === true) args.push("--workspace");
187
+ else if (cwd) args.push("--cwd", cwd);
144
188
  if (typeof params.cli === "string" && params.cli) args.push("--cli", params.cli);
145
189
  const r = await mu(args);
146
- if (r.code !== 0 && /already exists/.test(r.stderr) && attempt < 3) continue;
190
+ if (signal?.aborted && r.code === 0) {
191
+ reserved.delete(name);
192
+ await mu(["agent", "close", name, "-w", W]);
193
+ signal.throwIfAborted();
194
+ }
195
+ if (r.code !== 0 && /already exists/.test(r.stderr) && attempt < 3) {
196
+ reserved.delete(name);
197
+ continue;
198
+ }
147
199
  if (r.code !== 0) {
148
200
  reserved.delete(name);
149
201
  throw failure("mu agent spawn", r);
@@ -157,10 +209,15 @@ function registerDelegate(pi, run = defaultRunner()) {
157
209
  }
158
210
  const steps = Array.isArray(out.nextSteps) ? out.nextSteps : [];
159
211
  const attach = steps.map((s) => s).find((s) => s.intent === "Attach the pane")?.command;
160
- return { name, ...typeof attach === "string" ? { attach } : {} };
212
+ const wsPath = out.workspace?.path;
213
+ return {
214
+ name,
215
+ ...typeof attach === "string" ? { attach } : {},
216
+ ...typeof wsPath === "string" ? { workspace: wsPath } : {}
217
+ };
161
218
  }
162
219
  }
163
- async function deliver(name, wait, keep, attach) {
220
+ async function deliver(name, wait, keep, extras) {
164
221
  const agent = json(wait)?.agents?.[0];
165
222
  const outcome = agent?.outcome;
166
223
  const tail = outcome === "done" || outcome === "timeout" || outcome === "pending" ? void 0 : (await mu(["agent", "read", name, "-n", "50", "-w", W])).stdout;
@@ -174,9 +231,17 @@ function registerDelegate(pi, run = defaultRunner()) {
174
231
  await pi.sendMessage(
175
232
  {
176
233
  customType: DELEGATE_MESSAGE_TYPE,
177
- content: delegateMessage(name, agent, pane, tail, attach),
234
+ content: delegateMessage(name, agent, pane, { ...extras, ...tail ? { tail } : {} }),
178
235
  display: true,
179
- details: { name, workstream: W, outcome: outcome ?? null, closed: pane.closed }
236
+ details: {
237
+ name,
238
+ workstream: W,
239
+ outcome: outcome ?? null,
240
+ closed: pane.closed,
241
+ answer: outcome === "done" ? agent?.lastText ?? null : null,
242
+ elapsedMs: extras.elapsedMs ?? null,
243
+ workspace: extras.workspace ?? null
244
+ }
180
245
  },
181
246
  { deliverAs: "followUp", triggerTurn: true }
182
247
  );
@@ -184,23 +249,36 @@ function registerDelegate(pi, run = defaultRunner()) {
184
249
  pi.registerTool({
185
250
  name: DELEGATE_TOOL,
186
251
  label: "mu delegate",
187
- description: `Hand one self-contained task to a fresh pi agent in its own mux pane (mu's ${W} workstream) and keep working. Returns at once; the delegate's final answer arrives later as a follow-up message, so do not wait or poll for it. Use it for independent side work (research, a review, a draft, an investigation) you would otherwise do serially. The delegate starts with no context: put everything it needs in task. Several calls run in parallel. The pane is closed after a clean finish unless keep is true; the user can attach to it meanwhile.`,
188
- promptSnippet: "Delegate a self-contained task to a background pi agent; its answer arrives later",
252
+ description: `Subagent: run one self-contained task in a fresh pi agent (its own mux pane in mu's ${W} workstream) while you keep working. Use it whenever you would reach for a subagent: research, a review, a draft, an investigation; call it several times to fan out in parallel. The subagent starts with no context, in your working directory: put everything it needs in task. The call returns at once and the answer arrives later as a follow-up message: carry on with other work, or end your turn if your next step needs the answer (the follow-up resumes you). The pane closes after a clean finish; keep: true leaves it open.`,
253
+ promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later as a follow-up",
189
254
  parameters: {
190
255
  type: "object",
191
256
  properties: {
192
257
  task: { type: "string", description: "The complete task, with all context it needs" },
258
+ label: {
259
+ type: "string",
260
+ description: "Short label naming the delegate, e.g. 'review' \u2192 delegate-review. Use one per subtask when fanning out."
261
+ },
193
262
  brief: {
194
263
  type: "string",
195
- description: "Optional persona or ground rules, sent before the task"
264
+ description: "Optional standing role or rules, sent before the task, e.g. 'You are a reviewer; read only.' The rules are instructions, not enforced. The job itself goes in task."
265
+ },
266
+ cwd: {
267
+ type: "string",
268
+ description: "Working directory for the delegate (default: yours; ignored with workspace)"
269
+ },
270
+ timeout: {
271
+ type: "number",
272
+ exclusiveMinimum: 0,
273
+ description: `Seconds to wait for the answer (default ${DELEGATE_TIMEOUT_S}). On timeout you get a "still running" follow-up instead of the answer, and the answer will not arrive later; the subagent keeps running in its pane.`
196
274
  },
197
275
  workspace: {
198
276
  type: "boolean",
199
- description: "Give it its own VCS workspace (for tasks that edit files)"
277
+ description: "For tasks that edit files: its own VCS checkout (jj workspace / git worktree / sl share) branched from the current commit. The result and follow-up name the path; changes stay there until you merge them. This isolates repository edits only: the subagent runs as you, with your files, environment and network."
200
278
  },
201
279
  cli: {
202
280
  type: "string",
203
- description: "Agent CLI key; resolves $MU_<CLI>_COMMAND (default pi)"
281
+ description: "Agent CLI key (default pi). Name a configured key such as 'pi_fast' to run a cheaper model."
204
282
  },
205
283
  keep: {
206
284
  type: "boolean",
@@ -210,16 +288,30 @@ function registerDelegate(pi, run = defaultRunner()) {
210
288
  required: ["task"],
211
289
  additionalProperties: false
212
290
  },
213
- async execute(_id, params) {
291
+ async execute(_id, params, signal, _onUpdate, ctx) {
292
+ track(ctx);
214
293
  const task = typeof params.task === "string" ? params.task.trim() : "";
215
294
  if (!task) throw new Error("mu_delegate needs a non-empty task");
216
295
  const brief = typeof params.brief === "string" ? params.brief.trim() : "";
217
296
  const keep = params.keep === true;
218
- const { name, attach } = await spawn(params);
219
- const entry = { abort: new AbortController(), cancelled: false };
297
+ const t = params.timeout;
298
+ if (t !== void 0 && !(typeof t === "number" && Number.isFinite(t) && t > 0))
299
+ throw new Error(
300
+ `mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
301
+ );
302
+ const timeoutS = typeof t === "number" ? Math.ceil(t) : DELEGATE_TIMEOUT_S;
303
+ let cwd;
304
+ if (params.workspace !== true) {
305
+ cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
306
+ if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
307
+ }
308
+ const { name, attach, workspace } = await spawn(params, cwd, signal);
309
+ const entry = { abort: new AbortController(), cancelling: false };
220
310
  inflight.set(name, entry);
311
+ showStatus();
312
+ const started = Date.now();
221
313
  const waiting = mu(
222
- ["agent", "wait", name, "-w", W, "--json", "--timeout", String(DELEGATE_TIMEOUT_S)],
314
+ ["agent", "wait", name, "-w", W, "--json", "--timeout", String(timeoutS)],
223
315
  entry.abort.signal
224
316
  );
225
317
  const sent = await mu(["agent", "send", name, brief ? `${brief}
@@ -227,23 +319,55 @@ function registerDelegate(pi, run = defaultRunner()) {
227
319
  ${task}` : task, "-w", W]);
228
320
  if (sent.code !== 0) {
229
321
  entry.abort.abort();
230
- inflight.delete(name);
231
- throw failure(`mu agent send to ${W}/${name}`, sent);
322
+ forget(name);
323
+ const c = await mu(["agent", "close", name, "-w", W]);
324
+ const pane = c.code === 0 ? "Pane closed." : `Pane kept; close it with: mu agent close ${name} -w ${W}`;
325
+ throw new Error(`${failure(`mu agent send to ${W}/${name}`, sent).message}. ${pane}`);
232
326
  }
327
+ const settle = async (r) => {
328
+ forget(name);
329
+ try {
330
+ await deliver(name, r, keep, {
331
+ timeoutS,
332
+ elapsedMs: Date.now() - started,
333
+ ...attach ? { attach } : {},
334
+ ...workspace ? { workspace } : {}
335
+ });
336
+ } catch (e) {
337
+ await Promise.resolve(
338
+ pi.sendMessage(
339
+ {
340
+ customType: DELEGATE_MESSAGE_TYPE,
341
+ content: `Delegate ${W}/${name} settled, but delivering its answer failed: ${e instanceof Error ? e.message : String(e)}. Read it with: mu agent read ${name} -n 50 -w ${W}`,
342
+ display: true,
343
+ details: { name, workstream: W, outcome: null, closed: false }
344
+ },
345
+ { deliverAs: "followUp", triggerTurn: true }
346
+ )
347
+ ).catch(() => {
348
+ });
349
+ }
350
+ };
233
351
  void waiting.then(async (r) => {
234
- if (inflight.get(name) !== entry || entry.cancelled) return;
235
- inflight.delete(name);
236
- reserved.delete(name);
237
- await deliver(name, r, keep, attach);
352
+ if (inflight.get(name) !== entry) return;
353
+ if (entry.cancelling) entry.parked = () => settle(r);
354
+ else await settle(r);
238
355
  });
239
356
  const lines = [
240
357
  `Delegated to ${W}/${name}. Its answer arrives as a follow-up message when it finishes; do not wait or poll.`,
358
+ workspace ? `Workspace: ${workspace}` : void 0,
241
359
  attach ? `Attach: ${attach}` : void 0,
242
360
  `Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${name}" }`
243
361
  ].filter((x) => x !== void 0);
244
362
  return {
245
363
  content: [{ type: "text", text: lines.join("\n") }],
246
- details: { name, workstream: W, attach: attach ?? null, keep }
364
+ details: {
365
+ name,
366
+ workstream: W,
367
+ attach: attach ?? null,
368
+ workspace: workspace ?? null,
369
+ keep
370
+ }
247
371
  };
248
372
  }
249
373
  });
@@ -260,19 +384,26 @@ ${task}` : task, "-w", W]);
260
384
  required: ["name"],
261
385
  additionalProperties: false
262
386
  },
263
- async execute(_id, params) {
387
+ async execute(_id, params, _signal, _onUpdate, ctx) {
388
+ track(ctx);
264
389
  const name = typeof params.name === "string" ? params.name : "";
265
390
  if (!name) throw new Error("mu_delegate_cancel needs a name");
266
391
  const entry = inflight.get(name);
267
- if (entry) entry.cancelled = true;
392
+ if (entry) entry.cancelling = true;
268
393
  const ab = await mu(["agent", "abort", name, "-w", W]);
269
394
  if (ab.code !== 0) {
270
- if (entry) entry.cancelled = false;
395
+ if (entry) {
396
+ entry.cancelling = false;
397
+ const parked = entry.parked;
398
+ entry.parked = void 0;
399
+ if (parked) void parked();
400
+ }
271
401
  throw failure(`mu agent abort ${W}/${name}`, ab);
272
402
  }
273
- entry?.abort.abort();
274
- inflight.delete(name);
275
- reserved.delete(name);
403
+ if (entry) {
404
+ entry.abort.abort();
405
+ forget(name);
406
+ }
276
407
  let text = `Aborted ${W}/${name}.`;
277
408
  if (params.keep === true) text += " Pane kept.";
278
409
  else {
@@ -287,6 +418,8 @@ ${task}` : task, "-w", W]);
287
418
  const names = [...inflight.keys()];
288
419
  for (const e of inflight.values()) e.abort.abort();
289
420
  inflight.clear();
421
+ showStatus();
422
+ ui = void 0;
290
423
  const msg = `${names.length} delegate${names.length === 1 ? "" : "s"} still running: ${names.join(", ")}. Their answers will not come back here; use mu agent wait <name> -w ${W} --json.`;
291
424
  if (ctx?.hasUI && ctx.ui) ctx.ui.notify(msg, "warning");
292
425
  else process.stderr.write(`mu: ${msg}
@@ -295,7 +428,7 @@ ${task}` : task, "-w", W]);
295
428
  }
296
429
 
297
430
  // extension/mu-pi.ts
298
- var EXT_VERSION = true ? "3.2.0" : void 0;
431
+ var EXT_VERSION = true ? "3.2.1" : void 0;
299
432
  var FRESH_COMMAND = "mu-fresh";
300
433
  var FRESH_TIMEOUT_MS = 3e4;
301
434
  var LAST_TEXT_MAX_BYTES = 64 * 1024;
@@ -370,7 +503,7 @@ ${LAST_TEXT_TRUNCATED}`;
370
503
  }
371
504
  function inodeOf(path) {
372
505
  try {
373
- return statSync(path).ino;
506
+ return statSync2(path).ino;
374
507
  } catch {
375
508
  return void 0;
376
509
  }