@mu-crew/mu 3.6.2 → 3.8.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 (38) hide show
  1. package/dist/cli.js +12652 -12264
  2. package/dist/cli.js.map +1 -1
  3. package/dist/extension/mu-pi.js +302 -48
  4. package/dist/extension/mu-pi.js.map +1 -1
  5. package/dist/index.d.ts +59 -13
  6. package/dist/index.js +1376 -1205
  7. package/dist/index.js.map +1 -1
  8. package/docs/ARCHITECTURE.md +4 -3
  9. package/docs/architecture/control-socket.md +10 -5
  10. package/docs/architecture/mux.md +4 -1
  11. package/docs/guide/delegate.md +25 -3
  12. package/docs/guide/dispatch.md +22 -5
  13. package/docs/guide/getting-started.md +2 -2
  14. package/docs/guide/recovery.md +11 -6
  15. package/docs/guide/upgrade.md +4 -4
  16. package/docs/reference/env.md +2 -2
  17. package/docs/specs/2026-10-03-keep-driving-nudge.md +21 -0
  18. package/package.json +1 -1
  19. package/skills/mu/SKILL.md +40 -16
  20. package/skills/mu/recipes/adversarial-review.md +14 -6
  21. package/skills/mu/recipes/brief.md +29 -1
  22. package/skills/mu/recipes/codemode-driver.md +19 -9
  23. package/skills/mu/recipes/deep-research.md +6 -5
  24. package/skills/mu/recipes/drift-audit.md +72 -0
  25. package/skills/mu/recipes/findings.md +24 -15
  26. package/skills/mu/recipes/hypothesis-panel.md +5 -3
  27. package/skills/mu/recipes/long-run.md +13 -3
  28. package/skills/mu/recipes/loop-until-done.md +4 -3
  29. package/skills/mu/recipes/orchestrator-loop.md +29 -16
  30. package/skills/mu/recipes/plan.md +10 -6
  31. package/skills/mu/recipes/refute.md +3 -2
  32. package/skills/mu/recipes/remote-workers.md +23 -6
  33. package/skills/mu/recipes/review-panel.md +2 -2
  34. package/skills/mu/recipes/rules-audit.md +10 -5
  35. package/skills/mu/recipes/tasks-or-calls.md +40 -23
  36. package/skills/mu/recipes/tournament.md +4 -3
  37. package/skills/mu/recipes/ultrathink.md +2 -2
  38. package/skills/mu/recipes/worker.md +3 -2
@@ -23,6 +23,7 @@ var CTL_OPS = [
23
23
  "send",
24
24
  "wait",
25
25
  "abort",
26
+ "interrupt",
26
27
  "fresh",
27
28
  "command"
28
29
  ];
@@ -152,6 +153,23 @@ ${agent.lastText ?? ""}`;
152
153
  return `${who}: mu agent wait gave no result. ${fate}${ws}${tailBlock}`;
153
154
  }
154
155
  }
156
+ var RECORD_NOTE_MAX = 4e3;
157
+ var RECORD_FALLBACK_TAIL = 1500;
158
+ var VERDICT_LINE = /^[\s>*_`-]*VERDICT:/;
159
+ var EVIDENCE_LINE = /^[\s>*_`-]*EVIDENCE:/;
160
+ function recordNote(label, name, outcome, lastText, elapsedMs) {
161
+ if (outcome !== "done" || !lastText?.trim())
162
+ return `REFUTER ${label}: no verdict (${outcome === "done" ? "empty" : outcome ?? "no result"})`;
163
+ const lines = lastText.trimEnd().split("\n");
164
+ let at = -1;
165
+ for (let i = lines.length - 1; i >= 0 && at < 0; i--)
166
+ if (VERDICT_LINE.test(lines[i] ?? "")) at = i;
167
+ const body = at >= 0 ? [lines[at] ?? "", ...lines.slice(at + 1).filter((l) => EVIDENCE_LINE.test(l))].join("\n") : `NO VERDICT LINE: ${lastText.trimEnd().slice(-RECORD_FALLBACK_TAIL)}`;
168
+ const note = `REFUTER ${label} (${name}, ${formatElapsed(elapsedMs)}):
169
+ ${body}`;
170
+ const mark = "\n[truncated]";
171
+ return note.length <= RECORD_NOTE_MAX ? note : note.slice(0, RECORD_NOTE_MAX - mark.length) + mark;
172
+ }
155
173
  function labelStem(label) {
156
174
  if (typeof label !== "string") return void 0;
157
175
  const stem = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20).replace(/-+$/, "");
@@ -200,7 +218,7 @@ function registerDelegate(pi, run = defaultRunner()) {
200
218
  const q = queue.shift();
201
219
  if (!q) break;
202
220
  starting++;
203
- void start(q.params, void 0, q.ctx, q.handle).then(
221
+ void start(q.params, void 0, q.ctx, q.handle, q.record).then(
204
222
  () => {
205
223
  },
206
224
  (e) => {
@@ -288,7 +306,35 @@ function registerDelegate(pi, run = defaultRunner()) {
288
306
  };
289
307
  }
290
308
  }
291
- async function deliver(name, wait, keep, extras) {
309
+ const recordLabel = (params, name) => typeof params.label === "string" && params.label.trim() ? params.label.trim() : name;
310
+ async function resolveRecord(raw) {
311
+ if (raw === void 0) return void 0;
312
+ const r = raw;
313
+ const task = typeof r?.task === "string" ? r.task.trim() : "";
314
+ if (!task) throw new Error("mu_delegate: record.task must name a task");
315
+ const ws = typeof r?.workstream === "string" ? r.workstream.trim() : "";
316
+ const shown = await mu(["task", "show", task, ...ws ? ["-w", ws] : [], "--json"]);
317
+ const out = json(shown);
318
+ const t = out?.task;
319
+ if (shown.code !== 0 || typeof t?.name !== "string" || typeof t.workstreamName !== "string") {
320
+ const why = typeof out?.message === "string" ? out.message : (shown.stderr || shown.stdout).trim();
321
+ throw new Error(
322
+ `mu_delegate: record task ${ws ? `${ws}/` : ""}${task} not found (exit ${shown.code}): ${why}. Nothing spawned.`
323
+ );
324
+ }
325
+ return { workstream: t.workstreamName, task: t.name };
326
+ }
327
+ async function writeRecord(rec, name, text) {
328
+ const where = `${rec.workstream}/${rec.task}`;
329
+ try {
330
+ const r = await mu(["task", "note", rec.task, "-w", rec.workstream, "--author", name, text]);
331
+ if (r.code === 0) return `Recorded on ${where} as a note.`;
332
+ return `Recording on ${where} failed (exit ${r.code}): ${(r.stderr || r.stdout).trim()}. Record it yourself.`;
333
+ } catch (e) {
334
+ return `Recording on ${where} failed: ${e instanceof Error ? e.message : String(e)}. Record it yourself.`;
335
+ }
336
+ }
337
+ async function deliver(name, wait, keep, extras, record) {
292
338
  const agent = json(wait)?.agents?.[0];
293
339
  const outcome = agent?.outcome;
294
340
  const tail = outcome === "done" || outcome === "timeout" || outcome === "pending" || outcome === "error" ? void 0 : (await mu(["agent", "read", name, "-n", "50", "-w", W])).stdout;
@@ -303,10 +349,18 @@ function registerDelegate(pi, run = defaultRunner()) {
303
349
  failed.add(name);
304
350
  showStatus();
305
351
  }
352
+ const recorded = record ? await writeRecord(
353
+ record.target,
354
+ name,
355
+ recordNote(record.label, name, outcome, agent?.lastText, extras.elapsedMs ?? 0)
356
+ ) : void 0;
357
+ const message = delegateMessage(name, agent, pane, { ...extras, ...tail ? { tail } : {} });
306
358
  await pi.sendMessage(
307
359
  {
308
360
  customType: DELEGATE_MESSAGE_TYPE,
309
- content: delegateMessage(name, agent, pane, { ...extras, ...tail ? { tail } : {} }),
361
+ content: recorded ? `${message}
362
+
363
+ ${recorded}` : message,
310
364
  display: true,
311
365
  details: {
312
366
  name,
@@ -321,7 +375,7 @@ function registerDelegate(pi, run = defaultRunner()) {
321
375
  { deliverAs: "followUp", triggerTurn: true }
322
376
  );
323
377
  }
324
- async function start(params, signal, ctx, queuedAs) {
378
+ async function start(params, signal, ctx, queuedAs, record) {
325
379
  let slotHeld = true;
326
380
  const freeSlot = () => {
327
381
  if (slotHeld) {
@@ -347,35 +401,68 @@ function registerDelegate(pi, run = defaultRunner()) {
347
401
  if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
348
402
  }
349
403
  const { name, attach, workspace } = await spawn(params, cwd, signal);
350
- const entry = { abort: new AbortController(), cancelling: false };
404
+ const entry = {
405
+ abort: new AbortController(),
406
+ cancelling: false,
407
+ ...record ? { record: { target: record, label: recordLabel(params, name) } } : {}
408
+ };
351
409
  inflight.set(name, entry);
352
410
  freeSlot();
353
411
  showStatus();
354
412
  const started = Date.now();
355
- const waiting = mu(
356
- ["agent", "wait", name, "-w", W, "--json", "--timeout", String(timeoutS)],
357
- entry.abort.signal
358
- );
359
- const sent = await mu(["agent", "send", name, brief ? `${brief}
413
+ const waitArgs = (after) => [
414
+ "agent",
415
+ "wait",
416
+ name,
417
+ "-w",
418
+ W,
419
+ "--json",
420
+ "--timeout",
421
+ String(timeoutS),
422
+ ...after !== void 0 ? ["--after-runs", String(after)] : []
423
+ ];
424
+ const plain = new AbortController();
425
+ entry.abort.signal.addEventListener("abort", () => plain.abort(), { once: true });
426
+ let waiting = mu(waitArgs(), plain.signal);
427
+ const sent = await mu([
428
+ "agent",
429
+ "send",
430
+ name,
431
+ brief ? `${brief}
360
432
 
361
- ${task}` : task, "-w", W]);
433
+ ${task}` : task,
434
+ "-w",
435
+ W,
436
+ "--json"
437
+ ]);
362
438
  if (sent.code !== 0) {
363
- entry.abort.abort();
439
+ plain.abort();
364
440
  forget(name);
365
441
  const c = await mu(["agent", "close", name, "-w", W]);
366
442
  const pane = c.code === 0 ? "Pane closed." : `Pane kept; close it with: mu agent close ${name} -w ${W}`;
367
443
  throw new Error(`${failure(`mu agent send to ${W}/${name}`, sent).message}. ${pane}`);
368
444
  }
445
+ const runs = json(sent)?.runs;
446
+ if (typeof runs === "number") {
447
+ plain.abort();
448
+ waiting = mu(waitArgs(runs), entry.abort.signal);
449
+ }
369
450
  const settle = async (r) => {
370
451
  forget(name);
371
452
  try {
372
- await deliver(name, r, keep, {
373
- timeoutS,
374
- elapsedMs: Date.now() - started,
375
- ...queuedAs ? { queuedAs } : {},
376
- ...attach ? { attach } : {},
377
- ...workspace ? { workspace } : {}
378
- });
453
+ await deliver(
454
+ name,
455
+ r,
456
+ keep,
457
+ {
458
+ timeoutS,
459
+ elapsedMs: Date.now() - started,
460
+ ...queuedAs ? { queuedAs } : {},
461
+ ...attach ? { attach } : {},
462
+ ...workspace ? { workspace } : {}
463
+ },
464
+ entry.record
465
+ );
379
466
  } catch (e) {
380
467
  await Promise.resolve(
381
468
  pi.sendMessage(
@@ -420,7 +507,7 @@ ${task}` : task, "-w", W]);
420
507
  pi.registerTool({
421
508
  name: DELEGATE_TOOL,
422
509
  label: "mu delegate",
423
- 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()} run at once; up to ${DELEGATE_QUEUE_FACTOR * delegateMax()} more are queued and start as slots free). 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.`,
510
+ 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, or a refuter of a claim, plan or fix before you act on it; call it several times to fan out in parallel (at most ${delegateMax()} run at once; up to ${DELEGATE_QUEUE_FACTOR * delegateMax()} more are queued and start as slots free). 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. When the check judges a mu task, set record so its verdict lands on that task; leave it out when there is no task.`,
424
511
  promptSnippet: "Subagent: delegate a self-contained task to a background pi agent; its answer arrives later as a follow-up",
425
512
  parameters: {
426
513
  type: "object",
@@ -454,6 +541,16 @@ ${task}` : task, "-w", W]);
454
541
  keep: {
455
542
  type: "boolean",
456
543
  description: "Keep the pane after it finishes, to talk to it again"
544
+ },
545
+ record: {
546
+ type: "object",
547
+ description: "When the answer arrives, write its VERDICT/EVIDENCE onto this task as a note (for a check that judges a mu task; omit with no task); task may be <ws>/<task>.",
548
+ properties: {
549
+ task: { type: "string" },
550
+ workstream: { type: "string" }
551
+ },
552
+ required: ["task"],
553
+ additionalProperties: false
457
554
  }
458
555
  },
459
556
  required: ["task"],
@@ -472,10 +569,11 @@ ${task}` : task, "-w", W]);
472
569
  const cwd = typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd();
473
570
  if (!isDir(cwd)) throw new Error(`mu_delegate: cwd ${cwd} is not a directory`);
474
571
  }
572
+ const record = await resolveRecord(params.record);
475
573
  const max = delegateMax();
476
574
  if (inflight.size + starting < max) {
477
575
  starting++;
478
- return start(params, signal, ctx);
576
+ return start(params, signal, ctx, void 0, record);
479
577
  }
480
578
  if (queue.length >= DELEGATE_QUEUE_FACTOR * max)
481
579
  throw new Error(
@@ -488,7 +586,8 @@ ${task}` : task, "-w", W]);
488
586
  ...params,
489
587
  cwd: typeof params.cwd === "string" && params.cwd ? params.cwd : ctx?.cwd ?? process.cwd()
490
588
  },
491
- ctx
589
+ ctx,
590
+ ...record ? { record } : {}
492
591
  });
493
592
  showStatus();
494
593
  return {
@@ -521,10 +620,13 @@ ${task}` : task, "-w", W]);
521
620
  if (!name) throw new Error("mu_delegate_cancel needs a name");
522
621
  const qi = queue.findIndex((q) => q.handle === name);
523
622
  if (qi >= 0) {
524
- queue.splice(qi, 1);
623
+ const [q] = queue.splice(qi, 1);
525
624
  showStatus();
625
+ const recorded = q?.record ? ` ${await writeRecord(q.record, name, recordNote(recordLabel(q.params, name), name, "cancelled", void 0, 0))}` : "";
526
626
  return {
527
- content: [{ type: "text", text: `Dropped ${name} from the queue; it never started.` }],
627
+ content: [
628
+ { type: "text", text: `Dropped ${name} from the queue; it never started.${recorded}` }
629
+ ],
528
630
  details: { name, workstream: W }
529
631
  };
530
632
  }
@@ -546,6 +648,8 @@ ${task}` : task, "-w", W]);
546
648
  }
547
649
  failed.delete(name);
548
650
  let text = `Aborted ${W}/${name}.`;
651
+ if (entry?.record)
652
+ text += ` ${await writeRecord(entry.record.target, name, recordNote(entry.record.label, name, "cancelled", void 0, 0))}`;
549
653
  if (params.keep === true) text += " Pane kept.";
550
654
  else {
551
655
  const c = await mu(["agent", "close", name, "-w", W]);
@@ -582,9 +686,14 @@ import { join as join2 } from "path";
582
686
  var NUDGE_MESSAGE_TYPE = "mu-keep-driving";
583
687
  var NUDGE_LOG_KIND = "nudge";
584
688
  var CLOSE_NUDGE_MESSAGE_TYPE = "mu-close-task";
689
+ var REFUTE_NUDGE_MESSAGE_TYPE = "mu-refute-brief";
585
690
  var MARK_START = "<!-- mu:keep-driving -->";
586
691
  var MARK_END = "<!-- /mu:keep-driving -->";
587
692
  var WAIT_HINT_MAX = 8;
693
+ var notNested = () => false;
694
+ function noUI(ctx) {
695
+ return ctx?.hasUI === false;
696
+ }
588
697
  function nudgeEnabled(env = process.env) {
589
698
  return env.MU_NUDGE !== "0";
590
699
  }
@@ -604,8 +713,7 @@ function readSkill() {
604
713
  }
605
714
  function dispatchedWorkstreams(command) {
606
715
  const found = /* @__PURE__ */ new Set();
607
- for (const seg of command.split(/&&|\|\||;|\n|\|/)) {
608
- const words = seg.trim().split(/\s+/);
716
+ for (const words of shellSegments(command)) {
609
717
  const at = words.findIndex((w) => w === "mu" || w.endsWith("/mu"));
610
718
  if (at < 0) continue;
611
719
  const [noun, verb] = [words[at + 1], words[at + 2]];
@@ -615,19 +723,78 @@ function dispatchedWorkstreams(command) {
615
723
  let ws = "";
616
724
  for (let k = at + 3; k < words.length; k++) {
617
725
  const w = words[k] ?? "";
618
- if (w === "-w" || w === "--workstream") ws = unquote(words[k + 1] ?? "");
619
- else if (w.startsWith("--workstream=")) ws = unquote(w.slice("--workstream=".length));
726
+ if (w === "-w" || w === "--workstream") ws = words[k + 1] ?? "";
727
+ else if (w.startsWith("--workstream=")) ws = w.slice("--workstream=".length);
620
728
  }
621
729
  if (ws === "" && isClaimFor) {
622
730
  const ref = words[at + 3] ?? "";
623
- if (ref.includes("/")) ws = unquote(ref.split("/")[0] ?? "");
731
+ if (ref.includes("/")) ws = ref.split("/")[0] ?? "";
624
732
  }
625
733
  if (ws !== "scratch") found.add(ws);
626
734
  }
627
735
  return found.size > 0 ? [...found] : void 0;
628
736
  }
629
- function unquote(s) {
630
- return s.replace(/^['"]|['"]$/g, "");
737
+ var CLAIM_VALUE_OPTS = /* @__PURE__ */ new Set(["--for", "-f", "-w", "--workstream", "--evidence", "--actor"]);
738
+ function shellSegments(command) {
739
+ const segs = [];
740
+ let words = [];
741
+ let word;
742
+ const endWord = () => {
743
+ if (word !== void 0) words.push(word);
744
+ word = void 0;
745
+ };
746
+ const endSeg = () => {
747
+ endWord();
748
+ if (words.length > 0) segs.push(words);
749
+ words = [];
750
+ };
751
+ for (let i = 0; i < command.length; i++) {
752
+ const c = command[i] ?? "";
753
+ if (c === "'" || c === '"') {
754
+ const close = command.indexOf(c, i + 1);
755
+ const end = close < 0 ? command.length : close;
756
+ word = (word ?? "") + command.slice(i + 1, end);
757
+ i = end;
758
+ } else if (c === "\\" && i + 1 < command.length) {
759
+ word = (word ?? "") + command[++i];
760
+ } else if (c === ";" || c === "|" || c === "\n" || c === "&" && command[i + 1] === "&") {
761
+ if (c !== ";" && c !== "\n" && command[i + 1] === c) i++;
762
+ endSeg();
763
+ } else if (/\s/.test(c)) {
764
+ endWord();
765
+ } else {
766
+ word = (word ?? "") + c;
767
+ }
768
+ }
769
+ endSeg();
770
+ return segs;
771
+ }
772
+ function dispatchedTasks(command) {
773
+ const out = [];
774
+ for (const words of shellSegments(command)) {
775
+ if (words[0] !== "mu" && !words[0]?.endsWith("/mu")) continue;
776
+ if (words[1] !== "task" || words[2] !== "claim") continue;
777
+ let ws = "";
778
+ let id;
779
+ let isFor = false;
780
+ for (let k = 3; k < words.length; k++) {
781
+ const w = words[k] ?? "";
782
+ const [opt, eq] = w.startsWith("--") && w.includes("=") ? w.split("=", 2) : [w, void 0];
783
+ if (opt === "--for" || opt === "-f") isFor = true;
784
+ if (opt === "-w" || opt === "--workstream") ws = eq ?? words[k + 1] ?? "";
785
+ if (CLAIM_VALUE_OPTS.has(opt ?? "")) {
786
+ if (eq === void 0) k++;
787
+ } else if (id === void 0 && !w.startsWith("-")) id = w;
788
+ }
789
+ if (!isFor || id === void 0) continue;
790
+ const slash = id.indexOf("/");
791
+ if (slash >= 0) {
792
+ if (ws === "") ws = id.slice(0, slash);
793
+ id = id.slice(slash + 1);
794
+ }
795
+ if (ws !== "scratch") out.push({ ws, id });
796
+ }
797
+ return out;
631
798
  }
632
799
  async function inProgress(run, workstreams) {
633
800
  const named = workstreams.filter((w) => w !== "");
@@ -667,7 +834,7 @@ function bashCommand(event) {
667
834
  const e = event;
668
835
  return e.toolName === "bash" && typeof e.input?.command === "string" ? e.input.command : void 0;
669
836
  }
670
- function registerNudge(pi, run = defaultRunner(), skillMd = readSkill()) {
837
+ function registerNudge(pi, run = defaultRunner(), skillMd = readSkill(), nested = notNested) {
671
838
  if (!nudgeEnabled()) return;
672
839
  const rule = skillMd === void 0 ? void 0 : keepDrivingRule(skillMd);
673
840
  if (rule === void 0) return;
@@ -683,7 +850,7 @@ function registerNudge(pi, run = defaultRunner(), skillMd = readSkill()) {
683
850
  for (const ws of dispatchedWorkstreams(cmd) ?? []) armed.add(ws);
684
851
  });
685
852
  pi.on("agent_before_settle", async (event) => {
686
- if (fired || armed.size === 0) return;
853
+ if (fired || armed.size === 0 || nested()) return;
687
854
  if (outcomeOf(event) !== "completed") return;
688
855
  fired = true;
689
856
  const refs = await inProgress(run, [...armed]);
@@ -732,7 +899,7 @@ function closeNudgeText(workstream, names) {
732
899
  "If you are blocked or need an answer, say so in one line and stop; the orchestrator will read your pane."
733
900
  ].join("\n");
734
901
  }
735
- function registerCloseNudge(pi, run = defaultRunner(), env = process.env) {
902
+ function registerCloseNudge(pi, run = defaultRunner(), env = process.env, nested = notNested) {
736
903
  if (!nudgeEnabled(env)) return;
737
904
  const me = workerIdentity(env);
738
905
  if (me === void 0) return;
@@ -740,8 +907,8 @@ function registerCloseNudge(pi, run = defaultRunner(), env = process.env) {
740
907
  pi.on("input", () => {
741
908
  fired = false;
742
909
  });
743
- pi.on("agent_before_settle", async (event) => {
744
- if (fired) return;
910
+ pi.on("agent_before_settle", async (event, ctx) => {
911
+ if (fired || nested() || noUI(ctx)) return;
745
912
  if (outcomeOf(event) !== "completed") return;
746
913
  fired = true;
747
914
  const names = await ownedInProgress(run, me.agent, me.workstream);
@@ -767,9 +934,67 @@ function registerCloseNudge(pi, run = defaultRunner(), env = process.env) {
767
934
  };
768
935
  });
769
936
  }
937
+ var REFUTE_MARK = /^(REFUTER |VERDICT:|REFUTE-EXEMPT:)/m;
938
+ async function briefRefuted(run, ws, id) {
939
+ const r = await run(["task", "notes", id, ...ws ? ["-w", ws] : [], "--json"]);
940
+ if (r.code !== 0) return void 0;
941
+ try {
942
+ const json = JSON.parse(r.stdout);
943
+ return (json.items ?? []).some((n) => REFUTE_MARK.test(n.content ?? ""));
944
+ } catch {
945
+ return void 0;
946
+ }
947
+ }
948
+ function refuteNudgeText(refs) {
949
+ return `[mu] dispatched ${refs.join(", ")} with an unrefuted brief: refute it (delegate, record) or note REFUTE-EXEMPT: <why>`;
950
+ }
951
+ function registerRefuteNudge(pi, run = defaultRunner(), nested = notNested) {
952
+ if (!nudgeEnabled()) return;
953
+ const armed = /* @__PURE__ */ new Map();
954
+ let fired = false;
955
+ pi.on("input", () => {
956
+ armed.clear();
957
+ fired = false;
958
+ });
959
+ pi.on("tool_call", (event) => {
960
+ const cmd = bashCommand(event);
961
+ if (cmd === void 0) return;
962
+ for (const t of dispatchedTasks(cmd)) {
963
+ if (!t.id.startsWith("review_")) armed.set(`${t.ws}/${t.id}`, t);
964
+ }
965
+ });
966
+ pi.on("agent_before_settle", async (event, ctx) => {
967
+ if (fired || armed.size === 0 || nested() || noUI(ctx)) return;
968
+ if (outcomeOf(event) !== "completed") return;
969
+ fired = true;
970
+ const refs = [];
971
+ for (const [ref, t] of armed) {
972
+ if (await briefRefuted(run, t.ws, t.id) === false) refs.push(ref);
973
+ }
974
+ if (refs.length === 0) return;
975
+ const logWs = [...armed.values()].find((t) => t.ws !== "")?.ws;
976
+ await run([
977
+ "log",
978
+ ...logWs ? ["-w", logWs] : [],
979
+ "--kind",
980
+ NUDGE_LOG_KIND,
981
+ `refute: ${refs.join(", ")}`
982
+ ]);
983
+ return {
984
+ entries: [
985
+ {
986
+ type: "custom_message",
987
+ customType: REFUTE_NUDGE_MESSAGE_TYPE,
988
+ content: refuteNudgeText(refs),
989
+ display: true
990
+ }
991
+ ]
992
+ };
993
+ });
994
+ }
770
995
 
771
996
  // extension/mu-pi.ts
772
- var EXT_VERSION = true ? "3.6.2" : void 0;
997
+ var EXT_VERSION = true ? "3.8.0" : void 0;
773
998
  var FRESH_COMMAND = "mu-fresh";
774
999
  var FRESH_TIMEOUT_MS = 3e4;
775
1000
  function commandName(name) {
@@ -908,16 +1133,17 @@ function registerPrompts(pi) {
908
1133
  return { promptPaths: paths };
909
1134
  });
910
1135
  }
911
- function muPi(pi) {
912
- registerDelegate(pi);
1136
+ function muPi(pi, run = defaultRunner()) {
1137
+ registerDelegate(pi, run);
913
1138
  registerPrompts(pi);
914
- registerNudge(pi);
915
- registerCloseNudge(pi);
916
- serveCtl(pi);
1139
+ const nested = serveCtl(pi);
1140
+ registerNudge(pi, run, void 0, nested);
1141
+ registerCloseNudge(pi, run, process.env, nested);
1142
+ registerRefuteNudge(pi, run, nested);
917
1143
  }
918
1144
  function serveCtl(pi) {
919
1145
  const sock = process.env.MU_CTL_SOCK;
920
- if (!sock) return;
1146
+ if (!sock) return () => false;
921
1147
  const sockPath = sock;
922
1148
  const piVersion = detectPiVersion();
923
1149
  const map = sharedMap();
@@ -1092,9 +1318,10 @@ function serveCtl(pi) {
1092
1318
  const text = str(req, "text");
1093
1319
  if (text === void 0) return fail("send needs a string text");
1094
1320
  const mode = req.mode === "steer" ? "steer" : "followUp";
1321
+ const before = status();
1095
1322
  if (g.ctx?.isIdle() ?? g.state === "idle") await g.pi.sendUserMessage(text);
1096
1323
  else await g.pi.sendUserMessage(text, { deliverAs: mode });
1097
- return { v: V, ok: true, state: g.state };
1324
+ return { v: V, ok: true, ...before };
1098
1325
  }
1099
1326
  case "fresh":
1100
1327
  return fresh(req);
@@ -1106,10 +1333,28 @@ function serveCtl(pi) {
1106
1333
  if (!g.ctx) return fail("no pi context yet");
1107
1334
  g.ctx.abort();
1108
1335
  return { v: V, ok: true, state: g.state };
1336
+ case "interrupt":
1337
+ return interrupt(req, conn);
1109
1338
  default:
1110
1339
  return { ...fail(`${UNKNOWN_OP_PREFIX}${String(req.op)}`), ops: [...CTL_OPS] };
1111
1340
  }
1112
1341
  }
1342
+ async function interrupt(req, conn) {
1343
+ const text = str(req, "text");
1344
+ if (text === void 0) return fail("interrupt needs a string text");
1345
+ const wasBusy = !(g.ctx?.isIdle() ?? g.state === "idle");
1346
+ const { pending } = status();
1347
+ if (wasBusy) {
1348
+ if (!g.ctx) return fail("no pi context yet");
1349
+ const settled = wait(g.runs, num(req, "timeoutMs"), conn);
1350
+ g.ctx.abort();
1351
+ const w = await settled;
1352
+ if (!w.ok) return w;
1353
+ }
1354
+ const before = status();
1355
+ await g.pi.sendUserMessage(text);
1356
+ return { v: V, ok: true, ...before, pending, wasBusy };
1357
+ }
1113
1358
  const settledRun = () => ({
1114
1359
  lastText: g.lastText,
1115
1360
  ...g.lastError !== void 0 ? { lastError: g.lastError } : {}
@@ -1126,7 +1371,10 @@ function serveCtl(pi) {
1126
1371
  conn.off("close", onClose);
1127
1372
  resolve(r);
1128
1373
  };
1129
- const waiter = () => done({ v: V, ok: true, ...status(), ...settledRun() });
1374
+ const waiter = () => {
1375
+ if (afterRuns !== void 0 && g.runs <= afterRuns) return;
1376
+ done({ v: V, ok: true, ...status(), ...settledRun() });
1377
+ };
1130
1378
  const onClose = () => done(fail("client closed"));
1131
1379
  g.waiters.add(waiter);
1132
1380
  conn.on("close", onClose);
@@ -1169,8 +1417,9 @@ function serveCtl(pi) {
1169
1417
  async function bind() {
1170
1418
  mkdirSync(dirname(sockPath), { recursive: true, mode: 448 });
1171
1419
  if (await socketIsLive(sockPath)) {
1420
+ g.nested = true;
1172
1421
  process.stderr.write(
1173
- `mu: control socket ${sockPath} is served by another pi; this pi will not take it
1422
+ `mu: control socket ${sockPath} is served by another pi; nested pi: mu agent features off
1174
1423
  `
1175
1424
  );
1176
1425
  return;
@@ -1192,14 +1441,18 @@ function serveCtl(pi) {
1192
1441
  } catch (e) {
1193
1442
  s.close();
1194
1443
  if (e.code !== "EEXIST") throw e;
1195
- process.stderr.write(`mu: control socket ${sockPath} was taken by another pi
1196
- `);
1444
+ g.nested = true;
1445
+ process.stderr.write(
1446
+ `mu: control socket ${sockPath} was taken by another pi; nested pi: mu agent features off
1447
+ `
1448
+ );
1197
1449
  return;
1198
1450
  } finally {
1199
1451
  rmSync(tmp, { force: true });
1200
1452
  }
1201
1453
  g.server = s;
1202
1454
  g.ino = inodeOf(sockPath);
1455
+ g.nested = false;
1203
1456
  }
1204
1457
  pi.on("session_start", async (e, c) => {
1205
1458
  g.ctx = c;
@@ -1231,6 +1484,7 @@ function serveCtl(pi) {
1231
1484
  await new Promise((resolve) => s.close(() => resolve()));
1232
1485
  if (ino !== void 0 && inodeOf(sockPath) === ino) rmSync(sockPath, { force: true });
1233
1486
  });
1487
+ return () => g.nested === true;
1234
1488
  }
1235
1489
  export {
1236
1490
  COMMAND_TIMEOUT_MS,