@mu-crew/mu 4.0.0 → 4.1.1

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 (35) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-5HECDBK3.js → chunk-B74N2EAS.js} +16 -16
  3. package/dist/{chunk-GQNHIPJN.js → chunk-DEYBQDTO.js} +21 -421
  4. package/dist/chunk-DEYBQDTO.js.map +1 -0
  5. package/dist/{chunk-S424D5EP.js → chunk-L7I3BLXP.js} +635 -142
  6. package/dist/chunk-L7I3BLXP.js.map +1 -0
  7. package/dist/{chunk-AW2NONYU.js → chunk-LBBZAG5C.js} +81 -7
  8. package/dist/chunk-LBBZAG5C.js.map +1 -0
  9. package/dist/{cli-GB2CAVVW.js → cli-H2PWJAJN.js} +346 -44
  10. package/dist/cli-H2PWJAJN.js.map +1 -0
  11. package/dist/cli.js +1 -1
  12. package/dist/{errors-KHOYKD7D.js → errors-G5IVXSMY.js} +2 -2
  13. package/dist/extension/mu-pi.js +28 -14
  14. package/dist/extension/mu-pi.js.map +1 -1
  15. package/dist/index.d.ts +63 -11
  16. package/dist/index.js +15 -15
  17. package/dist/{tui-ALPHYTA2.js → tui-EN2CDP6G.js} +102 -27
  18. package/dist/tui-EN2CDP6G.js.map +1 -0
  19. package/docs/ARCHITECTURE.md +5 -4
  20. package/docs/VOCABULARY.md +6 -5
  21. package/docs/guide/cleanup.md +8 -0
  22. package/docs/guide/delegate.md +11 -3
  23. package/docs/guide/remote.md +19 -12
  24. package/docs/guide/sql.md +1 -0
  25. package/docs/reference/env.md +1 -0
  26. package/docs/reference/naming.md +2 -0
  27. package/package.json +1 -1
  28. package/skills/mu/recipes/remote-workers.md +7 -6
  29. package/dist/chunk-AW2NONYU.js.map +0 -1
  30. package/dist/chunk-GQNHIPJN.js.map +0 -1
  31. package/dist/chunk-S424D5EP.js.map +0 -1
  32. package/dist/cli-GB2CAVVW.js.map +0 -1
  33. package/dist/tui-ALPHYTA2.js.map +0 -1
  34. /package/dist/{chunk-5HECDBK3.js.map → chunk-B74N2EAS.js.map} +0 -0
  35. /package/dist/{errors-KHOYKD7D.js.map → errors-G5IVXSMY.js.map} +0 -0
@@ -41,11 +41,8 @@ import {
41
41
  steerStallHandler,
42
42
  waitForEvents,
43
43
  waitWithActions
44
- } from "./chunk-5HECDBK3.js";
44
+ } from "./chunk-B74N2EAS.js";
45
45
  import {
46
- DelegateCallConflictError,
47
- DelegateNotFoundError,
48
- DelegateStateError,
49
46
  FRONTIER_MAX_DEPTH,
50
47
  HumanNeededInvalidError,
51
48
  NEXT_BYTES,
@@ -56,7 +53,6 @@ import {
56
53
  SyncPeerRefAmbiguousError,
57
54
  SyncSourceNotFoundError,
58
55
  SyncSourceTooNewError,
59
- TERMINAL_DELEGATE_STATES,
60
56
  TaskAutoArgError,
61
57
  TaskAutoRefusedError,
62
58
  TaskBriefMissingError,
@@ -75,27 +71,23 @@ import {
75
71
  currentCallers,
76
72
  encodeEnvelope,
77
73
  escapeRecordText,
78
- finishDelegate,
79
74
  footerLine,
80
75
  foreignVersionMessage,
81
- getDelegateCall,
82
76
  ingestFromDb,
83
77
  jsonDocument,
84
78
  linesBytes,
85
- listDelegates,
86
79
  liveAgentToOutput,
87
80
  loadOpenTaskFrontier,
81
+ localMachineId,
88
82
  logOut,
89
83
  logPage,
90
84
  logReceipt,
91
- markDelegate,
92
85
  noticesField,
93
86
  okEnvelope,
94
87
  pageOut,
95
88
  pageRecords,
96
89
  peerStatuses,
97
90
  recordHumanNeeded,
98
- registerDelegateCall,
99
91
  renderArgv,
100
92
  repairPeer,
101
93
  resumeArgv,
@@ -108,9 +100,8 @@ import {
108
100
  taskPage,
109
101
  taskRecordCut,
110
102
  taskToOutput,
111
- verdictTally,
112
103
  workstreamIdOf
113
- } from "./chunk-GQNHIPJN.js";
104
+ } from "./chunk-DEYBQDTO.js";
114
105
  import {
115
106
  AgentAbortNeedsCtlError,
116
107
  AgentAbortTimeoutError,
@@ -141,6 +132,9 @@ import {
141
132
  CrossWorkstreamEdgeError,
142
133
  CtlDeliveryUnknownError,
143
134
  CycleError,
135
+ DelegateCallConflictError,
136
+ DelegateNotFoundError,
137
+ DelegateStateError,
144
138
  EXPECTED_TABLES,
145
139
  GUARD_ALREADY_OWNED,
146
140
  GUARD_HUGE_ID,
@@ -182,6 +176,7 @@ import {
182
176
  SubstateReasonRequiredError,
183
177
  TASK_STATUS_LIST,
184
178
  TASK_SUBSTATES,
179
+ TERMINAL_DELEGATE_STATES,
185
180
  TaskAlreadyOwnedError,
186
181
  TaskExistsError,
187
182
  TaskHasOpenBlockersError,
@@ -240,6 +235,8 @@ import {
240
235
  deadStagings,
241
236
  defaultDbPath,
242
237
  delegateOutcome,
238
+ delegateProcessDir,
239
+ delegateSessionDir,
243
240
  deleteTask,
244
241
  dirtyFiles,
245
242
  ensureWorkstream,
@@ -247,10 +244,12 @@ import {
247
244
  errCode,
248
245
  expectsCtl,
249
246
  findEdgeCycles,
247
+ finishDelegate,
250
248
  forget,
251
249
  formatPair,
252
250
  getAgent,
253
251
  getAgentByPane,
252
+ getDelegateCall,
254
253
  getHost,
255
254
  getTask,
256
255
  getTaskEdgesWithStatus,
@@ -263,6 +262,7 @@ import {
263
262
  headSha,
264
263
  hostStarter,
265
264
  interruptAgent,
265
+ isAgentListArgv,
266
266
  isCommandGate,
267
267
  isJsonMode,
268
268
  isScratchWorkstream,
@@ -274,6 +274,7 @@ import {
274
274
  lastClaimActor,
275
275
  latestSeq,
276
276
  listAgents,
277
+ listDelegates,
277
278
  listEmptyWorkstreams,
278
279
  listForgetCandidates,
279
280
  listHostChannels,
@@ -288,6 +289,7 @@ import {
288
289
  listTornDownWorkstreams,
289
290
  listWorkstreams,
290
291
  listWorktrees,
292
+ markDelegate,
291
293
  muTable,
292
294
  murmurAvailable,
293
295
  openDb,
@@ -307,6 +309,7 @@ import {
307
309
  reconcile,
308
310
  recoveriesFor,
309
311
  recoveryWorkstreams,
312
+ registerDelegateCall,
310
313
  registerOwnedWait,
311
314
  registerWorktree,
312
315
  reinitWorkstream,
@@ -340,20 +343,21 @@ import {
340
343
  unparkTask,
341
344
  updateTask,
342
345
  usageLines,
346
+ verdictTally,
343
347
  weakDecisionWarning,
344
348
  withOpContext,
345
349
  withTaskFence,
346
350
  workstreamRevision,
347
351
  worktreePath,
348
352
  worktreesRoot
349
- } from "./chunk-S424D5EP.js";
353
+ } from "./chunk-L7I3BLXP.js";
350
354
  import {
351
355
  defaultStateDir
352
356
  } from "./chunk-PZ5GOCWH.js";
353
357
 
354
358
  // src/cli.ts
355
359
  import { readFileSync as readFileSync3 } from "fs";
356
- import { dirname as dirname6, join as join4 } from "path";
360
+ import { dirname as dirname7, join as join5 } from "path";
357
361
  import { fileURLToPath as fileURLToPath3 } from "url";
358
362
 
359
363
  // node_modules/commander/lib/error.js
@@ -4049,7 +4053,7 @@ function emitError(err) {
4049
4053
  agentId: err.agentId
4050
4054
  });
4051
4055
  }
4052
- const humanNoMux = err instanceof NoMultiplexerError && isStateVerbArgv(process.argv) && !process.argv.some((a) => a === "--json" || a.startsWith("--json="));
4056
+ const humanNoMux = err instanceof NoMultiplexerError && (isStateVerbArgv(process.argv) || isAgentListArgv(process.argv)) && !process.argv.some((a) => a === "--json" || a.startsWith("--json="));
4053
4057
  if (isJsonMode() && !humanNoMux) {
4054
4058
  const extra = {};
4055
4059
  if (usageCmd) extra.usage = usageLines(usageCmd);
@@ -4650,7 +4654,7 @@ async function parseWaitRef(db, raw, scope, legacy = {}) {
4650
4654
  return { kind, workstream, id: parsed.name };
4651
4655
  }
4652
4656
  if (getAgent(db, parsed.name, workstream) === void 0) {
4653
- const { AgentNotFoundError: AgentNotFoundError2 } = await import("./errors-KHOYKD7D.js");
4657
+ const { AgentNotFoundError: AgentNotFoundError2 } = await import("./errors-G5IVXSMY.js");
4654
4658
  throw new AgentNotFoundError2(parsed.name);
4655
4659
  }
4656
4660
  return {
@@ -6994,6 +6998,7 @@ function assertWorktreeFlags(opts) {
6994
6998
  return;
6995
6999
  }
6996
7000
  for (const flag of ["from", "root"]) {
7001
+ if (flag === "root" && opts.host !== void 0) continue;
6997
7002
  if (opts[flag] !== void 0) throw new UsageError(say("agent.spawn_needs_worktree", flag));
6998
7003
  }
6999
7004
  }
@@ -7847,10 +7852,10 @@ function wireAgentCommands(program2) {
7847
7852
  "run in its own git worktree (git worktree add --detach under the state dir); its path is the cwd"
7848
7853
  ).option("--from <ref>", "with --worktree: start at this commit or branch (default: HEAD)").option(
7849
7854
  "--root <repo>",
7850
- "with --worktree: the repo to add it to (default: the git root of your cwd)"
7855
+ "with --worktree: the repo to add it to (default: the git root of your cwd). With --host: a REMOTE repo (default the profile's --repo), also without --worktree, as the start directory"
7851
7856
  ).option(
7852
7857
  "--host <profile>",
7853
- "run on this host profile (mu host add): the pane runs its --shell template, which starts the remote pi and forwards its control socket here; --cwd and --worktree name REMOTE paths (the worktree is made by its --exec, under ~/.local/state/mu-next/remote-worktrees/<ws>/<name>); the pane dying is the agent dying. tmux only; not with --root"
7858
+ "run on this host profile (mu host add): the pane runs its --shell template, which starts the remote pi and forwards its control socket here; --cwd and --worktree name REMOTE paths (the worktree is made by its --exec, under ~/.local/state/mu-next/remote-worktrees/<ws>/<name>); --root names the remote repo; the pane dying is the agent dying. tmux only"
7854
7859
  ).option("--no-ctl", "skip the control-socket handshake with the mu pi extension").option(
7855
7860
  "--next-free",
7856
7861
  "if <name> is taken, take the next free one (worker-1 \u2192 worker-2, review \u2192 review-2); --json's agent.name says which"
@@ -7912,8 +7917,11 @@ function wireAgentCommands(program2) {
7912
7917
  const opts = this.opts();
7913
7918
  return handle((db) => cmdRead(db, name, opts), this)();
7914
7919
  });
7915
- agent.command("list").description("List agents in the current workstream (reconciled with the active mux)").option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function() {
7920
+ agent.command("list").description(
7921
+ "List agents in the current workstream (reconciled with the active mux). One JSON document by default; --table for the human table"
7922
+ ).option(...WORKSTREAM_OPT).option(...JSON_OPT).option("--table", "print the human table instead of the JSON document").action(function() {
7916
7923
  const opts = this.opts();
7924
+ opts.json = opts.json === true || opts.table !== true;
7917
7925
  return handle((db) => cmdList(db, opts), this)();
7918
7926
  });
7919
7927
  agent.command("show <name>").description("Show an agent: registry row + recent scrollback (last N lines, default 20)").option("-n, --lines <n>", "how many scrollback lines to show (default 20)", parseLines).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(name) {
@@ -8037,7 +8045,7 @@ function openDashboardOrHelp(program2) {
8037
8045
  (syncDb) => withShutdown(syncDb, async (shutdown) => {
8038
8046
  const key = "NODE_ENV";
8039
8047
  process.env[key] ??= "production";
8040
- const { runDashboard } = await import("./tui-ALPHYTA2.js");
8048
+ const { runDashboard } = await import("./tui-EN2CDP6G.js");
8041
8049
  const first = program2.opts().workstream?.[0];
8042
8050
  await runDashboard({ dbPath: defaultDbPath(), syncDb, shutdown, initialWorkstream: first });
8043
8051
  }),
@@ -8176,7 +8184,7 @@ function checkStrayDbFiles() {
8176
8184
  `Live DB: ${defaultDbPath()}`,
8177
8185
  "",
8178
8186
  "These are hand-made copies, pre-upgrade saves, and the backups",
8179
- "`mu db compact` / `mu db forget` write first (mu.db.pre-<verb>-<time>).",
8187
+ "`mu db compact` / `mu db forget` / `mu delegates prune` write first (mu.db.pre-<verb>-<time>).",
8180
8188
  "No mu code path reads them and none prunes them. Keep a pre-forget",
8181
8189
  "backup until you are sure: it is the only way back from a forget.",
8182
8190
  "Otherwise disaster recovery is the ops log, so a copy is a convenience:",
@@ -9106,7 +9114,7 @@ var LOG_TAIL_BYTES = 2e3;
9106
9114
  var sha = (s, n) => createHash("sha256").update(s).digest("hex").slice(0, n);
9107
9115
  function delegateProcessPaths(db, row) {
9108
9116
  const base = dirname3(db.name);
9109
- const dir = join2(base, "delegate-process", sha(row.callId, 16));
9117
+ const dir = delegateProcessDir(db.name, row.callId);
9110
9118
  return {
9111
9119
  dir,
9112
9120
  log: join2(dir, "pi.log"),
@@ -9437,6 +9445,196 @@ ${tail(p.log)}` : void 0
9437
9445
  }
9438
9446
  }
9439
9447
 
9448
+ // src/delegate-prune.ts
9449
+ import { lstatSync, readdirSync as readdirSync2, rmSync as rmSync2 } from "fs";
9450
+ import { dirname as dirname4, join as join3 } from "path";
9451
+ var PRUNE_DEFAULT_DAYS = 30;
9452
+ function sizeOf3(p) {
9453
+ let st;
9454
+ try {
9455
+ st = lstatSync(p);
9456
+ } catch {
9457
+ return 0;
9458
+ }
9459
+ if (!st.isDirectory()) return st.size;
9460
+ let n = 0;
9461
+ for (const e of readdirSync2(p)) n += sizeOf3(join3(p, e));
9462
+ return n;
9463
+ }
9464
+ var hasFileDir = (db) => db.name !== "" && db.name !== ":memory:";
9465
+ var LOOSE_MIN_AGE_MS = 864e5;
9466
+ function looseSessions(db, cutoff, workstream, now) {
9467
+ const root = join3(dirname4(db.name), "delegate-sessions");
9468
+ const before = Math.min(Date.parse(cutoff), now - LOOSE_MIN_AGE_MS);
9469
+ const out = [];
9470
+ const wss = db.prepare(
9471
+ "SELECT name FROM workstreams WHERE torn_down_at IS NULL AND (? IS NULL OR name = ?)"
9472
+ ).all(workstream ?? null, workstream ?? null).map((r) => r.name);
9473
+ for (const ws of wss) {
9474
+ let names;
9475
+ try {
9476
+ names = readdirSync2(join3(root, ws));
9477
+ } catch {
9478
+ continue;
9479
+ }
9480
+ for (const f of names) {
9481
+ const p = join3(root, ws, f);
9482
+ if (!f.endsWith(".jsonl")) continue;
9483
+ try {
9484
+ const st = lstatSync(p);
9485
+ if (st.isFile() && st.mtimeMs < before) out.push(p);
9486
+ } catch {
9487
+ }
9488
+ }
9489
+ }
9490
+ return out.sort();
9491
+ }
9492
+ var callFiles = (db, c) => [
9493
+ delegateSessionDir(db.name, c.workstream, c.task),
9494
+ delegateProcessDir(db.name, c.callId)
9495
+ ];
9496
+ var TASK_PUTS_WHERE = `entity IN ('task', 'note') AND op = 'put'
9497
+ AND key >= @k AND key < @k || '$' AND (key = @k OR key >= @k || '#')
9498
+ AND (@own = 1 OR machine_id <> @me)`;
9499
+ var MIN_DATE_MS = -864e13;
9500
+ function planDelegatePrune(db, opts = {}) {
9501
+ const days2 = opts.days ?? PRUNE_DEFAULT_DAYS;
9502
+ const ownHistory = opts.ownHistory ?? !syncEnabled();
9503
+ const cutoff = new Date(
9504
+ Math.max(MIN_DATE_MS, (opts.now ?? Date.now()) - days2 * 864e5)
9505
+ ).toISOString();
9506
+ const rows = db.prepare(
9507
+ `SELECT t.id AS id, w.name AS workstream, t.local_id AS task, t.updated_at AS updated_at,
9508
+ d.call_id AS call_id, d.state AS call_state,
9509
+ (SELECT COUNT(*) FROM task_edges e WHERE e.from_task_id = t.id OR e.to_task_id = t.id) AS edges,
9510
+ EXISTS (SELECT 1 FROM tasks jt JOIN workstreams jw ON jw.id = jt.workstream_id
9511
+ JOIN task_auto ja ON ja.task_id = jt.id AND ja.enabled = 1
9512
+ WHERE jw.name = d.record_workstream AND jt.local_id = d.record_task) AS judged_auto
9513
+ FROM tasks t
9514
+ JOIN workstreams w ON w.id = t.workstream_id AND w.torn_down_at IS NULL
9515
+ JOIN delegate_calls d ON d.task_id = t.id
9516
+ WHERE t.kind = 'delegate' AND t.status = 'CLOSED' AND t.updated_at < ?
9517
+ AND (? IS NULL OR w.name = ?)
9518
+ ORDER BY w.name, t.updated_at, t.local_id`
9519
+ ).all(cutoff, opts.workstream ?? null, opts.workstream ?? null);
9520
+ const plan = {
9521
+ cutoff,
9522
+ days: days2,
9523
+ candidates: [],
9524
+ kept: { edges: 0, refuteTarget: 0 },
9525
+ ownHistory,
9526
+ ops: 0,
9527
+ bytes: 0,
9528
+ files: { paths: [], bytes: 0 }
9529
+ };
9530
+ const cost = db.prepare(
9531
+ `SELECT COUNT(*) AS n, COALESCE(SUM(length(payload)), 0) AS b FROM ops WHERE ${TASK_PUTS_WHERE}`
9532
+ );
9533
+ const me = localMachineId(db);
9534
+ const own = ownHistory ? 1 : 0;
9535
+ for (const r of rows) {
9536
+ if (!["done", "failed", "cancelled"].includes(r.call_state)) continue;
9537
+ if (r.edges > 0) {
9538
+ plan.kept.edges++;
9539
+ continue;
9540
+ }
9541
+ if (r.call_id.startsWith("refute:") && r.judged_auto === 1) {
9542
+ plan.kept.refuteTarget++;
9543
+ continue;
9544
+ }
9545
+ plan.candidates.push({
9546
+ workstream: r.workstream,
9547
+ task: r.task,
9548
+ callId: r.call_id,
9549
+ closedAt: r.updated_at
9550
+ });
9551
+ const c = cost.get({ k: `${r.workstream}/${r.task}`, own, me });
9552
+ plan.ops += c.n;
9553
+ plan.bytes += c.b;
9554
+ }
9555
+ if (hasFileDir(db)) {
9556
+ const paths = [
9557
+ ...plan.candidates.flatMap((c) => callFiles(db, c)).filter(exists2),
9558
+ ...looseSessions(db, cutoff, opts.workstream, opts.now ?? Date.now())
9559
+ ];
9560
+ plan.files = { paths, bytes: paths.reduce((n, p) => n + sizeOf3(p), 0) };
9561
+ }
9562
+ return plan;
9563
+ }
9564
+ function exists2(p) {
9565
+ try {
9566
+ lstatSync(p);
9567
+ return true;
9568
+ } catch {
9569
+ return false;
9570
+ }
9571
+ }
9572
+ function pruneDelegates(db, plan) {
9573
+ let tasks = 0;
9574
+ let ops = 0;
9575
+ const pruned = [];
9576
+ withOpContext(
9577
+ db,
9578
+ { intent: "delegate.prune", actor: "mu delegates prune", group: "new" },
9579
+ () => db.transaction(() => {
9580
+ const del = db.prepare(
9581
+ `DELETE FROM tasks WHERE kind = 'delegate' AND status = 'CLOSED' AND updated_at < ?
9582
+ AND local_id = ? AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)
9583
+ AND NOT EXISTS (SELECT 1 FROM task_edges e
9584
+ WHERE e.from_task_id = tasks.id OR e.to_task_id = tasks.id)`
9585
+ );
9586
+ const puts = db.prepare(`DELETE FROM ops WHERE ${TASK_PUTS_WHERE}`);
9587
+ const me = localMachineId(db);
9588
+ const own = plan.ownHistory ? 1 : 0;
9589
+ const cursor = db.prepare("DELETE FROM wait_cursors WHERE ref = ?");
9590
+ for (const c of plan.candidates) {
9591
+ if (del.run(plan.cutoff, c.task, c.workstream).changes === 0) continue;
9592
+ tasks++;
9593
+ pruned.push(c);
9594
+ ops += puts.run({ k: `${c.workstream}/${c.task}`, own, me }).changes;
9595
+ cursor.run(`delegate:${c.workstream}/${c.callId}`);
9596
+ }
9597
+ }).immediate()
9598
+ );
9599
+ let files = 0;
9600
+ if (hasFileDir(db)) {
9601
+ const loose = new Set(plan.files.paths);
9602
+ const go = [
9603
+ ...pruned.flatMap((c) => callFiles(db, c)),
9604
+ ...looseSessions(db, plan.cutoff, void 0, Date.now()).filter((p) => loose.has(p))
9605
+ ];
9606
+ for (const p of go) {
9607
+ if (!exists2(p)) continue;
9608
+ try {
9609
+ rmSync2(p, { recursive: true, force: true });
9610
+ files++;
9611
+ } catch {
9612
+ }
9613
+ }
9614
+ }
9615
+ return { tasks, ops, files };
9616
+ }
9617
+ function checkOldDelegates(db, opts = {}) {
9618
+ const plan = planDelegatePrune(db, opts);
9619
+ const n = plan.candidates.length;
9620
+ if (n === 0)
9621
+ return {
9622
+ name: "delegates-old",
9623
+ severity: "ok",
9624
+ detail: `no closed delegate task older than ${plan.days} days`
9625
+ };
9626
+ return {
9627
+ name: "delegates-old",
9628
+ severity: "ok",
9629
+ detail: `${n} closed delegate task(s) older than ${plan.days} days (${plan.ops} ops)`,
9630
+ remediation: [
9631
+ "Each holds its whole brief and report, synced to every peer. Their verdicts stay on the judged tasks.",
9632
+ " mu delegates prune # dry run: what goes, and what is kept",
9633
+ " mu delegates prune --yes # backs up beside the DB first; irreversible"
9634
+ ]
9635
+ };
9636
+ }
9637
+
9440
9638
  // src/workstream-scope.ts
9441
9639
  function workstreamNameProblem(value, flag = "-w/--workstream") {
9442
9640
  if (value.length > 0 && value.trim().length === 0) {
@@ -9648,6 +9846,82 @@ async function cmdDelegatesRun(db, callId, opts) {
9648
9846
  }
9649
9847
  if (r.ran && r.state !== "done") throw new CliExitError(1);
9650
9848
  }
9849
+ function keptLine(plan) {
9850
+ const parts = [
9851
+ plan.kept.edges > 0 ? `${plan.kept.edges} with edges` : "",
9852
+ plan.kept.refuteTarget > 0 ? `${plan.kept.refuteTarget} refute checks of tasks still under mu task auto` : ""
9853
+ ].filter((p) => p !== "");
9854
+ return parts.length > 0 ? `kept: ${parts.join(", ")}` : "";
9855
+ }
9856
+ async function cmdDelegatesPrune(db, opts) {
9857
+ if (opts.workstream !== void 0) await resolveExistingWorkstream(db, opts.workstream);
9858
+ const plan = planDelegatePrune(db, {
9859
+ ...opts.olderThan !== void 0 ? { days: opts.olderThan } : {},
9860
+ ...opts.workstream !== void 0 ? { workstream: opts.workstream } : {}
9861
+ });
9862
+ const n = plan.candidates.length;
9863
+ const files = plan.files.paths.length;
9864
+ const filesNote = files > 0 ? `, ${files} pi session(s) (${formatBytes(plan.files.bytes)})` : "";
9865
+ if (opts.yes === true && n === 0 && files === 0) {
9866
+ if (opts.json)
9867
+ return emitJson({ dryRun: false, ...plan, pruned: 0, deletedOps: 0, deletedFiles: 0 });
9868
+ console.log(pc.dim(`nothing to prune: no closed delegate task older than ${plan.days} days`));
9869
+ const kept2 = keptLine(plan);
9870
+ if (kept2) console.log(pc.dim(kept2));
9871
+ return;
9872
+ }
9873
+ if (opts.yes !== true) {
9874
+ if (opts.json) return emitJson({ dryRun: true, ...plan });
9875
+ if (n === 0 && files === 0)
9876
+ console.log(pc.dim(`nothing to prune: no closed delegate task older than ${plan.days} days`));
9877
+ else {
9878
+ const t = muTable({ head: ["TASK", "CALL", "CLOSED"] });
9879
+ for (const c of plan.candidates.slice(0, 20))
9880
+ t.push([`${c.workstream}/${c.task}`, c.callId, c.closedAt.slice(0, 10)]);
9881
+ console.log(t.toString());
9882
+ if (n > 20) console.log(pc.dim(`... and ${n - 20} more`));
9883
+ console.log(
9884
+ `${n} closed delegate task(s) older than ${plan.days} days: ${plan.ops} ops (${formatBytes(plan.bytes)})${filesNote}. Dry run; nothing changed (--yes applies; irreversible).`
9885
+ );
9886
+ if (!plan.ownHistory)
9887
+ console.log(
9888
+ pc.dim(
9889
+ "sync is on: the rows go everywhere, but this machine's own history stays (its segment is rebuilt from it)"
9890
+ )
9891
+ );
9892
+ }
9893
+ const kept2 = keptLine(plan);
9894
+ if (kept2) console.log(pc.dim(kept2));
9895
+ return;
9896
+ }
9897
+ if (n === 0) {
9898
+ const r2 = pruneDelegates(db, plan);
9899
+ if (opts.json)
9900
+ return emitJson({ dryRun: false, ...plan, pruned: 0, deletedOps: 0, deletedFiles: r2.files });
9901
+ console.log(`Pruned ${r2.files} pi session(s); no delegate task was old enough.`);
9902
+ return;
9903
+ }
9904
+ const backup = backupBeside(db, "prune");
9905
+ const r = pruneDelegates(db, plan);
9906
+ const size = vacuumAndCheck(db, backup);
9907
+ if (opts.json)
9908
+ return emitJson({
9909
+ dryRun: false,
9910
+ ...plan,
9911
+ pruned: r.tasks,
9912
+ deletedOps: r.ops,
9913
+ deletedFiles: r.files,
9914
+ backup,
9915
+ ...size
9916
+ });
9917
+ console.log(
9918
+ `Pruned ${r.tasks} delegate task(s), ${r.ops} ops${r.files > 0 ? `, ${r.files} pi session(s)` : ""}: ${formatBytes(size.before)} \u2192 ${formatBytes(size.after)}. Drift check clean.`
9919
+ );
9920
+ const kept = keptLine(plan);
9921
+ if (kept) console.log(pc.dim(kept));
9922
+ if (!size.checkpointed) console.log(pc.dim(CHECKPOINT_PENDING));
9923
+ console.log(pc.dim(`backup (the only way back): ${backup}`));
9924
+ }
9651
9925
  async function cmdDelegatesReissue(_db, callId, _opts) {
9652
9926
  throw new UsageError(
9653
9927
  `reissue runs inside the pi session that owns the call: use ${reissueHint(callId)} there (a shell would leave it queued with nothing to start it)`,
@@ -9699,6 +9973,12 @@ function wireDelegatesCommands(program2) {
9699
9973
  const opts = this.optsWithGlobals();
9700
9974
  return handle((db) => cmdDelegatesRun(db, callId, opts), this)();
9701
9975
  });
9976
+ d.command("prune").description(
9977
+ `Delete this machine's closed delegate tasks older than --older-than days (default ${PRUNE_DEFAULT_DAYS}) and their pi session files; the deletion syncs. Dry run without --yes; --yes backs up, VACUUMs, checks drift`
9978
+ ).option("--older-than <days>", "age in days since the task closed", parsePositiveNumber).option("--yes", "apply (default: dry run)").option("-w, --workstream <name>", "only this workstream (default: every live one)").option(...JSON_OPT).action(function() {
9979
+ const opts = this.optsWithGlobals();
9980
+ return handle((db) => cmdDelegatesPrune(db, opts), this)();
9981
+ });
9702
9982
  d.command("reissue <call-id>").description(
9703
9983
  "Refuses: reissue a refused call with the `mu_delegate_reissue` tool in its pi session (only that runtime can start or drain it)"
9704
9984
  ).option(...JSON_OPT).action(function(callId) {
@@ -9708,15 +9988,15 @@ function wireDelegatesCommands(program2) {
9708
9988
  }
9709
9989
 
9710
9990
  // src/cli/doctor.ts
9711
- import { dirname as dirname5 } from "path";
9991
+ import { dirname as dirname6 } from "path";
9712
9992
 
9713
9993
  // src/doctor-summary.ts
9714
9994
  import { accessSync, constants, existsSync as existsSync3, readFileSync as readFileSync2, realpathSync } from "fs";
9715
- import { delimiter, dirname as dirname4, join as join3, parse } from "path";
9995
+ import { delimiter, dirname as dirname5, join as join4, parse } from "path";
9716
9996
  import { fileURLToPath as fileURLToPath2 } from "url";
9717
9997
  function resolveExecutable(name) {
9718
9998
  for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
9719
- const candidate = join3(dir, name);
9999
+ const candidate = join4(dir, name);
9720
10000
  try {
9721
10001
  accessSync(candidate, constants.X_OK);
9722
10002
  return realpathSync(candidate);
@@ -9726,10 +10006,10 @@ function resolveExecutable(name) {
9726
10006
  return null;
9727
10007
  }
9728
10008
  function murmurVersion(executable) {
9729
- let dir = dirname4(executable);
10009
+ let dir = dirname5(executable);
9730
10010
  const root = parse(dir).root;
9731
10011
  while (dir !== root) {
9732
- const packagePath = join3(dir, "package.json");
10012
+ const packagePath = join4(dir, "package.json");
9733
10013
  if (existsSync3(packagePath)) {
9734
10014
  try {
9735
10015
  const pkg = JSON.parse(readFileSync2(packagePath, "utf8"));
@@ -9739,7 +10019,7 @@ function murmurVersion(executable) {
9739
10019
  } catch {
9740
10020
  }
9741
10021
  }
9742
- dir = dirname4(dir);
10022
+ dir = dirname5(dir);
9743
10023
  }
9744
10024
  return null;
9745
10025
  }
@@ -9811,7 +10091,7 @@ function ctlExtensionDoctorCheck(reports) {
9811
10091
  var DOCTOR_CTL_TIMEOUT_MS = 500;
9812
10092
  function installedMuVersion() {
9813
10093
  try {
9814
- const pkgPath = join3(dirname4(fileURLToPath2(import.meta.url)), "..", "package.json");
10094
+ const pkgPath = join4(dirname5(fileURLToPath2(import.meta.url)), "..", "package.json");
9815
10095
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
9816
10096
  const v = typeof pkg === "object" && pkg !== null ? pkg.version : null;
9817
10097
  return typeof v === "string" ? v : null;
@@ -10055,7 +10335,7 @@ async function cmdDoctor(db, opts = {}) {
10055
10335
  console.log(` ${pad(fact.name)}: ${fact.value ? pc.green(fact.value) : pc.dim("not set")}`);
10056
10336
  }
10057
10337
  }
10058
- const ctlSockets = await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname5(db.name) });
10338
+ const ctlSockets = await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname6(db.name) });
10059
10339
  const muExt = ctlExtensionDoctorCheck(ctlSockets.agents);
10060
10340
  printCheck("agent state", agentStateCheck(health, extensionLive(ctlSockets.agents)));
10061
10341
  printCheck("mu ext", muExt);
@@ -10199,6 +10479,14 @@ state (workstream=${ws})`));
10199
10479
  console.log("");
10200
10480
  for (const line2 of dormant.remediation) console.log(` ${line2}`);
10201
10481
  }
10482
+ const oldDelegates = checkOldDelegates(db);
10483
+ console.log(
10484
+ ` ${oldDelegates.name.padEnd(16)} : ${oldDelegates.remediation === void 0 ? pc.green("ok") : pc.yellow("note")} ${pc.dim(oldDelegates.detail)}`
10485
+ );
10486
+ if (oldDelegates.remediation !== void 0) {
10487
+ console.log("");
10488
+ for (const line2 of oldDelegates.remediation) console.log(` ${line2}`);
10489
+ }
10202
10490
  console.log(pc.bold("\ndisk"));
10203
10491
  const disk = checkDiskRecon(db);
10204
10492
  if (printHazards(disk)) sawHazard = true;
@@ -10301,7 +10589,7 @@ function opsSizeHint(db) {
10301
10589
  }
10302
10590
  async function cmdDoctorJson(db, opts = {}) {
10303
10591
  const health = await muxHealth();
10304
- const ctlSockets = await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname5(db.name) });
10592
+ const ctlSockets = await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname6(db.name) });
10305
10593
  const muExt = ctlExtensionDoctorCheck(ctlSockets.agents);
10306
10594
  const env = {
10307
10595
  // `mux` is the backend-agnostic key. `tmux` is kept as an alias for
@@ -10410,7 +10698,16 @@ async function cmdDoctorJson(db, opts = {}) {
10410
10698
  remediation: dormantHazard.remediation ?? [],
10411
10699
  workstreams: findDormantWorkstreams(db, { currentWorkstream }),
10412
10700
  thresholds: { finishedIdleDays: FINISHED_IDLE_DAYS, abandonedIdleDays: ABANDONED_IDLE_DAYS }
10413
- }
10701
+ },
10702
+ oldDelegates: (() => {
10703
+ const h = checkOldDelegates(db);
10704
+ return {
10705
+ name: h.name,
10706
+ severity: h.severity,
10707
+ detail: h.detail,
10708
+ remediation: h.remediation ?? []
10709
+ };
10710
+ })()
10414
10711
  };
10415
10712
  const worktreeUsage = opts.disk === true ? measureWorktreeUsage(db) : null;
10416
10713
  let drift;
@@ -10514,7 +10811,7 @@ function cmdHostAdd(db, name, opts) {
10514
10811
  shell: opts.shell,
10515
10812
  exec: opts.exec,
10516
10813
  fetch: opts.fetch,
10517
- repo: opts.repo,
10814
+ repo: opts.repo ?? null,
10518
10815
  max_sessions: opts.maxSessions ?? null
10519
10816
  });
10520
10817
  if (isJsonMode()) {
@@ -10535,9 +10832,9 @@ function cmdHostList(db) {
10535
10832
  }
10536
10833
  const held = /* @__PURE__ */ new Map();
10537
10834
  for (const c of listHostChannels(db)) held.set(c.host, (held.get(c.host) ?? 0) + 1);
10538
- const t = muTable({ head: ["HOST", "REPO", "CHANNELS"] });
10835
+ const t = muTable({ head: ["HOST", "DEFAULT REPO", "CHANNELS"] });
10539
10836
  for (const h of hosts) {
10540
- t.push([h.name, h.repo, `${held.get(h.name) ?? 0}/${h.max_sessions ?? "\u221E"}`]);
10837
+ t.push([h.name, h.repo ?? "-", `${held.get(h.name) ?? 0}/${h.max_sessions ?? "\u221E"}`]);
10541
10838
  }
10542
10839
  console.log(t.toString());
10543
10840
  }
@@ -10549,7 +10846,7 @@ function cmdHostShow(db, name) {
10549
10846
  return;
10550
10847
  }
10551
10848
  console.log(
10552
- `${pc.bold(host.name)} repo ${host.repo} max-sessions ${host.max_sessions ?? "none"}`
10849
+ `${pc.bold(host.name)} default repo ${host.repo ?? "-"} max-sessions ${host.max_sessions ?? "none"}`
10553
10850
  );
10554
10851
  for (const f of ["shell", "exec", "fetch"]) console.log(` ${f.padEnd(6)} ${host[f]}`);
10555
10852
  console.log(
@@ -10580,7 +10877,6 @@ function cmdHostInit(name, opts) {
10580
10877
  `--shell ${shellQuote(s.shell)}`,
10581
10878
  `--exec ${shellQuote(s.exec)}`,
10582
10879
  `--fetch ${shellQuote(s.fetch)}`,
10583
- "--repo '<remote repo path>'",
10584
10880
  "--max-sessions 1"
10585
10881
  ].join(" \\\n ");
10586
10882
  if (isJsonMode()) {
@@ -10593,7 +10889,10 @@ function cmdHostInit(name, opts) {
10593
10889
  }
10594
10890
  async function cmdHostCheck(db, name, opts) {
10595
10891
  const agent = opts.agent === void 0 ? void 0 : { name: opts.agent, workstream: await resolveWorkstream(opts.workstream) };
10596
- const report = await checkHost(db, name, agent === void 0 ? {} : { agent });
10892
+ const report = await checkHost(db, name, {
10893
+ ...agent !== void 0 ? { agent } : {},
10894
+ ...opts.root !== void 0 ? { root: opts.root } : {}
10895
+ });
10597
10896
  if (isJsonMode()) {
10598
10897
  if (!report.ok) throw failureWithData(failedError(report), report);
10599
10898
  emitJson(report);
@@ -10617,7 +10916,10 @@ function wireHostCommands(program2) {
10617
10916
  );
10618
10917
  host.command("add <name>").description(
10619
10918
  "Store a machine-local host profile of command templates: --shell opens the pane, --exec runs one remote command, --fetch brings commits home. Placeholders {env} {cmd} {local_sock} {remote_sock} {cwd}; grammar in docs/guide/remote.md."
10620
- ).requiredOption("--shell <template>", "pane command template (opens the remote shell)").requiredOption("--exec <template>", "template that runs one remote command ({cmd})").requiredOption("--fetch <template>", "local template that fetches commits home ({cwd})").requiredOption("--repo <path>", "the repository's path on the host").option(
10919
+ ).requiredOption("--shell <template>", "pane command template (opens the remote shell)").requiredOption("--exec <template>", "template that runs one remote command ({cmd})").requiredOption("--fetch <template>", "local template that fetches commits home ({cwd})").option(
10920
+ "--repo <path>",
10921
+ "default remote repo for `agent spawn --host` without --root (a profile names a machine, not a repo)"
10922
+ ).option(
10621
10923
  "--max-sessions <n>",
10622
10924
  "cap on channels mu holds to this host at once (panes, --exec, --fetch)",
10623
10925
  parseNonNegativeInt
@@ -10641,8 +10943,8 @@ function wireHostCommands(program2) {
10641
10943
  return handle(async () => cmdHostInit(name, opts), this)();
10642
10944
  });
10643
10945
  host.command("check <name>").description(
10644
- "Run the profile's --exec under one channel: host answers, repo is git, pi runs, `pi list` names mu. --agent also probes that agent's forwarded control socket. Reports each failure, retries nothing; exit 5 if a step fails."
10645
- ).option("--agent <name>", "also probe this agent's control socket (the forward rides its pane)").option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(name) {
10946
+ "Run the profile's --exec under one channel: host answers, the repo (--root, else the default) is git, pi runs, `pi list` names mu. --agent also probes that agent's control socket. Exit 5 if a step fails; retries nothing."
10947
+ ).option("--agent <name>", "also probe this agent's control socket (the forward rides its pane)").option("--root <path>", "the remote repo the repo step checks (default: the profile's)").option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(name) {
10646
10948
  const opts = this.optsWithGlobals();
10647
10949
  return handle((db) => cmdHostCheck(db, name, opts), this)();
10648
10950
  });
@@ -11994,8 +12296,8 @@ function emitJsonCollection(items) {
11994
12296
  }
11995
12297
  function readPackageVersion() {
11996
12298
  try {
11997
- const here = dirname6(fileURLToPath3(import.meta.url));
11998
- const pkgPath = join4(here, "..", "package.json");
12299
+ const here = dirname7(fileURLToPath3(import.meta.url));
12300
+ const pkgPath = join5(here, "..", "package.json");
11999
12301
  const raw = readFileSync3(pkgPath, "utf8");
12000
12302
  const parsed = JSON.parse(raw);
12001
12303
  return typeof parsed.version === "string" ? parsed.version : "unknown";
@@ -12170,4 +12472,4 @@ export {
12170
12472
  truncate,
12171
12473
  truncateFront
12172
12474
  };
12173
- //# sourceMappingURL=cli-GB2CAVVW.js.map
12475
+ //# sourceMappingURL=cli-H2PWJAJN.js.map