@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
package/dist/cli.js CHANGED
@@ -14,18 +14,20 @@ function encode(msg) {
14
14
  return `${JSON.stringify(msg)}
15
15
  `;
16
16
  }
17
- var CTL_PROTOCOL_VERSION, CTL_OPS, UNKNOWN_OP_PREFIX, LineDecoder;
17
+ var CTL_PROTOCOL_VERSION, CTL_COMMANDS, CTL_OPS, UNKNOWN_OP_PREFIX, LineDecoder;
18
18
  var init_protocol = __esm({
19
19
  "src/ctl/protocol.ts"() {
20
20
  "use strict";
21
21
  CTL_PROTOCOL_VERSION = 1;
22
+ CTL_COMMANDS = ["new", "reload", "compact"];
22
23
  CTL_OPS = [
23
24
  "hello",
24
25
  "status",
25
26
  "send",
26
27
  "wait",
27
28
  "abort",
28
- "fresh"
29
+ "fresh",
30
+ "command"
29
31
  ];
30
32
  UNKNOWN_OP_PREFIX = "unknown op: ";
31
33
  LineDecoder = class {
@@ -44,7 +46,7 @@ var init_protocol = __esm({
44
46
  import { createConnection } from "net";
45
47
  function clientTimeout(req, override) {
46
48
  if (override !== void 0) return override;
47
- if (req.op === "fresh") return CTL_FRESH_TIMEOUT_MS;
49
+ if (req.op === "fresh" || req.op === "command") return CTL_FRESH_TIMEOUT_MS;
48
50
  if (req.op !== "wait") return CTL_DEFAULT_TIMEOUT_MS;
49
51
  return req.timeoutMs === void 0 ? void 0 : req.timeoutMs + CTL_WAIT_SLACK_MS;
50
52
  }
@@ -391,7 +393,7 @@ var init_status = __esm({
391
393
  TASK_STATUSES = ["OPEN", "IN_PROGRESS", "CLOSED"];
392
394
  TASK_STATUS_LIST = TASK_STATUSES.join(" | ");
393
395
  TASK_SUBSTATES = {
394
- OPEN: ["todo", "parked"],
396
+ OPEN: ["todo", "parked", "triage"],
395
397
  IN_PROGRESS: ["active"],
396
398
  CLOSED: ["done", "rejected", "wontfix", "duplicate", "superseded"]
397
399
  };
@@ -605,7 +607,7 @@ CREATE VIEW ready AS
605
607
  SELECT t.*
606
608
  FROM tasks t
607
609
  WHERE t.status = 'OPEN'
608
- AND t.substate <> 'parked'
610
+ AND t.substate NOT IN ('parked', 'triage')
609
611
  AND NOT EXISTS (
610
612
  SELECT 1
611
613
  FROM task_edges e
@@ -1289,27 +1291,6 @@ async function awaitPaneQuiescence(paneId, budgetMs) {
1289
1291
  await currentSleep(Math.min(SEND_READINESS_POLL_MS, remaining));
1290
1292
  }
1291
1293
  }
1292
- async function awaitNewSession(paneId, baseline, budgetMs) {
1293
- if (budgetMs <= 0) return true;
1294
- const deadline = Date.now() + budgetMs;
1295
- let previous = baseline;
1296
- let stable = 0;
1297
- for (; ; ) {
1298
- const scrollback = await capturePane(paneId, { lines: 50 }).catch(() => void 0);
1299
- if (scrollback !== void 0 && scrollback !== baseline) {
1300
- if (scrollback === previous) {
1301
- stable++;
1302
- if (stable >= QUIESCENCE_CONFIRMATIONS) return true;
1303
- } else {
1304
- stable = 0;
1305
- }
1306
- previous = scrollback;
1307
- }
1308
- const remaining = deadline - Date.now();
1309
- if (remaining <= 0) return false;
1310
- await currentSleep(Math.min(SEND_READINESS_POLL_MS, remaining));
1311
- }
1312
- }
1313
1294
  function pasteProbe(text2) {
1314
1295
  const firstLine = (text2.split("\n")[0] ?? "").trim();
1315
1296
  return firstLine.slice(0, 24);
@@ -1337,7 +1318,6 @@ async function sendToPane(paneId, text2, opts = {}) {
1337
1318
  assertValidPaneId(paneId);
1338
1319
  const readinessMs = opts.readinessMs ?? defaultSendReadinessMs();
1339
1320
  const quiesced = await awaitPaneQuiescence(paneId, readinessMs);
1340
- const newSessionBaseline = readinessMs > 0 && text2.trim() === "/new" ? await capturePane(paneId, { lines: 50 }).catch(() => void 0) : void 0;
1341
1321
  const copyResult = await currentExecutor(["copy-mode", "-q", "-t", paneId]);
1342
1322
  if (copyResult.exitCode !== 0 && /can't find pane|no current target/i.test(copyResult.stderr)) {
1343
1323
  throw new TmuxError(
@@ -1360,15 +1340,6 @@ async function sendToPane(paneId, text2, opts = {}) {
1360
1340
  if (delay2 > 0) await currentSleep(delay2);
1361
1341
  await tmux(["send-keys", "-t", paneId, "Enter"]);
1362
1342
  if (readinessMs <= 0) return;
1363
- if (newSessionBaseline !== void 0 && !await awaitNewSession(paneId, newSessionBaseline, readinessMs)) {
1364
- const warn3 = opts.onUndelivered ?? defaultUndeliveredWarning;
1365
- warn3({
1366
- paneId,
1367
- reason: "transition-unconfirmed",
1368
- 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.`
1369
- });
1370
- return;
1371
- }
1372
1343
  const probe = pasteProbe(text2);
1373
1344
  if (probe.length === 0) return;
1374
1345
  for (let attempt = 0; attempt <= SUBMIT_RETRIES; attempt++) {
@@ -4401,7 +4372,7 @@ async function spawnAgent(db, opts) {
4401
4372
  agent = created.row;
4402
4373
  const startAgent = (await activeMux()).startAgentInPane;
4403
4374
  if (startAgent === void 0) {
4404
- await awaitSpawnLiveness(paneId, opts.name, opts.workstream, command);
4375
+ await awaitSpawnLiveness(paneId, opts.name, command);
4405
4376
  } else {
4406
4377
  await startAgent({ paneId, name: opts.name, cli, command, commandSource });
4407
4378
  }
@@ -4510,7 +4481,7 @@ function detectSpawnStartupError(scrollback) {
4510
4481
  }
4511
4482
  return void 0;
4512
4483
  }
4513
- async function awaitSpawnLiveness(paneId, agentName, workstreamName, command) {
4484
+ async function awaitSpawnLiveness(paneId, agentName, command) {
4514
4485
  const ms = defaultSpawnLivenessMs();
4515
4486
  if (ms === 0) return;
4516
4487
  await sleep(ms);
@@ -4525,30 +4496,6 @@ async function awaitSpawnLiveness(paneId, agentName, workstreamName, command) {
4525
4496
  throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback);
4526
4497
  }
4527
4498
  }
4528
- await awaitSpawnReadiness(paneId, agentName, workstreamName, command);
4529
- }
4530
- function defaultSpawnReadinessMs() {
4531
- const raw = process.env.MU_SPAWN_READINESS_MS;
4532
- if (raw === void 0) return 1e4;
4533
- const parsed = Number.parseInt(raw, 10);
4534
- if (Number.isNaN(parsed) || parsed < 0) return 1e4;
4535
- return parsed;
4536
- }
4537
- async function awaitSpawnReadiness(paneId, agentName, workstreamName, command) {
4538
- const budgetMs = defaultSpawnReadinessMs();
4539
- if (budgetMs === 0 || !murmurAvailable()) return;
4540
- const mux = await activeMux();
4541
- const deadline = Date.now() + budgetMs;
4542
- const agent = { name: agentName, workstreamName, paneId };
4543
- while (Date.now() < deadline) {
4544
- if (!await mux.paneExists(paneId)) {
4545
- const scrollback = await mux.capturePane(paneId, { lines: 50 }).catch(() => void 0);
4546
- throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command);
4547
- }
4548
- const reading = (await readAgentStates([agent])).get(agentKey(agent));
4549
- if (reading?.source === "murmur") return;
4550
- await sleep(Math.min(READINESS_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
4551
- }
4552
4499
  }
4553
4500
  async function createOrReusePane(opts) {
4554
4501
  const mux = await activeMux();
@@ -4594,11 +4541,10 @@ async function firstPaneInWindow(mux, session, windowId) {
4594
4541
  }
4595
4542
  return pane.paneId;
4596
4543
  }
4597
- var AGENT_NAME_CONVENTION_RE, execFileP, activeCommandResolver, CTL_POLL_INTERVAL_MS, STARTUP_ERROR_PATTERNS, STARTUP_ERROR_TAIL_LINES, READINESS_POLL_INTERVAL_MS;
4544
+ var AGENT_NAME_CONVENTION_RE, execFileP, activeCommandResolver, CTL_POLL_INTERVAL_MS, STARTUP_ERROR_PATTERNS, STARTUP_ERROR_TAIL_LINES;
4598
4545
  var init_spawn = __esm({
4599
4546
  "src/agents/spawn.ts"() {
4600
4547
  "use strict";
4601
- init_agent_state();
4602
4548
  init_agents();
4603
4549
  init_client();
4604
4550
  init_path();
@@ -4644,12 +4590,11 @@ var init_spawn = __esm({
4644
4590
  /^(?:.*: )?(?:command not found|No such file or directory)(?::\s*\S+)?$/i
4645
4591
  ];
4646
4592
  STARTUP_ERROR_TAIL_LINES = 30;
4647
- READINESS_POLL_INTERVAL_MS = 250;
4648
4593
  }
4649
4594
  });
4650
4595
 
4651
4596
  // src/agents/errors.ts
4652
- var AgentSpawnCliNotFoundError, AgentExistsError, AgentNotFoundError, AgentNotInWorkstreamError, AgentDiedOnSpawnError, AgentSpawnStartupError, WorkspacePreservedError, AgentCtlUnreachableError, AgentAbortNeedsCtlError, AgentAbortTimeoutError, AgentFreshNeedsCtlError, AgentExtensionOutdatedError, AgentBusyError;
4597
+ var AgentSpawnCliNotFoundError, AgentExistsError, AgentNotFoundError, AgentNotInWorkstreamError, AgentDiedOnSpawnError, AgentSpawnStartupError, WorkspacePreservedError, AgentCtlUnreachableError, AgentAbortNeedsCtlError, AgentAbortTimeoutError, AgentFreshNeedsCtlError, AgentExtensionOutdatedError, AgentBusyError, AgentSlashCommandUnsupportedError;
4653
4598
  var init_errors = __esm({
4654
4599
  "src/agents/errors.ts"() {
4655
4600
  "use strict";
@@ -5029,23 +4974,47 @@ ${scrollback.trim()}
5029
4974
  }
5030
4975
  };
5031
4976
  AgentBusyError = class extends Error {
5032
- constructor(agentName, workstream) {
5033
- super(`agent ${agentName} is busy: --fresh would abandon its running turn`);
4977
+ constructor(agentName, workstream, command) {
4978
+ super(`agent ${agentName} is busy: ${command ?? "--fresh"} would abandon its running turn`);
5034
4979
  this.agentName = agentName;
5035
4980
  this.workstream = workstream;
4981
+ this.command = command;
5036
4982
  }
5037
4983
  agentName;
5038
4984
  workstream;
4985
+ command;
5039
4986
  name = "AgentBusyError";
5040
4987
  errorNextSteps() {
4988
+ 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}`;
5041
4989
  return [
5042
4990
  {
5043
4991
  intent: "Stop the turn first, then retry",
5044
4992
  command: `mu agent abort ${this.agentName} -w ${this.workstream}`
5045
4993
  },
4994
+ { intent: "Or abandon the turn in one step", command: retry }
4995
+ ];
4996
+ }
4997
+ };
4998
+ AgentSlashCommandUnsupportedError = class extends Error {
4999
+ constructor(agentName, workstream, text2, supported) {
5000
+ super(
5001
+ `agent ${agentName} runs pi: over the control socket mu runs only ${supported.join(", ")}; ${JSON.stringify(text2.split("\n")[0])} is not one of them. Pass --via mux to type it into the pane`
5002
+ );
5003
+ this.agentName = agentName;
5004
+ this.workstream = workstream;
5005
+ this.text = text2;
5006
+ this.supported = supported;
5007
+ }
5008
+ agentName;
5009
+ workstream;
5010
+ text;
5011
+ supported;
5012
+ name = "AgentSlashCommandUnsupportedError";
5013
+ errorNextSteps() {
5014
+ return [
5046
5015
  {
5047
- intent: "Or abandon the turn in one step",
5048
- command: `mu agent send ${this.agentName} --fresh --force '...' -w ${this.workstream}`
5016
+ intent: "Type it into the pane on purpose (tmux paste; unconfirmed)",
5017
+ command: `mu agent send ${this.agentName} '${this.text.split("\n")[0]}' --via mux -w ${this.workstream}`
5049
5018
  }
5050
5019
  ];
5051
5020
  }
@@ -5062,10 +5031,21 @@ function agentCtlSocket(db, agent) {
5062
5031
  function expectsCtl(agent, sock = ctlSocketPath(agent.workstreamName, agent.name)) {
5063
5032
  return speaksMuCtl(agent.cli, resolveCliCommand(agent.cli)) || existsSync7(sock);
5064
5033
  }
5065
- function chooseTransport(agent, text2, sock) {
5066
- if (text2.startsWith("/")) return "mux";
5034
+ function chooseTransport(agent, sock) {
5067
5035
  return expectsCtl(agent, sock) ? "ctl" : "mux";
5068
5036
  }
5037
+ function parseSessionCommand(text2) {
5038
+ const t = text2.trim();
5039
+ if (t === "/new") return { name: "new" };
5040
+ if (t === "/reload") return { name: "reload" };
5041
+ const m = /^\/compact(?:\s+([\s\S]*))?$/.exec(t);
5042
+ if (!m) return void 0;
5043
+ const instructions = m[1]?.trim();
5044
+ return instructions ? { name: "compact", instructions } : { name: "compact" };
5045
+ }
5046
+ function isSlashCommand(text2) {
5047
+ return /^\/[A-Za-z][\w:-]*(?:\s|$)/.test(text2.trimStart());
5048
+ }
5069
5049
  async function extensionOutdated(agent, sock, e) {
5070
5050
  let extVersion;
5071
5051
  try {
@@ -5094,11 +5074,23 @@ async function sendViaTransport(agent, text2, opts) {
5094
5074
  if (opts.fresh && (opts.via === "mux" || !expectsCtl(agent, sock))) {
5095
5075
  throw new AgentFreshNeedsCtlError(agent.name, agent.workstreamName, agent.cli);
5096
5076
  }
5097
- const transport = opts.fresh ? "ctl" : opts.via ?? chooseTransport(agent, text2, sock);
5077
+ const transport = opts.fresh ? "ctl" : opts.via ?? chooseTransport(agent, sock);
5098
5078
  if (transport === "mux") {
5099
5079
  await (await activeMux()).sendToPane(agent.paneId, text2, opts);
5100
5080
  return { transport };
5101
5081
  }
5082
+ if (!opts.fresh) {
5083
+ const cmd = parseSessionCommand(text2);
5084
+ if (cmd) return runSessionCommand(agent, sock, cmd, opts.force === true);
5085
+ if (isSlashCommand(text2)) {
5086
+ throw new AgentSlashCommandUnsupportedError(
5087
+ agent.name,
5088
+ agent.workstreamName,
5089
+ text2,
5090
+ CTL_SLASH_COMMANDS
5091
+ );
5092
+ }
5093
+ }
5102
5094
  const reply = await ctlRequestFor(
5103
5095
  agent,
5104
5096
  sock,
@@ -5112,14 +5104,36 @@ async function sendViaTransport(agent, text2, opts) {
5112
5104
  }
5113
5105
  return reply.state === void 0 ? { transport } : { transport, state: reply.state };
5114
5106
  }
5107
+ async function runSessionCommand(agent, sock, cmd, force) {
5108
+ const reply = await ctlRequestFor(agent, sock, {
5109
+ op: "command",
5110
+ name: cmd.name,
5111
+ ...cmd.instructions !== void 0 ? { instructions: cmd.instructions } : {},
5112
+ ...force ? { force: true } : {}
5113
+ });
5114
+ if (!reply.ok) {
5115
+ if (reply.error === "busy") {
5116
+ throw new AgentBusyError(agent.name, agent.workstreamName, `/${cmd.name}`);
5117
+ }
5118
+ throw new Error(`pi refused /${cmd.name}: ${reply.error}`);
5119
+ }
5120
+ return {
5121
+ transport: "ctl",
5122
+ command: cmd.name,
5123
+ ...reply.state !== void 0 ? { state: reply.state } : {}
5124
+ };
5125
+ }
5126
+ var CTL_SLASH_COMMANDS;
5115
5127
  var init_transport = __esm({
5116
5128
  "src/agents/transport.ts"() {
5117
5129
  "use strict";
5118
5130
  init_client();
5119
5131
  init_path();
5132
+ init_protocol();
5120
5133
  init_mux2();
5121
5134
  init_errors();
5122
5135
  init_spawn();
5136
+ CTL_SLASH_COMMANDS = CTL_COMMANDS.map((n) => `/${n}`);
5123
5137
  }
5124
5138
  });
5125
5139
 
@@ -5836,7 +5850,7 @@ var init_id = __esm({
5836
5850
  });
5837
5851
 
5838
5852
  // src/tasks/errors.ts
5839
- var TaskNotFoundError, TaskIdInvalidError, TaskExistsError, TaskNotInWorkstreamError, TaskAlreadyOwnedError, ClaimerNotRegisteredError, TaskClaimStaleWorkspaceError, CycleError, ReaperDetectedDuringWaitError, StallDetectedDuringWaitError, CrossWorkstreamEdgeError, SubstateReasonRequiredError, TaskParkStateError, TaskParkedError, InvalidSubstateError;
5853
+ var TaskNotFoundError, TaskIdInvalidError, TaskExistsError, TaskNotInWorkstreamError, TaskAlreadyOwnedError, ClaimerNotRegisteredError, TaskClaimStaleWorkspaceError, CycleError, ReaperDetectedDuringWaitError, StallDetectedDuringWaitError, CrossWorkstreamEdgeError, SubstateReasonRequiredError, TaskParkStateError, TaskInTriageError, TaskParkedError, InvalidSubstateError;
5840
5854
  var init_errors2 = __esm({
5841
5855
  "src/tasks/errors.ts"() {
5842
5856
  "use strict";
@@ -6184,6 +6198,30 @@ var init_errors2 = __esm({
6184
6198
  ];
6185
6199
  }
6186
6200
  };
6201
+ TaskInTriageError = class extends Error {
6202
+ constructor(taskId, workstream) {
6203
+ super(`task ${taskId} is in triage (OPEN/triage); accept it or claim with --force`);
6204
+ this.taskId = taskId;
6205
+ this.workstream = workstream;
6206
+ }
6207
+ taskId;
6208
+ workstream;
6209
+ name = "TaskInTriageError";
6210
+ errorNextSteps() {
6211
+ const ws = this.workstream;
6212
+ return [
6213
+ {
6214
+ intent: "Accept it as work, then claim",
6215
+ command: `mu task accept ${this.taskId} -w ${ws}`
6216
+ },
6217
+ {
6218
+ intent: "Decline it",
6219
+ command: `mu task close ${this.taskId} --as rejected --why "<reason>" -w ${ws}`
6220
+ },
6221
+ { intent: "Claim it anyway", command: `mu task claim ${this.taskId} --force -w ${ws}` }
6222
+ ];
6223
+ }
6224
+ };
6187
6225
  TaskParkedError = class extends Error {
6188
6226
  constructor(taskId, workstream) {
6189
6227
  super(`task ${taskId} is parked (OPEN/parked); unpark it or claim with --force`);
@@ -6746,8 +6784,17 @@ function addTaskImpl(db, opts) {
6746
6784
  const now = (/* @__PURE__ */ new Date()).toISOString();
6747
6785
  const insertResult = db.prepare(
6748
6786
  `INSERT INTO tasks (workstream_id, local_id, title, status, substate, impact, effort_days, created_at, updated_at)
6749
- VALUES (?, ?, ?, 'OPEN', 'todo', ?, ?, ?, ?)`
6750
- ).run(wsId, opts.localId, opts.title, opts.impact, opts.effortDays, now, now);
6787
+ VALUES (?, ?, ?, 'OPEN', ?, ?, ?, ?, ?)`
6788
+ ).run(
6789
+ wsId,
6790
+ opts.localId,
6791
+ opts.title,
6792
+ opts.triage === true ? "triage" : "todo",
6793
+ opts.impact,
6794
+ opts.effortDays,
6795
+ now,
6796
+ now
6797
+ );
6751
6798
  const newTaskId = Number(insertResult.lastInsertRowid);
6752
6799
  if (opts.blockedBy && opts.blockedBy.length > 0) {
6753
6800
  const blockerLookupSameWs = db.prepare(
@@ -7035,6 +7082,28 @@ function parkTask(db, localId, opts) {
7035
7082
  })()
7036
7083
  );
7037
7084
  }
7085
+ function acceptTask(db, localId, opts) {
7086
+ return withOpContext(
7087
+ db,
7088
+ { intent: "task.accept", group: "new" },
7089
+ () => db.transaction(() => {
7090
+ const before = getTask(db, localId, opts.workstream);
7091
+ if (!before) throw new TaskNotFoundError(localId);
7092
+ if (before.status !== "OPEN" || before.substate !== "triage") {
7093
+ return {
7094
+ previousStatus: before.status,
7095
+ status: before.status,
7096
+ previousSubstate: before.substate,
7097
+ substate: before.substate,
7098
+ changed: false
7099
+ };
7100
+ }
7101
+ const r = setTaskStatus(db, localId, "OPEN", { workstream: opts.workstream });
7102
+ recordEvidenceNote(db, localId, before.workstreamName, "ACCEPT", opts);
7103
+ return r;
7104
+ })()
7105
+ );
7106
+ }
7038
7107
  function unparkTask(db, localId, opts) {
7039
7108
  return withOpContext(
7040
7109
  db,
@@ -7177,6 +7246,9 @@ function assertNotParked(task, opts) {
7177
7246
  if (task.status === "OPEN" && task.substate === "parked" && opts.force !== true) {
7178
7247
  throw new TaskParkedError(task.name, task.workstreamName);
7179
7248
  }
7249
+ if (task.status === "OPEN" && task.substate === "triage" && opts.force !== true) {
7250
+ throw new TaskInTriageError(task.name, task.workstreamName);
7251
+ }
7180
7252
  }
7181
7253
  async function resolveActorIdentity() {
7182
7254
  const worker = await resolveWorkerIdentity();
@@ -9597,6 +9669,15 @@ function murmurDoctorCheck(agentStateSource, opts = {}) {
9597
9669
  detail: version === null ? "agent state from murmur" : `agent state from murmur ${version}`
9598
9670
  };
9599
9671
  }
9672
+ function extFeatures(env) {
9673
+ const features = [
9674
+ ["mu_delegate", "MU_DELEGATE"],
9675
+ ["nudge", "MU_NUDGE"]
9676
+ ];
9677
+ const on = ["ctl", ...features.filter(([, v]) => env[v] !== "0").map(([f]) => f)];
9678
+ const off = features.filter(([, v]) => env[v] === "0").map(([f, v]) => `${f} disabled by ${v}=0`);
9679
+ return `(${on.join(", ")}${off.length ? `; ${off.join("; ")}` : ""})`;
9680
+ }
9600
9681
  function ctlExtensionDoctorCheck(opts) {
9601
9682
  const { extension } = inspectLinks(opts);
9602
9683
  switch (extension.state) {
@@ -9604,7 +9685,7 @@ function ctlExtensionDoctorCheck(opts) {
9604
9685
  return {
9605
9686
  name: "mu ext",
9606
9687
  status: "ok",
9607
- detail: process.env.MU_DELEGATE === "0" ? "(ctl; mu_delegate disabled by MU_DELEGATE=0)" : "(ctl, mu_delegate)"
9688
+ detail: extFeatures(process.env)
9608
9689
  };
9609
9690
  case "stale-copy":
9610
9691
  return { name: "mu ext", status: "warn", detail: `stale copy: ${RELINK}` };
@@ -10060,6 +10141,10 @@ async function loadWorkstreamSnapshotFast(db, workstream, opts = {}) {
10060
10141
  blocked: listBlocked(db, workstream),
10061
10142
  recentClosed: listRecentClosed(db, workstream),
10062
10143
  parkedCount: listTasks(db, workstream, { status: "OPEN", substate: "parked" }).length,
10144
+ triage: listTasks(db, workstream, { status: "OPEN", substate: "triage" }),
10145
+ taskCount: db.prepare(
10146
+ "SELECT count(*) AS n FROM tasks t JOIN workstreams w ON w.id = t.workstream_id WHERE w.name = ?"
10147
+ ).get(workstream).n,
10063
10148
  allTasks: opts.withAllTasks === true ? listTasks(db, workstream) : [],
10064
10149
  workspaces: listWorkspaces2(db, workstream),
10065
10150
  workspaceOrphans: listWorkspaceOrphans(db, workstream),
@@ -10133,6 +10218,8 @@ function minimalSnapshot(workstream) {
10133
10218
  blocked: [],
10134
10219
  recentClosed: [],
10135
10220
  parkedCount: 0,
10221
+ triage: [],
10222
+ taskCount: 0,
10136
10223
  allTasks: [],
10137
10224
  workspaces: [],
10138
10225
  workspaceOrphans: [],
@@ -11301,7 +11388,7 @@ function colorPair(pair) {
11301
11388
  const text2 = formatPair(pair);
11302
11389
  switch (pair.status) {
11303
11390
  case "OPEN":
11304
- return pair.substate === "parked" ? pc.dim(text2) : pc.cyan(text2);
11391
+ return pair.substate === "parked" || pair.substate === "triage" ? pc.dim(text2) : pc.cyan(text2);
11305
11392
  case "IN_PROGRESS":
11306
11393
  return pc.yellow(text2);
11307
11394
  case "CLOSED":
@@ -11321,7 +11408,7 @@ function colorStatus(status) {
11321
11408
  function inkColorForPair(pair) {
11322
11409
  switch (pair.status) {
11323
11410
  case "OPEN":
11324
- return pair.substate === "parked" ? "gray" : "cyan";
11411
+ return pair.substate === "parked" || pair.substate === "triage" ? "gray" : "cyan";
11325
11412
  case "IN_PROGRESS":
11326
11413
  return "yellow";
11327
11414
  case "CLOSED":
@@ -16514,6 +16601,7 @@ function yankCommandForTask2(t, ws) {
16514
16601
  switch (t.status) {
16515
16602
  case "OPEN":
16516
16603
  if (t.substate === "parked") return `mu task unpark ${t.name} -w ${ws}`;
16604
+ if (t.substate === "triage") return `mu task accept ${t.name} -w ${ws}`;
16517
16605
  return t.ownerName === null ? `mu task claim ${t.name} -w ${ws}` : `mu task release ${t.name} -w ${ws}`;
16518
16606
  case "IN_PROGRESS":
16519
16607
  return `mu task close ${t.name} -w ${ws} --evidence "..."`;
@@ -17573,6 +17661,7 @@ function snapshotKey(s) {
17573
17661
  blocked: s.blocked.map(taskKey2),
17574
17662
  recentClosed: s.recentClosed.map(taskKey2),
17575
17663
  parkedCount: s.parkedCount,
17664
+ triage: s.triage.map(taskKey2),
17576
17665
  allTasks: s.allTasks.map(taskKey2),
17577
17666
  commitsBackend: s.commitsBackend ?? null,
17578
17667
  recentCommits: s.recentCommits.map((c) => [c.sha, c.subject, c.author, c.relTime]),
@@ -18746,7 +18835,8 @@ async function cmdTaskAdd(db, localId, opts) {
18746
18835
  title: opts.title,
18747
18836
  impact: opts.impact,
18748
18837
  effortDays: opts.effortDays,
18749
- ...hasBlockers ? { blockedBy } : {}
18838
+ ...hasBlockers ? { blockedBy } : {},
18839
+ ...opts.triage === true ? { triage: true } : {}
18750
18840
  });
18751
18841
  const createdNote = opts.note !== void 0 ? addNote(db, createdTask.name, unescapeNoteText(opts.note), {
18752
18842
  author: initialNoteAuthor,
@@ -18770,7 +18860,7 @@ async function cmdTaskAdd(db, localId, opts) {
18770
18860
  intent: "Add a blocker",
18771
18861
  command: `mu task block ${task.name} --by <other-id> -w ${workstream}`
18772
18862
  },
18773
- {
18863
+ opts.triage === true ? { intent: "Accept it as work", command: `mu task accept ${task.name} -w ${workstream}` } : {
18774
18864
  intent: "Claim and start",
18775
18865
  command: `mu task claim ${task.name} -w ${workstream} --self (or --for <worker>)`
18776
18866
  }
@@ -18893,6 +18983,12 @@ async function cmdTaskShow(db, rawId, opts = {}) {
18893
18983
  if (task.substate === "parked") {
18894
18984
  console.log("");
18895
18985
  printNextSteps([{ intent: "Return it to next", command: `mu task unpark ${ref}` }]);
18986
+ } else if (task.substate === "triage") {
18987
+ console.log("");
18988
+ printNextSteps([
18989
+ { intent: "Accept it as work", command: `mu task accept ${ref}` },
18990
+ { intent: "Decline it", command: `mu task close ${ref} --as rejected --why "<reason>"` }
18991
+ ]);
18896
18992
  } else if (task.status === "CLOSED" && task.substate !== "done") {
18897
18993
  console.log("");
18898
18994
  printNextSteps([{ intent: "Reopen it", command: `mu task open ${ref}` }]);
@@ -19017,6 +19113,17 @@ async function cmdTaskList(db, opts) {
19017
19113
  const basis = relTimeBasisForSort(sortKey);
19018
19114
  if (basis !== null) tableOpts.relTimeBasis = basis;
19019
19115
  console.log(formatTaskListTable(tasks, tableOpts));
19116
+ const first = tasks.find((t) => t.status === "OPEN" && t.substate === "triage");
19117
+ if (first !== void 0) {
19118
+ printNextSteps([
19119
+ { intent: "Accept as work", command: `mu task accept ${first.name} -w ${workstream}` },
19120
+ {
19121
+ intent: "Decline",
19122
+ command: `mu task close ${first.name} --as rejected|duplicate --why "<reason>" -w ${workstream}`
19123
+ },
19124
+ { intent: "Read the evidence", command: `mu task notes ${first.name} -w ${workstream}` }
19125
+ ]);
19126
+ }
19020
19127
  }
19021
19128
  async function cmdTaskNext(db, opts) {
19022
19129
  const workstream = await resolveWorkstream(opts.workstream);
@@ -19037,16 +19144,26 @@ async function cmdTaskNext(db, opts) {
19037
19144
  }
19038
19145
  if (tasks.length === 0) {
19039
19146
  const parked = listTasks(db, workstream, { substate: "parked" }).length;
19040
- if (parked === 0) {
19147
+ const triage = listTasks(db, workstream, { substate: "triage" }).length;
19148
+ if (parked === 0 && triage === 0) {
19041
19149
  console.log(pc.dim("(no ready tasks)"));
19042
19150
  return;
19043
19151
  }
19044
- console.log(pc.dim(`(no ready tasks; ${parked} parked)`));
19152
+ const held = [parked > 0 ? `${parked} parked` : "", triage > 0 ? `${triage} in triage` : ""].filter(Boolean).join(", ");
19153
+ console.log(pc.dim(`(no ready tasks; ${held})`));
19045
19154
  printNextSteps([
19046
- {
19047
- intent: "List parked tasks",
19048
- command: `mu task list --substate parked -w ${workstream}`
19049
- }
19155
+ ...triage > 0 ? [
19156
+ {
19157
+ intent: "Triage findings",
19158
+ command: `mu task list --substate triage -w ${workstream}`
19159
+ }
19160
+ ] : [],
19161
+ ...parked > 0 ? [
19162
+ {
19163
+ intent: "List parked tasks",
19164
+ command: `mu task list --substate parked -w ${workstream}`
19165
+ }
19166
+ ] : []
19050
19167
  ]);
19051
19168
  return;
19052
19169
  }
@@ -19615,7 +19732,14 @@ async function cmdTaskClose(db, rawId, opts = {}) {
19615
19732
  const taskRow = getTask(db, localId, ws);
19616
19733
  const r = closeTask(db, localId, sdkOpts);
19617
19734
  if ("skipped" in r) {
19735
+ const inTriage = r.blockingIds.filter((id) => getTask(db, id, ws)?.substate === "triage");
19618
19736
  const blockingNextSteps = [
19737
+ ...inTriage.length > 0 ? [
19738
+ {
19739
+ intent: `Triage ${inTriage.length} blocking finding(s) (accept, or close --as rejected|duplicate)`,
19740
+ command: `mu task list --substate triage -w ${ws}`
19741
+ }
19742
+ ] : [],
19619
19743
  {
19620
19744
  intent: "Watch the remaining blockers (returns when one closes)",
19621
19745
  command: `mu task wait ${r.blockingIds.join(" ")} -w ${ws} --first --any --on-stall exit`
@@ -19748,6 +19872,32 @@ async function cmdTaskPark(db, rawId, opts) {
19748
19872
  }
19749
19873
  printNextSteps(nextSteps3);
19750
19874
  }
19875
+ async function cmdTaskAccept(db, rawId, opts = {}) {
19876
+ const { name: localId } = await resolveEntityRef(db, rawId, opts, "task");
19877
+ assertTaskInWorkstream(db, localId, opts.workstream);
19878
+ const ws = await resolveWorkstream(opts.workstream);
19879
+ const sdkOpts = { workstream: ws };
19880
+ if (opts.evidence !== void 0) sdkOpts.evidence = opts.evidence;
19881
+ const r = acceptTask(db, localId, sdkOpts);
19882
+ const nextSteps3 = [
19883
+ {
19884
+ intent: "Claim it",
19885
+ command: `mu task claim ${localId} -w ${ws} (--self / --for <worker>)`
19886
+ },
19887
+ { intent: "Triage the next one", command: `mu task list --substate triage -w ${ws}` }
19888
+ ];
19889
+ if (opts.json) {
19890
+ emitJson({ taskName: localId, ...r, nextSteps: nextSteps3 });
19891
+ return;
19892
+ }
19893
+ if (!r.changed) {
19894
+ const pair = formatPair({ status: r.status, substate: r.substate });
19895
+ console.log(pc.dim(`${localId} is ${pair}, not in triage (no-op)`));
19896
+ } else {
19897
+ console.log(`Accepted ${pc.bold(localId)} ${pc.dim(`(${transition(r)})`)}`);
19898
+ }
19899
+ printNextSteps(nextSteps3);
19900
+ }
19751
19901
  async function cmdTaskUnpark(db, rawId, opts = {}) {
19752
19902
  const { name: localId } = await resolveEntityRef(db, rawId, opts, "task");
19753
19903
  assertTaskInWorkstream(db, localId, opts.workstream);
@@ -19826,7 +19976,10 @@ function wireTaskCommands(program) {
19826
19976
  ).requiredOption("-t, --title <title>", "task title").requiredOption("-i, --impact <n>", "impact 1..100", parseImpact).requiredOption("-e, --effort-days <days>", "effort in days (>0)", parsePositiveNumber).option(
19827
19977
  "-b, --blocked-by <ids...>",
19828
19978
  "task ids that block this one (repeat or comma-separate; or both)"
19829
- ).option("--note <text>", "append an initial note after creating the task").option("--note-author <name>", "author label for --note (default: current actor)").option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(id) {
19979
+ ).option("--note <text>", "append an initial note after creating the task").option("--note-author <name>", "author label for --note (default: current actor)").option(
19980
+ "--triage",
19981
+ "start as OPEN/triage: a proposal (e.g. a review finding) kept out of next/claim until `mu task accept`"
19982
+ ).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(id) {
19830
19983
  const opts = this.opts();
19831
19984
  return handle((db) => cmdTaskAdd(db, id, opts), this)();
19832
19985
  });
@@ -19931,6 +20084,12 @@ function wireTaskCommands(program) {
19931
20084
  const opts = this.opts();
19932
20085
  return handle((db) => cmdTaskPark(db, id, opts), this)();
19933
20086
  });
20087
+ task.command("accept <id>").description(
20088
+ "Accept an OPEN/triage task (a proposed finding) as work: OPEN/todo, back in `next`. No-op on any other state. Decline with `close --as rejected|duplicate --why`."
20089
+ ).option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(id) {
20090
+ const opts = this.opts();
20091
+ return handle((db) => cmdTaskAccept(db, id, opts), this)();
20092
+ });
19934
20093
  task.command("unpark <id>").description("Return a parked task to OPEN (back in `next`). No-op on any other state.").option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(id) {
19935
20094
  const opts = this.opts();
19936
20095
  return handle((db) => cmdTaskUnpark(db, id, opts), this)();
@@ -19955,7 +20114,7 @@ function wireTaskCommands(program) {
19955
20114
  ).option(
19956
20115
  "--actor <name>",
19957
20116
  "override the actor name used for the log (only valid with --self; defaults to pane title or $USER)"
19958
- ).option("--force", "claim even when the task is parked (OPEN/parked)").option(
20117
+ ).option("--force", "claim even when the task is parked or in triage (OPEN/parked, OPEN/triage)").option(
19959
20118
  "--strict-staleness",
19960
20119
  "refuse --for dispatch when the target agent's workspace is stale (default: warn and proceed)"
19961
20120
  ).option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(taskId) {
@@ -20117,7 +20276,9 @@ function parseVia(raw) {
20117
20276
  }
20118
20277
  async function cmdSend(db, rawName, text2, opts = {}) {
20119
20278
  const via = parseVia(opts.via);
20120
- if (opts.force && !opts.fresh) throw new UsageError("--force only applies with --fresh");
20279
+ if (opts.force && !opts.fresh && parseSessionCommand(text2) === void 0) {
20280
+ throw new UsageError("--force only applies with --fresh or a /new, /reload, /compact");
20281
+ }
20121
20282
  if (opts.fresh && opts.steer) throw new UsageError("--fresh and --steer are mutually exclusive");
20122
20283
  const { name } = await resolveEntityRef(db, rawName, opts, "agent");
20123
20284
  assertAgentInWorkstream(db, name, opts.workstream);
@@ -20127,7 +20288,7 @@ async function cmdSend(db, rawName, text2, opts = {}) {
20127
20288
  });
20128
20289
  let undelivered;
20129
20290
  const agentRow = getAgent(db, name, ws);
20130
- const plainCtl = agentRow !== void 0 && !opts.fresh && !opts.steer && (via ?? chooseTransport(agentRow, text2, agentCtlSocket(db, agentRow))) === "ctl";
20291
+ const plainCtl = agentRow !== void 0 && !opts.fresh && !opts.steer && parseSessionCommand(text2) === void 0 && (via ?? chooseTransport(agentRow, agentCtlSocket(db, agentRow))) === "ctl";
20131
20292
  const before = plainCtl && agentRow !== void 0 ? await ctlProbe(agentCtlSocket(db, agentRow)) : void 0;
20132
20293
  const sent = await sendToAgent(db, name, text2, {
20133
20294
  workstream: ws,
@@ -20161,6 +20322,7 @@ async function cmdSend(db, rawName, text2, opts = {}) {
20161
20322
  sentBytes: text2.length,
20162
20323
  transport: sent.transport,
20163
20324
  fresh: opts.fresh === true,
20325
+ ...sent.command !== void 0 ? { command: sent.command } : {},
20164
20326
  ...sent.state !== void 0 ? { state: sent.state } : {},
20165
20327
  delivered: undelivered === void 0,
20166
20328
  ...undelivered !== void 0 ? { undelivered: { reason: undelivered.reason, message: undelivered.message } } : {},
@@ -20172,7 +20334,7 @@ async function cmdSend(db, rawName, text2, opts = {}) {
20172
20334
  if (undelivered !== void 0) {
20173
20335
  console.error(pc.yellow(`warning: ${undelivered.message}`));
20174
20336
  } else {
20175
- const how = sent.transport === "mux" ? "via mux paste" : opts.fresh ? "via ctl, fresh session" : `via ctl, pi ${sent.state ?? "?"}`;
20337
+ const how = sent.transport === "mux" ? "via mux paste" : opts.fresh ? "via ctl, fresh session" : sent.command !== void 0 ? `via ctl, pi ran /${sent.command}` : `via ctl, pi ${sent.state ?? "?"}`;
20176
20338
  console.log(pc.dim(`sent ${text2.length} bytes to ${name} (${how})`));
20177
20339
  }
20178
20340
  printNextSteps(nextSteps3);
@@ -20613,14 +20775,17 @@ function wireAgentCommands(program) {
20613
20775
  return handle((db) => cmdSpawn(db, name, opts), this)();
20614
20776
  });
20615
20777
  agent.command("send <name> <text>").description(
20616
- "Send text to an agent. pi agents: through the control socket (fails loud if it does not answer; no paste fallback). Non-pi CLIs and text starting with '/' (slash commands such as /new): pasted into the pane (atomic on herdr; bracketed-paste on tmux). --fresh (pi only): start a new session and send the text into it as one operation; returns once the prompt's run has started, so an immediate next send cannot be lost. Use it instead of sending '/new' then the prompt"
20778
+ "Send text to an agent. pi agents: through the control socket (fails loud if it does not answer; no paste fallback); '/new', '/reload' and '/compact [instructions]' run inside pi over the socket (refused while busy unless --force), any other slash command is refused (--via mux types it into the pane). Non-pi CLIs: pasted into the pane (atomic on herdr; bracketed-paste on tmux). --fresh (pi only): start a new session and send the text into it as one operation; returns once the prompt's run has started, so an immediate next send cannot be lost. Use it instead of sending '/new' then the prompt"
20617
20779
  ).option(
20618
20780
  "--strict-staleness",
20619
20781
  "refuse send when the target agent's workspace is stale (default: warn and proceed)"
20620
20782
  ).option("--steer", "if pi is busy, interrupt the current run (default: queue as a follow-up)").option("--via <transport>", "force the transport: ctl or mux (mux = paste into the pane)").option(
20621
20783
  "--fresh",
20622
20784
  "pi only: new session + this prompt, one operation (refused with exit 4 while pi is busy)"
20623
- ).option("--force", "with --fresh: abandon a running turn instead of refusing").option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(name, text2) {
20785
+ ).option(
20786
+ "--force",
20787
+ "with --fresh or a pi /new, /reload, /compact: abandon a running turn instead of refusing"
20788
+ ).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(name, text2) {
20624
20789
  const opts = this.opts();
20625
20790
  return handle((db) => cmdSend(db, name, text2, opts), this)();
20626
20791
  });
@@ -21348,7 +21513,8 @@ var NameAmbiguousError = class extends Error {
21348
21513
  function classifyError(err) {
21349
21514
  if (err instanceof UsageError || // abort on a non-pi agent: the operator picked the wrong verb (kick).
21350
21515
  err instanceof AgentAbortNeedsCtlError || // send --fresh on an agent with no control socket: wrong recipe.
21351
- err instanceof AgentFreshNeedsCtlError || err instanceof WorkstreamNameInvalidError || err instanceof WorkstreamNameReservedError || // The operator asked the ACTIVE backend for something it cannot do:
21516
+ err instanceof AgentFreshNeedsCtlError || // an unsupported slash command to a pi agent: --via mux is the recipe.
21517
+ err instanceof AgentSlashCommandUnsupportedError || err instanceof WorkstreamNameInvalidError || err instanceof WorkstreamNameReservedError || // The operator asked the ACTIVE backend for something it cannot do:
21352
21518
  // a `--cli` the mux does not recognise as an agent kind, or a
21353
21519
  // `--command` / `MU_<CLI>_COMMAND` override it cannot honour. The
21354
21520
  // substrate is healthy and answered precisely, so this is the usage
@@ -21365,7 +21531,7 @@ function classifyError(err) {
21365
21531
  }
21366
21532
  if (err instanceof NameAmbiguousError || err instanceof AgentExistsError || err instanceof TaskExistsError || err instanceof TaskAlreadyOwnedError || // Substate refusals (ts_4): a missing --why, a park from the wrong
21367
21533
  // phase, a claim on a parked task, a pair outside TASK_SUBSTATES.
21368
- err instanceof SubstateReasonRequiredError || err instanceof TaskParkStateError || err instanceof TaskParkedError || err instanceof InvalidSubstateError || err instanceof TaskClaimStaleWorkspaceError || err instanceof TaskNotInWorkstreamError || err instanceof AgentNotInWorkstreamError || err instanceof CycleError || err instanceof CrossWorkstreamEdgeError || err instanceof WorkspaceExistsError || err instanceof WorkspacePathNotEmptyError || err instanceof WorkspacePreservedError || err instanceof HomeDirAsProjectRootError || err instanceof WorkspaceVcsRequiredError || err instanceof WorkspaceDirtyError || err instanceof ClaimerNotRegisteredError || err instanceof SchemaTooOldError || err instanceof SchemaTooNewError || err instanceof TaskIdInvalidError || err instanceof WorkstreamExistsError || // Rebuild refuses to overwrite an existing target or to write onto
21534
+ err instanceof SubstateReasonRequiredError || err instanceof TaskParkStateError || err instanceof TaskParkedError || err instanceof TaskInTriageError || err instanceof InvalidSubstateError || err instanceof TaskClaimStaleWorkspaceError || err instanceof TaskNotInWorkstreamError || err instanceof AgentNotInWorkstreamError || err instanceof CycleError || err instanceof CrossWorkstreamEdgeError || err instanceof WorkspaceExistsError || err instanceof WorkspacePathNotEmptyError || err instanceof WorkspacePreservedError || err instanceof HomeDirAsProjectRootError || err instanceof WorkspaceVcsRequiredError || err instanceof WorkspaceDirtyError || err instanceof ClaimerNotRegisteredError || err instanceof SchemaTooOldError || err instanceof SchemaTooNewError || err instanceof TaskIdInvalidError || err instanceof WorkstreamExistsError || // Rebuild refuses to overwrite an existing target or to write onto
21369
21535
  // the source DB: both are name/state collisions, not usage errors.
21370
21536
  err instanceof RebuildTargetExistsError || err instanceof RebuildTargetIsSourceError || // An abbreviated group id matching several groups: the operator must
21371
21537
  // disambiguate. Guessing would be catastrophic for `mu undo`.
@@ -22990,6 +23156,7 @@ function fullJsonShape(d) {
22990
23156
  orphans: d.view.orphans,
22991
23157
  tracks: d.tracks,
22992
23158
  ready: withRoiAll(d.ready),
23159
+ triage: withRoiAll(d.triage),
22993
23160
  inProgress: withRoiAll(d.inProgress),
22994
23161
  blocked: withRoiAll(d.blocked),
22995
23162
  recentClosed: withRoiAll(d.recentClosed),
@@ -23062,6 +23229,7 @@ async function cmdState(db, opts) {
23062
23229
  }
23063
23230
  renderFullMode(perWs);
23064
23231
  }
23232
+ var BIG_WORKSTREAM_TASKS = 300;
23065
23233
  function renderFullMode(perWs) {
23066
23234
  perWs.forEach((d, i) => {
23067
23235
  if (i > 0) console.log("");
@@ -23094,6 +23262,15 @@ function renderFullCard(d) {
23094
23262
  );
23095
23263
  }
23096
23264
  }
23265
+ const total = d.taskCount;
23266
+ const open = ready.length + inProgress.length + blocked.length + d.triage.length;
23267
+ if (total >= BIG_WORKSTREAM_TASKS && open * 10 < total) {
23268
+ console.log(
23269
+ pc.dim(
23270
+ `hint: ${total} tasks, ${open} open; start the next effort in a new <project>-<purpose> workstream`
23271
+ )
23272
+ );
23273
+ }
23097
23274
  console.log("");
23098
23275
  console.log(pc.bold(`Tracks (${tracks.length})`));
23099
23276
  console.log(formatTracks(tracks));
@@ -23101,6 +23278,13 @@ function renderFullCard(d) {
23101
23278
  console.log(pc.bold(`Ready (${ready.length})`));
23102
23279
  console.log(ready.length === 0 ? pc.dim(" (none)") : formatTaskListTable(ready));
23103
23280
  console.log("");
23281
+ if (d.triage.length > 0) {
23282
+ console.log(
23283
+ `${pc.bold(`Triage (${d.triage.length})`)} ${pc.dim(`accept: mu task accept <id> -w ${workstreamName} \xB7 decline: mu task close <id> --as rejected|duplicate --why ...`)}`
23284
+ );
23285
+ console.log(formatTaskListTable(d.triage));
23286
+ console.log("");
23287
+ }
23104
23288
  console.log(pc.bold(`In progress (${inProgress.length})`));
23105
23289
  console.log(inProgress.length === 0 ? pc.dim(" (none)") : formatTaskListTable(inProgress));
23106
23290
  console.log("");
@@ -23706,6 +23890,7 @@ function wireWorkspaceCommands(program) {
23706
23890
  }
23707
23891
 
23708
23892
  // src/cli/workstream.ts
23893
+ import { basename as basename2 } from "path";
23709
23894
  init_mux2();
23710
23895
  init_output();
23711
23896
  init_workstream();
@@ -23734,6 +23919,7 @@ async function cmdInit(db, name, opts = {}) {
23734
23919
  await mux.enableMuPaneBordersForSession(sessionName).catch(() => {
23735
23920
  });
23736
23921
  const created = !sessionAlready || dbCreated;
23922
+ const nameHint = name.includes("-") ? void 0 : `name workstreams <project>-<purpose> (e.g. ${projectGuess() ?? "repo"}-${name}); one per effort`;
23737
23923
  const nextSteps3 = [
23738
23924
  { intent: "Attach the session", command: mux.attachHint({ session: sessionName }) },
23739
23925
  {
@@ -23751,6 +23937,7 @@ async function cmdInit(db, name, opts = {}) {
23751
23937
  muxSessionAlreadyExisted: sessionAlready,
23752
23938
  dbRowAlreadyExisted: !dbCreated,
23753
23939
  muWindowRepaired,
23940
+ ...nameHint && created ? { hint: nameHint } : {},
23754
23941
  nextSteps: nextSteps3
23755
23942
  });
23756
23943
  return;
@@ -23766,8 +23953,13 @@ async function cmdInit(db, name, opts = {}) {
23766
23953
  return;
23767
23954
  }
23768
23955
  console.log(`Created workstream ${pc.bold(name)} (mux session ${pc.bold(sessionName)})`);
23956
+ if (nameHint && created) console.log(pc.dim(`hint: ${nameHint}`));
23769
23957
  printNextSteps(nextSteps3);
23770
23958
  }
23959
+ function projectGuess() {
23960
+ const base = basename2(process.cwd()).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^[^a-z]+/, "").replace(/-+$/, "");
23961
+ return base === "" ? void 0 : base;
23962
+ }
23771
23963
  async function cmdWorkstreamList(db, opts = {}) {
23772
23964
  if (opts.tornDown === true) {
23773
23965
  const gone = listTornDownWorkstreams(db);
@@ -24023,8 +24215,12 @@ function withPositionalWorkstream(opts, name) {
24023
24215
  return { ...opts, workstream: name };
24024
24216
  }
24025
24217
  function wireWorkstreamCommands(program) {
24026
- const workstream = program.command("workstream").description("Workstream-level commands");
24027
- workstream.command("init <name>").description("Create the workstream's mux session and register it in the DB").option(...JSON_OPT).action(function(name) {
24218
+ const workstream = program.command("workstream").description(
24219
+ "Workstream-level commands. Name them <project>-<purpose> (hail-auth), one per effort; tear down when it ships. Reviews nobody will track belong in scratch delegates, not a workstream."
24220
+ );
24221
+ workstream.command("init <name>").description(
24222
+ "Create the workstream's mux session and register it in the DB. Name it <project>-<purpose>."
24223
+ ).option(...JSON_OPT).action(function(name) {
24028
24224
  const opts = this.opts();
24029
24225
  return handle((db) => cmdInit(db, name, opts), this)();
24030
24226
  });