@mu-crew/mu 3.2.0 → 3.3.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 (71) hide show
  1. package/AGENTS.md +208 -599
  2. package/README.md +125 -329
  3. package/dist/cli.js +108 -78
  4. package/dist/cli.js.map +1 -1
  5. package/dist/extension/mu-pi.js +421 -56
  6. package/dist/extension/mu-pi.js.map +1 -1
  7. package/dist/index.d.ts +36 -10
  8. package/dist/index.js +89 -67
  9. package/dist/index.js.map +1 -1
  10. package/docs/ARCHITECTURE.md +176 -1072
  11. package/docs/ROADMAP.md +88 -169
  12. package/docs/VISION.md +205 -415
  13. package/docs/VOCABULARY.md +204 -466
  14. package/docs/architecture/control-socket.md +133 -0
  15. package/docs/architecture/dag.md +113 -0
  16. package/docs/architecture/mux.md +162 -0
  17. package/docs/architecture/ops-log.md +140 -0
  18. package/docs/architecture/sdk.md +185 -0
  19. package/docs/architecture/sync.md +129 -0
  20. package/docs/architecture/tui.md +146 -0
  21. package/docs/guide/README.md +30 -0
  22. package/docs/guide/backends.md +61 -0
  23. package/docs/guide/cleanup.md +74 -0
  24. package/docs/guide/delegate.md +87 -0
  25. package/docs/guide/dispatch.md +144 -0
  26. package/docs/guide/getting-started.md +180 -0
  27. package/docs/guide/recovery.md +135 -0
  28. package/docs/guide/remote.md +74 -0
  29. package/docs/guide/sql.md +76 -0
  30. package/docs/guide/stop-a-worker.md +64 -0
  31. package/docs/guide/sync.md +78 -0
  32. package/docs/guide/tui.md +98 -0
  33. package/docs/guide/upgrade.md +76 -0
  34. package/docs/history/CHANGELOG-pre-3.md +4866 -0
  35. package/docs/reference/env.md +87 -0
  36. package/docs/reference/naming.md +82 -0
  37. package/docs/specs/2026-09-28-agent-state-from-murmur.md +1 -1
  38. package/docs/specs/2026-09-29-task-substates.md +1 -1
  39. package/docs/specs/2026-10-03-keep-driving-nudge.md +142 -0
  40. package/package.json +2 -1
  41. package/prompts/mu-refute.md +8 -0
  42. package/prompts/mu-research.md +8 -0
  43. package/prompts/mu-review.md +8 -0
  44. package/prompts/mu-rules-audit.md +8 -0
  45. package/prompts/mu-tournament.md +9 -0
  46. package/prompts/ultrathink.md +9 -0
  47. package/skills/mu/SKILL.md +126 -255
  48. package/skills/mu/recipes/adversarial-review.md +83 -0
  49. package/skills/mu/recipes/brief.md +53 -0
  50. package/skills/mu/recipes/codemode-driver.md +54 -0
  51. package/skills/mu/recipes/deep-research.md +52 -0
  52. package/skills/mu/recipes/fan-out.md +36 -0
  53. package/skills/mu/recipes/hypothesis-panel.md +40 -0
  54. package/skills/mu/recipes/long-run.md +20 -0
  55. package/skills/mu/recipes/loop-until-done.md +38 -0
  56. package/skills/mu/recipes/orchestrator-loop.md +87 -0
  57. package/skills/mu/recipes/plan.md +54 -0
  58. package/skills/mu/recipes/recovery.md +26 -0
  59. package/skills/mu/recipes/refute.md +49 -0
  60. package/skills/mu/recipes/remote-workers.md +292 -0
  61. package/skills/mu/recipes/review-panel.md +57 -0
  62. package/skills/mu/recipes/rules-audit.md +70 -0
  63. package/skills/mu/recipes/tournament.md +37 -0
  64. package/skills/mu/recipes/triage.md +38 -0
  65. package/skills/mu/recipes/ultrathink.md +73 -0
  66. package/skills/mu/recipes/watcher.md +16 -0
  67. package/skills/mu/recipes/waves.md +19 -0
  68. package/skills/mu/recipes/worker.md +24 -0
  69. package/docs/HANDOVER.md +0 -486
  70. package/docs/USAGE_GUIDE.md +0 -2738
  71. package/skills/mu/REMOTE_WORKERS.md +0 -617
@@ -1,26 +1,30 @@
1
1
  // extension/mu-pi.ts
2
2
  import {
3
3
  chmodSync,
4
- existsSync as existsSync2,
4
+ existsSync as existsSync3,
5
5
  linkSync,
6
6
  mkdirSync,
7
- readFileSync,
7
+ readdirSync,
8
+ readFileSync as readFileSync2,
8
9
  realpathSync,
9
10
  rmSync,
10
- statSync
11
+ statSync as statSync2
11
12
  } from "fs";
12
13
  import { connect, createServer } from "net";
13
- import { dirname, join as join2 } from "path";
14
+ import { dirname, join as join3 } from "path";
15
+ import { fileURLToPath as fileURLToPath2 } from "url";
14
16
 
15
17
  // src/ctl/protocol.ts
16
18
  var CTL_PROTOCOL_VERSION = 1;
19
+ var CTL_COMMANDS = ["new", "reload", "compact"];
17
20
  var CTL_OPS = [
18
21
  "hello",
19
22
  "status",
20
23
  "send",
21
24
  "wait",
22
25
  "abort",
23
- "fresh"
26
+ "fresh",
27
+ "command"
24
28
  ];
25
29
  var UNKNOWN_OP_PREFIX = "unknown op: ";
26
30
  function encode(msg) {
@@ -39,13 +43,17 @@ var LineDecoder = class {
39
43
 
40
44
  // extension/delegate.ts
41
45
  import { execFile } from "child_process";
42
- import { existsSync } from "fs";
46
+ import { existsSync, statSync } from "fs";
43
47
  import { delimiter, join } from "path";
44
48
  import { fileURLToPath } from "url";
45
49
  var DELEGATE_TOOL = "mu_delegate";
46
50
  var DELEGATE_CANCEL_TOOL = "mu_delegate_cancel";
47
51
  var DELEGATE_MESSAGE_TYPE = "mu-delegate";
48
52
  var DELEGATE_WORKSTREAM = "scratch";
53
+ var DELEGATE_STATUS_KEY = "mu-delegate";
54
+ function delegateStatus(n) {
55
+ return n > 0 ? `${n} delegate${n === 1 ? "" : "s"} running` : void 0;
56
+ }
49
57
  var DELEGATE_TIMEOUT_S = 3600;
50
58
  var W = DELEGATE_WORKSTREAM;
51
59
  function delegateEnabled(env = process.env) {
@@ -54,6 +62,13 @@ function delegateEnabled(env = process.env) {
54
62
  if (env.MU_MUX || env.HERDR_ENV === "1" || env.TMUX || env.TMUX_PANE) return true;
55
63
  return onPath("tmux", env) || onPath("herdr", env);
56
64
  }
65
+ function isDir(p) {
66
+ try {
67
+ return statSync(p).isDirectory();
68
+ } catch {
69
+ return false;
70
+ }
71
+ }
57
72
  function onPath(bin, env) {
58
73
  return (env.PATH ?? "").split(delimiter).some((d) => d !== "" && existsSync(join(d, bin)));
59
74
  }
@@ -77,41 +92,71 @@ function defaultRunner() {
77
92
  );
78
93
  });
79
94
  }
80
- function delegateMessage(name, agent, pane, tail, attach) {
95
+ function formatElapsed(ms) {
96
+ const t = Math.max(0, Math.round(ms / 1e3));
97
+ if (t < 60) return `${t}s`;
98
+ const m = Math.floor(t / 60);
99
+ if (m < 60) return `${m}m ${String(t % 60).padStart(2, "0")}s`;
100
+ return `${Math.floor(m / 60)}h ${String(m % 60).padStart(2, "0")}m`;
101
+ }
102
+ function delegateMessage(name, agent, pane, x = {}) {
103
+ const timeoutS = x.timeoutS ?? DELEGATE_TIMEOUT_S;
81
104
  const who = `Delegate ${W}/${name}`;
105
+ const took = x.elapsedMs !== void 0 ? ` after ${formatElapsed(x.elapsedMs)}` : "";
82
106
  const fate = pane.closed ? "Pane closed." : `Pane kept (${pane.why}).`;
83
107
  const look = [
84
- attach ? `Attach: ${attach}` : void 0,
108
+ x.attach ? `Attach: ${x.attach}` : void 0,
85
109
  `Read: mu agent read ${name} -n 50 -w ${W}`
86
- ].filter((x) => x !== void 0);
87
- const tailBlock = tail?.trim() ? `
110
+ ].filter((v) => v !== void 0);
111
+ const tailBlock = x.tail?.trim() ? `
88
112
 
89
113
  Pane tail:
90
- ${tail.trimEnd()}` : "";
114
+ ${x.tail.trimEnd()}` : "";
115
+ const ws = x.workspace ? ` Workspace: ${x.workspace}` : "";
91
116
  switch (agent?.outcome) {
92
117
  case "done":
93
- return `${who} finished. ${fate}
118
+ return `${who} finished${took}. ${fate}${ws}
94
119
 
95
120
  ${agent.lastText ?? ""}`;
96
121
  case "empty":
97
- return `${who} finished without a text answer. ${fate}${tailBlock}`;
122
+ return `${who} finished${took} without a text answer. ${fate}${ws}${tailBlock}`;
98
123
  case "died":
99
- return `${who} died before answering. ${fate}${tailBlock}`;
124
+ return `${who} died${took} before answering. ${fate}${ws}${tailBlock}`;
100
125
  case "timeout":
101
126
  case "pending":
102
127
  return [
103
- `${who} is still running after ${DELEGATE_TIMEOUT_S}s; stopped waiting. ${fate}`,
128
+ `${who} is still running after ${timeoutS}s; stopped waiting, so its answer will not arrive here. ${fate}${ws}`,
104
129
  ...look,
105
130
  `Wait again: mu agent wait ${name} -w ${W} --json`
106
131
  ].join("\n");
107
132
  default:
108
- return `${who}: mu agent wait gave no result. ${fate}${tailBlock}`;
133
+ return `${who}: mu agent wait gave no result. ${fate}${ws}${tailBlock}`;
109
134
  }
110
135
  }
136
+ function labelStem(label) {
137
+ if (typeof label !== "string") return void 0;
138
+ const stem = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20).replace(/-+$/, "");
139
+ return stem || void 0;
140
+ }
111
141
  function registerDelegate(pi, run = defaultRunner()) {
112
142
  if (!delegateEnabled()) return;
113
143
  const inflight = /* @__PURE__ */ new Map();
114
144
  const reserved = /* @__PURE__ */ new Set();
145
+ let ui;
146
+ const showStatus = () => {
147
+ try {
148
+ ui?.setStatus?.(DELEGATE_STATUS_KEY, delegateStatus(inflight.size));
149
+ } catch {
150
+ }
151
+ };
152
+ const track = (ctx) => {
153
+ if (ctx?.hasUI !== false && ctx?.ui) ui = ctx.ui;
154
+ };
155
+ const forget = (name) => {
156
+ inflight.delete(name);
157
+ reserved.delete(name);
158
+ showStatus();
159
+ };
115
160
  const mu = run;
116
161
  const json = (r) => {
117
162
  try {
@@ -122,28 +167,39 @@ function registerDelegate(pi, run = defaultRunner()) {
122
167
  }
123
168
  };
124
169
  const failure = (what, r) => new Error(`${what} failed (exit ${r.code}): ${(r.stderr || r.stdout).trim()}`);
125
- async function pickName() {
170
+ async function pickName(stem) {
126
171
  const r = await mu(["agent", "list", "-w", W, "--json"]);
127
172
  const agents = json(r)?.agents;
128
173
  const taken = new Set(
129
174
  Array.isArray(agents) ? agents.map((a) => a.name).filter((n) => n) : []
130
175
  );
131
176
  for (let n = 1; ; n++) {
132
- const name = `delegate-${n}`;
177
+ const name = stem ? n === 1 ? `delegate-${stem}` : `delegate-${stem}-${n}` : `delegate-${n}`;
133
178
  if (!taken.has(name) && !reserved.has(name)) {
134
179
  reserved.add(name);
135
180
  return name;
136
181
  }
137
182
  }
138
183
  }
139
- async function spawn(params) {
184
+ async function spawn(params, cwd, signal) {
185
+ const stem = labelStem(params.label);
140
186
  for (let attempt = 0; ; attempt++) {
141
- const name = await pickName();
187
+ signal?.throwIfAborted();
188
+ const name = await pickName(stem);
142
189
  const args = ["agent", "spawn", name, "-w", W, "--json"];
143
190
  if (params.workspace === true) args.push("--workspace");
191
+ else if (cwd) args.push("--cwd", cwd);
144
192
  if (typeof params.cli === "string" && params.cli) args.push("--cli", params.cli);
145
193
  const r = await mu(args);
146
- if (r.code !== 0 && /already exists/.test(r.stderr) && attempt < 3) continue;
194
+ if (signal?.aborted && r.code === 0) {
195
+ reserved.delete(name);
196
+ await mu(["agent", "close", name, "-w", W]);
197
+ signal.throwIfAborted();
198
+ }
199
+ if (r.code !== 0 && /already exists/.test(r.stderr) && attempt < 3) {
200
+ reserved.delete(name);
201
+ continue;
202
+ }
147
203
  if (r.code !== 0) {
148
204
  reserved.delete(name);
149
205
  throw failure("mu agent spawn", r);
@@ -157,10 +213,15 @@ function registerDelegate(pi, run = defaultRunner()) {
157
213
  }
158
214
  const steps = Array.isArray(out.nextSteps) ? out.nextSteps : [];
159
215
  const attach = steps.map((s) => s).find((s) => s.intent === "Attach the pane")?.command;
160
- return { name, ...typeof attach === "string" ? { attach } : {} };
216
+ const wsPath = out.workspace?.path;
217
+ return {
218
+ name,
219
+ ...typeof attach === "string" ? { attach } : {},
220
+ ...typeof wsPath === "string" ? { workspace: wsPath } : {}
221
+ };
161
222
  }
162
223
  }
163
- async function deliver(name, wait, keep, attach) {
224
+ async function deliver(name, wait, keep, extras) {
164
225
  const agent = json(wait)?.agents?.[0];
165
226
  const outcome = agent?.outcome;
166
227
  const tail = outcome === "done" || outcome === "timeout" || outcome === "pending" ? void 0 : (await mu(["agent", "read", name, "-n", "50", "-w", W])).stdout;
@@ -174,9 +235,17 @@ function registerDelegate(pi, run = defaultRunner()) {
174
235
  await pi.sendMessage(
175
236
  {
176
237
  customType: DELEGATE_MESSAGE_TYPE,
177
- content: delegateMessage(name, agent, pane, tail, attach),
238
+ content: delegateMessage(name, agent, pane, { ...extras, ...tail ? { tail } : {} }),
178
239
  display: true,
179
- details: { name, workstream: W, outcome: outcome ?? null, closed: pane.closed }
240
+ details: {
241
+ name,
242
+ workstream: W,
243
+ outcome: outcome ?? null,
244
+ closed: pane.closed,
245
+ answer: outcome === "done" ? agent?.lastText ?? null : null,
246
+ elapsedMs: extras.elapsedMs ?? null,
247
+ workspace: extras.workspace ?? null
248
+ }
180
249
  },
181
250
  { deliverAs: "followUp", triggerTurn: true }
182
251
  );
@@ -184,23 +253,36 @@ function registerDelegate(pi, run = defaultRunner()) {
184
253
  pi.registerTool({
185
254
  name: DELEGATE_TOOL,
186
255
  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",
256
+ 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.`,
257
+ promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later as a follow-up",
189
258
  parameters: {
190
259
  type: "object",
191
260
  properties: {
192
261
  task: { type: "string", description: "The complete task, with all context it needs" },
262
+ label: {
263
+ type: "string",
264
+ description: "Short label naming the delegate, e.g. 'review' \u2192 delegate-review. Use one per subtask when fanning out."
265
+ },
193
266
  brief: {
194
267
  type: "string",
195
- description: "Optional persona or ground rules, sent before the task"
268
+ 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."
269
+ },
270
+ cwd: {
271
+ type: "string",
272
+ description: "Working directory for the delegate (default: yours; ignored with workspace)"
273
+ },
274
+ timeout: {
275
+ type: "number",
276
+ exclusiveMinimum: 0,
277
+ 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
278
  },
197
279
  workspace: {
198
280
  type: "boolean",
199
- description: "Give it its own VCS workspace (for tasks that edit files)"
281
+ 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
282
  },
201
283
  cli: {
202
284
  type: "string",
203
- description: "Agent CLI key; resolves $MU_<CLI>_COMMAND (default pi)"
285
+ description: "Agent CLI key (default pi). Name a configured key such as 'pi_fast' to run a cheaper model."
204
286
  },
205
287
  keep: {
206
288
  type: "boolean",
@@ -210,16 +292,30 @@ function registerDelegate(pi, run = defaultRunner()) {
210
292
  required: ["task"],
211
293
  additionalProperties: false
212
294
  },
213
- async execute(_id, params) {
295
+ async execute(_id, params, signal, _onUpdate, ctx) {
296
+ track(ctx);
214
297
  const task = typeof params.task === "string" ? params.task.trim() : "";
215
298
  if (!task) throw new Error("mu_delegate needs a non-empty task");
216
299
  const brief = typeof params.brief === "string" ? params.brief.trim() : "";
217
300
  const keep = params.keep === true;
218
- const { name, attach } = await spawn(params);
219
- const entry = { abort: new AbortController(), cancelled: false };
301
+ const t = params.timeout;
302
+ if (t !== void 0 && !(typeof t === "number" && Number.isFinite(t) && t > 0))
303
+ throw new Error(
304
+ `mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
305
+ );
306
+ const timeoutS = typeof t === "number" ? Math.ceil(t) : DELEGATE_TIMEOUT_S;
307
+ let cwd;
308
+ if (params.workspace !== true) {
309
+ cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
310
+ if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
311
+ }
312
+ const { name, attach, workspace } = await spawn(params, cwd, signal);
313
+ const entry = { abort: new AbortController(), cancelling: false };
220
314
  inflight.set(name, entry);
315
+ showStatus();
316
+ const started = Date.now();
221
317
  const waiting = mu(
222
- ["agent", "wait", name, "-w", W, "--json", "--timeout", String(DELEGATE_TIMEOUT_S)],
318
+ ["agent", "wait", name, "-w", W, "--json", "--timeout", String(timeoutS)],
223
319
  entry.abort.signal
224
320
  );
225
321
  const sent = await mu(["agent", "send", name, brief ? `${brief}
@@ -227,23 +323,55 @@ function registerDelegate(pi, run = defaultRunner()) {
227
323
  ${task}` : task, "-w", W]);
228
324
  if (sent.code !== 0) {
229
325
  entry.abort.abort();
230
- inflight.delete(name);
231
- throw failure(`mu agent send to ${W}/${name}`, sent);
326
+ forget(name);
327
+ const c = await mu(["agent", "close", name, "-w", W]);
328
+ const pane = c.code === 0 ? "Pane closed." : `Pane kept; close it with: mu agent close ${name} -w ${W}`;
329
+ throw new Error(`${failure(`mu agent send to ${W}/${name}`, sent).message}. ${pane}`);
232
330
  }
331
+ const settle = async (r) => {
332
+ forget(name);
333
+ try {
334
+ await deliver(name, r, keep, {
335
+ timeoutS,
336
+ elapsedMs: Date.now() - started,
337
+ ...attach ? { attach } : {},
338
+ ...workspace ? { workspace } : {}
339
+ });
340
+ } catch (e) {
341
+ await Promise.resolve(
342
+ pi.sendMessage(
343
+ {
344
+ customType: DELEGATE_MESSAGE_TYPE,
345
+ 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}`,
346
+ display: true,
347
+ details: { name, workstream: W, outcome: null, closed: false }
348
+ },
349
+ { deliverAs: "followUp", triggerTurn: true }
350
+ )
351
+ ).catch(() => {
352
+ });
353
+ }
354
+ };
233
355
  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);
356
+ if (inflight.get(name) !== entry) return;
357
+ if (entry.cancelling) entry.parked = () => settle(r);
358
+ else await settle(r);
238
359
  });
239
360
  const lines = [
240
361
  `Delegated to ${W}/${name}. Its answer arrives as a follow-up message when it finishes; do not wait or poll.`,
362
+ workspace ? `Workspace: ${workspace}` : void 0,
241
363
  attach ? `Attach: ${attach}` : void 0,
242
364
  `Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${name}" }`
243
365
  ].filter((x) => x !== void 0);
244
366
  return {
245
367
  content: [{ type: "text", text: lines.join("\n") }],
246
- details: { name, workstream: W, attach: attach ?? null, keep }
368
+ details: {
369
+ name,
370
+ workstream: W,
371
+ attach: attach ?? null,
372
+ workspace: workspace ?? null,
373
+ keep
374
+ }
247
375
  };
248
376
  }
249
377
  });
@@ -260,19 +388,26 @@ ${task}` : task, "-w", W]);
260
388
  required: ["name"],
261
389
  additionalProperties: false
262
390
  },
263
- async execute(_id, params) {
391
+ async execute(_id, params, _signal, _onUpdate, ctx) {
392
+ track(ctx);
264
393
  const name = typeof params.name === "string" ? params.name : "";
265
394
  if (!name) throw new Error("mu_delegate_cancel needs a name");
266
395
  const entry = inflight.get(name);
267
- if (entry) entry.cancelled = true;
396
+ if (entry) entry.cancelling = true;
268
397
  const ab = await mu(["agent", "abort", name, "-w", W]);
269
398
  if (ab.code !== 0) {
270
- if (entry) entry.cancelled = false;
399
+ if (entry) {
400
+ entry.cancelling = false;
401
+ const parked = entry.parked;
402
+ entry.parked = void 0;
403
+ if (parked) void parked();
404
+ }
271
405
  throw failure(`mu agent abort ${W}/${name}`, ab);
272
406
  }
273
- entry?.abort.abort();
274
- inflight.delete(name);
275
- reserved.delete(name);
407
+ if (entry) {
408
+ entry.abort.abort();
409
+ forget(name);
410
+ }
276
411
  let text = `Aborted ${W}/${name}.`;
277
412
  if (params.keep === true) text += " Pane kept.";
278
413
  else {
@@ -287,6 +422,8 @@ ${task}` : task, "-w", W]);
287
422
  const names = [...inflight.keys()];
288
423
  for (const e of inflight.values()) e.abort.abort();
289
424
  inflight.clear();
425
+ showStatus();
426
+ ui = void 0;
290
427
  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
428
  if (ctx?.hasUI && ctx.ui) ctx.ui.notify(msg, "warning");
292
429
  else process.stderr.write(`mu: ${msg}
@@ -294,10 +431,143 @@ ${task}` : task, "-w", W]);
294
431
  });
295
432
  }
296
433
 
434
+ // extension/nudge.ts
435
+ import { existsSync as existsSync2, readFileSync } from "fs";
436
+ import { join as join2 } from "path";
437
+ var NUDGE_MESSAGE_TYPE = "mu-keep-driving";
438
+ var NUDGE_LOG_KIND = "nudge";
439
+ var MARK_START = "<!-- mu:keep-driving -->";
440
+ var MARK_END = "<!-- /mu:keep-driving -->";
441
+ var WAIT_HINT_MAX = 8;
442
+ function nudgeEnabled(env = process.env) {
443
+ return env.MU_NUDGE !== "0";
444
+ }
445
+ function keepDrivingRule(skillMd) {
446
+ const i = skillMd.indexOf(MARK_START);
447
+ const j = skillMd.indexOf(MARK_END);
448
+ if (i < 0 || j < i) return void 0;
449
+ const text = skillMd.slice(i + MARK_START.length, j).trim();
450
+ return text === "" ? void 0 : text;
451
+ }
452
+ function readSkill() {
453
+ for (const up of ["..", "../.."]) {
454
+ const p = join2(import.meta.dirname ?? "", up, "skills", "mu", "SKILL.md");
455
+ if (existsSync2(p)) return readFileSync(p, "utf8");
456
+ }
457
+ return void 0;
458
+ }
459
+ function dispatchedWorkstreams(command) {
460
+ const found = /* @__PURE__ */ new Set();
461
+ for (const seg of command.split(/&&|\|\||;|\n|\|/)) {
462
+ const words = seg.trim().split(/\s+/);
463
+ const at = words.findIndex((w) => w === "mu" || w.endsWith("/mu"));
464
+ if (at < 0) continue;
465
+ const [noun, verb] = [words[at + 1], words[at + 2]];
466
+ const isSendSpawn = noun === "agent" && (verb === "send" || verb === "spawn");
467
+ const isClaimFor = noun === "task" && verb === "claim" && words.includes("--for");
468
+ if (!isSendSpawn && !isClaimFor) continue;
469
+ let ws = "";
470
+ for (let k = at + 3; k < words.length; k++) {
471
+ const w = words[k] ?? "";
472
+ if (w === "-w" || w === "--workstream") ws = unquote(words[k + 1] ?? "");
473
+ else if (w.startsWith("--workstream=")) ws = unquote(w.slice("--workstream=".length));
474
+ }
475
+ if (ws === "" && isClaimFor) {
476
+ const ref = words[at + 3] ?? "";
477
+ if (ref.includes("/")) ws = unquote(ref.split("/")[0] ?? "");
478
+ }
479
+ if (ws !== "scratch") found.add(ws);
480
+ }
481
+ return found.size > 0 ? [...found] : void 0;
482
+ }
483
+ function unquote(s) {
484
+ return s.replace(/^['"]|['"]$/g, "");
485
+ }
486
+ async function inProgress(run, workstreams) {
487
+ const named = workstreams.filter((w) => w !== "");
488
+ const args = ["state", "--json", "--events", "0"];
489
+ for (const w of named) args.push("-w", w);
490
+ const r = await run(args);
491
+ if (r.code !== 0) return void 0;
492
+ let json;
493
+ try {
494
+ json = JSON.parse(r.stdout);
495
+ } catch {
496
+ return void 0;
497
+ }
498
+ const cards = json.workstreams ?? [json];
499
+ const refs = [];
500
+ for (const ws of cards) {
501
+ for (const t of ws.inProgress ?? []) {
502
+ if (t.name) refs.push(`${ws.workstreamName ?? ""}/${t.name}`);
503
+ }
504
+ }
505
+ return refs;
506
+ }
507
+ function nudgeText(rule, refs) {
508
+ const shown = refs.slice(0, WAIT_HINT_MAX).join(" ");
509
+ const more = refs.length > WAIT_HINT_MAX ? ` (+${refs.length - WAIT_HINT_MAX} more)` : "";
510
+ return [
511
+ `[mu] ${rule}`,
512
+ "",
513
+ `${refs.length} task(s) IN_PROGRESS. Next: mu task wait ${shown} --first --on-stall exit --json${more}`
514
+ ].join("\n");
515
+ }
516
+ function outcomeOf(event) {
517
+ return typeof event === "object" && event !== null ? event.outcome : void 0;
518
+ }
519
+ function bashCommand(event) {
520
+ if (typeof event !== "object" || event === null) return void 0;
521
+ const e = event;
522
+ return e.toolName === "bash" && typeof e.input?.command === "string" ? e.input.command : void 0;
523
+ }
524
+ function registerNudge(pi, run = defaultRunner(), skillMd = readSkill()) {
525
+ if (!nudgeEnabled()) return;
526
+ const rule = skillMd === void 0 ? void 0 : keepDrivingRule(skillMd);
527
+ if (rule === void 0) return;
528
+ const armed = /* @__PURE__ */ new Set();
529
+ let fired = false;
530
+ pi.on("input", () => {
531
+ armed.clear();
532
+ fired = false;
533
+ });
534
+ pi.on("tool_call", (event) => {
535
+ const cmd = bashCommand(event);
536
+ if (cmd === void 0) return;
537
+ for (const ws of dispatchedWorkstreams(cmd) ?? []) armed.add(ws);
538
+ });
539
+ pi.on("agent_before_settle", async (event) => {
540
+ if (fired || armed.size === 0) return;
541
+ if (outcomeOf(event) !== "completed") return;
542
+ fired = true;
543
+ const refs = await inProgress(run, [...armed]);
544
+ if (refs === void 0 || refs.length === 0) return;
545
+ const text = nudgeText(rule, refs);
546
+ const logWs = [...armed].find((w) => w !== "");
547
+ await run([
548
+ "log",
549
+ ...logWs ? ["-w", logWs] : [],
550
+ "--kind",
551
+ NUDGE_LOG_KIND,
552
+ `keep-driving: ${refs.length} in progress`
553
+ ]);
554
+ return {
555
+ entries: [
556
+ { type: "custom_message", customType: NUDGE_MESSAGE_TYPE, content: text, display: true }
557
+ ],
558
+ continue: true
559
+ };
560
+ });
561
+ }
562
+
297
563
  // extension/mu-pi.ts
298
- var EXT_VERSION = true ? "3.2.0" : void 0;
564
+ var EXT_VERSION = true ? "3.3.0" : void 0;
299
565
  var FRESH_COMMAND = "mu-fresh";
300
566
  var FRESH_TIMEOUT_MS = 3e4;
567
+ function commandName(name) {
568
+ return `mu-${name}`;
569
+ }
570
+ var COMMAND_TIMEOUT_MS = 3e4;
301
571
  var LAST_TEXT_MAX_BYTES = 64 * 1024;
302
572
  var LAST_TEXT_TRUNCATED = "[truncated, see pane]";
303
573
  var LIVE_PROBE_MS = 500;
@@ -311,9 +581,9 @@ function detectPiVersion() {
311
581
  if (!entry) return void 0;
312
582
  let dir = dirname(realpathSync(entry));
313
583
  for (let i = 0; i < 6; i++) {
314
- const pkgPath = join2(dir, "package.json");
315
- if (existsSync2(pkgPath)) {
316
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
584
+ const pkgPath = join3(dir, "package.json");
585
+ if (existsSync3(pkgPath)) {
586
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
317
587
  if (typeof pkg === "object" && pkg !== null) {
318
588
  const { name, version } = pkg;
319
589
  if (typeof name === "string" && /pi/.test(name) && typeof version === "string") {
@@ -370,7 +640,7 @@ ${LAST_TEXT_TRUNCATED}`;
370
640
  }
371
641
  function inodeOf(path) {
372
642
  try {
373
- return statSync(path).ino;
643
+ return statSync2(path).ino;
374
644
  } catch {
375
645
  return void 0;
376
646
  }
@@ -396,8 +666,31 @@ function reasonOf(event) {
396
666
  const r = typeof event === "object" && event !== null ? event.reason : void 0;
397
667
  return typeof r === "string" ? r : void 0;
398
668
  }
669
+ function promptsDir(from = import.meta.url) {
670
+ let here;
671
+ try {
672
+ here = dirname(fileURLToPath2(from));
673
+ } catch {
674
+ return void 0;
675
+ }
676
+ for (const up of ["..", "../.."]) {
677
+ const d = join3(here, up, "prompts");
678
+ if (existsSync3(join3(d, "ultrathink.md"))) return d;
679
+ }
680
+ return void 0;
681
+ }
682
+ function registerPrompts(pi) {
683
+ pi.on("resources_discover", () => {
684
+ const d = promptsDir();
685
+ if (!d) return {};
686
+ const paths = readdirSync(d).filter((f) => f.endsWith(".md")).sort().map((f) => join3(d, f));
687
+ return { promptPaths: paths };
688
+ });
689
+ }
399
690
  function muPi(pi) {
400
691
  registerDelegate(pi);
692
+ registerPrompts(pi);
693
+ registerNudge(pi);
401
694
  serveCtl(pi);
402
695
  }
403
696
  function serveCtl(pi) {
@@ -432,6 +725,12 @@ function serveCtl(pi) {
432
725
  g.fresh = void 0;
433
726
  f.settle(r);
434
727
  };
728
+ const settleCommand = (r) => {
729
+ const c = g.command;
730
+ if (!c) return;
731
+ g.command = void 0;
732
+ c.settle(r);
733
+ };
435
734
  pi.on("agent_start", (_e, c) => {
436
735
  g.ctx = c;
437
736
  g.state = "busy";
@@ -470,10 +769,69 @@ function serveCtl(pi) {
470
769
  settleFresh({ v: V, ok: true, ...status() });
471
770
  }
472
771
  });
772
+ pi.on("session_before_compact", () => {
773
+ if (g.command?.name === "compact") settleCommand({ v: V, ok: true, ...status() });
774
+ });
775
+ async function runCommand(name, ctx) {
776
+ const c = g.command;
777
+ if (c?.name !== name) return;
778
+ const ok = () => settleCommand({ v: V, ok: true, ...status() });
779
+ const failWith = (e) => settleCommand(fail(e instanceof Error ? e.message : String(e)));
780
+ try {
781
+ if (name === "new") {
782
+ const r = await ctx.newSession();
783
+ if (r.cancelled) settleCommand(fail("new session was cancelled"));
784
+ else ok();
785
+ } else if (name === "reload") {
786
+ await ctx.reload();
787
+ if (c.reloaded) ok();
788
+ else settleCommand(fail("pi did not reload (busy or compacting)"));
789
+ } else {
790
+ ctx.compact({
791
+ ...c.instructions !== void 0 ? { customInstructions: c.instructions } : {},
792
+ onComplete: ok,
793
+ onError: failWith
794
+ });
795
+ }
796
+ } catch (e) {
797
+ failWith(e);
798
+ }
799
+ }
800
+ for (const name of CTL_COMMANDS) {
801
+ pi.registerCommand(commandName(name), {
802
+ description: `mu internal: /${name} requested over the control socket (mu agent send '/${name}')`,
803
+ handler: (_args, ctx) => runCommand(name, ctx)
804
+ });
805
+ }
806
+ function command(req) {
807
+ const name = CTL_COMMANDS.find((n) => n === req.name);
808
+ if (name === void 0) {
809
+ return fail(`command must be one of ${CTL_COMMANDS.join(", ")}`);
810
+ }
811
+ if (g.fresh || g.command) return fail("a fresh send or command is already in progress");
812
+ const busy = !(g.ctx?.isIdle() ?? g.state === "idle");
813
+ if (busy && req.force !== true) return fail("busy");
814
+ const instructions = str(req, "instructions");
815
+ return new Promise((resolve) => {
816
+ const timer = setTimeout(() => settleCommand(fail(`/${name} timed out`)), COMMAND_TIMEOUT_MS);
817
+ g.command = {
818
+ name,
819
+ ...instructions !== void 0 ? { instructions } : {},
820
+ reloaded: false,
821
+ settle: (r) => {
822
+ clearTimeout(timer);
823
+ resolve(r);
824
+ }
825
+ };
826
+ Promise.resolve(
827
+ g.pi.sendUserMessage(`/${commandName(name)}`, { expandPromptTemplates: true })
828
+ ).catch((e) => settleCommand(fail(e instanceof Error ? e.message : String(e))));
829
+ });
830
+ }
473
831
  function fresh(req) {
474
832
  const text = str(req, "text");
475
833
  if (text === void 0) return fail("fresh needs a string text");
476
- if (g.fresh) return fail("a fresh send is already in progress");
834
+ if (g.fresh || g.command) return fail("a fresh send or command is already in progress");
477
835
  const busy = !(g.ctx?.isIdle() ?? g.state === "idle");
478
836
  if (busy && req.force !== true) return fail("busy");
479
837
  return new Promise((resolve) => {
@@ -515,6 +873,8 @@ function serveCtl(pi) {
515
873
  }
516
874
  case "fresh":
517
875
  return fresh(req);
876
+ case "command":
877
+ return command(req);
518
878
  case "wait":
519
879
  return wait(num(req, "afterRuns"), num(req, "timeoutMs"), conn);
520
880
  case "abort":
@@ -587,7 +947,7 @@ function serveCtl(pi) {
587
947
  return;
588
948
  }
589
949
  rmSync(sockPath, { force: true });
590
- const tmp = join2(dirname(sockPath), `.${process.pid.toString(36)}${(++bindSeq).toString(36)}`);
950
+ const tmp = join3(dirname(sockPath), `.${process.pid.toString(36)}${(++bindSeq).toString(36)}`);
591
951
  rmSync(tmp, { force: true });
592
952
  const s = createServer(onConnection);
593
953
  await new Promise((resolve, reject) => {
@@ -612,8 +972,9 @@ function serveCtl(pi) {
612
972
  g.server = s;
613
973
  g.ino = inodeOf(sockPath);
614
974
  }
615
- pi.on("session_start", async (_e, c) => {
975
+ pi.on("session_start", async (e, c) => {
616
976
  g.ctx = c;
977
+ if (reasonOf(e) === "reload" && g.command?.name === "reload") g.command.reloaded = true;
617
978
  const s = g.server;
618
979
  if (s) {
619
980
  if (g.ino !== void 0 && inodeOf(sockPath) === g.ino) return;
@@ -636,18 +997,22 @@ function serveCtl(pi) {
636
997
  if (!s) return;
637
998
  g.waiters.clear();
638
999
  settleFresh(fail("pi is shutting down"));
1000
+ settleCommand(fail("pi is shutting down"));
639
1001
  for (const c of g.conns) c.destroy();
640
1002
  await new Promise((resolve) => s.close(() => resolve()));
641
1003
  if (ino !== void 0 && inodeOf(sockPath) === ino) rmSync(sockPath, { force: true });
642
1004
  });
643
1005
  }
644
1006
  export {
1007
+ COMMAND_TIMEOUT_MS,
645
1008
  FRESH_COMMAND,
646
1009
  FRESH_TIMEOUT_MS,
647
1010
  LAST_TEXT_MAX_BYTES,
648
1011
  LAST_TEXT_TRUNCATED,
649
1012
  LIVE_PROBE_MS,
1013
+ commandName,
650
1014
  muPi as default,
651
- lastAssistantText
1015
+ lastAssistantText,
1016
+ promptsDir
652
1017
  };
653
1018
  //# sourceMappingURL=mu-pi.js.map