@mu-crew/mu 3.6.2 → 3.7.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.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Database } from 'better-sqlite3';
2
2
 
3
- type CtlState = "busy" | "idle" | "needs_input";
3
+ type CtlState = "busy" | "idle";
4
4
  /** Session commands the `command` op runs: `/new`, `/reload`, `/compact`. */
5
5
  declare const CTL_COMMANDS: readonly ["new", "reload", "compact"];
6
6
  type CtlCommandName = (typeof CTL_COMMANDS)[number];
@@ -20,6 +20,8 @@ interface StateReading {
20
20
  /** ctl only: pi holds queued messages, so an idle reading is about to
21
21
  * turn busy. */
22
22
  pending?: boolean;
23
+ /** ctl only: pi's settled-run count (`mu agent wait --after-runs`). */
24
+ runs?: number;
23
25
  }
24
26
  interface StateAgentRef {
25
27
  name: string;
@@ -1463,7 +1465,10 @@ type SpawnedAgent = AgentRow & {
1463
1465
  * 4. setPaneTitle + enableMuPaneBordersForPane.
1464
1466
  * 5. finalizeAgentRow() — patch placeholder pane_id to real (workspace
1465
1467
  * path), or insert a fresh agent row (no-workspace path).
1466
- * 6. awaitSpawnLiveness().
1468
+ * 6. Liveness: awaitSpawnLiveness() (fixed sleep, then pane + scan
1469
+ * check), or for a tmux pi agent the ctl handshake with a pane
1470
+ * check per tick and the scan at the end. herdr: startAgentInPane.
1471
+ * 7. ctl handshake for herdr pi agents (outside the rollback).
1467
1472
  *
1468
1473
  * Failure between any of (3)–(6) calls rollbackSpawn() to undo the
1469
1474
  * pane + row + workspace. The caller-visible error is preserved.
@@ -1648,7 +1653,9 @@ declare class AgentDiedOnSpawnError extends Error implements HasNextSteps {
1648
1653
  readonly remote: boolean;
1649
1654
  constructor(agentName: string, paneId: string, scrollback: string | undefined,
1650
1655
  /** The command the pane ran; an `ssh ...` command gets remote hints. */
1651
- command?: string | undefined);
1656
+ command?: string | undefined,
1657
+ /** When it died; default `within <MU_SPAWN_LIVENESS_MS>ms of spawn`. */
1658
+ when?: string);
1652
1659
  errorNextSteps(): NextStep[];
1653
1660
  }
1654
1661
  /**
@@ -1694,7 +1701,9 @@ declare class AgentSpawnStartupError extends Error implements HasNextSteps {
1694
1701
  matchedLine: string,
1695
1702
  /** Full captured scrollback (tail-trimmed already by
1696
1703
  * awaitSpawnLiveness). Attached to the message for context. */
1697
- scrollback: string);
1704
+ scrollback: string,
1705
+ /** When it was seen; default `within <MU_SPAWN_LIVENESS_MS>ms of spawn`. */
1706
+ when?: string);
1698
1707
  errorNextSteps(): NextStep[];
1699
1708
  }
1700
1709
  /**
@@ -1921,6 +1930,13 @@ type Transport = "ctl" | "mux";
1921
1930
  type SendResult = {
1922
1931
  transport: Transport;
1923
1932
  state?: CtlState;
1933
+ /**
1934
+ * ctl only: pi's completed-run count. For a plain send it is measured
1935
+ * before dispatch (the `mu agent wait --after-runs` baseline); for
1936
+ * `--fresh` it is the new session's count once its run started.
1937
+ * Absent from an extension that predates it.
1938
+ */
1939
+ runs?: number;
1924
1940
  /** Set when the text ran as a pi session command over ctl. */
1925
1941
  command?: CtlCommandName;
1926
1942
  };
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { execa as execa3 } from "execa";
12
12
 
13
13
  // src/agents/transport.ts
14
14
  import { existsSync as existsSync8 } from "fs";
15
- import { dirname as dirname6 } from "path";
15
+ import { dirname as dirname7 } from "path";
16
16
 
17
17
  // src/ctl/client.ts
18
18
  import { createConnection } from "net";
@@ -144,6 +144,7 @@ async function ctlProbe(sock, timeoutMs = CTL_DEFAULT_TIMEOUT_MS) {
144
144
 
145
145
  // src/ctl/path.ts
146
146
  import { createHash } from "crypto";
147
+ import { rmSync } from "fs";
147
148
  import { dirname as dirname2, join as join2 } from "path";
148
149
 
149
150
  // src/db.ts
@@ -854,6 +855,13 @@ function ctlSocketPath(workstream, agent, stateDir) {
854
855
  const base = stateDir ?? (dbPath ? dirname2(dbPath) : defaultStateDir());
855
856
  return sockPathUnder(join2(base, "sock"), workstream, agent);
856
857
  }
858
+ function unlinkCtlSocket(db, name, workstream) {
859
+ if (db.memory) return;
860
+ try {
861
+ rmSync(ctlSocketPath(workstream, name, dirname2(db.name)), { force: true });
862
+ } catch {
863
+ }
864
+ }
857
865
 
858
866
  // src/mux.ts
859
867
  var mux_exports = {};
@@ -2254,13 +2262,12 @@ function resetMux() {
2254
2262
  // src/agents/spawn.ts
2255
2263
  import { execFile as execFile2 } from "child_process";
2256
2264
  import { mkdirSync as mkdirSync2, rmSync as rmSync4 } from "fs";
2257
- import { basename, dirname as dirname5 } from "path";
2265
+ import { basename, dirname as dirname6 } from "path";
2258
2266
  import { setTimeout as delay } from "timers/promises";
2259
2267
  import { promisify as promisify2 } from "util";
2260
2268
 
2261
2269
  // src/agents.ts
2262
- import { rmSync as rmSync3 } from "fs";
2263
- import { dirname as dirname4 } from "path";
2270
+ import { dirname as dirname5 } from "path";
2264
2271
 
2265
2272
  // src/glyphs.ts
2266
2273
  var AGENT_STATE_GLYPH = {
@@ -2856,7 +2863,7 @@ function workspacesRoot(workstream) {
2856
2863
  }
2857
2864
 
2858
2865
  // src/workspace/crud.ts
2859
- import { existsSync as existsSync6, rmSync as rmSync2 } from "fs";
2866
+ import { existsSync as existsSync6, rmSync as rmSync3 } from "fs";
2860
2867
  import { homedir as homedir2 } from "os";
2861
2868
  import { resolve as resolve3 } from "path";
2862
2869
 
@@ -2866,7 +2873,7 @@ import { resolve as resolve2 } from "path";
2866
2873
 
2867
2874
  // src/vcs/helpers.ts
2868
2875
  import { execFile } from "child_process";
2869
- import { existsSync, rmSync } from "fs";
2876
+ import { existsSync, rmSync as rmSync2 } from "fs";
2870
2877
  import { mkdir } from "fs/promises";
2871
2878
  import { dirname as dirname3 } from "path";
2872
2879
  import { promisify } from "util";
@@ -2972,7 +2979,7 @@ async function ensureParent(path) {
2972
2979
  }
2973
2980
  function rmDirSync(path) {
2974
2981
  if (!existsSync(path)) return false;
2975
- rmSync(path, { recursive: true, force: true });
2982
+ rmSync2(path, { recursive: true, force: true });
2976
2983
  return true;
2977
2984
  }
2978
2985
  async function run(bin, args, cwd) {
@@ -3797,7 +3804,7 @@ async function createWorkspace(db, opts) {
3797
3804
  created = await backend.createWorkspace(createOpts);
3798
3805
  } catch (err) {
3799
3806
  try {
3800
- rmSync2(path, { recursive: true, force: true });
3807
+ rmSync3(path, { recursive: true, force: true });
3801
3808
  } catch {
3802
3809
  }
3803
3810
  throw err;
@@ -4438,8 +4445,17 @@ var CycleError = class extends Error {
4438
4445
  ];
4439
4446
  }
4440
4447
  };
4448
+ function stallWaitHint(owner, ws, runs) {
4449
+ if (runs === void 0 || runs < 1) return [];
4450
+ return [
4451
+ {
4452
+ intent: "Read the worker's last answer (its final text, at once)",
4453
+ command: `mu agent wait ${owner} --after-runs ${runs - 1} --json -w ${ws}`
4454
+ }
4455
+ ];
4456
+ }
4441
4457
  var StallDetectedDuringWaitError = class extends Error {
4442
- constructor(taskName, owner, workstream, ageSecs, ownerExpectsCtl = false) {
4458
+ constructor(taskName, owner, workstream, ageSecs, ownerExpectsCtl = false, ownerRuns) {
4443
4459
  const ownerBit = owner !== null ? owner : "<unknown>";
4444
4460
  super(
4445
4461
  `task ${taskName} owned by ${ownerBit} has been needs_input for ${ageSecs}s; exiting per --on-stall exit. Read the pane to see which it is \u2014 finished without closing, waiting on an answer, or sitting at a prompt.`
@@ -4449,12 +4465,14 @@ var StallDetectedDuringWaitError = class extends Error {
4449
4465
  this.workstream = workstream;
4450
4466
  this.ageSecs = ageSecs;
4451
4467
  this.ownerExpectsCtl = ownerExpectsCtl;
4468
+ this.ownerRuns = ownerRuns;
4452
4469
  }
4453
4470
  taskName;
4454
4471
  owner;
4455
4472
  workstream;
4456
4473
  ageSecs;
4457
4474
  ownerExpectsCtl;
4475
+ ownerRuns;
4458
4476
  name = "StallDetectedDuringWaitError";
4459
4477
  /**
4460
4478
  * Diagnose before acting. `mu agent read` comes FIRST because the
@@ -4469,6 +4487,7 @@ var StallDetectedDuringWaitError = class extends Error {
4469
4487
  const ws = this.workstream;
4470
4488
  const ownerBit = this.owner !== null ? this.owner : "<owner>";
4471
4489
  return [
4490
+ ...stallWaitHint(ownerBit, ws, this.ownerRuns),
4472
4491
  {
4473
4492
  intent: "Read the worker's pane to see what it is waiting on",
4474
4493
  command: `mu agent read ${ownerBit} -w ${ws} --lines 60`
@@ -4822,7 +4841,7 @@ function reparentTaskImpl(db, taskLocalId, blockers, scope) {
4822
4841
 
4823
4842
  // src/workstream.ts
4824
4843
  import { existsSync as existsSync7, readdirSync as readdirSync2, rmdirSync } from "fs";
4825
- import { join as join5 } from "path";
4844
+ import { dirname as dirname4, join as join5 } from "path";
4826
4845
  var WORKSTREAM_NAME_RE = /^[a-z][a-z0-9_-]{0,31}$/;
4827
4846
  var RESERVED_WORKSTREAM_PREFIX = "mu-";
4828
4847
  var RESERVED_WORKSTREAM_NAMES = /* @__PURE__ */ new Set(["scratch"]);
@@ -4936,6 +4955,7 @@ async function teardownWorkstream(db, opts) {
4936
4955
  const notesBefore = countNotes(db, opts.workstream);
4937
4956
  const edgesBefore = countEdges(db, opts.workstream);
4938
4957
  const workspacesBefore = listWorkspaces2(db, opts.workstream);
4958
+ const agentNames = listAgentNames(db, opts.workstream);
4939
4959
  const mux = await activeMux();
4940
4960
  const muxAliveBefore = await mux.sessionExists(muxSession);
4941
4961
  if (muxAliveBefore) {
@@ -4973,6 +4993,14 @@ async function teardownWorkstream(db, opts) {
4973
4993
  } catch {
4974
4994
  }
4975
4995
  }
4996
+ for (const name of agentNames) unlinkCtlSocket(db, name, opts.workstream);
4997
+ if (!db.memory) {
4998
+ const sockDir = join5(dirname4(db.name), "sock", opts.workstream);
4999
+ try {
5000
+ if (readdirSync2(sockDir).length === 0) rmdirSync(sockDir);
5001
+ } catch {
5002
+ }
5003
+ }
4976
5004
  withOpContext(
4977
5005
  db,
4978
5006
  { intent: "workstream.teardown", group: "new" },
@@ -4989,6 +5017,14 @@ async function teardownWorkstream(db, opts) {
4989
5017
  failedWorkspaces
4990
5018
  };
4991
5019
  }
5020
+ function listAgentNames(db, workstream) {
5021
+ const rows = db.prepare(
5022
+ `SELECT a.name FROM agents a
5023
+ JOIN workstreams ws ON ws.id = a.workstream_id
5024
+ WHERE ws.name = ?`
5025
+ ).all(workstream);
5026
+ return rows.map((r) => r.name);
5027
+ }
4992
5028
  function countAgents(db, workstream) {
4993
5029
  const row = db.prepare(
4994
5030
  `SELECT COUNT(*) AS n FROM agents a
@@ -5601,12 +5637,15 @@ async function waitForTasks(db, input, opts) {
5601
5637
  const ctlBrokenSince = /* @__PURE__ */ new Map();
5602
5638
  const stuckLabel = /* @__PURE__ */ new Map();
5603
5639
  const ctlOwners = /* @__PURE__ */ new Set();
5640
+ const ownerRuns = /* @__PURE__ */ new Map();
5604
5641
  const stuckAgeMs = async (status, owner, workstreamName) => {
5605
5642
  if (stuckAfterMs <= 0 || status !== "IN_PROGRESS" || owner === null || opts.readOwnerState === void 0)
5606
5643
  return null;
5607
5644
  const reading = await opts.readOwnerState({ name: owner, workstreamName });
5608
5645
  const ownerKey = `${workstreamName}/${owner}`;
5609
5646
  if (reading?.ctl !== void 0) ctlOwners.add(ownerKey);
5647
+ if (reading?.runs !== void 0) ownerRuns.set(ownerKey, reading.runs);
5648
+ else ownerRuns.delete(ownerKey);
5610
5649
  if (reading?.alive === true && (reading.ctl === "missing" || reading.ctl === "refused")) {
5611
5650
  const firstSeen = ctlBrokenSince.get(ownerKey) ?? Date.now();
5612
5651
  ctlBrokenSince.set(ownerKey, firstSeen);
@@ -5636,10 +5675,13 @@ async function waitForTasks(db, input, opts) {
5636
5675
  stuckWarned.add(key);
5637
5676
  const ageSecs = Math.round(ageMs / 1e3);
5638
5677
  const ownerBit = owner ?? "<none>";
5678
+ const runs = ownerRuns.get(`${ref.workstreamName}/${ownerBit}`);
5639
5679
  const stateBit = stuckLabel.get(`${ref.workstreamName}/${ownerBit}`) ?? "needs_input";
5680
+ const waitLine = stallWaitHint(ownerBit, ref.workstreamName, runs).map((s) => ` ${s.command}
5681
+ `).join("");
5640
5682
  currentStuckWarn(
5641
5683
  `\x1B[33mmu task wait: ${key} needs attention \u2014 owner=${ownerBit} has been in ${stateBit} for ${formatStallAge(ageMs)}. It may have finished without closing, be waiting on an answer, or be sitting at a prompt.\x1B[0m
5642
- mu agent read ${ownerBit} -w ${ref.workstreamName} --lines 60
5684
+ ` + waitLine + ` mu agent read ${ownerBit} -w ${ref.workstreamName} --lines 60
5643
5685
  `
5644
5686
  );
5645
5687
  emitEvent(
@@ -5655,7 +5697,8 @@ async function waitForTasks(db, input, opts) {
5655
5697
  owner,
5656
5698
  ref.workstreamName,
5657
5699
  ageSecs,
5658
- ctlOwners.has(`${ref.workstreamName}/${owner ?? ""}`)
5700
+ ctlOwners.has(`${ref.workstreamName}/${owner ?? ""}`),
5701
+ runs
5659
5702
  );
5660
5703
  }
5661
5704
  }
@@ -6089,13 +6132,6 @@ function deleteAgent(db, name, workstream) {
6089
6132
  if (deleted) unlinkCtlSocket(db, name, workstream);
6090
6133
  return deleted;
6091
6134
  }
6092
- function unlinkCtlSocket(db, name, workstream) {
6093
- if (db.memory) return;
6094
- try {
6095
- rmSync3(ctlSocketPath(workstream, name, dirname4(db.name)), { force: true });
6096
- } catch {
6097
- }
6098
- }
6099
6135
  function deleteAgentRow(db, name, workstream) {
6100
6136
  return withOpContext(
6101
6137
  db,
@@ -6191,7 +6227,7 @@ async function listLiveAgents(db, opts) {
6191
6227
  ...opts.mode !== void 0 ? { mode: opts.mode } : {}
6192
6228
  });
6193
6229
  const baseAgents = listAgents(db, { workstream: opts.workstream });
6194
- const readings = await readAgentStates(baseAgents, { stateDir: dirname4(db.name) });
6230
+ const readings = await readAgentStates(baseAgents, { stateDir: dirname5(db.name) });
6195
6231
  const now = Date.now();
6196
6232
  const agents = baseAgents.map((agent) => {
6197
6233
  const reading = readings.get(agentKey(agent)) ?? {
@@ -6456,7 +6492,7 @@ function defaultSpawnCtlMs() {
6456
6492
  return parsed;
6457
6493
  }
6458
6494
  var CTL_POLL_INTERVAL_MS = 250;
6459
- async function awaitCtlHandshake(sock) {
6495
+ async function awaitCtlHandshake(sock, onMiss) {
6460
6496
  const budgetMs = defaultSpawnCtlMs();
6461
6497
  const deadline = Date.now() + budgetMs;
6462
6498
  for (; ; ) {
@@ -6464,6 +6500,7 @@ async function awaitCtlHandshake(sock) {
6464
6500
  const probe = await ctlProbe(sock, Math.max(1, Math.min(remaining, CTL_POLL_INTERVAL_MS * 4)));
6465
6501
  if (probe.kind === "ok") return "ok";
6466
6502
  const last = probeToSpawnCtl(probe.kind);
6503
+ if (onMiss) await onMiss();
6467
6504
  const left = deadline - Date.now();
6468
6505
  if (left <= 0) return last;
6469
6506
  await delay(Math.min(CTL_POLL_INTERVAL_MS, left));
@@ -6491,8 +6528,8 @@ async function spawnAgent(db, opts) {
6491
6528
  }
6492
6529
  }
6493
6530
  const workspacePathStr = opts.workspace ? await prestageWorkspace(db, opts, cli) : void 0;
6494
- const ctlSocket = ctlSocketPath(opts.workstream, opts.name, dirname5(db.name));
6495
- mkdirSync2(dirname5(ctlSocket), { recursive: true, mode: 448 });
6531
+ const ctlSocket = ctlSocketPath(opts.workstream, opts.name, dirname6(db.name));
6532
+ mkdirSync2(dirname6(ctlSocket), { recursive: true, mode: 448 });
6496
6533
  rmSync4(ctlSocket, { force: true });
6497
6534
  const paneEnv = {
6498
6535
  MU_MANAGED_AGENT: "1",
@@ -6503,6 +6540,8 @@ async function spawnAgent(db, opts) {
6503
6540
  const hasWorkspace = workspacePathStr !== void 0;
6504
6541
  let paneId;
6505
6542
  let agent;
6543
+ const wantsCtl = opts.ctl !== false && speaksMuCtl(cli, command) && defaultSpawnCtlMs() !== 0;
6544
+ let ctl;
6506
6545
  try {
6507
6546
  const created = await withSpawnLock(session, async () => {
6508
6547
  const pid = await createOrReusePane({
@@ -6523,7 +6562,23 @@ async function spawnAgent(db, opts) {
6523
6562
  agent = created.row;
6524
6563
  const startAgent = (await activeMux()).startAgentInPane;
6525
6564
  if (startAgent === void 0) {
6526
- await awaitSpawnLiveness(paneId, opts.name, command);
6565
+ if (wantsCtl) {
6566
+ const pid = paneId;
6567
+ const check = defaultSpawnLivenessMs() !== 0;
6568
+ const when = `during the ctl handshake (up to ${defaultSpawnCtlMs()}ms after spawn)`;
6569
+ let lastScrollback;
6570
+ const assertPaneAlive = async () => {
6571
+ const mux = await activeMux();
6572
+ const sb = await mux.capturePane(pid, { lines: 50 }).catch(() => void 0);
6573
+ lastScrollback = sb ?? lastScrollback;
6574
+ if (await mux.paneExists(pid)) return;
6575
+ throw new AgentDiedOnSpawnError(opts.name, pid, lastScrollback, command, when);
6576
+ };
6577
+ ctl = await awaitCtlHandshake(ctlSocket, check ? assertPaneAlive : void 0);
6578
+ if (check) await checkSpawnHealth(pid, opts.name, command, when);
6579
+ } else {
6580
+ await awaitSpawnLiveness(paneId, opts.name, command);
6581
+ }
6527
6582
  } else {
6528
6583
  await startAgent({ paneId, name: opts.name, cli, command, commandSource });
6529
6584
  }
@@ -6532,14 +6587,13 @@ async function spawnAgent(db, opts) {
6532
6587
  if (hasWorkspace) attachOrphanCleanupHint(err, opts.name, opts.workstream);
6533
6588
  throw err;
6534
6589
  }
6535
- const ctl = opts.ctl === false || !speaksMuCtl(cli, command) || defaultSpawnCtlMs() === 0 ? "skipped" : await awaitCtlHandshake(ctlSocket);
6590
+ ctl ??= wantsCtl ? await awaitCtlHandshake(ctlSocket) : "skipped";
6536
6591
  emitEvent(
6537
6592
  db,
6538
6593
  opts.workstream,
6539
6594
  "agent.spawn",
6540
6595
  `agent spawn ${opts.name} (cli=${cli}, role=${opts.role ?? "full-access"}, pane=${paneId})`
6541
6596
  );
6542
- await refreshAgentTitle(db, opts.name, opts.workstream);
6543
6597
  maybeWarnNonConventionalAgentName(opts.name);
6544
6598
  return { ...agent, ctl, ctlSocket };
6545
6599
  }
@@ -6667,15 +6721,18 @@ async function awaitSpawnLiveness(paneId, agentName, command) {
6667
6721
  const ms = defaultSpawnLivenessMs();
6668
6722
  if (ms === 0) return;
6669
6723
  await sleep(ms);
6724
+ await checkSpawnHealth(paneId, agentName, command);
6725
+ }
6726
+ async function checkSpawnHealth(paneId, agentName, command, when) {
6670
6727
  const mux = await activeMux();
6671
6728
  const scrollback = await mux.capturePane(paneId, { lines: 50 }).catch(() => void 0);
6672
6729
  if (!await mux.paneExists(paneId)) {
6673
- throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command);
6730
+ throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command, when);
6674
6731
  }
6675
6732
  if (scrollback !== void 0) {
6676
6733
  const matchedLine = detectSpawnStartupError(scrollback);
6677
6734
  if (matchedLine !== void 0) {
6678
- throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback);
6735
+ throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback, when);
6679
6736
  }
6680
6737
  }
6681
6738
  }
@@ -6830,7 +6887,7 @@ var AgentNotInWorkstreamError = class extends Error {
6830
6887
  }
6831
6888
  };
6832
6889
  var AgentDiedOnSpawnError = class extends Error {
6833
- constructor(agentName, paneId, scrollback, command) {
6890
+ constructor(agentName, paneId, scrollback, command, when = `within ${defaultSpawnLivenessMs()}ms of spawn`) {
6834
6891
  const tail = scrollback?.trim();
6835
6892
  const detail = tail ? `
6836
6893
 
@@ -6839,9 +6896,7 @@ ${tail}
6839
6896
  --- end scrollback ---` : "";
6840
6897
  const remote = /^\s*ssh\s/.test(command ?? "");
6841
6898
  const cause = remote ? 'The command is an ssh hop, so the likely causes are remote: the remote binary is missing, or its env (PATH, provider keys) is absent because ssh runs a non-interactive shell that skips ~/.zshrc / ~/.bashrc (wrap it: $SHELL -ilc "pi --approve"); or sshd refused the socket forward (ExitOnForwardFailure=yes kills the connection; check AllowStreamLocalForwarding).' : "Most common cause: the spawned CLI exited immediately (e.g. a wrapper CLI blocking on its instance lock; set MU_<UPPER_CLI>_COMMAND to a non-blocking variant to bypass).";
6842
- super(
6843
- `agent ${agentName} died within ${defaultSpawnLivenessMs()}ms of spawn (pane ${paneId}). ${cause}${detail}`
6844
- );
6899
+ super(`agent ${agentName} died ${when} (pane ${paneId}). ${cause}${detail}`);
6845
6900
  this.agentName = agentName;
6846
6901
  this.paneId = paneId;
6847
6902
  this.scrollback = scrollback;
@@ -6900,9 +6955,9 @@ ${tail}
6900
6955
  }
6901
6956
  };
6902
6957
  var AgentSpawnStartupError = class extends Error {
6903
- constructor(agentName, paneId, matchedLine, scrollback) {
6958
+ constructor(agentName, paneId, matchedLine, scrollback, when = `within ${defaultSpawnLivenessMs()}ms of spawn`) {
6904
6959
  super(
6905
- `agent ${agentName} reported a startup error within ${defaultSpawnLivenessMs()}ms of spawn (pane ${paneId}). The pane is alive but the spawned CLI parked at an error prompt instead of becoming a working agent.
6960
+ `agent ${agentName} reported a startup error ${when} (pane ${paneId}). The pane is alive but the spawned CLI parked at an error prompt instead of becoming a working agent.
6906
6961
 
6907
6962
  Matched line: ${matchedLine.trim()}
6908
6963
 
@@ -6988,7 +7043,14 @@ var AgentCtlUnreachableError = class extends Error {
6988
7043
  kind;
6989
7044
  name = "AgentCtlUnreachableError";
6990
7045
  errorNextSteps() {
7046
+ const trust = this.kind === "missing" ? [
7047
+ {
7048
+ intent: "Just spawned? pi may sit at its project trust prompt: answer /trust in the pane, or respawn with --command 'pi --approve'",
7049
+ command: `mu agent read ${this.agentName} -w ${this.workstream}`
7050
+ }
7051
+ ] : [];
6991
7052
  return [
7053
+ ...trust,
6992
7054
  { intent: "Check whether the mu pi extension is linked", command: "mu doctor" },
6993
7055
  {
6994
7056
  intent: "Stop a runaway tool without the socket",
@@ -7071,7 +7133,7 @@ var AgentFreshNeedsCtlError = class extends Error {
7071
7133
  }
7072
7134
  };
7073
7135
  var AgentExtensionOutdatedError = class extends Error {
7074
- constructor(agentName, workstream, op, extVersion) {
7136
+ constructor(agentName, workstream, op, extVersion, ops) {
7075
7137
  super(
7076
7138
  `agent ${agentName}'s mu pi extension (${extVersion ?? "version unknown"}) predates op ${op}: its pi loaded an older mu build; reload or respawn it`
7077
7139
  );
@@ -7079,18 +7141,21 @@ var AgentExtensionOutdatedError = class extends Error {
7079
7141
  this.workstream = workstream;
7080
7142
  this.op = op;
7081
7143
  this.extVersion = extVersion;
7144
+ this.ops = ops;
7082
7145
  }
7083
7146
  agentName;
7084
7147
  workstream;
7085
7148
  op;
7086
7149
  extVersion;
7150
+ ops;
7087
7151
  name = "AgentExtensionOutdatedError";
7088
7152
  errorNextSteps() {
7089
7153
  const w = `-w ${this.workstream}`;
7154
+ const via = this.ops?.includes("command") ? "" : " --via mux";
7090
7155
  return [
7091
7156
  {
7092
- intent: "Reload pi's extensions in the pane (keeps the session and context)",
7093
- command: `mu agent send ${this.agentName} '/reload' --via mux ${w}`
7157
+ intent: "Reload pi's extensions (keeps the session and context)",
7158
+ command: `mu agent send ${this.agentName} '/reload'${via} ${w}`
7094
7159
  },
7095
7160
  {
7096
7161
  intent: "Or respawn the agent (loses its context)",
@@ -7148,7 +7213,7 @@ var AgentSlashCommandUnsupportedError = class extends Error {
7148
7213
 
7149
7214
  // src/agents/transport.ts
7150
7215
  function agentCtlSocket(db, agent) {
7151
- return ctlSocketPath(agent.workstreamName, agent.name, dirname6(db.name));
7216
+ return ctlSocketPath(agent.workstreamName, agent.name, dirname7(db.name));
7152
7217
  }
7153
7218
  function expectsCtl(agent, sock = ctlSocketPath(agent.workstreamName, agent.name)) {
7154
7219
  return speaksMuCtl(agent.cli, resolveCliCommand(agent.cli)) || existsSync8(sock);
@@ -7176,7 +7241,7 @@ async function extensionOutdated(agent, sock, e) {
7176
7241
  if (hello.ok) extVersion = hello.extVersion;
7177
7242
  } catch {
7178
7243
  }
7179
- return new AgentExtensionOutdatedError(agent.name, agent.workstreamName, e.op, extVersion);
7244
+ return new AgentExtensionOutdatedError(agent.name, agent.workstreamName, e.op, extVersion, e.ops);
7180
7245
  }
7181
7246
  async function ctlRequestFor(agent, sock, req) {
7182
7247
  try {
@@ -7225,7 +7290,11 @@ async function sendViaTransport(agent, text, opts) {
7225
7290
  }
7226
7291
  throw new Error(`control socket refused the send: ${reply.error}`);
7227
7292
  }
7228
- return reply.state === void 0 ? { transport } : { transport, state: reply.state };
7293
+ return {
7294
+ transport,
7295
+ ...reply.state !== void 0 ? { state: reply.state } : {},
7296
+ ...reply.runs !== void 0 ? { runs: reply.runs } : {}
7297
+ };
7229
7298
  }
7230
7299
  async function runSessionCommand(agent, sock, cmd, force) {
7231
7300
  const reply = await ctlRequestFor(agent, sock, {
@@ -7427,7 +7496,8 @@ async function readAgentStates(agents, opts = {}) {
7427
7496
  since: probe.status.since,
7428
7497
  alive: true,
7429
7498
  ctl: "ok",
7430
- pending: probe.status.pending
7499
+ pending: probe.status.pending,
7500
+ runs: probe.status.runs
7431
7501
  });
7432
7502
  } else {
7433
7503
  failed.set(agentKey(agent), probe?.kind === "missing" ? "missing" : "refused");
@@ -8078,7 +8148,7 @@ function formatTreeNodeLabel(t, statusFn, opts = {}) {
8078
8148
 
8079
8149
  // src/doctor-summary.ts
8080
8150
  import { accessSync as accessSync2, constants as constants2, existsSync as existsSync13, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
8081
- import { delimiter as delimiter2, dirname as dirname9, join as join11, parse } from "path";
8151
+ import { delimiter as delimiter2, dirname as dirname10, join as join11, parse } from "path";
8082
8152
  import { fileURLToPath as fileURLToPath2 } from "url";
8083
8153
 
8084
8154
  // src/drift.ts
@@ -8088,7 +8158,7 @@ import { join as join9 } from "path";
8088
8158
 
8089
8159
  // src/rebuild.ts
8090
8160
  import { existsSync as existsSync10, mkdirSync as mkdirSync3, statSync } from "fs";
8091
- import { dirname as dirname7 } from "path";
8161
+ import { dirname as dirname8 } from "path";
8092
8162
  var RebuildTargetExistsError = class extends Error {
8093
8163
  constructor(path) {
8094
8164
  super(`rebuild target already exists: ${path}`);
@@ -8151,7 +8221,7 @@ function rebuildInto(source, opts) {
8151
8221
  if (existsSync10(targetPath) && opts.force !== true) {
8152
8222
  throw new RebuildTargetExistsError(targetPath);
8153
8223
  }
8154
- mkdirSync3(dirname7(targetPath), { recursive: true });
8224
+ mkdirSync3(dirname8(targetPath), { recursive: true });
8155
8225
  const identity = source.prepare(
8156
8226
  "SELECT machine_id, hostname, created_at, last_wall, last_counter FROM machine_identity WHERE id = 1"
8157
8227
  ).get();
@@ -8673,7 +8743,7 @@ import {
8673
8743
  writeFileSync
8674
8744
  } from "fs";
8675
8745
  import { homedir as homedir4 } from "os";
8676
- import { dirname as dirname8, join as join10, resolve as resolve5 } from "path";
8746
+ import { dirname as dirname9, join as join10, resolve as resolve5 } from "path";
8677
8747
  import { fileURLToPath, pathToFileURL } from "url";
8678
8748
  var MU_SHIM_MARKER = "// mu:shim";
8679
8749
  var LinkConflictError = class extends Error {
@@ -8703,7 +8773,7 @@ function resolveHome(opts) {
8703
8773
  return opts?.home ?? process.env.MU_PI_HOME ?? homedir4();
8704
8774
  }
8705
8775
  function packageRoot() {
8706
- return resolve5(dirname8(fileURLToPath(import.meta.url)), "..");
8776
+ return resolve5(dirname9(fileURLToPath(import.meta.url)), "..");
8707
8777
  }
8708
8778
  function extensionEntry() {
8709
8779
  return process.env.MU_EXTENSION_ENTRY ?? join10(packageRoot(), "dist", "extension", "mu-pi.js");
@@ -8739,7 +8809,7 @@ function linkPi(opts) {
8739
8809
  replacedCopy = !readFileSync(path, "utf8").includes(MU_SHIM_MARKER);
8740
8810
  } catch {
8741
8811
  }
8742
- mkdirSync4(dirname8(path), { recursive: true });
8812
+ mkdirSync4(dirname9(path), { recursive: true });
8743
8813
  writeFileSync(path, opts?.copy === true ? readFileSync(entry, "utf8") : shim(entry));
8744
8814
  return { path, replacedCopy };
8745
8815
  }
@@ -8750,7 +8820,7 @@ function linkSkill(opts) {
8750
8820
  const st = lstatOrUndefined(path);
8751
8821
  if (st) {
8752
8822
  if (!st.isSymbolicLink()) throw new LinkConflictError(path, "not-a-symlink");
8753
- const current = resolve5(dirname8(path), readlinkSync(path));
8823
+ const current = resolve5(dirname9(path), readlinkSync(path));
8754
8824
  if (current === target) return { path, target };
8755
8825
  if (existsSync12(current) && opts?.force !== true) {
8756
8826
  throw new LinkConflictError(path, "foreign-symlink", current);
@@ -8758,7 +8828,7 @@ function linkSkill(opts) {
8758
8828
  previous = current;
8759
8829
  rmSync6(path);
8760
8830
  }
8761
- mkdirSync4(dirname8(path), { recursive: true });
8831
+ mkdirSync4(dirname9(path), { recursive: true });
8762
8832
  symlinkSync(target, path, "dir");
8763
8833
  return previous === void 0 ? { path, target } : { path, target, previous };
8764
8834
  }
@@ -8792,7 +8862,7 @@ function inspectSkill(path) {
8792
8862
  try {
8793
8863
  target = realpathSync(path);
8794
8864
  } catch {
8795
- return { path, state: "dangling", target: resolve5(dirname8(path), readlinkSync(path)) };
8865
+ return { path, state: "dangling", target: resolve5(dirname9(path), readlinkSync(path)) };
8796
8866
  }
8797
8867
  let skillMd;
8798
8868
  try {
@@ -8971,7 +9041,7 @@ function resolveExecutable(name) {
8971
9041
  return null;
8972
9042
  }
8973
9043
  function murmurVersion(executable) {
8974
- let dir = dirname9(executable);
9044
+ let dir = dirname10(executable);
8975
9045
  const root = parse(dir).root;
8976
9046
  while (dir !== root) {
8977
9047
  const packagePath = join11(dir, "package.json");
@@ -8984,7 +9054,7 @@ function murmurVersion(executable) {
8984
9054
  } catch {
8985
9055
  }
8986
9056
  }
8987
- dir = dirname9(dir);
9057
+ dir = dirname10(dir);
8988
9058
  }
8989
9059
  return null;
8990
9060
  }
@@ -9020,7 +9090,7 @@ function murmurDoctorCheck(agentStateSource, opts = {}) {
9020
9090
  return {
9021
9091
  name: "murmur",
9022
9092
  status: "warn",
9023
- detail: `murmur ${version} is older than 1.0.0; @murmur_pane_since missing, idle/stall timing unknown`
9093
+ detail: opts.muExtOk === true ? `murmur ${version} is older than 1.0.0; idle/stall timing unknown for non-pi CLIs (pi agents use ctl)` : `murmur ${version} is older than 1.0.0; @murmur_pane_since missing, idle/stall timing unknown`
9024
9094
  };
9025
9095
  }
9026
9096
  return {
@@ -9129,11 +9199,14 @@ function remediationParagraph(check) {
9129
9199
  return [
9130
9200
  "mu talks to pi agents over a per-agent control socket served by",
9131
9201
  "the mu pi extension. `missing`: no socket (extension not loaded,",
9132
- "or the agent predates `mu link pi`). `refused`: socket present but",
9133
- "not answering. `version`: protocol mismatch; upgrade mu or pi.",
9202
+ "the agent predates `mu link pi`, or a new pi still waits at its",
9203
+ "project trust prompt: answer /trust in the pane, or spawn with",
9204
+ "--command 'pi --approve'). `refused`: socket present but not",
9205
+ "answering. `version`: protocol mismatch; upgrade mu or pi.",
9134
9206
  "Run `mu link pi`, then respawn the agent. `extension X older than",
9135
- "installed Y`: the agent's pi loaded an older mu build; type /reload",
9136
- "in its pane (`mu agent send <a> '/reload' --via mux`) or respawn it."
9207
+ "installed Y` or `lacks ops`: the agent's pi loaded an older mu",
9208
+ "build; reload it with `mu agent send <a> '/reload'` (add",
9209
+ "`--via mux` when the extension lacks op command) or respawn it."
9137
9210
  ];
9138
9211
  case "agents":
9139
9212
  return [