@mu-crew/mu 3.2.1 → 3.4.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 (60) hide show
  1. package/AGENTS.md +10 -5
  2. package/README.md +83 -22
  3. package/dist/cli.js +294 -98
  4. package/dist/cli.js.map +1 -1
  5. package/dist/extension/mu-pi.js +346 -85
  6. package/dist/extension/mu-pi.js.map +1 -1
  7. package/dist/index.d.ts +68 -13
  8. package/dist/index.js +159 -71
  9. package/dist/index.js.map +1 -1
  10. package/docs/ARCHITECTURE.md +1 -0
  11. package/docs/VISION.md +3 -1
  12. package/docs/VOCABULARY.md +6 -1
  13. package/docs/architecture/control-socket.md +17 -3
  14. package/docs/architecture/dag.md +3 -3
  15. package/docs/architecture/mux.md +3 -4
  16. package/docs/guide/README.md +5 -0
  17. package/docs/guide/delegate.md +30 -3
  18. package/docs/guide/dispatch.md +11 -4
  19. package/docs/guide/getting-started.md +3 -0
  20. package/docs/guide/recovery.md +3 -1
  21. package/docs/guide/remote.md +1 -1
  22. package/docs/history/CHANGELOG-pre-3.md +1 -1
  23. package/docs/reference/env.md +2 -1
  24. package/docs/reference/naming.md +18 -0
  25. package/docs/specs/2026-09-29-task-substates.md +1 -1
  26. package/docs/specs/2026-10-03-keep-driving-nudge.md +142 -0
  27. package/package.json +2 -1
  28. package/prompts/mu-debug.md +8 -0
  29. package/prompts/mu-refute.md +8 -0
  30. package/prompts/mu-research.md +8 -0
  31. package/prompts/mu-review.md +8 -0
  32. package/prompts/mu-rules-audit.md +8 -0
  33. package/prompts/mu-sweep.md +8 -0
  34. package/prompts/mu-tournament.md +9 -0
  35. package/prompts/mu-until.md +8 -0
  36. package/prompts/ultrathink.md +10 -0
  37. package/skills/mu/SKILL.md +124 -145
  38. package/skills/mu/recipes/adversarial-review.md +91 -0
  39. package/skills/mu/recipes/backlog-triage.md +43 -0
  40. package/skills/mu/recipes/brief.md +53 -0
  41. package/skills/mu/recipes/codemode-driver.md +102 -0
  42. package/skills/mu/recipes/deep-research.md +67 -0
  43. package/skills/mu/recipes/fan-out.md +40 -0
  44. package/skills/mu/recipes/findings.md +103 -0
  45. package/skills/mu/recipes/hypothesis-panel.md +45 -0
  46. package/skills/mu/recipes/long-run.md +20 -0
  47. package/skills/mu/recipes/loop-until-done.md +42 -0
  48. package/skills/mu/recipes/orchestrator-loop.md +98 -0
  49. package/skills/mu/recipes/plan.md +59 -0
  50. package/skills/mu/recipes/recovery.md +28 -0
  51. package/skills/mu/recipes/refute.md +51 -0
  52. package/skills/mu/{REMOTE_WORKERS.md → recipes/remote-workers.md} +7 -2
  53. package/skills/mu/recipes/review-panel.md +62 -0
  54. package/skills/mu/recipes/rules-audit.md +83 -0
  55. package/skills/mu/recipes/tasks-or-calls.md +67 -0
  56. package/skills/mu/recipes/tournament.md +40 -0
  57. package/skills/mu/recipes/ultrathink.md +82 -0
  58. package/skills/mu/recipes/watcher.md +19 -0
  59. package/skills/mu/recipes/waves.md +20 -0
  60. package/skills/mu/recipes/worker.md +26 -0
@@ -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
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) {
@@ -51,6 +55,11 @@ function delegateStatus(n) {
51
55
  return n > 0 ? `${n} delegate${n === 1 ? "" : "s"} running` : void 0;
52
56
  }
53
57
  var DELEGATE_TIMEOUT_S = 3600;
58
+ var DELEGATE_MAX_DEFAULT = 16;
59
+ function delegateMax(env = process.env) {
60
+ const n = Number(env.MU_DELEGATE_MAX);
61
+ return Number.isInteger(n) && n > 0 ? n : DELEGATE_MAX_DEFAULT;
62
+ }
54
63
  var W = DELEGATE_WORKSTREAM;
55
64
  function delegateEnabled(env = process.env) {
56
65
  if (env.MU_MANAGED_AGENT) return false;
@@ -137,6 +146,7 @@ function labelStem(label) {
137
146
  function registerDelegate(pi, run = defaultRunner()) {
138
147
  if (!delegateEnabled()) return;
139
148
  const inflight = /* @__PURE__ */ new Map();
149
+ let starting = 0;
140
150
  const reserved = /* @__PURE__ */ new Set();
141
151
  let ui;
142
152
  const showStatus = () => {
@@ -249,7 +259,7 @@ function registerDelegate(pi, run = defaultRunner()) {
249
259
  pi.registerTool({
250
260
  name: DELEGATE_TOOL,
251
261
  label: "mu delegate",
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.`,
262
+ 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 (at most ${delegateMax()} in flight). 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
263
  promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later as a follow-up",
254
264
  parameters: {
255
265
  type: "object",
@@ -292,83 +302,106 @@ function registerDelegate(pi, run = defaultRunner()) {
292
302
  track(ctx);
293
303
  const task = typeof params.task === "string" ? params.task.trim() : "";
294
304
  if (!task) throw new Error("mu_delegate needs a non-empty task");
295
- const brief = typeof params.brief === "string" ? params.brief.trim() : "";
296
- const keep = params.keep === true;
297
- const t = params.timeout;
298
- if (t !== void 0 && !(typeof t === "number" && Number.isFinite(t) && t > 0))
305
+ const max = delegateMax();
306
+ if (inflight.size + starting >= max)
299
307
  throw new Error(
300
- `mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
308
+ `mu_delegate: ${inflight.size + starting} delegates already running (MU_DELEGATE_MAX=${max}). End your turn; issue the rest as answers arrive (each answer resumes you).`
301
309
  );
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 };
310
- inflight.set(name, entry);
311
- showStatus();
312
- const started = Date.now();
313
- const waiting = mu(
314
- ["agent", "wait", name, "-w", W, "--json", "--timeout", String(timeoutS)],
315
- entry.abort.signal
316
- );
317
- const sent = await mu(["agent", "send", name, brief ? `${brief}
318
-
319
- ${task}` : task, "-w", W]);
320
- if (sent.code !== 0) {
321
- entry.abort.abort();
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}`);
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
- }
310
+ starting++;
311
+ let slotHeld = true;
312
+ const freeSlot = () => {
313
+ if (slotHeld) starting--;
314
+ slotHeld = false;
350
315
  };
351
- void waiting.then(async (r) => {
352
- if (inflight.get(name) !== entry) return;
353
- if (entry.cancelling) entry.parked = () => settle(r);
354
- else await settle(r);
355
- });
356
- const lines = [
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,
359
- attach ? `Attach: ${attach}` : void 0,
360
- `Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${name}" }`
361
- ].filter((x) => x !== void 0);
362
- return {
363
- content: [{ type: "text", text: lines.join("\n") }],
364
- details: {
316
+ try {
317
+ const brief = typeof params.brief === "string" ? params.brief.trim() : "";
318
+ const keep = params.keep === true;
319
+ const t = params.timeout;
320
+ if (t !== void 0 && !(typeof t === "number" && Number.isFinite(t) && t > 0))
321
+ throw new Error(
322
+ `mu_delegate: timeout must be a positive number of seconds (got ${JSON.stringify(t)})`
323
+ );
324
+ const timeoutS = typeof t === "number" ? Math.ceil(t) : DELEGATE_TIMEOUT_S;
325
+ let cwd;
326
+ if (params.workspace !== true) {
327
+ cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
328
+ if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
329
+ }
330
+ const { name, attach, workspace } = await spawn(params, cwd, signal);
331
+ const entry = { abort: new AbortController(), cancelling: false };
332
+ inflight.set(name, entry);
333
+ freeSlot();
334
+ showStatus();
335
+ const started = Date.now();
336
+ const waiting = mu(
337
+ ["agent", "wait", name, "-w", W, "--json", "--timeout", String(timeoutS)],
338
+ entry.abort.signal
339
+ );
340
+ const sent = await mu([
341
+ "agent",
342
+ "send",
365
343
  name,
366
- workstream: W,
367
- attach: attach ?? null,
368
- workspace: workspace ?? null,
369
- keep
344
+ brief ? `${brief}
345
+
346
+ ${task}` : task,
347
+ "-w",
348
+ W
349
+ ]);
350
+ if (sent.code !== 0) {
351
+ entry.abort.abort();
352
+ forget(name);
353
+ const c = await mu(["agent", "close", name, "-w", W]);
354
+ const pane = c.code === 0 ? "Pane closed." : `Pane kept; close it with: mu agent close ${name} -w ${W}`;
355
+ throw new Error(`${failure(`mu agent send to ${W}/${name}`, sent).message}. ${pane}`);
370
356
  }
371
- };
357
+ const settle = async (r) => {
358
+ forget(name);
359
+ try {
360
+ await deliver(name, r, keep, {
361
+ timeoutS,
362
+ elapsedMs: Date.now() - started,
363
+ ...attach ? { attach } : {},
364
+ ...workspace ? { workspace } : {}
365
+ });
366
+ } catch (e) {
367
+ await Promise.resolve(
368
+ pi.sendMessage(
369
+ {
370
+ customType: DELEGATE_MESSAGE_TYPE,
371
+ 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}`,
372
+ display: true,
373
+ details: { name, workstream: W, outcome: null, closed: false }
374
+ },
375
+ { deliverAs: "followUp", triggerTurn: true }
376
+ )
377
+ ).catch(() => {
378
+ });
379
+ }
380
+ };
381
+ void waiting.then(async (r) => {
382
+ if (inflight.get(name) !== entry) return;
383
+ if (entry.cancelling) entry.parked = () => settle(r);
384
+ else await settle(r);
385
+ });
386
+ const lines = [
387
+ `Delegated to ${W}/${name}. Its answer arrives as a follow-up message when it finishes; do not wait or poll.`,
388
+ workspace ? `Workspace: ${workspace}` : void 0,
389
+ attach ? `Attach: ${attach}` : void 0,
390
+ `Cancel: ${DELEGATE_CANCEL_TOOL} { name: "${name}" }`
391
+ ].filter((x) => x !== void 0);
392
+ return {
393
+ content: [{ type: "text", text: lines.join("\n") }],
394
+ details: {
395
+ name,
396
+ workstream: W,
397
+ attach: attach ?? null,
398
+ workspace: workspace ?? null,
399
+ keep
400
+ }
401
+ };
402
+ } finally {
403
+ freeSlot();
404
+ }
372
405
  }
373
406
  });
374
407
  pi.registerTool({
@@ -427,10 +460,143 @@ ${task}` : task, "-w", W]);
427
460
  });
428
461
  }
429
462
 
463
+ // extension/nudge.ts
464
+ import { existsSync as existsSync2, readFileSync } from "fs";
465
+ import { join as join2 } from "path";
466
+ var NUDGE_MESSAGE_TYPE = "mu-keep-driving";
467
+ var NUDGE_LOG_KIND = "nudge";
468
+ var MARK_START = "<!-- mu:keep-driving -->";
469
+ var MARK_END = "<!-- /mu:keep-driving -->";
470
+ var WAIT_HINT_MAX = 8;
471
+ function nudgeEnabled(env = process.env) {
472
+ return env.MU_NUDGE !== "0";
473
+ }
474
+ function keepDrivingRule(skillMd) {
475
+ const i = skillMd.indexOf(MARK_START);
476
+ const j = skillMd.indexOf(MARK_END);
477
+ if (i < 0 || j < i) return void 0;
478
+ const text = skillMd.slice(i + MARK_START.length, j).trim();
479
+ return text === "" ? void 0 : text;
480
+ }
481
+ function readSkill() {
482
+ for (const up of ["..", "../.."]) {
483
+ const p = join2(import.meta.dirname ?? "", up, "skills", "mu", "SKILL.md");
484
+ if (existsSync2(p)) return readFileSync(p, "utf8");
485
+ }
486
+ return void 0;
487
+ }
488
+ function dispatchedWorkstreams(command) {
489
+ const found = /* @__PURE__ */ new Set();
490
+ for (const seg of command.split(/&&|\|\||;|\n|\|/)) {
491
+ const words = seg.trim().split(/\s+/);
492
+ const at = words.findIndex((w) => w === "mu" || w.endsWith("/mu"));
493
+ if (at < 0) continue;
494
+ const [noun, verb] = [words[at + 1], words[at + 2]];
495
+ const isSendSpawn = noun === "agent" && (verb === "send" || verb === "spawn");
496
+ const isClaimFor = noun === "task" && verb === "claim" && words.includes("--for");
497
+ if (!isSendSpawn && !isClaimFor) continue;
498
+ let ws = "";
499
+ for (let k = at + 3; k < words.length; k++) {
500
+ const w = words[k] ?? "";
501
+ if (w === "-w" || w === "--workstream") ws = unquote(words[k + 1] ?? "");
502
+ else if (w.startsWith("--workstream=")) ws = unquote(w.slice("--workstream=".length));
503
+ }
504
+ if (ws === "" && isClaimFor) {
505
+ const ref = words[at + 3] ?? "";
506
+ if (ref.includes("/")) ws = unquote(ref.split("/")[0] ?? "");
507
+ }
508
+ if (ws !== "scratch") found.add(ws);
509
+ }
510
+ return found.size > 0 ? [...found] : void 0;
511
+ }
512
+ function unquote(s) {
513
+ return s.replace(/^['"]|['"]$/g, "");
514
+ }
515
+ async function inProgress(run, workstreams) {
516
+ const named = workstreams.filter((w) => w !== "");
517
+ const args = ["state", "--json", "--events", "0"];
518
+ for (const w of named) args.push("-w", w);
519
+ const r = await run(args);
520
+ if (r.code !== 0) return void 0;
521
+ let json;
522
+ try {
523
+ json = JSON.parse(r.stdout);
524
+ } catch {
525
+ return void 0;
526
+ }
527
+ const cards = json.workstreams ?? [json];
528
+ const refs = [];
529
+ for (const ws of cards) {
530
+ for (const t of ws.inProgress ?? []) {
531
+ if (t.name) refs.push(`${ws.workstreamName ?? ""}/${t.name}`);
532
+ }
533
+ }
534
+ return refs;
535
+ }
536
+ function nudgeText(rule, refs) {
537
+ const shown = refs.slice(0, WAIT_HINT_MAX).join(" ");
538
+ const more = refs.length > WAIT_HINT_MAX ? ` (+${refs.length - WAIT_HINT_MAX} more)` : "";
539
+ return [
540
+ `[mu] ${rule}`,
541
+ "",
542
+ `${refs.length} task(s) IN_PROGRESS. Next: mu task wait ${shown} --first --on-stall exit --json${more}`
543
+ ].join("\n");
544
+ }
545
+ function outcomeOf(event) {
546
+ return typeof event === "object" && event !== null ? event.outcome : void 0;
547
+ }
548
+ function bashCommand(event) {
549
+ if (typeof event !== "object" || event === null) return void 0;
550
+ const e = event;
551
+ return e.toolName === "bash" && typeof e.input?.command === "string" ? e.input.command : void 0;
552
+ }
553
+ function registerNudge(pi, run = defaultRunner(), skillMd = readSkill()) {
554
+ if (!nudgeEnabled()) return;
555
+ const rule = skillMd === void 0 ? void 0 : keepDrivingRule(skillMd);
556
+ if (rule === void 0) return;
557
+ const armed = /* @__PURE__ */ new Set();
558
+ let fired = false;
559
+ pi.on("input", () => {
560
+ armed.clear();
561
+ fired = false;
562
+ });
563
+ pi.on("tool_call", (event) => {
564
+ const cmd = bashCommand(event);
565
+ if (cmd === void 0) return;
566
+ for (const ws of dispatchedWorkstreams(cmd) ?? []) armed.add(ws);
567
+ });
568
+ pi.on("agent_before_settle", async (event) => {
569
+ if (fired || armed.size === 0) return;
570
+ if (outcomeOf(event) !== "completed") return;
571
+ fired = true;
572
+ const refs = await inProgress(run, [...armed]);
573
+ if (refs === void 0 || refs.length === 0) return;
574
+ const text = nudgeText(rule, refs);
575
+ const logWs = [...armed].find((w) => w !== "");
576
+ await run([
577
+ "log",
578
+ ...logWs ? ["-w", logWs] : [],
579
+ "--kind",
580
+ NUDGE_LOG_KIND,
581
+ `keep-driving: ${refs.length} in progress`
582
+ ]);
583
+ return {
584
+ entries: [
585
+ { type: "custom_message", customType: NUDGE_MESSAGE_TYPE, content: text, display: true }
586
+ ],
587
+ continue: true
588
+ };
589
+ });
590
+ }
591
+
430
592
  // extension/mu-pi.ts
431
- var EXT_VERSION = true ? "3.2.1" : void 0;
593
+ var EXT_VERSION = true ? "3.4.0" : void 0;
432
594
  var FRESH_COMMAND = "mu-fresh";
433
595
  var FRESH_TIMEOUT_MS = 3e4;
596
+ function commandName(name) {
597
+ return `mu-${name}`;
598
+ }
599
+ var COMMAND_TIMEOUT_MS = 3e4;
434
600
  var LAST_TEXT_MAX_BYTES = 64 * 1024;
435
601
  var LAST_TEXT_TRUNCATED = "[truncated, see pane]";
436
602
  var LIVE_PROBE_MS = 500;
@@ -444,9 +610,9 @@ function detectPiVersion() {
444
610
  if (!entry) return void 0;
445
611
  let dir = dirname(realpathSync(entry));
446
612
  for (let i = 0; i < 6; i++) {
447
- const pkgPath = join2(dir, "package.json");
448
- if (existsSync2(pkgPath)) {
449
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
613
+ const pkgPath = join3(dir, "package.json");
614
+ if (existsSync3(pkgPath)) {
615
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
450
616
  if (typeof pkg === "object" && pkg !== null) {
451
617
  const { name, version } = pkg;
452
618
  if (typeof name === "string" && /pi/.test(name) && typeof version === "string") {
@@ -529,8 +695,31 @@ function reasonOf(event) {
529
695
  const r = typeof event === "object" && event !== null ? event.reason : void 0;
530
696
  return typeof r === "string" ? r : void 0;
531
697
  }
698
+ function promptsDir(from = import.meta.url) {
699
+ let here;
700
+ try {
701
+ here = dirname(fileURLToPath2(from));
702
+ } catch {
703
+ return void 0;
704
+ }
705
+ for (const up of ["..", "../.."]) {
706
+ const d = join3(here, up, "prompts");
707
+ if (existsSync3(join3(d, "ultrathink.md"))) return d;
708
+ }
709
+ return void 0;
710
+ }
711
+ function registerPrompts(pi) {
712
+ pi.on("resources_discover", () => {
713
+ const d = promptsDir();
714
+ if (!d) return {};
715
+ const paths = readdirSync(d).filter((f) => f.endsWith(".md")).sort().map((f) => join3(d, f));
716
+ return { promptPaths: paths };
717
+ });
718
+ }
532
719
  function muPi(pi) {
533
720
  registerDelegate(pi);
721
+ registerPrompts(pi);
722
+ registerNudge(pi);
534
723
  serveCtl(pi);
535
724
  }
536
725
  function serveCtl(pi) {
@@ -565,6 +754,12 @@ function serveCtl(pi) {
565
754
  g.fresh = void 0;
566
755
  f.settle(r);
567
756
  };
757
+ const settleCommand = (r) => {
758
+ const c = g.command;
759
+ if (!c) return;
760
+ g.command = void 0;
761
+ c.settle(r);
762
+ };
568
763
  pi.on("agent_start", (_e, c) => {
569
764
  g.ctx = c;
570
765
  g.state = "busy";
@@ -603,10 +798,69 @@ function serveCtl(pi) {
603
798
  settleFresh({ v: V, ok: true, ...status() });
604
799
  }
605
800
  });
801
+ pi.on("session_before_compact", () => {
802
+ if (g.command?.name === "compact") settleCommand({ v: V, ok: true, ...status() });
803
+ });
804
+ async function runCommand(name, ctx) {
805
+ const c = g.command;
806
+ if (c?.name !== name) return;
807
+ const ok = () => settleCommand({ v: V, ok: true, ...status() });
808
+ const failWith = (e) => settleCommand(fail(e instanceof Error ? e.message : String(e)));
809
+ try {
810
+ if (name === "new") {
811
+ const r = await ctx.newSession();
812
+ if (r.cancelled) settleCommand(fail("new session was cancelled"));
813
+ else ok();
814
+ } else if (name === "reload") {
815
+ await ctx.reload();
816
+ if (c.reloaded) ok();
817
+ else settleCommand(fail("pi did not reload (busy or compacting)"));
818
+ } else {
819
+ ctx.compact({
820
+ ...c.instructions !== void 0 ? { customInstructions: c.instructions } : {},
821
+ onComplete: ok,
822
+ onError: failWith
823
+ });
824
+ }
825
+ } catch (e) {
826
+ failWith(e);
827
+ }
828
+ }
829
+ for (const name of CTL_COMMANDS) {
830
+ pi.registerCommand(commandName(name), {
831
+ description: `mu internal: /${name} requested over the control socket (mu agent send '/${name}')`,
832
+ handler: (_args, ctx) => runCommand(name, ctx)
833
+ });
834
+ }
835
+ function command(req) {
836
+ const name = CTL_COMMANDS.find((n) => n === req.name);
837
+ if (name === void 0) {
838
+ return fail(`command must be one of ${CTL_COMMANDS.join(", ")}`);
839
+ }
840
+ if (g.fresh || g.command) return fail("a fresh send or command is already in progress");
841
+ const busy = !(g.ctx?.isIdle() ?? g.state === "idle");
842
+ if (busy && req.force !== true) return fail("busy");
843
+ const instructions = str(req, "instructions");
844
+ return new Promise((resolve) => {
845
+ const timer = setTimeout(() => settleCommand(fail(`/${name} timed out`)), COMMAND_TIMEOUT_MS);
846
+ g.command = {
847
+ name,
848
+ ...instructions !== void 0 ? { instructions } : {},
849
+ reloaded: false,
850
+ settle: (r) => {
851
+ clearTimeout(timer);
852
+ resolve(r);
853
+ }
854
+ };
855
+ Promise.resolve(
856
+ g.pi.sendUserMessage(`/${commandName(name)}`, { expandPromptTemplates: true })
857
+ ).catch((e) => settleCommand(fail(e instanceof Error ? e.message : String(e))));
858
+ });
859
+ }
606
860
  function fresh(req) {
607
861
  const text = str(req, "text");
608
862
  if (text === void 0) return fail("fresh needs a string text");
609
- if (g.fresh) return fail("a fresh send is already in progress");
863
+ if (g.fresh || g.command) return fail("a fresh send or command is already in progress");
610
864
  const busy = !(g.ctx?.isIdle() ?? g.state === "idle");
611
865
  if (busy && req.force !== true) return fail("busy");
612
866
  return new Promise((resolve) => {
@@ -648,6 +902,8 @@ function serveCtl(pi) {
648
902
  }
649
903
  case "fresh":
650
904
  return fresh(req);
905
+ case "command":
906
+ return command(req);
651
907
  case "wait":
652
908
  return wait(num(req, "afterRuns"), num(req, "timeoutMs"), conn);
653
909
  case "abort":
@@ -720,7 +976,7 @@ function serveCtl(pi) {
720
976
  return;
721
977
  }
722
978
  rmSync(sockPath, { force: true });
723
- const tmp = join2(dirname(sockPath), `.${process.pid.toString(36)}${(++bindSeq).toString(36)}`);
979
+ const tmp = join3(dirname(sockPath), `.${process.pid.toString(36)}${(++bindSeq).toString(36)}`);
724
980
  rmSync(tmp, { force: true });
725
981
  const s = createServer(onConnection);
726
982
  await new Promise((resolve, reject) => {
@@ -745,8 +1001,9 @@ function serveCtl(pi) {
745
1001
  g.server = s;
746
1002
  g.ino = inodeOf(sockPath);
747
1003
  }
748
- pi.on("session_start", async (_e, c) => {
1004
+ pi.on("session_start", async (e, c) => {
749
1005
  g.ctx = c;
1006
+ if (reasonOf(e) === "reload" && g.command?.name === "reload") g.command.reloaded = true;
750
1007
  const s = g.server;
751
1008
  if (s) {
752
1009
  if (g.ino !== void 0 && inodeOf(sockPath) === g.ino) return;
@@ -769,18 +1026,22 @@ function serveCtl(pi) {
769
1026
  if (!s) return;
770
1027
  g.waiters.clear();
771
1028
  settleFresh(fail("pi is shutting down"));
1029
+ settleCommand(fail("pi is shutting down"));
772
1030
  for (const c of g.conns) c.destroy();
773
1031
  await new Promise((resolve) => s.close(() => resolve()));
774
1032
  if (ino !== void 0 && inodeOf(sockPath) === ino) rmSync(sockPath, { force: true });
775
1033
  });
776
1034
  }
777
1035
  export {
1036
+ COMMAND_TIMEOUT_MS,
778
1037
  FRESH_COMMAND,
779
1038
  FRESH_TIMEOUT_MS,
780
1039
  LAST_TEXT_MAX_BYTES,
781
1040
  LAST_TEXT_TRUNCATED,
782
1041
  LIVE_PROBE_MS,
1042
+ commandName,
783
1043
  muPi as default,
784
- lastAssistantText
1044
+ lastAssistantText,
1045
+ promptsDir
785
1046
  };
786
1047
  //# sourceMappingURL=mu-pi.js.map