@mu-crew/mu 3.3.0 → 3.5.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 (45) hide show
  1. package/README.md +45 -31
  2. package/dist/cli.js +812 -429
  3. package/dist/cli.js.map +1 -1
  4. package/dist/extension/mu-pi.js +221 -55
  5. package/dist/extension/mu-pi.js.map +1 -1
  6. package/dist/index.d.ts +33 -4
  7. package/dist/index.js +76 -5
  8. package/dist/index.js.map +1 -1
  9. package/docs/VISION.md +5 -1
  10. package/docs/VOCABULARY.md +4 -1
  11. package/docs/architecture/dag.md +3 -3
  12. package/docs/architecture/ops-log.md +26 -7
  13. package/docs/guide/cleanup.md +19 -0
  14. package/docs/guide/delegate.md +43 -4
  15. package/docs/reference/env.md +1 -0
  16. package/docs/reference/naming.md +18 -0
  17. package/docs/specs/2026-09-29-task-substates.md +1 -1
  18. package/package.json +1 -1
  19. package/prompts/mu-debug.md +8 -0
  20. package/prompts/mu-sweep.md +8 -0
  21. package/prompts/mu-until.md +8 -0
  22. package/prompts/ultrathink.md +2 -1
  23. package/skills/mu/SKILL.md +70 -31
  24. package/skills/mu/recipes/adversarial-review.md +27 -19
  25. package/skills/mu/recipes/{triage.md → backlog-triage.md} +9 -4
  26. package/skills/mu/recipes/codemode-driver.md +68 -20
  27. package/skills/mu/recipes/deep-research.md +23 -8
  28. package/skills/mu/recipes/fan-out.md +15 -11
  29. package/skills/mu/recipes/findings.md +103 -0
  30. package/skills/mu/recipes/hypothesis-panel.md +18 -13
  31. package/skills/mu/recipes/long-run.md +4 -4
  32. package/skills/mu/recipes/loop-until-done.md +7 -3
  33. package/skills/mu/recipes/orchestrator-loop.md +44 -27
  34. package/skills/mu/recipes/plan.md +8 -3
  35. package/skills/mu/recipes/recovery.md +5 -0
  36. package/skills/mu/recipes/refute.md +38 -36
  37. package/skills/mu/recipes/remote-workers.md +7 -2
  38. package/skills/mu/recipes/review-panel.md +36 -31
  39. package/skills/mu/recipes/rules-audit.md +30 -17
  40. package/skills/mu/recipes/tasks-or-calls.md +71 -0
  41. package/skills/mu/recipes/tournament.md +13 -10
  42. package/skills/mu/recipes/ultrathink.md +20 -11
  43. package/skills/mu/recipes/watcher.md +4 -1
  44. package/skills/mu/recipes/waves.md +6 -5
  45. package/skills/mu/recipes/worker.md +3 -1
package/dist/cli.js CHANGED
@@ -175,6 +175,11 @@ function fullOldPayload(cols) {
175
175
  const pairs = cols.map((c) => `'${c}', OLD.${c}`).join(", ");
176
176
  return `json_object(${pairs})`;
177
177
  }
178
+ function noteTombstonePayload(keyExpr, cols) {
179
+ return `CASE WHEN EXISTS (
180
+ SELECT 1 FROM ops WHERE entity = 'note' AND key = ${keyExpr} AND op = 'put'
181
+ ) THEN '{}' ELSE ${fullOldPayload(cols)} END`;
182
+ }
178
183
  function anyChanged(cols) {
179
184
  return cols.map((c) => `NEW.${c} IS NOT OLD.${c}`).join("\n OR ");
180
185
  }
@@ -258,7 +263,7 @@ END;
258
263
  CREATE TEMP TRIGGER IF NOT EXISTS _cap_task_notes_del
259
264
  BEFORE DELETE ON task_notes WHEN ${NOT_APPLYING}
260
265
  BEGIN
261
- ${emitOp("note", "del", `${taskKey("OLD.task_id")} || '#' || OLD.id`, fullOldPayload(noteCols))}
266
+ ${emitOp("note", "del", `${taskKey("OLD.task_id")} || '#' || OLD.id`, noteTombstonePayload(`${taskKey("OLD.task_id")} || '#' || OLD.id`, noteCols))}
262
267
  END;
263
268
 
264
269
  -- \u2500\u2500\u2500 task_edges \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -393,7 +398,7 @@ var init_status = __esm({
393
398
  TASK_STATUSES = ["OPEN", "IN_PROGRESS", "CLOSED"];
394
399
  TASK_STATUS_LIST = TASK_STATUSES.join(" | ");
395
400
  TASK_SUBSTATES = {
396
- OPEN: ["todo", "parked"],
401
+ OPEN: ["todo", "parked", "triage"],
397
402
  IN_PROGRESS: ["active"],
398
403
  CLOSED: ["done", "rejected", "wontfix", "duplicate", "superseded"]
399
404
  };
@@ -607,7 +612,7 @@ CREATE VIEW ready AS
607
612
  SELECT t.*
608
613
  FROM tasks t
609
614
  WHERE t.status = 'OPEN'
610
- AND t.substate <> 'parked'
615
+ AND t.substate NOT IN ('parked', 'triage')
611
616
  AND NOT EXISTS (
612
617
  SELECT 1
613
618
  FROM task_edges e
@@ -5850,7 +5855,7 @@ var init_id = __esm({
5850
5855
  });
5851
5856
 
5852
5857
  // src/tasks/errors.ts
5853
- var TaskNotFoundError, TaskIdInvalidError, TaskExistsError, TaskNotInWorkstreamError, TaskAlreadyOwnedError, ClaimerNotRegisteredError, TaskClaimStaleWorkspaceError, CycleError, ReaperDetectedDuringWaitError, StallDetectedDuringWaitError, CrossWorkstreamEdgeError, SubstateReasonRequiredError, TaskParkStateError, TaskParkedError, InvalidSubstateError;
5858
+ var TaskNotFoundError, TaskIdInvalidError, TaskExistsError, TaskNotInWorkstreamError, TaskAlreadyOwnedError, ClaimerNotRegisteredError, TaskClaimStaleWorkspaceError, CycleError, ReaperDetectedDuringWaitError, StallDetectedDuringWaitError, CrossWorkstreamEdgeError, SubstateReasonRequiredError, TaskParkStateError, TaskInTriageError, TaskParkedError, InvalidSubstateError;
5854
5859
  var init_errors2 = __esm({
5855
5860
  "src/tasks/errors.ts"() {
5856
5861
  "use strict";
@@ -6198,6 +6203,30 @@ var init_errors2 = __esm({
6198
6203
  ];
6199
6204
  }
6200
6205
  };
6206
+ TaskInTriageError = class extends Error {
6207
+ constructor(taskId, workstream) {
6208
+ super(`task ${taskId} is in triage (OPEN/triage); accept it or claim with --force`);
6209
+ this.taskId = taskId;
6210
+ this.workstream = workstream;
6211
+ }
6212
+ taskId;
6213
+ workstream;
6214
+ name = "TaskInTriageError";
6215
+ errorNextSteps() {
6216
+ const ws = this.workstream;
6217
+ return [
6218
+ {
6219
+ intent: "Accept it as work, then claim",
6220
+ command: `mu task accept ${this.taskId} -w ${ws}`
6221
+ },
6222
+ {
6223
+ intent: "Decline it",
6224
+ command: `mu task close ${this.taskId} --as rejected --why "<reason>" -w ${ws}`
6225
+ },
6226
+ { intent: "Claim it anyway", command: `mu task claim ${this.taskId} --force -w ${ws}` }
6227
+ ];
6228
+ }
6229
+ };
6201
6230
  TaskParkedError = class extends Error {
6202
6231
  constructor(taskId, workstream) {
6203
6232
  super(`task ${taskId} is parked (OPEN/parked); unpark it or claim with --force`);
@@ -6760,8 +6789,17 @@ function addTaskImpl(db, opts) {
6760
6789
  const now = (/* @__PURE__ */ new Date()).toISOString();
6761
6790
  const insertResult = db.prepare(
6762
6791
  `INSERT INTO tasks (workstream_id, local_id, title, status, substate, impact, effort_days, created_at, updated_at)
6763
- VALUES (?, ?, ?, 'OPEN', 'todo', ?, ?, ?, ?)`
6764
- ).run(wsId, opts.localId, opts.title, opts.impact, opts.effortDays, now, now);
6792
+ VALUES (?, ?, ?, 'OPEN', ?, ?, ?, ?, ?)`
6793
+ ).run(
6794
+ wsId,
6795
+ opts.localId,
6796
+ opts.title,
6797
+ opts.triage === true ? "triage" : "todo",
6798
+ opts.impact,
6799
+ opts.effortDays,
6800
+ now,
6801
+ now
6802
+ );
6765
6803
  const newTaskId = Number(insertResult.lastInsertRowid);
6766
6804
  if (opts.blockedBy && opts.blockedBy.length > 0) {
6767
6805
  const blockerLookupSameWs = db.prepare(
@@ -7049,6 +7087,28 @@ function parkTask(db, localId, opts) {
7049
7087
  })()
7050
7088
  );
7051
7089
  }
7090
+ function acceptTask(db, localId, opts) {
7091
+ return withOpContext(
7092
+ db,
7093
+ { intent: "task.accept", group: "new" },
7094
+ () => db.transaction(() => {
7095
+ const before = getTask(db, localId, opts.workstream);
7096
+ if (!before) throw new TaskNotFoundError(localId);
7097
+ if (before.status !== "OPEN" || before.substate !== "triage") {
7098
+ return {
7099
+ previousStatus: before.status,
7100
+ status: before.status,
7101
+ previousSubstate: before.substate,
7102
+ substate: before.substate,
7103
+ changed: false
7104
+ };
7105
+ }
7106
+ const r = setTaskStatus(db, localId, "OPEN", { workstream: opts.workstream });
7107
+ recordEvidenceNote(db, localId, before.workstreamName, "ACCEPT", opts);
7108
+ return r;
7109
+ })()
7110
+ );
7111
+ }
7052
7112
  function unparkTask(db, localId, opts) {
7053
7113
  return withOpContext(
7054
7114
  db,
@@ -7191,6 +7251,9 @@ function assertNotParked(task, opts) {
7191
7251
  if (task.status === "OPEN" && task.substate === "parked" && opts.force !== true) {
7192
7252
  throw new TaskParkedError(task.name, task.workstreamName);
7193
7253
  }
7254
+ if (task.status === "OPEN" && task.substate === "triage" && opts.force !== true) {
7255
+ throw new TaskInTriageError(task.name, task.workstreamName);
7256
+ }
7194
7257
  }
7195
7258
  async function resolveActorIdentity() {
7196
7259
  const worker = await resolveWorkerIdentity();
@@ -7562,7 +7625,10 @@ var init_adopt = __esm({
7562
7625
  // src/agents/delegate.ts
7563
7626
  function delegateOutcome(agent, timedOut) {
7564
7627
  if (agent.dead) return "died";
7565
- if (agent.fired) return agent.lastText ? "done" : "empty";
7628
+ if (agent.fired) {
7629
+ if (agent.lastError !== void 0) return "error";
7630
+ return agent.lastText ? "done" : "empty";
7631
+ }
7566
7632
  return timedOut ? "timeout" : "pending";
7567
7633
  }
7568
7634
  var init_delegate = __esm({
@@ -7775,6 +7841,7 @@ async function waitForAgents(db, input, opts) {
7775
7841
  st.wasBusy = true;
7776
7842
  st.fired = true;
7777
7843
  if (snap.lastText !== void 0) st.lastText = snap.lastText;
7844
+ if (snap.lastError !== void 0) st.lastError = snap.lastError;
7778
7845
  }
7779
7846
  wake();
7780
7847
  },
@@ -10083,6 +10150,10 @@ async function loadWorkstreamSnapshotFast(db, workstream, opts = {}) {
10083
10150
  blocked: listBlocked(db, workstream),
10084
10151
  recentClosed: listRecentClosed(db, workstream),
10085
10152
  parkedCount: listTasks(db, workstream, { status: "OPEN", substate: "parked" }).length,
10153
+ triage: listTasks(db, workstream, { status: "OPEN", substate: "triage" }),
10154
+ taskCount: db.prepare(
10155
+ "SELECT count(*) AS n FROM tasks t JOIN workstreams w ON w.id = t.workstream_id WHERE w.name = ?"
10156
+ ).get(workstream).n,
10086
10157
  allTasks: opts.withAllTasks === true ? listTasks(db, workstream) : [],
10087
10158
  workspaces: listWorkspaces2(db, workstream),
10088
10159
  workspaceOrphans: listWorkspaceOrphans(db, workstream),
@@ -10156,6 +10227,8 @@ function minimalSnapshot(workstream) {
10156
10227
  blocked: [],
10157
10228
  recentClosed: [],
10158
10229
  parkedCount: 0,
10230
+ triage: [],
10231
+ taskCount: 0,
10159
10232
  allTasks: [],
10160
10233
  workspaces: [],
10161
10234
  workspaceOrphans: [],
@@ -10336,13 +10409,13 @@ import {
10336
10409
  appendFileSync,
10337
10410
  existsSync as existsSync14,
10338
10411
  mkdirSync as mkdirSync5,
10339
- readdirSync as readdirSync3,
10412
+ readdirSync as readdirSync4,
10340
10413
  readFileSync as readFileSync3,
10341
- statSync as statSync2,
10414
+ statSync as statSync3,
10342
10415
  writeFileSync as writeFileSync2
10343
10416
  } from "fs";
10344
10417
  import { tmpdir as tmpdir2 } from "os";
10345
- import { join as join12, resolve as resolve6, sep as sep2 } from "path";
10418
+ import { join as join13, resolve as resolve6, sep as sep2 } from "path";
10346
10419
  import { crc32 } from "zlib";
10347
10420
  function syncDir() {
10348
10421
  const dir = process.env.MU_SYNC_DIR;
@@ -10366,7 +10439,7 @@ function localMachineId(db) {
10366
10439
  return row2.machine_id;
10367
10440
  }
10368
10441
  function segmentPath(dir, machineId2) {
10369
- return join12(dir, `${machineId2}${SEGMENT_EXT}`);
10442
+ return join13(dir, `${machineId2}${SEGMENT_EXT}`);
10370
10443
  }
10371
10444
  function manifestPath(segment) {
10372
10445
  return segment.replace(new RegExp(`${SEGMENT_EXT}$`), MANIFEST_EXT);
@@ -10510,7 +10583,7 @@ async function flushSegment(db, dir = syncDir()) {
10510
10583
  const path2 = segmentPath(dir, machineId2);
10511
10584
  const lockName = createHash2("sha256").update(`${dir}${machineId2}`).digest("hex").slice(0, 16);
10512
10585
  return withFileLock(
10513
- join12(locksDir(), `segment-${lockName}.lock`),
10586
+ join13(locksDir(), `segment-${lockName}.lock`),
10514
10587
  `segment:${machineId2}`,
10515
10588
  () => Promise.resolve(flushLocked(db, path2, machineId2))
10516
10589
  );
@@ -10632,16 +10705,16 @@ function verifyAgainstManifest(segment) {
10632
10705
  function discoverPeers(dir, selfMachineId) {
10633
10706
  if (!existsSync14(dir)) return [];
10634
10707
  const peers = [];
10635
- for (const name of readdirSync3(dir)) {
10708
+ for (const name of readdirSync4(dir)) {
10636
10709
  if (!name.endsWith(SEGMENT_EXT)) continue;
10637
10710
  const stem = name.slice(0, -SEGMENT_EXT.length);
10638
10711
  const conflictAt = stem.indexOf(".sync-conflict-");
10639
10712
  const conflictCopy = conflictAt > 0;
10640
10713
  const machineId2 = conflictCopy ? stem.slice(0, conflictAt) : stem;
10641
10714
  if (machineId2 === selfMachineId && !conflictCopy) continue;
10642
- const path2 = join12(dir, name);
10715
+ const path2 = join13(dir, name);
10643
10716
  try {
10644
- if (!statSync2(path2).isFile()) continue;
10717
+ if (!statSync3(path2).isFile()) continue;
10645
10718
  } catch {
10646
10719
  continue;
10647
10720
  }
@@ -10846,7 +10919,7 @@ var init_segments = __esm({
10846
10919
  });
10847
10920
 
10848
10921
  // src/sync.ts
10849
- import { existsSync as existsSync15, statSync as statSync3 } from "fs";
10922
+ import { existsSync as existsSync15, statSync as statSync4 } from "fs";
10850
10923
  import Database2 from "better-sqlite3";
10851
10924
  function peerStatuses(db, dir) {
10852
10925
  const self = localMachineId(db);
@@ -10854,7 +10927,7 @@ function peerStatuses(db, dir) {
10854
10927
  const rows = discoverPeers(dir, self).map((peer) => {
10855
10928
  let lastSeenMs = null;
10856
10929
  try {
10857
- lastSeenMs = statSync3(peer.path).mtimeMs;
10930
+ lastSeenMs = statSync4(peer.path).mtimeMs;
10858
10931
  } catch {
10859
10932
  lastSeenMs = null;
10860
10933
  }
@@ -11324,7 +11397,7 @@ function colorPair(pair) {
11324
11397
  const text2 = formatPair(pair);
11325
11398
  switch (pair.status) {
11326
11399
  case "OPEN":
11327
- return pair.substate === "parked" ? pc.dim(text2) : pc.cyan(text2);
11400
+ return pair.substate === "parked" || pair.substate === "triage" ? pc.dim(text2) : pc.cyan(text2);
11328
11401
  case "IN_PROGRESS":
11329
11402
  return pc.yellow(text2);
11330
11403
  case "CLOSED":
@@ -11344,7 +11417,7 @@ function colorStatus(status) {
11344
11417
  function inkColorForPair(pair) {
11345
11418
  switch (pair.status) {
11346
11419
  case "OPEN":
11347
- return pair.substate === "parked" ? "gray" : "cyan";
11420
+ return pair.substate === "parked" || pair.substate === "triage" ? "gray" : "cyan";
11348
11421
  case "IN_PROGRESS":
11349
11422
  return "yellow";
11350
11423
  case "CLOSED":
@@ -16537,6 +16610,7 @@ function yankCommandForTask2(t, ws) {
16537
16610
  switch (t.status) {
16538
16611
  case "OPEN":
16539
16612
  if (t.substate === "parked") return `mu task unpark ${t.name} -w ${ws}`;
16613
+ if (t.substate === "triage") return `mu task accept ${t.name} -w ${ws}`;
16540
16614
  return t.ownerName === null ? `mu task claim ${t.name} -w ${ws}` : `mu task release ${t.name} -w ${ws}`;
16541
16615
  case "IN_PROGRESS":
16542
16616
  return `mu task close ${t.name} -w ${ws} --evidence "..."`;
@@ -17596,6 +17670,7 @@ function snapshotKey(s) {
17596
17670
  blocked: s.blocked.map(taskKey2),
17597
17671
  recentClosed: s.recentClosed.map(taskKey2),
17598
17672
  parkedCount: s.parkedCount,
17673
+ triage: s.triage.map(taskKey2),
17599
17674
  allTasks: s.allTasks.map(taskKey2),
17600
17675
  commitsBackend: s.commitsBackend ?? null,
17601
17676
  recentCommits: s.recentCommits.map((c) => [c.sha, c.subject, c.author, c.relTime]),
@@ -18769,7 +18844,8 @@ async function cmdTaskAdd(db, localId, opts) {
18769
18844
  title: opts.title,
18770
18845
  impact: opts.impact,
18771
18846
  effortDays: opts.effortDays,
18772
- ...hasBlockers ? { blockedBy } : {}
18847
+ ...hasBlockers ? { blockedBy } : {},
18848
+ ...opts.triage === true ? { triage: true } : {}
18773
18849
  });
18774
18850
  const createdNote = opts.note !== void 0 ? addNote(db, createdTask.name, unescapeNoteText(opts.note), {
18775
18851
  author: initialNoteAuthor,
@@ -18793,7 +18869,7 @@ async function cmdTaskAdd(db, localId, opts) {
18793
18869
  intent: "Add a blocker",
18794
18870
  command: `mu task block ${task.name} --by <other-id> -w ${workstream}`
18795
18871
  },
18796
- {
18872
+ opts.triage === true ? { intent: "Accept it as work", command: `mu task accept ${task.name} -w ${workstream}` } : {
18797
18873
  intent: "Claim and start",
18798
18874
  command: `mu task claim ${task.name} -w ${workstream} --self (or --for <worker>)`
18799
18875
  }
@@ -18916,6 +18992,12 @@ async function cmdTaskShow(db, rawId, opts = {}) {
18916
18992
  if (task.substate === "parked") {
18917
18993
  console.log("");
18918
18994
  printNextSteps([{ intent: "Return it to next", command: `mu task unpark ${ref}` }]);
18995
+ } else if (task.substate === "triage") {
18996
+ console.log("");
18997
+ printNextSteps([
18998
+ { intent: "Accept it as work", command: `mu task accept ${ref}` },
18999
+ { intent: "Decline it", command: `mu task close ${ref} --as rejected --why "<reason>"` }
19000
+ ]);
18919
19001
  } else if (task.status === "CLOSED" && task.substate !== "done") {
18920
19002
  console.log("");
18921
19003
  printNextSteps([{ intent: "Reopen it", command: `mu task open ${ref}` }]);
@@ -19040,6 +19122,17 @@ async function cmdTaskList(db, opts) {
19040
19122
  const basis = relTimeBasisForSort(sortKey);
19041
19123
  if (basis !== null) tableOpts.relTimeBasis = basis;
19042
19124
  console.log(formatTaskListTable(tasks, tableOpts));
19125
+ const first = tasks.find((t) => t.status === "OPEN" && t.substate === "triage");
19126
+ if (first !== void 0) {
19127
+ printNextSteps([
19128
+ { intent: "Accept as work", command: `mu task accept ${first.name} -w ${workstream}` },
19129
+ {
19130
+ intent: "Decline",
19131
+ command: `mu task close ${first.name} --as rejected|duplicate --why "<reason>" -w ${workstream}`
19132
+ },
19133
+ { intent: "Read the evidence", command: `mu task notes ${first.name} -w ${workstream}` }
19134
+ ]);
19135
+ }
19043
19136
  }
19044
19137
  async function cmdTaskNext(db, opts) {
19045
19138
  const workstream = await resolveWorkstream(opts.workstream);
@@ -19060,16 +19153,26 @@ async function cmdTaskNext(db, opts) {
19060
19153
  }
19061
19154
  if (tasks.length === 0) {
19062
19155
  const parked = listTasks(db, workstream, { substate: "parked" }).length;
19063
- if (parked === 0) {
19156
+ const triage = listTasks(db, workstream, { substate: "triage" }).length;
19157
+ if (parked === 0 && triage === 0) {
19064
19158
  console.log(pc.dim("(no ready tasks)"));
19065
19159
  return;
19066
19160
  }
19067
- console.log(pc.dim(`(no ready tasks; ${parked} parked)`));
19161
+ const held = [parked > 0 ? `${parked} parked` : "", triage > 0 ? `${triage} in triage` : ""].filter(Boolean).join(", ");
19162
+ console.log(pc.dim(`(no ready tasks; ${held})`));
19068
19163
  printNextSteps([
19069
- {
19070
- intent: "List parked tasks",
19071
- command: `mu task list --substate parked -w ${workstream}`
19072
- }
19164
+ ...triage > 0 ? [
19165
+ {
19166
+ intent: "Triage findings",
19167
+ command: `mu task list --substate triage -w ${workstream}`
19168
+ }
19169
+ ] : [],
19170
+ ...parked > 0 ? [
19171
+ {
19172
+ intent: "List parked tasks",
19173
+ command: `mu task list --substate parked -w ${workstream}`
19174
+ }
19175
+ ] : []
19073
19176
  ]);
19074
19177
  return;
19075
19178
  }
@@ -19638,7 +19741,14 @@ async function cmdTaskClose(db, rawId, opts = {}) {
19638
19741
  const taskRow = getTask(db, localId, ws);
19639
19742
  const r = closeTask(db, localId, sdkOpts);
19640
19743
  if ("skipped" in r) {
19744
+ const inTriage = r.blockingIds.filter((id) => getTask(db, id, ws)?.substate === "triage");
19641
19745
  const blockingNextSteps = [
19746
+ ...inTriage.length > 0 ? [
19747
+ {
19748
+ intent: `Triage ${inTriage.length} blocking finding(s) (accept, or close --as rejected|duplicate)`,
19749
+ command: `mu task list --substate triage -w ${ws}`
19750
+ }
19751
+ ] : [],
19642
19752
  {
19643
19753
  intent: "Watch the remaining blockers (returns when one closes)",
19644
19754
  command: `mu task wait ${r.blockingIds.join(" ")} -w ${ws} --first --any --on-stall exit`
@@ -19771,6 +19881,32 @@ async function cmdTaskPark(db, rawId, opts) {
19771
19881
  }
19772
19882
  printNextSteps(nextSteps3);
19773
19883
  }
19884
+ async function cmdTaskAccept(db, rawId, opts = {}) {
19885
+ const { name: localId } = await resolveEntityRef(db, rawId, opts, "task");
19886
+ assertTaskInWorkstream(db, localId, opts.workstream);
19887
+ const ws = await resolveWorkstream(opts.workstream);
19888
+ const sdkOpts = { workstream: ws };
19889
+ if (opts.evidence !== void 0) sdkOpts.evidence = opts.evidence;
19890
+ const r = acceptTask(db, localId, sdkOpts);
19891
+ const nextSteps3 = [
19892
+ {
19893
+ intent: "Claim it",
19894
+ command: `mu task claim ${localId} -w ${ws} (--self / --for <worker>)`
19895
+ },
19896
+ { intent: "Triage the next one", command: `mu task list --substate triage -w ${ws}` }
19897
+ ];
19898
+ if (opts.json) {
19899
+ emitJson({ taskName: localId, ...r, nextSteps: nextSteps3 });
19900
+ return;
19901
+ }
19902
+ if (!r.changed) {
19903
+ const pair = formatPair({ status: r.status, substate: r.substate });
19904
+ console.log(pc.dim(`${localId} is ${pair}, not in triage (no-op)`));
19905
+ } else {
19906
+ console.log(`Accepted ${pc.bold(localId)} ${pc.dim(`(${transition(r)})`)}`);
19907
+ }
19908
+ printNextSteps(nextSteps3);
19909
+ }
19774
19910
  async function cmdTaskUnpark(db, rawId, opts = {}) {
19775
19911
  const { name: localId } = await resolveEntityRef(db, rawId, opts, "task");
19776
19912
  assertTaskInWorkstream(db, localId, opts.workstream);
@@ -19849,7 +19985,10 @@ function wireTaskCommands(program) {
19849
19985
  ).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(
19850
19986
  "-b, --blocked-by <ids...>",
19851
19987
  "task ids that block this one (repeat or comma-separate; or both)"
19852
- ).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) {
19988
+ ).option("--note <text>", "append an initial note after creating the task").option("--note-author <name>", "author label for --note (default: current actor)").option(
19989
+ "--triage",
19990
+ "start as OPEN/triage: a proposal (e.g. a review finding) kept out of next/claim until `mu task accept`"
19991
+ ).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(id) {
19853
19992
  const opts = this.opts();
19854
19993
  return handle((db) => cmdTaskAdd(db, id, opts), this)();
19855
19994
  });
@@ -19954,6 +20093,12 @@ function wireTaskCommands(program) {
19954
20093
  const opts = this.opts();
19955
20094
  return handle((db) => cmdTaskPark(db, id, opts), this)();
19956
20095
  });
20096
+ task.command("accept <id>").description(
20097
+ "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`."
20098
+ ).option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(id) {
20099
+ const opts = this.opts();
20100
+ return handle((db) => cmdTaskAccept(db, id, opts), this)();
20101
+ });
19957
20102
  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) {
19958
20103
  const opts = this.opts();
19959
20104
  return handle((db) => cmdTaskUnpark(db, id, opts), this)();
@@ -19978,7 +20123,7 @@ function wireTaskCommands(program) {
19978
20123
  ).option(
19979
20124
  "--actor <name>",
19980
20125
  "override the actor name used for the log (only valid with --self; defaults to pane title or $USER)"
19981
- ).option("--force", "claim even when the task is parked (OPEN/parked)").option(
20126
+ ).option("--force", "claim even when the task is parked or in triage (OPEN/parked, OPEN/triage)").option(
19982
20127
  "--strict-staleness",
19983
20128
  "refuse --for dispatch when the target agent's workspace is stale (default: warn and proceed)"
19984
20129
  ).option(...WORKSTREAM_OPT).option(...EVIDENCE_OPT).option(...JSON_OPT).action(function(taskId) {
@@ -20541,7 +20686,8 @@ async function cmdAgentWait(db, rawNames, opts) {
20541
20686
  const settled = ctlRequest(sock, { op: "wait", afterRuns: probe.status.runs }, { signal }).then(
20542
20687
  (reply) => reply.ok && reply.state !== void 0 ? {
20543
20688
  status: ctlRuntimeState(reply.state),
20544
- ...reply.lastText !== void 0 ? { lastText: reply.lastText } : {}
20689
+ ...reply.lastText !== void 0 ? { lastText: reply.lastText } : {},
20690
+ ...reply.lastError !== void 0 ? { lastError: reply.lastError } : {}
20545
20691
  } : { status: "unknown", unknownReason: reply.ok ? "ctl wait: no state" : reply.error },
20546
20692
  // The connection dropped. A socket that still answers (pi's /new
20547
20693
  // restarts the session) hands back to polling; one that does not
@@ -20750,124 +20896,424 @@ function wireSelfCommands(program) {
20750
20896
  }
20751
20897
 
20752
20898
  // src/cli/db.ts
20753
- import { existsSync as existsSync16, mkdirSync as mkdirSync6 } from "fs";
20899
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, statSync as statSync5 } from "fs";
20754
20900
  import { dirname as dirname13, resolve as resolve7 } from "path";
20755
- init_output();
20756
-
20757
- // src/cli/handle.ts
20758
- init_agents();
20759
- init_db();
20760
- init_drift();
20761
- init_link();
20762
- init_logs();
20763
- init_mux2();
20764
- init_output();
20765
- init_rebuild();
20766
- init_sync();
20767
- init_tasks();
20768
- import { CommanderError } from "commander";
20769
20901
 
20770
- // src/undo.ts
20771
- init_apply();
20772
- init_db();
20902
+ // src/compact.ts
20773
20903
  init_legacy_ops();
20774
- init_logs();
20775
- init_op_context();
20776
- init_status();
20777
- var UndoGroupNotFoundError = class extends Error {
20778
- constructor(groupId) {
20779
- super(`no ops found for group ${JSON.stringify(groupId)}`);
20780
- this.groupId = groupId;
20781
- this.name = "UndoGroupNotFoundError";
20782
- }
20783
- groupId;
20784
- errorNextSteps() {
20785
- return [
20786
- { intent: "List recent undoable groups", command: "mu undo" },
20787
- { intent: "Inspect a group's ops", command: "mu log --group <id>" }
20788
- ];
20789
- }
20790
- };
20791
- var NothingToUndoError = class extends Error {
20792
- constructor() {
20793
- super("the ops log is empty; nothing to undo");
20794
- this.name = "NothingToUndoError";
20904
+ var REDUNDANT_TOMBSTONES_SQL = `
20905
+ SELECT d.seq AS seq, length(d.payload) AS bytes
20906
+ FROM ops d
20907
+ WHERE d.entity = 'note' AND d.op = 'del' AND d.payload <> '{}'
20908
+ AND EXISTS (SELECT 1 FROM ops p
20909
+ WHERE p.entity = 'note' AND p.key = d.key AND p.op = 'put')`;
20910
+ function planCompact(db) {
20911
+ const r = db.prepare(
20912
+ `SELECT COUNT(*) AS n, COALESCE(SUM(bytes), 0) AS b FROM (${REDUNDANT_TOMBSTONES_SQL})`
20913
+ ).get();
20914
+ return { tombstones: r.n, bytes: r.b };
20915
+ }
20916
+ function compact(db) {
20917
+ const plan = planCompact(db);
20918
+ db.transaction(() => {
20919
+ db.prepare(
20920
+ `UPDATE ops SET payload = '{}' WHERE seq IN (SELECT seq FROM (${REDUNDANT_TOMBSTONES_SQL}))`
20921
+ ).run();
20922
+ })();
20923
+ return plan;
20924
+ }
20925
+ var WORKSTREAM_OPS_WHERE = `(key = @ws OR substr(key, 1, length(@ws) + 1) = @ws || '/')`;
20926
+ function listForgetCandidates(db) {
20927
+ const spellings = intentSpellings("workstream.teardown");
20928
+ const names = db.prepare(
20929
+ `SELECT key AS name, MAX(created_at) AS at
20930
+ FROM ops
20931
+ WHERE entity = 'workstream' AND op = 'del'
20932
+ AND intent IN (${spellings.map(() => "?").join(", ")})
20933
+ AND key NOT IN (SELECT name FROM workstreams)
20934
+ GROUP BY key`
20935
+ ).all(...spellings);
20936
+ const cost = db.prepare(
20937
+ `SELECT COUNT(*) AS n, COALESCE(SUM(length(payload)), 0) AS b FROM ops WHERE ${WORKSTREAM_OPS_WHERE}`
20938
+ );
20939
+ return names.map((r) => {
20940
+ const c = cost.get({ ws: r.name });
20941
+ return { name: r.name, tornDownAt: r.at, ops: c.n, bytes: c.b };
20942
+ }).sort((a, b) => b.bytes - a.bytes);
20943
+ }
20944
+ function planForget(db, names) {
20945
+ const all = new Map(listForgetCandidates(db).map((c) => [c.name, c]));
20946
+ const live = new Set(
20947
+ db.prepare("SELECT name FROM workstreams").all().map((r) => r.name)
20948
+ );
20949
+ const plan = { candidates: [], refused: [] };
20950
+ for (const name of new Set(names)) {
20951
+ const c = all.get(name);
20952
+ if (c) plan.candidates.push(c);
20953
+ else plan.refused.push({ name, why: live.has(name) ? "live" : "never-torn-down" });
20795
20954
  }
20796
- errorNextSteps() {
20797
- return [{ intent: "Make a change first", command: "mu task add <id> -t <title>" }];
20955
+ return plan;
20956
+ }
20957
+ function forget(db, plan) {
20958
+ const del = db.prepare(`DELETE FROM ops WHERE ${WORKSTREAM_OPS_WHERE}`);
20959
+ let ops = 0;
20960
+ db.transaction(() => {
20961
+ for (const c of plan.candidates) ops += del.run({ ws: c.name }).changes;
20962
+ })();
20963
+ return { ops };
20964
+ }
20965
+
20966
+ // src/disk-recon.ts
20967
+ init_db();
20968
+ init_workspace();
20969
+ import { readdirSync as readdirSync3, statSync as statSync2 } from "fs";
20970
+ import { join as join12 } from "path";
20971
+ var STALE_LOCK_REPORT_MS = 60 * 60 * 1e3;
20972
+ function formatBytes(bytes) {
20973
+ if (bytes < 1024) return `${bytes}B`;
20974
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}K`;
20975
+ if (bytes < 1024 * 1024 * 1024) return `${Math.round(bytes / 1024 / 1024)}M`;
20976
+ return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}G`;
20977
+ }
20978
+ function dirNames(path2) {
20979
+ try {
20980
+ return readdirSync3(path2, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
20981
+ } catch {
20982
+ return [];
20798
20983
  }
20799
- };
20800
- var UndoSupersededError = class extends Error {
20801
- constructor(groupId, conflicts) {
20802
- const shown = conflicts.slice(0, 5).map((c) => `${c.key}.${c.field} (changed by ${c.groupId.slice(0, 8)})`).join(", ");
20803
- super(
20804
- `group ${groupId.slice(0, 8)} has been superseded: ${conflicts.length} field(s) were changed by a later action \u2014 ${shown}. Undoing would discard that newer work.`
20805
- );
20806
- this.groupId = groupId;
20807
- this.conflicts = conflicts;
20808
- this.name = "UndoSupersededError";
20984
+ }
20985
+ function exists(path2) {
20986
+ try {
20987
+ statSync2(path2);
20988
+ return true;
20989
+ } catch {
20990
+ return false;
20809
20991
  }
20810
- groupId;
20811
- conflicts;
20812
- errorNextSteps() {
20813
- return [
20814
- {
20815
- intent: "Inspect what changed since",
20816
- command: `mu log --group ${this.groupId.slice(0, 8)}`
20817
- },
20818
- {
20819
- intent: "Undo anyway, discarding the newer edits",
20820
- command: `mu undo ${this.groupId.slice(0, 8)} --yes --force`
20821
- }
20822
- ];
20992
+ }
20993
+ function sizeOf(path2) {
20994
+ try {
20995
+ return statSync2(path2).size;
20996
+ } catch {
20997
+ return 0;
20823
20998
  }
20824
- };
20825
- var ENTITY_RESTORE_ORDER = {
20826
- workstream: 0,
20827
- task: 1,
20828
- note: 2,
20829
- edge: 2
20830
- };
20831
- function restoreRank(entity) {
20832
- return ENTITY_RESTORE_ORDER[entity] ?? 3;
20833
20999
  }
20834
- function priorFieldValue(db, entity, key, hlc, field) {
20835
- const row2 = db.prepare(
20836
- `SELECT json_extract(payload, '$.' || @field) AS value
20837
- FROM ops
20838
- WHERE entity = @entity
20839
- AND key = @key
20840
- AND op = 'put'
20841
- AND hlc < @hlc
20842
- AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
20843
- AND json_type(payload, '$.' || @field) IS NOT NULL
20844
- ORDER BY hlc DESC
20845
- LIMIT 1`
20846
- ).get({ entity, key, hlc, field });
20847
- if (row2 === void 0) return { found: false };
20848
- return { found: true, value: row2.value };
21000
+ function findMissingWorkspaceDirs(db) {
21001
+ return listWorkspaces2(db).filter((row2) => !exists(row2.path)).map((row2) => ({
21002
+ agentName: row2.agentName,
21003
+ workstreamName: row2.workstreamName,
21004
+ path: row2.path
21005
+ }));
20849
21006
  }
20850
- function laterWriters(db, entity, key, hlc, field, excludeGroup) {
20851
- return db.prepare(
20852
- `SELECT DISTINCT group_id AS groupId, intent
20853
- FROM ops
20854
- WHERE entity = @entity
20855
- AND key = @key
20856
- AND op = 'put'
20857
- AND hlc > @hlc
20858
- AND group_id <> @excludeGroup
20859
- AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
20860
- AND json_type(payload, '$.' || @field) IS NOT NULL`
20861
- ).all({ entity, key, hlc, field, excludeGroup });
21007
+ function checkMissingWorkspaceDirs(db) {
21008
+ const missing = findMissingWorkspaceDirs(db);
21009
+ if (missing.length === 0) {
21010
+ return { name: "ws-rows", severity: "ok", detail: "every workspace row has its dir" };
21011
+ }
21012
+ return {
21013
+ name: "ws-rows",
21014
+ severity: "warn",
21015
+ detail: `${missing.length} workspace row(s) point at a path that is gone`,
21016
+ remediation: [
21017
+ "These vcs_workspaces rows name a directory that no longer exists:",
21018
+ ...missing.map((m) => ` ${m.workstreamName}/${m.agentName} ${m.path}`),
21019
+ "",
21020
+ "mu reports such a row as a healthy workspace on every read surface, and",
21021
+ "the next send or refresh against it fails inside the VCS backend rather",
21022
+ "than at the row that lied. Unregister them:",
21023
+ ...missing.map((m) => ` mu workspace free ${m.agentName} -w ${m.workstreamName}`)
21024
+ ]
21025
+ };
20862
21026
  }
20863
- function laterRowWriters(db, entity, key, hlc, excludeGroup) {
20864
- return db.prepare(
20865
- `SELECT DISTINCT group_id AS groupId, intent
20866
- FROM ops
20867
- WHERE entity = @entity
20868
- AND key = @key
20869
- AND op = 'put'
20870
- AND hlc > @hlc
21027
+ function checkWorkspaceOrphanDirs(db) {
21028
+ const orphans = listAllOrphanWorkspaces(db);
21029
+ if (orphans.length === 0) {
21030
+ return { name: "ws-dirs", severity: "ok", detail: "no orphan workspace dirs" };
21031
+ }
21032
+ const stranded = orphans.filter((o) => o.stranded);
21033
+ const strandedNote = stranded.length > 0 ? `, ${stranded.length} stranded` : "";
21034
+ return {
21035
+ name: "ws-dirs",
21036
+ severity: "warn",
21037
+ detail: `${orphans.length} workspace dir(s) with no DB row${strandedNote}`,
21038
+ remediation: [
21039
+ "These directories have no row in vcs_workspaces:",
21040
+ ...orphans.map((o) => ` ${o.path}${o.stranded ? " (workstream row also gone)" : ""}`),
21041
+ "",
21042
+ "Each one blocks the next `--workspace` spawn for that agent name. mu will",
21043
+ "not remove them: they may hold uncommitted work, and a stranded dir is the",
21044
+ "only remaining copy of it. Inspect, then remove by hand:",
21045
+ " mu workspace orphans --all",
21046
+ ...orphans.slice(0, 3).map((o) => ` rm -rf ${o.path}`)
21047
+ ]
21048
+ };
21049
+ }
21050
+ function checkEmptyWorkstreamDirs() {
21051
+ const root = join12(defaultStateDir(), "workspaces");
21052
+ const empty = dirNames(root).filter((name) => dirNames(join12(root, name)).length === 0);
21053
+ if (empty.length === 0) {
21054
+ return { name: "ws-empty", severity: "ok", detail: "no empty workstream dirs" };
21055
+ }
21056
+ return {
21057
+ name: "ws-empty",
21058
+ severity: "ok",
21059
+ detail: `${empty.length} empty workstream dir(s): ${empty.slice(0, 4).join(", ")}`,
21060
+ remediation: [
21061
+ "These per-workstream workspace dirs hold no checkouts:",
21062
+ ...empty.map((name) => ` ${join12(root, name)}`),
21063
+ "",
21064
+ "`mu workspace free` removes an agent's dir and its row but not the parent,",
21065
+ "so this is normal residue rather than a fault. Remove if you want the",
21066
+ "state dir to read cleanly:",
21067
+ ...empty.slice(0, 3).map((name) => ` rmdir ${join12(root, name)}`)
21068
+ ]
21069
+ };
21070
+ }
21071
+ function findStrayDbFiles(stateDir = defaultStateDir()) {
21072
+ const live = /* @__PURE__ */ new Set(["mu.db", "mu.db-wal", "mu.db-shm"]);
21073
+ let entries;
21074
+ try {
21075
+ entries = readdirSync3(stateDir, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
21076
+ } catch {
21077
+ return [];
21078
+ }
21079
+ return entries.filter((name) => name.startsWith("mu.db") && !live.has(name)).map((name) => ({ name, path: join12(stateDir, name), bytes: sizeOf(join12(stateDir, name)) })).sort((a, b) => b.bytes - a.bytes);
21080
+ }
21081
+ function checkStrayDbFiles() {
21082
+ const stateDir = defaultStateDir();
21083
+ const stray = findStrayDbFiles(stateDir);
21084
+ if (stray.length === 0) {
21085
+ return { name: "db-copies", severity: "ok", detail: "no stray DB copies" };
21086
+ }
21087
+ const total = stray.reduce((n, f) => n + f.bytes, 0);
21088
+ return {
21089
+ name: "db-copies",
21090
+ severity: "warn",
21091
+ detail: `${stray.length} stray DB file(s), ${formatBytes(total)} \u2014 nothing reads these`,
21092
+ remediation: [
21093
+ `Files matching mu.db* in ${stateDir} that are not the live WAL triple:`,
21094
+ ...stray.map((f) => ` ${formatBytes(f.bytes).padStart(6)} ${f.name}`),
21095
+ "",
21096
+ `Live DB: ${defaultDbPath()}`,
21097
+ "",
21098
+ "These are hand-made copies and pre-upgrade saves. No mu code path reads",
21099
+ "them and none prunes them. Real disaster recovery is the ops log, so a",
21100
+ "copy is only ever a convenience:",
21101
+ " mu rebuild <file> # DR from the ops log, not from a copy",
21102
+ " mu db backup <file> # a fresh copy, if that is what you wanted"
21103
+ ]
21104
+ };
21105
+ }
21106
+ function checkRemovedExportsDir() {
21107
+ const root = join12(defaultStateDir(), "exports");
21108
+ const entries = dirNames(root);
21109
+ if (entries.length === 0) {
21110
+ return { name: "exports", severity: "ok", detail: "no leftover export dirs" };
21111
+ }
21112
+ return {
21113
+ name: "exports",
21114
+ severity: "warn",
21115
+ detail: `${entries.length} export dir(s) from the removed \`mu workstream export\``,
21116
+ remediation: [
21117
+ `${root} holds ${entries.length} directory/ies.`,
21118
+ "",
21119
+ "`mu workstream export` and its markdown bucket were removed in 1.0; this",
21120
+ "is output from a version that still had it. Nothing in mu reads or prunes",
21121
+ "it. Keep anything you still want, then:",
21122
+ ` rm -rf ${root}`
21123
+ ]
21124
+ };
21125
+ }
21126
+ function checkStaleLocks(now = Date.now()) {
21127
+ const root = join12(defaultStateDir(), "locks");
21128
+ const stale = [];
21129
+ for (const name of dirNames(root)) {
21130
+ try {
21131
+ const ageMs2 = now - statSync2(join12(root, name)).mtimeMs;
21132
+ if (ageMs2 > STALE_LOCK_REPORT_MS) stale.push({ name, ageMs: ageMs2 });
21133
+ } catch {
21134
+ }
21135
+ }
21136
+ if (stale.length === 0) {
21137
+ return { name: "locks", severity: "ok", detail: "no stale lock dirs" };
21138
+ }
21139
+ return {
21140
+ name: "locks",
21141
+ severity: "warn",
21142
+ detail: `${stale.length} lock dir(s) older than 1h`,
21143
+ remediation: [
21144
+ "These advisory-lock dirs outlived the process that took them:",
21145
+ ...stale.map((s) => ` ${s.name} (${Math.round(s.ageMs / 6e4)}m old)`),
21146
+ "",
21147
+ "Not a deadlock: withFileLock steals a lock older than 30s. But it only",
21148
+ "steals under contention, so these will sit here forever. They are evidence",
21149
+ "of a spawn or segment flush that died mid-critical-section.",
21150
+ ` rm -rf ${root}/<name>`
21151
+ ]
21152
+ };
21153
+ }
21154
+ function walkBytes(path2) {
21155
+ let total = 0;
21156
+ let entries;
21157
+ try {
21158
+ entries = readdirSync3(path2, { withFileTypes: true });
21159
+ } catch {
21160
+ return 0;
21161
+ }
21162
+ for (const entry of entries) {
21163
+ const child = join12(path2, entry.name);
21164
+ if (entry.isDirectory()) total += walkBytes(child);
21165
+ else if (entry.isFile()) total += sizeOf(child);
21166
+ }
21167
+ return total;
21168
+ }
21169
+ function measureWorkspaceUsage(db) {
21170
+ const root = join12(defaultStateDir(), "workspaces");
21171
+ const registered = new Set(listWorkspaces2(db).map((w) => w.path));
21172
+ const usage = [];
21173
+ for (const wsName of dirNames(root)) {
21174
+ const wsRoot = join12(root, wsName);
21175
+ for (const agentName of dirNames(wsRoot)) {
21176
+ const path2 = join12(wsRoot, agentName);
21177
+ usage.push({
21178
+ workstreamName: wsName,
21179
+ agentName,
21180
+ path: path2,
21181
+ bytes: walkBytes(path2),
21182
+ orphan: !registered.has(path2)
21183
+ });
21184
+ }
21185
+ }
21186
+ return usage.sort((a, b) => b.bytes - a.bytes);
21187
+ }
21188
+ function checkDiskRecon(db) {
21189
+ return [
21190
+ checkMissingWorkspaceDirs(db),
21191
+ checkWorkspaceOrphanDirs(db),
21192
+ checkEmptyWorkstreamDirs(),
21193
+ checkStrayDbFiles(),
21194
+ checkRemovedExportsDir(),
21195
+ checkStaleLocks()
21196
+ ];
21197
+ }
21198
+
21199
+ // src/cli/db.ts
21200
+ init_drift();
21201
+ init_output();
21202
+
21203
+ // src/cli/handle.ts
21204
+ init_agents();
21205
+ init_db();
21206
+ init_drift();
21207
+ init_link();
21208
+ init_logs();
21209
+ init_mux2();
21210
+ init_output();
21211
+ init_rebuild();
21212
+ init_sync();
21213
+ init_tasks();
21214
+ import { CommanderError } from "commander";
21215
+
21216
+ // src/undo.ts
21217
+ init_apply();
21218
+ init_db();
21219
+ init_legacy_ops();
21220
+ init_logs();
21221
+ init_op_context();
21222
+ init_status();
21223
+ var UndoGroupNotFoundError = class extends Error {
21224
+ constructor(groupId) {
21225
+ super(`no ops found for group ${JSON.stringify(groupId)}`);
21226
+ this.groupId = groupId;
21227
+ this.name = "UndoGroupNotFoundError";
21228
+ }
21229
+ groupId;
21230
+ errorNextSteps() {
21231
+ return [
21232
+ { intent: "List recent undoable groups", command: "mu undo" },
21233
+ { intent: "Inspect a group's ops", command: "mu log --group <id>" }
21234
+ ];
21235
+ }
21236
+ };
21237
+ var NothingToUndoError = class extends Error {
21238
+ constructor() {
21239
+ super("the ops log is empty; nothing to undo");
21240
+ this.name = "NothingToUndoError";
21241
+ }
21242
+ errorNextSteps() {
21243
+ return [{ intent: "Make a change first", command: "mu task add <id> -t <title>" }];
21244
+ }
21245
+ };
21246
+ var UndoSupersededError = class extends Error {
21247
+ constructor(groupId, conflicts) {
21248
+ const shown = conflicts.slice(0, 5).map((c) => `${c.key}.${c.field} (changed by ${c.groupId.slice(0, 8)})`).join(", ");
21249
+ super(
21250
+ `group ${groupId.slice(0, 8)} has been superseded: ${conflicts.length} field(s) were changed by a later action \u2014 ${shown}. Undoing would discard that newer work.`
21251
+ );
21252
+ this.groupId = groupId;
21253
+ this.conflicts = conflicts;
21254
+ this.name = "UndoSupersededError";
21255
+ }
21256
+ groupId;
21257
+ conflicts;
21258
+ errorNextSteps() {
21259
+ return [
21260
+ {
21261
+ intent: "Inspect what changed since",
21262
+ command: `mu log --group ${this.groupId.slice(0, 8)}`
21263
+ },
21264
+ {
21265
+ intent: "Undo anyway, discarding the newer edits",
21266
+ command: `mu undo ${this.groupId.slice(0, 8)} --yes --force`
21267
+ }
21268
+ ];
21269
+ }
21270
+ };
21271
+ var ENTITY_RESTORE_ORDER = {
21272
+ workstream: 0,
21273
+ task: 1,
21274
+ note: 2,
21275
+ edge: 2
21276
+ };
21277
+ function restoreRank(entity) {
21278
+ return ENTITY_RESTORE_ORDER[entity] ?? 3;
21279
+ }
21280
+ function priorFieldValue(db, entity, key, hlc, field) {
21281
+ const row2 = db.prepare(
21282
+ `SELECT json_extract(payload, '$.' || @field) AS value
21283
+ FROM ops
21284
+ WHERE entity = @entity
21285
+ AND key = @key
21286
+ AND op = 'put'
21287
+ AND hlc < @hlc
21288
+ AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
21289
+ AND json_type(payload, '$.' || @field) IS NOT NULL
21290
+ ORDER BY hlc DESC
21291
+ LIMIT 1`
21292
+ ).get({ entity, key, hlc, field });
21293
+ if (row2 === void 0) return { found: false };
21294
+ return { found: true, value: row2.value };
21295
+ }
21296
+ function laterWriters(db, entity, key, hlc, field, excludeGroup) {
21297
+ return db.prepare(
21298
+ `SELECT DISTINCT group_id AS groupId, intent
21299
+ FROM ops
21300
+ WHERE entity = @entity
21301
+ AND key = @key
21302
+ AND op = 'put'
21303
+ AND hlc > @hlc
21304
+ AND group_id <> @excludeGroup
21305
+ AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}
21306
+ AND json_type(payload, '$.' || @field) IS NOT NULL`
21307
+ ).all({ entity, key, hlc, field, excludeGroup });
21308
+ }
21309
+ function laterRowWriters(db, entity, key, hlc, excludeGroup) {
21310
+ return db.prepare(
21311
+ `SELECT DISTINCT group_id AS groupId, intent
21312
+ FROM ops
21313
+ WHERE entity = @entity
21314
+ AND key = @key
21315
+ AND op = 'put'
21316
+ AND hlc > @hlc
20871
21317
  AND group_id <> @excludeGroup
20872
21318
  AND ${LEGACY_LOG_ONLY_SQL_EXCLUSION}`
20873
21319
  ).all({ entity, key, hlc, excludeGroup });
@@ -21395,7 +21841,7 @@ function classifyError(err) {
21395
21841
  }
21396
21842
  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
21397
21843
  // phase, a claim on a parked task, a pair outside TASK_SUBSTATES.
21398
- 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
21844
+ 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
21399
21845
  // the source DB: both are name/state collisions, not usage errors.
21400
21846
  err instanceof RebuildTargetExistsError || err instanceof RebuildTargetIsSourceError || // An abbreviated group id matching several groups: the operator must
21401
21847
  // disambiguate. Guessing would be catastrophic for `mu undo`.
@@ -21499,315 +21945,196 @@ function handle(fn, command, opts) {
21499
21945
  if (ambient) await ambientIngest(db);
21500
21946
  await fn(db);
21501
21947
  } catch (err) {
21502
- exitCode = emitError(err);
21503
- } finally {
21504
- activeCommand = void 0;
21505
- if (ambient && db !== void 0) {
21506
- try {
21507
- await ambientFlush(db);
21508
- } catch {
21509
- }
21510
- }
21511
- try {
21512
- db?.close();
21513
- } catch {
21514
- }
21515
- }
21516
- if (exitCode !== void 0) process.exit(exitCode);
21517
- };
21518
- }
21519
- function emitParseError(err, failingCommand) {
21520
- activeCommand = failingCommand;
21521
- try {
21522
- return emitError(err);
21523
- } finally {
21524
- activeCommand = void 0;
21525
- }
21526
- }
21527
- function findCommandForArgv(root, argv) {
21528
- let cur = root;
21529
- for (const t of argv) {
21530
- if (t.startsWith("-")) break;
21531
- const next = cur.commands.find(
21532
- (c) => c.name() === t || (c.aliases().includes(t) ?? false)
21533
- );
21534
- if (!next) break;
21535
- cur = next;
21536
- }
21537
- return cur;
21538
- }
21539
-
21540
- // src/cli/db.ts
21541
- function cmdDbBackup(db, file, opts = {}) {
21542
- const target = resolve7(file);
21543
- if (existsSync16(target)) {
21544
- throw new UsageError(`${target} already exists: mu db backup never overwrites`);
21545
- }
21546
- mkdirSync6(dirname13(target), { recursive: true });
21547
- db.prepare("VACUUM INTO ?").run(target);
21548
- const nextSteps3 = [
21549
- {
21550
- intent: "Inspect the copy without touching the live DB",
21551
- command: `MU_DB_PATH=${target} mu state`
21552
- },
21553
- { intent: "Disaster recovery is a rebuild, not a copy", command: "mu rebuild <file>" }
21554
- ];
21555
- if (opts.json === true) {
21556
- emitJson({ path: target, nextSteps: nextSteps3 });
21557
- return;
21558
- }
21559
- console.log(`Backed up to ${pc.bold(target)}`);
21560
- printNextSteps(nextSteps3);
21561
- }
21562
- function wireDbCommands(program) {
21563
- const dbCmd = program.command("db").description("Whole-DB file commands");
21564
- dbCmd.command("backup <file>").description(
21565
- "VACUUM INTO copy of the whole DB \u2014 the 'one file I can scp' convenience. Never overwrites. Real disaster recovery is `mu rebuild`."
21566
- ).option(...JSON_OPT).action(function(file) {
21567
- const opts = this.opts();
21568
- return handle(async (db) => cmdDbBackup(db, file, opts), this)();
21569
- });
21570
- }
21571
-
21572
- // src/cli/doctor.ts
21573
- init_agents();
21574
- import { dirname as dirname14 } from "path";
21575
- init_db();
21576
-
21577
- // src/disk-recon.ts
21578
- init_db();
21579
- init_workspace();
21580
- import { readdirSync as readdirSync4, statSync as statSync4 } from "fs";
21581
- import { join as join13 } from "path";
21582
- var STALE_LOCK_REPORT_MS = 60 * 60 * 1e3;
21583
- function formatBytes(bytes) {
21584
- if (bytes < 1024) return `${bytes}B`;
21585
- if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}K`;
21586
- if (bytes < 1024 * 1024 * 1024) return `${Math.round(bytes / 1024 / 1024)}M`;
21587
- return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}G`;
21588
- }
21589
- function dirNames(path2) {
21590
- try {
21591
- return readdirSync4(path2, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
21592
- } catch {
21593
- return [];
21594
- }
21595
- }
21596
- function exists(path2) {
21597
- try {
21598
- statSync4(path2);
21599
- return true;
21600
- } catch {
21601
- return false;
21602
- }
21603
- }
21604
- function sizeOf(path2) {
21605
- try {
21606
- return statSync4(path2).size;
21607
- } catch {
21608
- return 0;
21609
- }
21610
- }
21611
- function findMissingWorkspaceDirs(db) {
21612
- return listWorkspaces2(db).filter((row2) => !exists(row2.path)).map((row2) => ({
21613
- agentName: row2.agentName,
21614
- workstreamName: row2.workstreamName,
21615
- path: row2.path
21616
- }));
21617
- }
21618
- function checkMissingWorkspaceDirs(db) {
21619
- const missing = findMissingWorkspaceDirs(db);
21620
- if (missing.length === 0) {
21621
- return { name: "ws-rows", severity: "ok", detail: "every workspace row has its dir" };
21622
- }
21623
- return {
21624
- name: "ws-rows",
21625
- severity: "warn",
21626
- detail: `${missing.length} workspace row(s) point at a path that is gone`,
21627
- remediation: [
21628
- "These vcs_workspaces rows name a directory that no longer exists:",
21629
- ...missing.map((m) => ` ${m.workstreamName}/${m.agentName} ${m.path}`),
21630
- "",
21631
- "mu reports such a row as a healthy workspace on every read surface, and",
21632
- "the next send or refresh against it fails inside the VCS backend rather",
21633
- "than at the row that lied. Unregister them:",
21634
- ...missing.map((m) => ` mu workspace free ${m.agentName} -w ${m.workstreamName}`)
21635
- ]
21948
+ exitCode = emitError(err);
21949
+ } finally {
21950
+ activeCommand = void 0;
21951
+ if (ambient && db !== void 0) {
21952
+ try {
21953
+ await ambientFlush(db);
21954
+ } catch {
21955
+ }
21956
+ }
21957
+ try {
21958
+ db?.close();
21959
+ } catch {
21960
+ }
21961
+ }
21962
+ if (exitCode !== void 0) process.exit(exitCode);
21636
21963
  };
21637
21964
  }
21638
- function checkWorkspaceOrphanDirs(db) {
21639
- const orphans = listAllOrphanWorkspaces(db);
21640
- if (orphans.length === 0) {
21641
- return { name: "ws-dirs", severity: "ok", detail: "no orphan workspace dirs" };
21965
+ function emitParseError(err, failingCommand) {
21966
+ activeCommand = failingCommand;
21967
+ try {
21968
+ return emitError(err);
21969
+ } finally {
21970
+ activeCommand = void 0;
21642
21971
  }
21643
- const stranded = orphans.filter((o) => o.stranded);
21644
- const strandedNote = stranded.length > 0 ? `, ${stranded.length} stranded` : "";
21645
- return {
21646
- name: "ws-dirs",
21647
- severity: "warn",
21648
- detail: `${orphans.length} workspace dir(s) with no DB row${strandedNote}`,
21649
- remediation: [
21650
- "These directories have no row in vcs_workspaces:",
21651
- ...orphans.map((o) => ` ${o.path}${o.stranded ? " (workstream row also gone)" : ""}`),
21652
- "",
21653
- "Each one blocks the next `--workspace` spawn for that agent name. mu will",
21654
- "not remove them: they may hold uncommitted work, and a stranded dir is the",
21655
- "only remaining copy of it. Inspect, then remove by hand:",
21656
- " mu workspace orphans --all",
21657
- ...orphans.slice(0, 3).map((o) => ` rm -rf ${o.path}`)
21658
- ]
21659
- };
21660
21972
  }
21661
- function checkEmptyWorkstreamDirs() {
21662
- const root = join13(defaultStateDir(), "workspaces");
21663
- const empty = dirNames(root).filter((name) => dirNames(join13(root, name)).length === 0);
21664
- if (empty.length === 0) {
21665
- return { name: "ws-empty", severity: "ok", detail: "no empty workstream dirs" };
21973
+ function findCommandForArgv(root, argv) {
21974
+ let cur = root;
21975
+ for (const t of argv) {
21976
+ if (t.startsWith("-")) break;
21977
+ const next = cur.commands.find(
21978
+ (c) => c.name() === t || (c.aliases().includes(t) ?? false)
21979
+ );
21980
+ if (!next) break;
21981
+ cur = next;
21666
21982
  }
21667
- return {
21668
- name: "ws-empty",
21669
- severity: "ok",
21670
- detail: `${empty.length} empty workstream dir(s): ${empty.slice(0, 4).join(", ")}`,
21671
- remediation: [
21672
- "These per-workstream workspace dirs hold no checkouts:",
21673
- ...empty.map((name) => ` ${join13(root, name)}`),
21674
- "",
21675
- "`mu workspace free` removes an agent's dir and its row but not the parent,",
21676
- "so this is normal residue rather than a fault. Remove if you want the",
21677
- "state dir to read cleanly:",
21678
- ...empty.slice(0, 3).map((name) => ` rmdir ${join13(root, name)}`)
21679
- ]
21680
- };
21983
+ return cur;
21681
21984
  }
21682
- function findStrayDbFiles(stateDir = defaultStateDir()) {
21683
- const live = /* @__PURE__ */ new Set(["mu.db", "mu.db-wal", "mu.db-shm"]);
21684
- let entries;
21685
- try {
21686
- entries = readdirSync4(stateDir, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
21687
- } catch {
21688
- return [];
21985
+
21986
+ // src/cli/db.ts
21987
+ function cmdDbBackup(db, file, opts = {}) {
21988
+ const target = resolve7(file);
21989
+ if (existsSync16(target)) {
21990
+ throw new UsageError(`${target} already exists: mu db backup never overwrites`);
21689
21991
  }
21690
- return entries.filter((name) => name.startsWith("mu.db") && !live.has(name)).map((name) => ({ name, path: join13(stateDir, name), bytes: sizeOf(join13(stateDir, name)) })).sort((a, b) => b.bytes - a.bytes);
21691
- }
21692
- function checkStrayDbFiles() {
21693
- const stateDir = defaultStateDir();
21694
- const stray = findStrayDbFiles(stateDir);
21695
- if (stray.length === 0) {
21696
- return { name: "db-copies", severity: "ok", detail: "no stray DB copies" };
21992
+ mkdirSync6(dirname13(target), { recursive: true });
21993
+ db.prepare("VACUUM INTO ?").run(target);
21994
+ const nextSteps3 = [
21995
+ {
21996
+ intent: "Inspect the copy without touching the live DB",
21997
+ command: `MU_DB_PATH=${target} mu state`
21998
+ },
21999
+ { intent: "Disaster recovery is a rebuild, not a copy", command: "mu rebuild <file>" }
22000
+ ];
22001
+ if (opts.json === true) {
22002
+ emitJson({ path: target, nextSteps: nextSteps3 });
22003
+ return;
21697
22004
  }
21698
- const total = stray.reduce((n, f) => n + f.bytes, 0);
21699
- return {
21700
- name: "db-copies",
21701
- severity: "warn",
21702
- detail: `${stray.length} stray DB file(s), ${formatBytes(total)} \u2014 nothing reads these`,
21703
- remediation: [
21704
- `Files matching mu.db* in ${stateDir} that are not the live WAL triple:`,
21705
- ...stray.map((f) => ` ${formatBytes(f.bytes).padStart(6)} ${f.name}`),
21706
- "",
21707
- `Live DB: ${defaultDbPath()}`,
21708
- "",
21709
- "These are hand-made copies and pre-upgrade saves. No mu code path reads",
21710
- "them and none prunes them. Real disaster recovery is the ops log, so a",
21711
- "copy is only ever a convenience:",
21712
- " mu rebuild <file> # DR from the ops log, not from a copy",
21713
- " mu db backup <file> # a fresh copy, if that is what you wanted"
21714
- ]
21715
- };
22005
+ console.log(`Backed up to ${pc.bold(target)}`);
22006
+ printNextSteps(nextSteps3);
21716
22007
  }
21717
- function checkRemovedExportsDir() {
21718
- const root = join13(defaultStateDir(), "exports");
21719
- const entries = dirNames(root);
21720
- if (entries.length === 0) {
21721
- return { name: "exports", severity: "ok", detail: "no leftover export dirs" };
22008
+ function backupBeside(db, verb) {
22009
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
22010
+ const target = `${db.name}.pre-${verb}-${ts}`;
22011
+ db.prepare("VACUUM INTO ?").run(target);
22012
+ return target;
22013
+ }
22014
+ function vacuumAndCheck(db, backup) {
22015
+ const before = statSync5(db.name).size;
22016
+ db.exec("VACUUM");
22017
+ const after = statSync5(db.name).size;
22018
+ const report = checkDrift(db);
22019
+ if (!report.clean) {
22020
+ console.error(
22021
+ pc.red(`drift after the rewrite (${report.totalDrift}); the backup is ${backup}`)
22022
+ );
22023
+ throw new DriftDetectedError(report.totalDrift, report.records);
21722
22024
  }
21723
- return {
21724
- name: "exports",
21725
- severity: "warn",
21726
- detail: `${entries.length} export dir(s) from the removed \`mu workstream export\``,
21727
- remediation: [
21728
- `${root} holds ${entries.length} directory/ies.`,
21729
- "",
21730
- "`mu workstream export` and its markdown bucket were removed in 1.0; this",
21731
- "is output from a version that still had it. Nothing in mu reads or prunes",
21732
- "it. Keep anything you still want, then:",
21733
- ` rm -rf ${root}`
21734
- ]
21735
- };
22025
+ return { before, after };
21736
22026
  }
21737
- function checkStaleLocks(now = Date.now()) {
21738
- const root = join13(defaultStateDir(), "locks");
21739
- const stale = [];
21740
- for (const name of dirNames(root)) {
21741
- try {
21742
- const ageMs2 = now - statSync4(join13(root, name)).mtimeMs;
21743
- if (ageMs2 > STALE_LOCK_REPORT_MS) stale.push({ name, ageMs: ageMs2 });
21744
- } catch {
22027
+ function cmdDbCompact(db, opts = {}) {
22028
+ const plan = planCompact(db);
22029
+ if (opts.yes !== true || plan.tombstones === 0) {
22030
+ const nextSteps3 = plan.tombstones === 0 ? [] : [{ intent: "Apply it", command: "mu db compact --yes" }];
22031
+ if (opts.json === true) {
22032
+ emitJson({ dryRun: true, ...plan, nextSteps: nextSteps3 });
22033
+ return;
21745
22034
  }
22035
+ console.log(
22036
+ plan.tombstones === 0 ? pc.dim("nothing to compact: no redundant note tombstones") : `${plan.tombstones} note tombstones repeat a put already in the log (${formatBytes(plan.bytes)}). Dry run; nothing changed.`
22037
+ );
22038
+ printNextSteps(nextSteps3);
22039
+ return;
21746
22040
  }
21747
- if (stale.length === 0) {
21748
- return { name: "locks", severity: "ok", detail: "no stale lock dirs" };
22041
+ const backup = backupBeside(db, "compact");
22042
+ compact(db);
22043
+ const size = vacuumAndCheck(db, backup);
22044
+ if (opts.json === true) {
22045
+ emitJson({ dryRun: false, ...plan, backup, ...size });
22046
+ return;
21749
22047
  }
21750
- return {
21751
- name: "locks",
21752
- severity: "warn",
21753
- detail: `${stale.length} lock dir(s) older than 1h`,
21754
- remediation: [
21755
- "These advisory-lock dirs outlived the process that took them:",
21756
- ...stale.map((s) => ` ${s.name} (${Math.round(s.ageMs / 6e4)}m old)`),
21757
- "",
21758
- "Not a deadlock: withFileLock steals a lock older than 30s. But it only",
21759
- "steals under contention, so these will sit here forever. They are evidence",
21760
- "of a spawn or segment flush that died mid-critical-section.",
21761
- ` rm -rf ${root}/<name>`
21762
- ]
21763
- };
21764
- }
21765
- function walkBytes(path2) {
21766
- let total = 0;
21767
- let entries;
21768
- try {
21769
- entries = readdirSync4(path2, { withFileTypes: true });
21770
- } catch {
21771
- return 0;
22048
+ console.log(
22049
+ `Compacted ${plan.tombstones} note tombstones: ${formatBytes(size.before)} \u2192 ${formatBytes(size.after)}. Drift check clean.`
22050
+ );
22051
+ console.log(pc.dim(`backup: ${backup}`));
22052
+ }
22053
+ function printForgetTable(plan) {
22054
+ const t = muTable({ head: ["workstream", "torn down", "ops", "size"].map((h) => pc.bold(h)) });
22055
+ for (const c of plan.candidates)
22056
+ t.push([c.name, c.tornDownAt.slice(0, 10), String(c.ops), formatBytes(c.bytes)]);
22057
+ console.log(t.toString());
22058
+ }
22059
+ function cmdDbForget(db, names, opts = {}) {
22060
+ const plan = planForget(db, names);
22061
+ if (plan.refused.length > 0) {
22062
+ const why = plan.refused.map((r2) => `${r2.name} (${r2.why === "live" ? "is live" : "was never torn down"})`).join(", ");
22063
+ throw new UsageError(`cannot forget: ${why}. Only torn-down workstreams can be forgotten.`);
22064
+ }
22065
+ const ops = plan.candidates.reduce((n, c) => n + c.ops, 0);
22066
+ const bytes = plan.candidates.reduce((n, c) => n + c.bytes, 0);
22067
+ if (opts.yes !== true) {
22068
+ const nextSteps3 = [
22069
+ { intent: "Forget them (NOT undoable)", command: `mu db forget ${names.join(" ")} --yes` }
22070
+ ];
22071
+ if (opts.json === true) {
22072
+ emitJson({ dryRun: true, candidates: plan.candidates, ops, bytes, nextSteps: nextSteps3 });
22073
+ return;
22074
+ }
22075
+ printForgetTable(plan);
22076
+ console.log(
22077
+ `${ops} ops (${formatBytes(bytes)} of payload). Forgetting deletes their history: mu undo can no longer restore these teardowns. Dry run; nothing changed.`
22078
+ );
22079
+ printNextSteps(nextSteps3);
22080
+ return;
21772
22081
  }
21773
- for (const entry of entries) {
21774
- const child = join13(path2, entry.name);
21775
- if (entry.isDirectory()) total += walkBytes(child);
21776
- else if (entry.isFile()) total += sizeOf(child);
22082
+ const backup = backupBeside(db, "forget");
22083
+ const r = forget(db, plan);
22084
+ const size = vacuumAndCheck(db, backup);
22085
+ if (opts.json === true) {
22086
+ emitJson({
22087
+ dryRun: false,
22088
+ forgotten: plan.candidates.map((c) => c.name),
22089
+ ops: r.ops,
22090
+ backup,
22091
+ ...size
22092
+ });
22093
+ return;
21777
22094
  }
21778
- return total;
22095
+ console.log(
22096
+ `Forgot ${plan.candidates.length} workstream(s), ${r.ops} ops: ${formatBytes(size.before)} \u2192 ${formatBytes(size.after)}. Drift check clean.`
22097
+ );
22098
+ console.log(pc.dim(`backup (the only way back): ${backup}`));
21779
22099
  }
21780
- function measureWorkspaceUsage(db) {
21781
- const root = join13(defaultStateDir(), "workspaces");
21782
- const registered = new Set(listWorkspaces2(db).map((w) => w.path));
21783
- const usage = [];
21784
- for (const wsName of dirNames(root)) {
21785
- const wsRoot = join13(root, wsName);
21786
- for (const agentName of dirNames(wsRoot)) {
21787
- const path2 = join13(wsRoot, agentName);
21788
- usage.push({
21789
- workstreamName: wsName,
21790
- agentName,
21791
- path: path2,
21792
- bytes: walkBytes(path2),
21793
- orphan: !registered.has(path2)
21794
- });
21795
- }
21796
- }
21797
- return usage.sort((a, b) => b.bytes - a.bytes);
22100
+ function forgetHint(db) {
22101
+ const all = listForgetCandidates(db);
22102
+ const bytes = all.reduce((n, c) => n + c.bytes, 0);
22103
+ if (bytes < FORGET_HINT_MIN_BYTES) return void 0;
22104
+ return {
22105
+ total: all.length,
22106
+ ops: all.reduce((n, c) => n + c.ops, 0),
22107
+ bytes,
22108
+ top: all.slice(0, 3).map((c) => c.name)
22109
+ };
21798
22110
  }
21799
- function checkDiskRecon(db) {
21800
- return [
21801
- checkMissingWorkspaceDirs(db),
21802
- checkWorkspaceOrphanDirs(db),
21803
- checkEmptyWorkstreamDirs(),
21804
- checkStrayDbFiles(),
21805
- checkRemovedExportsDir(),
21806
- checkStaleLocks()
21807
- ];
22111
+ var FORGET_HINT_MIN_BYTES = 5 * 1024 * 1024;
22112
+ function wireDbCommands(program) {
22113
+ const dbCmd = program.command("db").description("Whole-DB file commands");
22114
+ dbCmd.command("backup <file>").description(
22115
+ "VACUUM INTO copy of the whole DB \u2014 the 'one file I can scp' convenience. Never overwrites. Real disaster recovery is `mu rebuild`."
22116
+ ).option(...JSON_OPT).action(function(file) {
22117
+ const opts = this.opts();
22118
+ return handle(async (db) => cmdDbBackup(db, file, opts), this)();
22119
+ });
22120
+ dbCmd.command("compact").description(
22121
+ "Shrink mu.db: blank note tombstones that repeat a put already in the log, then VACUUM. Dry run without --yes; --yes backs up beside the DB first and runs the drift check after. Local only; sync segments are untouched."
22122
+ ).option("--yes", "apply (default: dry run)").option(...JSON_OPT).action(function() {
22123
+ const opts = this.opts();
22124
+ return handle(async (db) => cmdDbCompact(db, opts), this)();
22125
+ });
22126
+ dbCmd.command("forget <workstreams...>").description(
22127
+ "Delete every op of torn-down workstreams to shrink mu.db. NOT undoable: mu undo can no longer restore them. Dry run without --yes; --yes backs up beside the DB first, VACUUMs, and runs the drift check. List candidates: mu workstream list --torn-down."
22128
+ ).option("--yes", "apply (default: dry run)").option(...JSON_OPT).action(function(names) {
22129
+ const opts = this.opts();
22130
+ return handle(async (db) => cmdDbForget(db, names, opts), this)();
22131
+ });
21808
22132
  }
21809
22133
 
21810
22134
  // src/cli/doctor.ts
22135
+ init_agents();
22136
+ import { dirname as dirname14 } from "path";
22137
+ init_db();
21811
22138
  init_doctor_summary();
21812
22139
 
21813
22140
  // src/dormant.ts
@@ -22136,12 +22463,37 @@ state (workstream=${ws})`));
22136
22463
  throw new DriftDetectedError(cheap.totalUnexplained, []);
22137
22464
  }
22138
22465
  }
22466
+ const size = opsSizeHint(db);
22467
+ if (size) {
22468
+ console.log(` size : ${pc.yellow("hint")} ${size.line}`);
22469
+ for (const c of size.commands) console.log(pc.dim(` ${c}`));
22470
+ }
22139
22471
  if (sawHazard) {
22140
22472
  console.log(
22141
22473
  pc.dim("\nSee the fleet / disk sections above: at least one finding needs attention.")
22142
22474
  );
22143
22475
  }
22144
22476
  }
22477
+ function opsSizeHint(db) {
22478
+ const forgettable = forgetHint(db);
22479
+ const tomb = planCompact(db);
22480
+ const parts = [];
22481
+ const commands = [];
22482
+ if (forgettable) {
22483
+ parts.push(
22484
+ `${forgettable.total} torn-down workstreams hold ${forgettable.ops} ops (${formatBytes(forgettable.bytes)}); largest: ${forgettable.top.join(", ")}`
22485
+ );
22486
+ commands.push(
22487
+ "see them: mu workstream list --torn-down",
22488
+ `forget some: mu db forget ${forgettable.top.join(" ")}`
22489
+ );
22490
+ }
22491
+ if (tomb.bytes >= 1024 * 1024) {
22492
+ parts.push(`${tomb.tombstones} redundant note tombstones (${formatBytes(tomb.bytes)})`);
22493
+ commands.push("compact: mu db compact");
22494
+ }
22495
+ return parts.length === 0 ? void 0 : { line: parts.join("; "), commands };
22496
+ }
22145
22497
  async function cmdDoctorJson(db, opts = {}) {
22146
22498
  const health = await muxHealth();
22147
22499
  const muExt = ctlExtensionDoctorCheck();
@@ -22283,6 +22635,7 @@ async function cmdDoctorJson(db, opts = {}) {
22283
22635
  disk,
22284
22636
  workspaceUsage,
22285
22637
  drift,
22638
+ size: opsSizeHint(db) ?? null,
22286
22639
  remediation: drift.ok === true ? [] : driftRemediation()
22287
22640
  });
22288
22641
  if (driftFailure !== null) throw driftFailure;
@@ -23020,6 +23373,7 @@ function fullJsonShape(d) {
23020
23373
  orphans: d.view.orphans,
23021
23374
  tracks: d.tracks,
23022
23375
  ready: withRoiAll(d.ready),
23376
+ triage: withRoiAll(d.triage),
23023
23377
  inProgress: withRoiAll(d.inProgress),
23024
23378
  blocked: withRoiAll(d.blocked),
23025
23379
  recentClosed: withRoiAll(d.recentClosed),
@@ -23092,6 +23446,7 @@ async function cmdState(db, opts) {
23092
23446
  }
23093
23447
  renderFullMode(perWs);
23094
23448
  }
23449
+ var BIG_WORKSTREAM_TASKS = 300;
23095
23450
  function renderFullMode(perWs) {
23096
23451
  perWs.forEach((d, i) => {
23097
23452
  if (i > 0) console.log("");
@@ -23124,6 +23479,15 @@ function renderFullCard(d) {
23124
23479
  );
23125
23480
  }
23126
23481
  }
23482
+ const total = d.taskCount;
23483
+ const open = ready.length + inProgress.length + blocked.length + d.triage.length;
23484
+ if (total >= BIG_WORKSTREAM_TASKS && open * 10 < total) {
23485
+ console.log(
23486
+ pc.dim(
23487
+ `hint: ${total} tasks, ${open} open; start the next effort in a new <project>-<purpose> workstream`
23488
+ )
23489
+ );
23490
+ }
23127
23491
  console.log("");
23128
23492
  console.log(pc.bold(`Tracks (${tracks.length})`));
23129
23493
  console.log(formatTracks(tracks));
@@ -23131,6 +23495,13 @@ function renderFullCard(d) {
23131
23495
  console.log(pc.bold(`Ready (${ready.length})`));
23132
23496
  console.log(ready.length === 0 ? pc.dim(" (none)") : formatTaskListTable(ready));
23133
23497
  console.log("");
23498
+ if (d.triage.length > 0) {
23499
+ console.log(
23500
+ `${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 ...`)}`
23501
+ );
23502
+ console.log(formatTaskListTable(d.triage));
23503
+ console.log("");
23504
+ }
23134
23505
  console.log(pc.bold(`In progress (${inProgress.length})`));
23135
23506
  console.log(inProgress.length === 0 ? pc.dim(" (none)") : formatTaskListTable(inProgress));
23136
23507
  console.log("");
@@ -23736,6 +24107,7 @@ function wireWorkspaceCommands(program) {
23736
24107
  }
23737
24108
 
23738
24109
  // src/cli/workstream.ts
24110
+ import { basename as basename2 } from "path";
23739
24111
  init_mux2();
23740
24112
  init_output();
23741
24113
  init_workstream();
@@ -23764,6 +24136,7 @@ async function cmdInit(db, name, opts = {}) {
23764
24136
  await mux.enableMuPaneBordersForSession(sessionName).catch(() => {
23765
24137
  });
23766
24138
  const created = !sessionAlready || dbCreated;
24139
+ const nameHint = name.includes("-") ? void 0 : `name workstreams <project>-<purpose> (e.g. ${projectGuess() ?? "repo"}-${name}); one per effort`;
23767
24140
  const nextSteps3 = [
23768
24141
  { intent: "Attach the session", command: mux.attachHint({ session: sessionName }) },
23769
24142
  {
@@ -23781,6 +24154,7 @@ async function cmdInit(db, name, opts = {}) {
23781
24154
  muxSessionAlreadyExisted: sessionAlready,
23782
24155
  dbRowAlreadyExisted: !dbCreated,
23783
24156
  muWindowRepaired,
24157
+ ...nameHint && created ? { hint: nameHint } : {},
23784
24158
  nextSteps: nextSteps3
23785
24159
  });
23786
24160
  return;
@@ -23796,8 +24170,13 @@ async function cmdInit(db, name, opts = {}) {
23796
24170
  return;
23797
24171
  }
23798
24172
  console.log(`Created workstream ${pc.bold(name)} (mux session ${pc.bold(sessionName)})`);
24173
+ if (nameHint && created) console.log(pc.dim(`hint: ${nameHint}`));
23799
24174
  printNextSteps(nextSteps3);
23800
24175
  }
24176
+ function projectGuess() {
24177
+ const base = basename2(process.cwd()).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^[^a-z]+/, "").replace(/-+$/, "");
24178
+ return base === "" ? void 0 : base;
24179
+ }
23801
24180
  async function cmdWorkstreamList(db, opts = {}) {
23802
24181
  if (opts.tornDown === true) {
23803
24182
  const gone = listTornDownWorkstreams(db);
@@ -24053,8 +24432,12 @@ function withPositionalWorkstream(opts, name) {
24053
24432
  return { ...opts, workstream: name };
24054
24433
  }
24055
24434
  function wireWorkstreamCommands(program) {
24056
- const workstream = program.command("workstream").description("Workstream-level commands");
24057
- workstream.command("init <name>").description("Create the workstream's mux session and register it in the DB").option(...JSON_OPT).action(function(name) {
24435
+ const workstream = program.command("workstream").description(
24436
+ "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."
24437
+ );
24438
+ workstream.command("init <name>").description(
24439
+ "Create the workstream's mux session and register it in the DB. Name it <project>-<purpose>."
24440
+ ).option(...JSON_OPT).action(function(name) {
24058
24441
  const opts = this.opts();
24059
24442
  return handle((db) => cmdInit(db, name, opts), this)();
24060
24443
  });