@mu-crew/mu 4.0.0 → 4.1.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 (34) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-5HECDBK3.js → chunk-4ULY3ZKO.js} +16 -16
  3. package/dist/{chunk-AW2NONYU.js → chunk-7M634JMD.js} +81 -7
  4. package/dist/chunk-7M634JMD.js.map +1 -0
  5. package/dist/{chunk-GQNHIPJN.js → chunk-SS7NQSSZ.js} +21 -421
  6. package/dist/chunk-SS7NQSSZ.js.map +1 -0
  7. package/dist/{chunk-S424D5EP.js → chunk-UCZPJX7K.js} +628 -141
  8. package/dist/chunk-UCZPJX7K.js.map +1 -0
  9. package/dist/{cli-GB2CAVVW.js → cli-TV3N47AZ.js} +340 -42
  10. package/dist/cli-TV3N47AZ.js.map +1 -0
  11. package/dist/cli.js +1 -1
  12. package/dist/{errors-KHOYKD7D.js → errors-GRONKWFS.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-FUBQ326D.js} +102 -27
  18. package/dist/tui-FUBQ326D.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/reference/env.md +1 -0
  25. package/docs/reference/naming.md +2 -0
  26. package/package.json +1 -1
  27. package/skills/mu/recipes/remote-workers.md +7 -6
  28. package/dist/chunk-AW2NONYU.js.map +0 -1
  29. package/dist/chunk-GQNHIPJN.js.map +0 -1
  30. package/dist/chunk-S424D5EP.js.map +0 -1
  31. package/dist/cli-GB2CAVVW.js.map +0 -1
  32. package/dist/tui-ALPHYTA2.js.map +0 -1
  33. /package/dist/{chunk-5HECDBK3.js.map → chunk-4ULY3ZKO.js.map} +0 -0
  34. /package/dist/{errors-KHOYKD7D.js.map → errors-GRONKWFS.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-4ULY3ZKO.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-SS7NQSSZ.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,
@@ -274,6 +273,7 @@ import {
274
273
  lastClaimActor,
275
274
  latestSeq,
276
275
  listAgents,
276
+ listDelegates,
277
277
  listEmptyWorkstreams,
278
278
  listForgetCandidates,
279
279
  listHostChannels,
@@ -288,6 +288,7 @@ import {
288
288
  listTornDownWorkstreams,
289
289
  listWorkstreams,
290
290
  listWorktrees,
291
+ markDelegate,
291
292
  muTable,
292
293
  murmurAvailable,
293
294
  openDb,
@@ -307,6 +308,7 @@ import {
307
308
  reconcile,
308
309
  recoveriesFor,
309
310
  recoveryWorkstreams,
311
+ registerDelegateCall,
310
312
  registerOwnedWait,
311
313
  registerWorktree,
312
314
  reinitWorkstream,
@@ -340,20 +342,21 @@ import {
340
342
  unparkTask,
341
343
  updateTask,
342
344
  usageLines,
345
+ verdictTally,
343
346
  weakDecisionWarning,
344
347
  withOpContext,
345
348
  withTaskFence,
346
349
  workstreamRevision,
347
350
  worktreePath,
348
351
  worktreesRoot
349
- } from "./chunk-S424D5EP.js";
352
+ } from "./chunk-UCZPJX7K.js";
350
353
  import {
351
354
  defaultStateDir
352
355
  } from "./chunk-PZ5GOCWH.js";
353
356
 
354
357
  // src/cli.ts
355
358
  import { readFileSync as readFileSync3 } from "fs";
356
- import { dirname as dirname6, join as join4 } from "path";
359
+ import { dirname as dirname7, join as join5 } from "path";
357
360
  import { fileURLToPath as fileURLToPath3 } from "url";
358
361
 
359
362
  // node_modules/commander/lib/error.js
@@ -4650,7 +4653,7 @@ async function parseWaitRef(db, raw, scope, legacy = {}) {
4650
4653
  return { kind, workstream, id: parsed.name };
4651
4654
  }
4652
4655
  if (getAgent(db, parsed.name, workstream) === void 0) {
4653
- const { AgentNotFoundError: AgentNotFoundError2 } = await import("./errors-KHOYKD7D.js");
4656
+ const { AgentNotFoundError: AgentNotFoundError2 } = await import("./errors-GRONKWFS.js");
4654
4657
  throw new AgentNotFoundError2(parsed.name);
4655
4658
  }
4656
4659
  return {
@@ -6994,6 +6997,7 @@ function assertWorktreeFlags(opts) {
6994
6997
  return;
6995
6998
  }
6996
6999
  for (const flag of ["from", "root"]) {
7000
+ if (flag === "root" && opts.host !== void 0) continue;
6997
7001
  if (opts[flag] !== void 0) throw new UsageError(say("agent.spawn_needs_worktree", flag));
6998
7002
  }
6999
7003
  }
@@ -7847,10 +7851,10 @@ function wireAgentCommands(program2) {
7847
7851
  "run in its own git worktree (git worktree add --detach under the state dir); its path is the cwd"
7848
7852
  ).option("--from <ref>", "with --worktree: start at this commit or branch (default: HEAD)").option(
7849
7853
  "--root <repo>",
7850
- "with --worktree: the repo to add it to (default: the git root of your cwd)"
7854
+ "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
7855
  ).option(
7852
7856
  "--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"
7857
+ "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
7858
  ).option("--no-ctl", "skip the control-socket handshake with the mu pi extension").option(
7855
7859
  "--next-free",
7856
7860
  "if <name> is taken, take the next free one (worker-1 \u2192 worker-2, review \u2192 review-2); --json's agent.name says which"
@@ -8037,7 +8041,7 @@ function openDashboardOrHelp(program2) {
8037
8041
  (syncDb) => withShutdown(syncDb, async (shutdown) => {
8038
8042
  const key = "NODE_ENV";
8039
8043
  process.env[key] ??= "production";
8040
- const { runDashboard } = await import("./tui-ALPHYTA2.js");
8044
+ const { runDashboard } = await import("./tui-FUBQ326D.js");
8041
8045
  const first = program2.opts().workstream?.[0];
8042
8046
  await runDashboard({ dbPath: defaultDbPath(), syncDb, shutdown, initialWorkstream: first });
8043
8047
  }),
@@ -8176,7 +8180,7 @@ function checkStrayDbFiles() {
8176
8180
  `Live DB: ${defaultDbPath()}`,
8177
8181
  "",
8178
8182
  "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>).",
8183
+ "`mu db compact` / `mu db forget` / `mu delegates prune` write first (mu.db.pre-<verb>-<time>).",
8180
8184
  "No mu code path reads them and none prunes them. Keep a pre-forget",
8181
8185
  "backup until you are sure: it is the only way back from a forget.",
8182
8186
  "Otherwise disaster recovery is the ops log, so a copy is a convenience:",
@@ -9106,7 +9110,7 @@ var LOG_TAIL_BYTES = 2e3;
9106
9110
  var sha = (s, n) => createHash("sha256").update(s).digest("hex").slice(0, n);
9107
9111
  function delegateProcessPaths(db, row) {
9108
9112
  const base = dirname3(db.name);
9109
- const dir = join2(base, "delegate-process", sha(row.callId, 16));
9113
+ const dir = delegateProcessDir(db.name, row.callId);
9110
9114
  return {
9111
9115
  dir,
9112
9116
  log: join2(dir, "pi.log"),
@@ -9437,6 +9441,196 @@ ${tail(p.log)}` : void 0
9437
9441
  }
9438
9442
  }
9439
9443
 
9444
+ // src/delegate-prune.ts
9445
+ import { lstatSync, readdirSync as readdirSync2, rmSync as rmSync2 } from "fs";
9446
+ import { dirname as dirname4, join as join3 } from "path";
9447
+ var PRUNE_DEFAULT_DAYS = 30;
9448
+ function sizeOf3(p) {
9449
+ let st;
9450
+ try {
9451
+ st = lstatSync(p);
9452
+ } catch {
9453
+ return 0;
9454
+ }
9455
+ if (!st.isDirectory()) return st.size;
9456
+ let n = 0;
9457
+ for (const e of readdirSync2(p)) n += sizeOf3(join3(p, e));
9458
+ return n;
9459
+ }
9460
+ var hasFileDir = (db) => db.name !== "" && db.name !== ":memory:";
9461
+ var LOOSE_MIN_AGE_MS = 864e5;
9462
+ function looseSessions(db, cutoff, workstream, now) {
9463
+ const root = join3(dirname4(db.name), "delegate-sessions");
9464
+ const before = Math.min(Date.parse(cutoff), now - LOOSE_MIN_AGE_MS);
9465
+ const out = [];
9466
+ const wss = db.prepare(
9467
+ "SELECT name FROM workstreams WHERE torn_down_at IS NULL AND (? IS NULL OR name = ?)"
9468
+ ).all(workstream ?? null, workstream ?? null).map((r) => r.name);
9469
+ for (const ws of wss) {
9470
+ let names;
9471
+ try {
9472
+ names = readdirSync2(join3(root, ws));
9473
+ } catch {
9474
+ continue;
9475
+ }
9476
+ for (const f of names) {
9477
+ const p = join3(root, ws, f);
9478
+ if (!f.endsWith(".jsonl")) continue;
9479
+ try {
9480
+ const st = lstatSync(p);
9481
+ if (st.isFile() && st.mtimeMs < before) out.push(p);
9482
+ } catch {
9483
+ }
9484
+ }
9485
+ }
9486
+ return out.sort();
9487
+ }
9488
+ var callFiles = (db, c) => [
9489
+ delegateSessionDir(db.name, c.workstream, c.task),
9490
+ delegateProcessDir(db.name, c.callId)
9491
+ ];
9492
+ var TASK_PUTS_WHERE = `entity IN ('task', 'note') AND op = 'put'
9493
+ AND key >= @k AND key < @k || '$' AND (key = @k OR key >= @k || '#')
9494
+ AND (@own = 1 OR machine_id <> @me)`;
9495
+ var MIN_DATE_MS = -864e13;
9496
+ function planDelegatePrune(db, opts = {}) {
9497
+ const days2 = opts.days ?? PRUNE_DEFAULT_DAYS;
9498
+ const ownHistory = opts.ownHistory ?? !syncEnabled();
9499
+ const cutoff = new Date(
9500
+ Math.max(MIN_DATE_MS, (opts.now ?? Date.now()) - days2 * 864e5)
9501
+ ).toISOString();
9502
+ const rows = db.prepare(
9503
+ `SELECT t.id AS id, w.name AS workstream, t.local_id AS task, t.updated_at AS updated_at,
9504
+ d.call_id AS call_id, d.state AS call_state,
9505
+ (SELECT COUNT(*) FROM task_edges e WHERE e.from_task_id = t.id OR e.to_task_id = t.id) AS edges,
9506
+ EXISTS (SELECT 1 FROM tasks jt JOIN workstreams jw ON jw.id = jt.workstream_id
9507
+ JOIN task_auto ja ON ja.task_id = jt.id AND ja.enabled = 1
9508
+ WHERE jw.name = d.record_workstream AND jt.local_id = d.record_task) AS judged_auto
9509
+ FROM tasks t
9510
+ JOIN workstreams w ON w.id = t.workstream_id AND w.torn_down_at IS NULL
9511
+ JOIN delegate_calls d ON d.task_id = t.id
9512
+ WHERE t.kind = 'delegate' AND t.status = 'CLOSED' AND t.updated_at < ?
9513
+ AND (? IS NULL OR w.name = ?)
9514
+ ORDER BY w.name, t.updated_at, t.local_id`
9515
+ ).all(cutoff, opts.workstream ?? null, opts.workstream ?? null);
9516
+ const plan = {
9517
+ cutoff,
9518
+ days: days2,
9519
+ candidates: [],
9520
+ kept: { edges: 0, refuteTarget: 0 },
9521
+ ownHistory,
9522
+ ops: 0,
9523
+ bytes: 0,
9524
+ files: { paths: [], bytes: 0 }
9525
+ };
9526
+ const cost = db.prepare(
9527
+ `SELECT COUNT(*) AS n, COALESCE(SUM(length(payload)), 0) AS b FROM ops WHERE ${TASK_PUTS_WHERE}`
9528
+ );
9529
+ const me = localMachineId(db);
9530
+ const own = ownHistory ? 1 : 0;
9531
+ for (const r of rows) {
9532
+ if (!["done", "failed", "cancelled"].includes(r.call_state)) continue;
9533
+ if (r.edges > 0) {
9534
+ plan.kept.edges++;
9535
+ continue;
9536
+ }
9537
+ if (r.call_id.startsWith("refute:") && r.judged_auto === 1) {
9538
+ plan.kept.refuteTarget++;
9539
+ continue;
9540
+ }
9541
+ plan.candidates.push({
9542
+ workstream: r.workstream,
9543
+ task: r.task,
9544
+ callId: r.call_id,
9545
+ closedAt: r.updated_at
9546
+ });
9547
+ const c = cost.get({ k: `${r.workstream}/${r.task}`, own, me });
9548
+ plan.ops += c.n;
9549
+ plan.bytes += c.b;
9550
+ }
9551
+ if (hasFileDir(db)) {
9552
+ const paths = [
9553
+ ...plan.candidates.flatMap((c) => callFiles(db, c)).filter(exists2),
9554
+ ...looseSessions(db, cutoff, opts.workstream, opts.now ?? Date.now())
9555
+ ];
9556
+ plan.files = { paths, bytes: paths.reduce((n, p) => n + sizeOf3(p), 0) };
9557
+ }
9558
+ return plan;
9559
+ }
9560
+ function exists2(p) {
9561
+ try {
9562
+ lstatSync(p);
9563
+ return true;
9564
+ } catch {
9565
+ return false;
9566
+ }
9567
+ }
9568
+ function pruneDelegates(db, plan) {
9569
+ let tasks = 0;
9570
+ let ops = 0;
9571
+ const pruned = [];
9572
+ withOpContext(
9573
+ db,
9574
+ { intent: "delegate.prune", actor: "mu delegates prune", group: "new" },
9575
+ () => db.transaction(() => {
9576
+ const del = db.prepare(
9577
+ `DELETE FROM tasks WHERE kind = 'delegate' AND status = 'CLOSED' AND updated_at < ?
9578
+ AND local_id = ? AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)
9579
+ AND NOT EXISTS (SELECT 1 FROM task_edges e
9580
+ WHERE e.from_task_id = tasks.id OR e.to_task_id = tasks.id)`
9581
+ );
9582
+ const puts = db.prepare(`DELETE FROM ops WHERE ${TASK_PUTS_WHERE}`);
9583
+ const me = localMachineId(db);
9584
+ const own = plan.ownHistory ? 1 : 0;
9585
+ const cursor = db.prepare("DELETE FROM wait_cursors WHERE ref = ?");
9586
+ for (const c of plan.candidates) {
9587
+ if (del.run(plan.cutoff, c.task, c.workstream).changes === 0) continue;
9588
+ tasks++;
9589
+ pruned.push(c);
9590
+ ops += puts.run({ k: `${c.workstream}/${c.task}`, own, me }).changes;
9591
+ cursor.run(`delegate:${c.workstream}/${c.callId}`);
9592
+ }
9593
+ }).immediate()
9594
+ );
9595
+ let files = 0;
9596
+ if (hasFileDir(db)) {
9597
+ const loose = new Set(plan.files.paths);
9598
+ const go = [
9599
+ ...pruned.flatMap((c) => callFiles(db, c)),
9600
+ ...looseSessions(db, plan.cutoff, void 0, Date.now()).filter((p) => loose.has(p))
9601
+ ];
9602
+ for (const p of go) {
9603
+ if (!exists2(p)) continue;
9604
+ try {
9605
+ rmSync2(p, { recursive: true, force: true });
9606
+ files++;
9607
+ } catch {
9608
+ }
9609
+ }
9610
+ }
9611
+ return { tasks, ops, files };
9612
+ }
9613
+ function checkOldDelegates(db, opts = {}) {
9614
+ const plan = planDelegatePrune(db, opts);
9615
+ const n = plan.candidates.length;
9616
+ if (n === 0)
9617
+ return {
9618
+ name: "delegates-old",
9619
+ severity: "ok",
9620
+ detail: `no closed delegate task older than ${plan.days} days`
9621
+ };
9622
+ return {
9623
+ name: "delegates-old",
9624
+ severity: "ok",
9625
+ detail: `${n} closed delegate task(s) older than ${plan.days} days (${plan.ops} ops)`,
9626
+ remediation: [
9627
+ "Each holds its whole brief and report, synced to every peer. Their verdicts stay on the judged tasks.",
9628
+ " mu delegates prune # dry run: what goes, and what is kept",
9629
+ " mu delegates prune --yes # backs up beside the DB first; irreversible"
9630
+ ]
9631
+ };
9632
+ }
9633
+
9440
9634
  // src/workstream-scope.ts
9441
9635
  function workstreamNameProblem(value, flag = "-w/--workstream") {
9442
9636
  if (value.length > 0 && value.trim().length === 0) {
@@ -9648,6 +9842,82 @@ async function cmdDelegatesRun(db, callId, opts) {
9648
9842
  }
9649
9843
  if (r.ran && r.state !== "done") throw new CliExitError(1);
9650
9844
  }
9845
+ function keptLine(plan) {
9846
+ const parts = [
9847
+ plan.kept.edges > 0 ? `${plan.kept.edges} with edges` : "",
9848
+ plan.kept.refuteTarget > 0 ? `${plan.kept.refuteTarget} refute checks of tasks still under mu task auto` : ""
9849
+ ].filter((p) => p !== "");
9850
+ return parts.length > 0 ? `kept: ${parts.join(", ")}` : "";
9851
+ }
9852
+ async function cmdDelegatesPrune(db, opts) {
9853
+ if (opts.workstream !== void 0) await resolveExistingWorkstream(db, opts.workstream);
9854
+ const plan = planDelegatePrune(db, {
9855
+ ...opts.olderThan !== void 0 ? { days: opts.olderThan } : {},
9856
+ ...opts.workstream !== void 0 ? { workstream: opts.workstream } : {}
9857
+ });
9858
+ const n = plan.candidates.length;
9859
+ const files = plan.files.paths.length;
9860
+ const filesNote = files > 0 ? `, ${files} pi session(s) (${formatBytes(plan.files.bytes)})` : "";
9861
+ if (opts.yes === true && n === 0 && files === 0) {
9862
+ if (opts.json)
9863
+ return emitJson({ dryRun: false, ...plan, pruned: 0, deletedOps: 0, deletedFiles: 0 });
9864
+ console.log(pc.dim(`nothing to prune: no closed delegate task older than ${plan.days} days`));
9865
+ const kept2 = keptLine(plan);
9866
+ if (kept2) console.log(pc.dim(kept2));
9867
+ return;
9868
+ }
9869
+ if (opts.yes !== true) {
9870
+ if (opts.json) return emitJson({ dryRun: true, ...plan });
9871
+ if (n === 0 && files === 0)
9872
+ console.log(pc.dim(`nothing to prune: no closed delegate task older than ${plan.days} days`));
9873
+ else {
9874
+ const t = muTable({ head: ["TASK", "CALL", "CLOSED"] });
9875
+ for (const c of plan.candidates.slice(0, 20))
9876
+ t.push([`${c.workstream}/${c.task}`, c.callId, c.closedAt.slice(0, 10)]);
9877
+ console.log(t.toString());
9878
+ if (n > 20) console.log(pc.dim(`... and ${n - 20} more`));
9879
+ console.log(
9880
+ `${n} closed delegate task(s) older than ${plan.days} days: ${plan.ops} ops (${formatBytes(plan.bytes)})${filesNote}. Dry run; nothing changed (--yes applies; irreversible).`
9881
+ );
9882
+ if (!plan.ownHistory)
9883
+ console.log(
9884
+ pc.dim(
9885
+ "sync is on: the rows go everywhere, but this machine's own history stays (its segment is rebuilt from it)"
9886
+ )
9887
+ );
9888
+ }
9889
+ const kept2 = keptLine(plan);
9890
+ if (kept2) console.log(pc.dim(kept2));
9891
+ return;
9892
+ }
9893
+ if (n === 0) {
9894
+ const r2 = pruneDelegates(db, plan);
9895
+ if (opts.json)
9896
+ return emitJson({ dryRun: false, ...plan, pruned: 0, deletedOps: 0, deletedFiles: r2.files });
9897
+ console.log(`Pruned ${r2.files} pi session(s); no delegate task was old enough.`);
9898
+ return;
9899
+ }
9900
+ const backup = backupBeside(db, "prune");
9901
+ const r = pruneDelegates(db, plan);
9902
+ const size = vacuumAndCheck(db, backup);
9903
+ if (opts.json)
9904
+ return emitJson({
9905
+ dryRun: false,
9906
+ ...plan,
9907
+ pruned: r.tasks,
9908
+ deletedOps: r.ops,
9909
+ deletedFiles: r.files,
9910
+ backup,
9911
+ ...size
9912
+ });
9913
+ console.log(
9914
+ `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.`
9915
+ );
9916
+ const kept = keptLine(plan);
9917
+ if (kept) console.log(pc.dim(kept));
9918
+ if (!size.checkpointed) console.log(pc.dim(CHECKPOINT_PENDING));
9919
+ console.log(pc.dim(`backup (the only way back): ${backup}`));
9920
+ }
9651
9921
  async function cmdDelegatesReissue(_db, callId, _opts) {
9652
9922
  throw new UsageError(
9653
9923
  `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 +9969,12 @@ function wireDelegatesCommands(program2) {
9699
9969
  const opts = this.optsWithGlobals();
9700
9970
  return handle((db) => cmdDelegatesRun(db, callId, opts), this)();
9701
9971
  });
9972
+ d.command("prune").description(
9973
+ `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`
9974
+ ).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() {
9975
+ const opts = this.optsWithGlobals();
9976
+ return handle((db) => cmdDelegatesPrune(db, opts), this)();
9977
+ });
9702
9978
  d.command("reissue <call-id>").description(
9703
9979
  "Refuses: reissue a refused call with the `mu_delegate_reissue` tool in its pi session (only that runtime can start or drain it)"
9704
9980
  ).option(...JSON_OPT).action(function(callId) {
@@ -9708,15 +9984,15 @@ function wireDelegatesCommands(program2) {
9708
9984
  }
9709
9985
 
9710
9986
  // src/cli/doctor.ts
9711
- import { dirname as dirname5 } from "path";
9987
+ import { dirname as dirname6 } from "path";
9712
9988
 
9713
9989
  // src/doctor-summary.ts
9714
9990
  import { accessSync, constants, existsSync as existsSync3, readFileSync as readFileSync2, realpathSync } from "fs";
9715
- import { delimiter, dirname as dirname4, join as join3, parse } from "path";
9991
+ import { delimiter, dirname as dirname5, join as join4, parse } from "path";
9716
9992
  import { fileURLToPath as fileURLToPath2 } from "url";
9717
9993
  function resolveExecutable(name) {
9718
9994
  for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
9719
- const candidate = join3(dir, name);
9995
+ const candidate = join4(dir, name);
9720
9996
  try {
9721
9997
  accessSync(candidate, constants.X_OK);
9722
9998
  return realpathSync(candidate);
@@ -9726,10 +10002,10 @@ function resolveExecutable(name) {
9726
10002
  return null;
9727
10003
  }
9728
10004
  function murmurVersion(executable) {
9729
- let dir = dirname4(executable);
10005
+ let dir = dirname5(executable);
9730
10006
  const root = parse(dir).root;
9731
10007
  while (dir !== root) {
9732
- const packagePath = join3(dir, "package.json");
10008
+ const packagePath = join4(dir, "package.json");
9733
10009
  if (existsSync3(packagePath)) {
9734
10010
  try {
9735
10011
  const pkg = JSON.parse(readFileSync2(packagePath, "utf8"));
@@ -9739,7 +10015,7 @@ function murmurVersion(executable) {
9739
10015
  } catch {
9740
10016
  }
9741
10017
  }
9742
- dir = dirname4(dir);
10018
+ dir = dirname5(dir);
9743
10019
  }
9744
10020
  return null;
9745
10021
  }
@@ -9811,7 +10087,7 @@ function ctlExtensionDoctorCheck(reports) {
9811
10087
  var DOCTOR_CTL_TIMEOUT_MS = 500;
9812
10088
  function installedMuVersion() {
9813
10089
  try {
9814
- const pkgPath = join3(dirname4(fileURLToPath2(import.meta.url)), "..", "package.json");
10090
+ const pkgPath = join4(dirname5(fileURLToPath2(import.meta.url)), "..", "package.json");
9815
10091
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
9816
10092
  const v = typeof pkg === "object" && pkg !== null ? pkg.version : null;
9817
10093
  return typeof v === "string" ? v : null;
@@ -10055,7 +10331,7 @@ async function cmdDoctor(db, opts = {}) {
10055
10331
  console.log(` ${pad(fact.name)}: ${fact.value ? pc.green(fact.value) : pc.dim("not set")}`);
10056
10332
  }
10057
10333
  }
10058
- const ctlSockets = await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname5(db.name) });
10334
+ const ctlSockets = await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname6(db.name) });
10059
10335
  const muExt = ctlExtensionDoctorCheck(ctlSockets.agents);
10060
10336
  printCheck("agent state", agentStateCheck(health, extensionLive(ctlSockets.agents)));
10061
10337
  printCheck("mu ext", muExt);
@@ -10199,6 +10475,14 @@ state (workstream=${ws})`));
10199
10475
  console.log("");
10200
10476
  for (const line2 of dormant.remediation) console.log(` ${line2}`);
10201
10477
  }
10478
+ const oldDelegates = checkOldDelegates(db);
10479
+ console.log(
10480
+ ` ${oldDelegates.name.padEnd(16)} : ${oldDelegates.remediation === void 0 ? pc.green("ok") : pc.yellow("note")} ${pc.dim(oldDelegates.detail)}`
10481
+ );
10482
+ if (oldDelegates.remediation !== void 0) {
10483
+ console.log("");
10484
+ for (const line2 of oldDelegates.remediation) console.log(` ${line2}`);
10485
+ }
10202
10486
  console.log(pc.bold("\ndisk"));
10203
10487
  const disk = checkDiskRecon(db);
10204
10488
  if (printHazards(disk)) sawHazard = true;
@@ -10301,7 +10585,7 @@ function opsSizeHint(db) {
10301
10585
  }
10302
10586
  async function cmdDoctorJson(db, opts = {}) {
10303
10587
  const health = await muxHealth();
10304
- const ctlSockets = await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname5(db.name) });
10588
+ const ctlSockets = await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname6(db.name) });
10305
10589
  const muExt = ctlExtensionDoctorCheck(ctlSockets.agents);
10306
10590
  const env = {
10307
10591
  // `mux` is the backend-agnostic key. `tmux` is kept as an alias for
@@ -10410,7 +10694,16 @@ async function cmdDoctorJson(db, opts = {}) {
10410
10694
  remediation: dormantHazard.remediation ?? [],
10411
10695
  workstreams: findDormantWorkstreams(db, { currentWorkstream }),
10412
10696
  thresholds: { finishedIdleDays: FINISHED_IDLE_DAYS, abandonedIdleDays: ABANDONED_IDLE_DAYS }
10413
- }
10697
+ },
10698
+ oldDelegates: (() => {
10699
+ const h = checkOldDelegates(db);
10700
+ return {
10701
+ name: h.name,
10702
+ severity: h.severity,
10703
+ detail: h.detail,
10704
+ remediation: h.remediation ?? []
10705
+ };
10706
+ })()
10414
10707
  };
10415
10708
  const worktreeUsage = opts.disk === true ? measureWorktreeUsage(db) : null;
10416
10709
  let drift;
@@ -10514,7 +10807,7 @@ function cmdHostAdd(db, name, opts) {
10514
10807
  shell: opts.shell,
10515
10808
  exec: opts.exec,
10516
10809
  fetch: opts.fetch,
10517
- repo: opts.repo,
10810
+ repo: opts.repo ?? null,
10518
10811
  max_sessions: opts.maxSessions ?? null
10519
10812
  });
10520
10813
  if (isJsonMode()) {
@@ -10535,9 +10828,9 @@ function cmdHostList(db) {
10535
10828
  }
10536
10829
  const held = /* @__PURE__ */ new Map();
10537
10830
  for (const c of listHostChannels(db)) held.set(c.host, (held.get(c.host) ?? 0) + 1);
10538
- const t = muTable({ head: ["HOST", "REPO", "CHANNELS"] });
10831
+ const t = muTable({ head: ["HOST", "DEFAULT REPO", "CHANNELS"] });
10539
10832
  for (const h of hosts) {
10540
- t.push([h.name, h.repo, `${held.get(h.name) ?? 0}/${h.max_sessions ?? "\u221E"}`]);
10833
+ t.push([h.name, h.repo ?? "-", `${held.get(h.name) ?? 0}/${h.max_sessions ?? "\u221E"}`]);
10541
10834
  }
10542
10835
  console.log(t.toString());
10543
10836
  }
@@ -10549,7 +10842,7 @@ function cmdHostShow(db, name) {
10549
10842
  return;
10550
10843
  }
10551
10844
  console.log(
10552
- `${pc.bold(host.name)} repo ${host.repo} max-sessions ${host.max_sessions ?? "none"}`
10845
+ `${pc.bold(host.name)} default repo ${host.repo ?? "-"} max-sessions ${host.max_sessions ?? "none"}`
10553
10846
  );
10554
10847
  for (const f of ["shell", "exec", "fetch"]) console.log(` ${f.padEnd(6)} ${host[f]}`);
10555
10848
  console.log(
@@ -10580,7 +10873,6 @@ function cmdHostInit(name, opts) {
10580
10873
  `--shell ${shellQuote(s.shell)}`,
10581
10874
  `--exec ${shellQuote(s.exec)}`,
10582
10875
  `--fetch ${shellQuote(s.fetch)}`,
10583
- "--repo '<remote repo path>'",
10584
10876
  "--max-sessions 1"
10585
10877
  ].join(" \\\n ");
10586
10878
  if (isJsonMode()) {
@@ -10593,7 +10885,10 @@ function cmdHostInit(name, opts) {
10593
10885
  }
10594
10886
  async function cmdHostCheck(db, name, opts) {
10595
10887
  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 });
10888
+ const report = await checkHost(db, name, {
10889
+ ...agent !== void 0 ? { agent } : {},
10890
+ ...opts.root !== void 0 ? { root: opts.root } : {}
10891
+ });
10597
10892
  if (isJsonMode()) {
10598
10893
  if (!report.ok) throw failureWithData(failedError(report), report);
10599
10894
  emitJson(report);
@@ -10617,7 +10912,10 @@ function wireHostCommands(program2) {
10617
10912
  );
10618
10913
  host.command("add <name>").description(
10619
10914
  "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(
10915
+ ).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(
10916
+ "--repo <path>",
10917
+ "default remote repo for `agent spawn --host` without --root (a profile names a machine, not a repo)"
10918
+ ).option(
10621
10919
  "--max-sessions <n>",
10622
10920
  "cap on channels mu holds to this host at once (panes, --exec, --fetch)",
10623
10921
  parseNonNegativeInt
@@ -10641,8 +10939,8 @@ function wireHostCommands(program2) {
10641
10939
  return handle(async () => cmdHostInit(name, opts), this)();
10642
10940
  });
10643
10941
  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) {
10942
+ "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."
10943
+ ).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
10944
  const opts = this.optsWithGlobals();
10647
10945
  return handle((db) => cmdHostCheck(db, name, opts), this)();
10648
10946
  });
@@ -11994,8 +12292,8 @@ function emitJsonCollection(items) {
11994
12292
  }
11995
12293
  function readPackageVersion() {
11996
12294
  try {
11997
- const here = dirname6(fileURLToPath3(import.meta.url));
11998
- const pkgPath = join4(here, "..", "package.json");
12295
+ const here = dirname7(fileURLToPath3(import.meta.url));
12296
+ const pkgPath = join5(here, "..", "package.json");
11999
12297
  const raw = readFileSync3(pkgPath, "utf8");
12000
12298
  const parsed = JSON.parse(raw);
12001
12299
  return typeof parsed.version === "string" ? parsed.version : "unknown";
@@ -12170,4 +12468,4 @@ export {
12170
12468
  truncate,
12171
12469
  truncateFront
12172
12470
  };
12173
- //# sourceMappingURL=cli-GB2CAVVW.js.map
12471
+ //# sourceMappingURL=cli-TV3N47AZ.js.map