@mu-crew/mu 3.2.1 → 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 (50) hide show
  1. package/AGENTS.md +10 -5
  2. package/README.md +49 -2
  3. package/dist/cli.js +108 -78
  4. package/dist/cli.js.map +1 -1
  5. package/dist/extension/mu-pi.js +244 -12
  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 +1 -0
  11. package/docs/VOCABULARY.md +2 -0
  12. package/docs/architecture/control-socket.md +17 -3
  13. package/docs/architecture/mux.md +3 -4
  14. package/docs/guide/README.md +5 -0
  15. package/docs/guide/dispatch.md +11 -4
  16. package/docs/guide/getting-started.md +3 -0
  17. package/docs/guide/recovery.md +3 -1
  18. package/docs/guide/remote.md +1 -1
  19. package/docs/history/CHANGELOG-pre-3.md +1 -1
  20. package/docs/reference/env.md +1 -1
  21. package/docs/specs/2026-10-03-keep-driving-nudge.md +142 -0
  22. package/package.json +2 -1
  23. package/prompts/mu-refute.md +8 -0
  24. package/prompts/mu-research.md +8 -0
  25. package/prompts/mu-review.md +8 -0
  26. package/prompts/mu-rules-audit.md +8 -0
  27. package/prompts/mu-tournament.md +9 -0
  28. package/prompts/ultrathink.md +9 -0
  29. package/skills/mu/SKILL.md +71 -131
  30. package/skills/mu/recipes/adversarial-review.md +83 -0
  31. package/skills/mu/recipes/brief.md +53 -0
  32. package/skills/mu/recipes/codemode-driver.md +54 -0
  33. package/skills/mu/recipes/deep-research.md +52 -0
  34. package/skills/mu/recipes/fan-out.md +36 -0
  35. package/skills/mu/recipes/hypothesis-panel.md +40 -0
  36. package/skills/mu/recipes/long-run.md +20 -0
  37. package/skills/mu/recipes/loop-until-done.md +38 -0
  38. package/skills/mu/recipes/orchestrator-loop.md +87 -0
  39. package/skills/mu/recipes/plan.md +54 -0
  40. package/skills/mu/recipes/recovery.md +26 -0
  41. package/skills/mu/recipes/refute.md +49 -0
  42. package/skills/mu/recipes/review-panel.md +57 -0
  43. package/skills/mu/recipes/rules-audit.md +70 -0
  44. package/skills/mu/recipes/tournament.md +37 -0
  45. package/skills/mu/recipes/triage.md +38 -0
  46. package/skills/mu/recipes/ultrathink.md +73 -0
  47. package/skills/mu/recipes/watcher.md +16 -0
  48. package/skills/mu/recipes/waves.md +19 -0
  49. package/skills/mu/recipes/worker.md +24 -0
  50. /package/skills/mu/{REMOTE_WORKERS.md → recipes/remote-workers.md} +0 -0
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import { createConnection } from "net";
19
19
 
20
20
  // src/ctl/protocol.ts
21
21
  var CTL_PROTOCOL_VERSION = 1;
22
+ var CTL_COMMANDS = ["new", "reload", "compact"];
22
23
  var UNKNOWN_OP_PREFIX = "unknown op: ";
23
24
  function encode(msg) {
24
25
  return `${JSON.stringify(msg)}
@@ -66,7 +67,7 @@ var CtlUnknownOpError = class extends Error {
66
67
  };
67
68
  function clientTimeout(req, override) {
68
69
  if (override !== void 0) return override;
69
- if (req.op === "fresh") return CTL_FRESH_TIMEOUT_MS;
70
+ if (req.op === "fresh" || req.op === "command") return CTL_FRESH_TIMEOUT_MS;
70
71
  if (req.op !== "wait") return CTL_DEFAULT_TIMEOUT_MS;
71
72
  return req.timeoutMs === void 0 ? void 0 : req.timeoutMs + CTL_WAIT_SLACK_MS;
72
73
  }
@@ -1339,27 +1340,6 @@ async function awaitPaneQuiescence(paneId, budgetMs) {
1339
1340
  await currentSleep(Math.min(SEND_READINESS_POLL_MS, remaining));
1340
1341
  }
1341
1342
  }
1342
- async function awaitNewSession(paneId, baseline, budgetMs) {
1343
- if (budgetMs <= 0) return true;
1344
- const deadline = Date.now() + budgetMs;
1345
- let previous = baseline;
1346
- let stable = 0;
1347
- for (; ; ) {
1348
- const scrollback = await capturePane(paneId, { lines: 50 }).catch(() => void 0);
1349
- if (scrollback !== void 0 && scrollback !== baseline) {
1350
- if (scrollback === previous) {
1351
- stable++;
1352
- if (stable >= QUIESCENCE_CONFIRMATIONS) return true;
1353
- } else {
1354
- stable = 0;
1355
- }
1356
- previous = scrollback;
1357
- }
1358
- const remaining = deadline - Date.now();
1359
- if (remaining <= 0) return false;
1360
- await currentSleep(Math.min(SEND_READINESS_POLL_MS, remaining));
1361
- }
1362
- }
1363
1343
  function pasteProbe(text) {
1364
1344
  const firstLine = (text.split("\n")[0] ?? "").trim();
1365
1345
  return firstLine.slice(0, 24);
@@ -1387,7 +1367,6 @@ async function sendToPane(paneId, text, opts = {}) {
1387
1367
  assertValidPaneId(paneId);
1388
1368
  const readinessMs = opts.readinessMs ?? defaultSendReadinessMs();
1389
1369
  const quiesced = await awaitPaneQuiescence(paneId, readinessMs);
1390
- const newSessionBaseline = readinessMs > 0 && text.trim() === "/new" ? await capturePane(paneId, { lines: 50 }).catch(() => void 0) : void 0;
1391
1370
  const copyResult = await currentExecutor(["copy-mode", "-q", "-t", paneId]);
1392
1371
  if (copyResult.exitCode !== 0 && /can't find pane|no current target/i.test(copyResult.stderr)) {
1393
1372
  throw new TmuxError(
@@ -1410,15 +1389,6 @@ async function sendToPane(paneId, text, opts = {}) {
1410
1389
  if (delay2 > 0) await currentSleep(delay2);
1411
1390
  await tmux(["send-keys", "-t", paneId, "Enter"]);
1412
1391
  if (readinessMs <= 0) return;
1413
- if (newSessionBaseline !== void 0 && !await awaitNewSession(paneId, newSessionBaseline, readinessMs)) {
1414
- const warn3 = opts.onUndelivered ?? defaultUndeliveredWarning;
1415
- warn3({
1416
- paneId,
1417
- reason: "transition-unconfirmed",
1418
- message: `send to ${paneId} submitted /new, but the new session did not become ready within ${readinessMs}ms. Wait for the pane to settle before sending more work.`
1419
- });
1420
- return;
1421
- }
1422
1392
  const probe = pasteProbe(text);
1423
1393
  if (probe.length === 0) return;
1424
1394
  for (let attempt = 0; attempt <= SUBMIT_RETRIES; attempt++) {
@@ -6485,7 +6455,7 @@ async function spawnAgent(db, opts) {
6485
6455
  agent = created.row;
6486
6456
  const startAgent = (await activeMux()).startAgentInPane;
6487
6457
  if (startAgent === void 0) {
6488
- await awaitSpawnLiveness(paneId, opts.name, opts.workstream, command);
6458
+ await awaitSpawnLiveness(paneId, opts.name, command);
6489
6459
  } else {
6490
6460
  await startAgent({ paneId, name: opts.name, cli, command, commandSource });
6491
6461
  }
@@ -6625,7 +6595,7 @@ function detectSpawnStartupError(scrollback) {
6625
6595
  }
6626
6596
  return void 0;
6627
6597
  }
6628
- async function awaitSpawnLiveness(paneId, agentName, workstreamName, command) {
6598
+ async function awaitSpawnLiveness(paneId, agentName, command) {
6629
6599
  const ms = defaultSpawnLivenessMs();
6630
6600
  if (ms === 0) return;
6631
6601
  await sleep(ms);
@@ -6640,31 +6610,6 @@ async function awaitSpawnLiveness(paneId, agentName, workstreamName, command) {
6640
6610
  throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback);
6641
6611
  }
6642
6612
  }
6643
- await awaitSpawnReadiness(paneId, agentName, workstreamName, command);
6644
- }
6645
- function defaultSpawnReadinessMs() {
6646
- const raw = process.env.MU_SPAWN_READINESS_MS;
6647
- if (raw === void 0) return 1e4;
6648
- const parsed = Number.parseInt(raw, 10);
6649
- if (Number.isNaN(parsed) || parsed < 0) return 1e4;
6650
- return parsed;
6651
- }
6652
- var READINESS_POLL_INTERVAL_MS = 250;
6653
- async function awaitSpawnReadiness(paneId, agentName, workstreamName, command) {
6654
- const budgetMs = defaultSpawnReadinessMs();
6655
- if (budgetMs === 0 || !murmurAvailable()) return;
6656
- const mux = await activeMux();
6657
- const deadline = Date.now() + budgetMs;
6658
- const agent = { name: agentName, workstreamName, paneId };
6659
- while (Date.now() < deadline) {
6660
- if (!await mux.paneExists(paneId)) {
6661
- const scrollback = await mux.capturePane(paneId, { lines: 50 }).catch(() => void 0);
6662
- throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command);
6663
- }
6664
- const reading = (await readAgentStates([agent])).get(agentKey(agent));
6665
- if (reading?.source === "murmur") return;
6666
- await sleep(Math.min(READINESS_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
6667
- }
6668
6613
  }
6669
6614
  async function createOrReusePane(opts) {
6670
6615
  const mux = await activeMux();
@@ -7087,23 +7032,47 @@ var AgentExtensionOutdatedError = class extends Error {
7087
7032
  }
7088
7033
  };
7089
7034
  var AgentBusyError = class extends Error {
7090
- constructor(agentName, workstream) {
7091
- super(`agent ${agentName} is busy: --fresh would abandon its running turn`);
7035
+ constructor(agentName, workstream, command) {
7036
+ super(`agent ${agentName} is busy: ${command ?? "--fresh"} would abandon its running turn`);
7092
7037
  this.agentName = agentName;
7093
7038
  this.workstream = workstream;
7039
+ this.command = command;
7094
7040
  }
7095
7041
  agentName;
7096
7042
  workstream;
7043
+ command;
7097
7044
  name = "AgentBusyError";
7098
7045
  errorNextSteps() {
7046
+ const retry = this.command === void 0 ? `mu agent send ${this.agentName} --fresh --force '...' -w ${this.workstream}` : `mu agent send ${this.agentName} '${this.command}' --force -w ${this.workstream}`;
7099
7047
  return [
7100
7048
  {
7101
7049
  intent: "Stop the turn first, then retry",
7102
7050
  command: `mu agent abort ${this.agentName} -w ${this.workstream}`
7103
7051
  },
7052
+ { intent: "Or abandon the turn in one step", command: retry }
7053
+ ];
7054
+ }
7055
+ };
7056
+ var AgentSlashCommandUnsupportedError = class extends Error {
7057
+ constructor(agentName, workstream, text, supported) {
7058
+ super(
7059
+ `agent ${agentName} runs pi: over the control socket mu runs only ${supported.join(", ")}; ${JSON.stringify(text.split("\n")[0])} is not one of them. Pass --via mux to type it into the pane`
7060
+ );
7061
+ this.agentName = agentName;
7062
+ this.workstream = workstream;
7063
+ this.text = text;
7064
+ this.supported = supported;
7065
+ }
7066
+ agentName;
7067
+ workstream;
7068
+ text;
7069
+ supported;
7070
+ name = "AgentSlashCommandUnsupportedError";
7071
+ errorNextSteps() {
7072
+ return [
7104
7073
  {
7105
- intent: "Or abandon the turn in one step",
7106
- command: `mu agent send ${this.agentName} --fresh --force '...' -w ${this.workstream}`
7074
+ intent: "Type it into the pane on purpose (tmux paste; unconfirmed)",
7075
+ command: `mu agent send ${this.agentName} '${this.text.split("\n")[0]}' --via mux -w ${this.workstream}`
7107
7076
  }
7108
7077
  ];
7109
7078
  }
@@ -7116,10 +7085,22 @@ function agentCtlSocket(db, agent) {
7116
7085
  function expectsCtl(agent, sock = ctlSocketPath(agent.workstreamName, agent.name)) {
7117
7086
  return speaksMuCtl(agent.cli, resolveCliCommand(agent.cli)) || existsSync8(sock);
7118
7087
  }
7119
- function chooseTransport(agent, text, sock) {
7120
- if (text.startsWith("/")) return "mux";
7088
+ function chooseTransport(agent, sock) {
7121
7089
  return expectsCtl(agent, sock) ? "ctl" : "mux";
7122
7090
  }
7091
+ var CTL_SLASH_COMMANDS = CTL_COMMANDS.map((n) => `/${n}`);
7092
+ function parseSessionCommand(text) {
7093
+ const t = text.trim();
7094
+ if (t === "/new") return { name: "new" };
7095
+ if (t === "/reload") return { name: "reload" };
7096
+ const m = /^\/compact(?:\s+([\s\S]*))?$/.exec(t);
7097
+ if (!m) return void 0;
7098
+ const instructions = m[1]?.trim();
7099
+ return instructions ? { name: "compact", instructions } : { name: "compact" };
7100
+ }
7101
+ function isSlashCommand(text) {
7102
+ return /^\/[A-Za-z][\w:-]*(?:\s|$)/.test(text.trimStart());
7103
+ }
7123
7104
  async function extensionOutdated(agent, sock, e) {
7124
7105
  let extVersion;
7125
7106
  try {
@@ -7148,11 +7129,23 @@ async function sendViaTransport(agent, text, opts) {
7148
7129
  if (opts.fresh && (opts.via === "mux" || !expectsCtl(agent, sock))) {
7149
7130
  throw new AgentFreshNeedsCtlError(agent.name, agent.workstreamName, agent.cli);
7150
7131
  }
7151
- const transport = opts.fresh ? "ctl" : opts.via ?? chooseTransport(agent, text, sock);
7132
+ const transport = opts.fresh ? "ctl" : opts.via ?? chooseTransport(agent, sock);
7152
7133
  if (transport === "mux") {
7153
7134
  await (await activeMux()).sendToPane(agent.paneId, text, opts);
7154
7135
  return { transport };
7155
7136
  }
7137
+ if (!opts.fresh) {
7138
+ const cmd = parseSessionCommand(text);
7139
+ if (cmd) return runSessionCommand(agent, sock, cmd, opts.force === true);
7140
+ if (isSlashCommand(text)) {
7141
+ throw new AgentSlashCommandUnsupportedError(
7142
+ agent.name,
7143
+ agent.workstreamName,
7144
+ text,
7145
+ CTL_SLASH_COMMANDS
7146
+ );
7147
+ }
7148
+ }
7156
7149
  const reply = await ctlRequestFor(
7157
7150
  agent,
7158
7151
  sock,
@@ -7166,6 +7159,25 @@ async function sendViaTransport(agent, text, opts) {
7166
7159
  }
7167
7160
  return reply.state === void 0 ? { transport } : { transport, state: reply.state };
7168
7161
  }
7162
+ async function runSessionCommand(agent, sock, cmd, force) {
7163
+ const reply = await ctlRequestFor(agent, sock, {
7164
+ op: "command",
7165
+ name: cmd.name,
7166
+ ...cmd.instructions !== void 0 ? { instructions: cmd.instructions } : {},
7167
+ ...force ? { force: true } : {}
7168
+ });
7169
+ if (!reply.ok) {
7170
+ if (reply.error === "busy") {
7171
+ throw new AgentBusyError(agent.name, agent.workstreamName, `/${cmd.name}`);
7172
+ }
7173
+ throw new Error(`pi refused /${cmd.name}: ${reply.error}`);
7174
+ }
7175
+ return {
7176
+ transport: "ctl",
7177
+ command: cmd.name,
7178
+ ...reply.state !== void 0 ? { state: reply.state } : {}
7179
+ };
7180
+ }
7169
7181
 
7170
7182
  // src/agent-state.ts
7171
7183
  var CTL_STATE_PROBE_MS = 1e3;
@@ -8948,6 +8960,15 @@ function murmurDoctorCheck(agentStateSource, opts = {}) {
8948
8960
  };
8949
8961
  }
8950
8962
  var RELINK = "run mu link pi";
8963
+ function extFeatures(env) {
8964
+ const features = [
8965
+ ["mu_delegate", "MU_DELEGATE"],
8966
+ ["nudge", "MU_NUDGE"]
8967
+ ];
8968
+ const on = ["ctl", ...features.filter(([, v]) => env[v] !== "0").map(([f]) => f)];
8969
+ const off = features.filter(([, v]) => env[v] === "0").map(([f, v]) => `${f} disabled by ${v}=0`);
8970
+ return `(${on.join(", ")}${off.length ? `; ${off.join("; ")}` : ""})`;
8971
+ }
8951
8972
  function ctlExtensionDoctorCheck(opts) {
8952
8973
  const { extension } = inspectLinks(opts);
8953
8974
  switch (extension.state) {
@@ -8955,7 +8976,7 @@ function ctlExtensionDoctorCheck(opts) {
8955
8976
  return {
8956
8977
  name: "mu ext",
8957
8978
  status: "ok",
8958
- detail: process.env.MU_DELEGATE === "0" ? "(ctl; mu_delegate disabled by MU_DELEGATE=0)" : "(ctl, mu_delegate)"
8979
+ detail: extFeatures(process.env)
8959
8980
  };
8960
8981
  case "stale-copy":
8961
8982
  return { name: "mu ext", status: "warn", detail: `stale copy: ${RELINK}` };
@@ -10919,6 +10940,7 @@ export {
10919
10940
  AgentFreshNeedsCtlError,
10920
10941
  AgentNotFoundError,
10921
10942
  AgentNotInWorkstreamError,
10943
+ AgentSlashCommandUnsupportedError,
10922
10944
  AgentSpawnCliNotFoundError,
10923
10945
  AgentSpawnStartupError,
10924
10946
  CAPTURE_TRIGGER_DDL,