@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/cli.js CHANGED
@@ -861,6 +861,7 @@ ${GOALS_VIEW_SQL}
861
861
 
862
862
  // src/ctl/path.ts
863
863
  import { createHash } from "crypto";
864
+ import { rmSync } from "fs";
864
865
  import { dirname as dirname2, join as join2 } from "path";
865
866
  function sockPathUnder(dir, workstream, agent) {
866
867
  const readable = join2(dir, workstream, `${agent}.sock`);
@@ -873,6 +874,13 @@ function ctlSocketPath(workstream, agent, stateDir) {
873
874
  const base = stateDir ?? (dbPath ? dirname2(dbPath) : defaultStateDir());
874
875
  return sockPathUnder(join2(base, "sock"), workstream, agent);
875
876
  }
877
+ function unlinkCtlSocket(db, name, workstream) {
878
+ if (db.memory) return;
879
+ try {
880
+ rmSync(ctlSocketPath(workstream, name, dirname2(db.name)), { force: true });
881
+ } catch {
882
+ }
883
+ }
876
884
  function remoteCtlSocketPath(workstream, agent, uid) {
877
885
  return sockPathUnder(`/tmp/mu-${uid}`, workstream, agent);
878
886
  }
@@ -2698,12 +2706,6 @@ var init_tmux2 = __esm({
2698
2706
  function isWorkspaceStale(behind) {
2699
2707
  return behind !== null && behind !== void 0 && behind >= WORKSPACE_STALE_THRESHOLD;
2700
2708
  }
2701
- function isLingeringScratchAgent(updatedAt, thresholdMs, now = Date.now()) {
2702
- if (thresholdMs <= 0) return false;
2703
- const updated = Date.parse(updatedAt);
2704
- if (!Number.isFinite(updated)) return false;
2705
- return now - updated >= thresholdMs;
2706
- }
2707
2709
  var WORKSPACE_STALE_THRESHOLD;
2708
2710
  var init_staleness = __esm({
2709
2711
  "src/staleness.ts"() {
@@ -2935,7 +2937,7 @@ var init_types2 = __esm({
2935
2937
 
2936
2938
  // src/vcs/helpers.ts
2937
2939
  import { execFile } from "child_process";
2938
- import { existsSync, rmSync } from "fs";
2940
+ import { existsSync, rmSync as rmSync2 } from "fs";
2939
2941
  import { mkdir } from "fs/promises";
2940
2942
  import { dirname as dirname3 } from "path";
2941
2943
  import { promisify } from "util";
@@ -2959,7 +2961,7 @@ async function ensureParent(path2) {
2959
2961
  }
2960
2962
  function rmDirSync(path2) {
2961
2963
  if (!existsSync(path2)) return false;
2962
- rmSync(path2, { recursive: true, force: true });
2964
+ rmSync2(path2, { recursive: true, force: true });
2963
2965
  return true;
2964
2966
  }
2965
2967
  async function run(bin, args, cwd) {
@@ -3826,7 +3828,7 @@ var init_vcs2 = __esm({
3826
3828
  });
3827
3829
 
3828
3830
  // src/workspace/crud.ts
3829
- import { existsSync as existsSync6, rmSync as rmSync2 } from "fs";
3831
+ import { existsSync as existsSync6, rmSync as rmSync3 } from "fs";
3830
3832
  import { homedir as homedir2 } from "os";
3831
3833
  import { resolve as resolve3 } from "path";
3832
3834
  async function createWorkspace(db, opts) {
@@ -3852,7 +3854,7 @@ async function createWorkspace(db, opts) {
3852
3854
  created = await backend.createWorkspace(createOpts);
3853
3855
  } catch (err) {
3854
3856
  try {
3855
- rmSync2(path2, { recursive: true, force: true });
3857
+ rmSync3(path2, { recursive: true, force: true });
3856
3858
  } catch {
3857
3859
  }
3858
3860
  throw err;
@@ -4241,7 +4243,7 @@ var init_spawn_lock = __esm({
4241
4243
 
4242
4244
  // src/agents/spawn.ts
4243
4245
  import { execFile as execFile2 } from "child_process";
4244
- import { mkdirSync as mkdirSync2, rmSync as rmSync3 } from "fs";
4246
+ import { mkdirSync as mkdirSync2, rmSync as rmSync4 } from "fs";
4245
4247
  import { basename, dirname as dirname4 } from "path";
4246
4248
  import { setTimeout as delay } from "timers/promises";
4247
4249
  import { promisify as promisify2 } from "util";
@@ -4310,7 +4312,7 @@ function defaultSpawnCtlMs() {
4310
4312
  if (Number.isNaN(parsed) || parsed < 0) return 3e4;
4311
4313
  return parsed;
4312
4314
  }
4313
- async function awaitCtlHandshake(sock) {
4315
+ async function awaitCtlHandshake(sock, onMiss) {
4314
4316
  const budgetMs = defaultSpawnCtlMs();
4315
4317
  const deadline = Date.now() + budgetMs;
4316
4318
  for (; ; ) {
@@ -4318,6 +4320,7 @@ async function awaitCtlHandshake(sock) {
4318
4320
  const probe = await ctlProbe(sock, Math.max(1, Math.min(remaining, CTL_POLL_INTERVAL_MS * 4)));
4319
4321
  if (probe.kind === "ok") return "ok";
4320
4322
  const last = probeToSpawnCtl(probe.kind);
4323
+ if (onMiss) await onMiss();
4321
4324
  const left = deadline - Date.now();
4322
4325
  if (left <= 0) return last;
4323
4326
  await delay(Math.min(CTL_POLL_INTERVAL_MS, left));
@@ -4347,7 +4350,7 @@ async function spawnAgent(db, opts) {
4347
4350
  const workspacePathStr = opts.workspace ? await prestageWorkspace(db, opts, cli) : void 0;
4348
4351
  const ctlSocket = ctlSocketPath(opts.workstream, opts.name, dirname4(db.name));
4349
4352
  mkdirSync2(dirname4(ctlSocket), { recursive: true, mode: 448 });
4350
- rmSync3(ctlSocket, { force: true });
4353
+ rmSync4(ctlSocket, { force: true });
4351
4354
  const paneEnv = {
4352
4355
  MU_MANAGED_AGENT: "1",
4353
4356
  MU_AGENT_NAME: opts.name,
@@ -4357,6 +4360,8 @@ async function spawnAgent(db, opts) {
4357
4360
  const hasWorkspace = workspacePathStr !== void 0;
4358
4361
  let paneId;
4359
4362
  let agent;
4363
+ const wantsCtl = opts.ctl !== false && speaksMuCtl(cli, command) && defaultSpawnCtlMs() !== 0;
4364
+ let ctl;
4360
4365
  try {
4361
4366
  const created = await withSpawnLock(session, async () => {
4362
4367
  const pid = await createOrReusePane({
@@ -4377,7 +4382,23 @@ async function spawnAgent(db, opts) {
4377
4382
  agent = created.row;
4378
4383
  const startAgent = (await activeMux()).startAgentInPane;
4379
4384
  if (startAgent === void 0) {
4380
- await awaitSpawnLiveness(paneId, opts.name, command);
4385
+ if (wantsCtl) {
4386
+ const pid = paneId;
4387
+ const check = defaultSpawnLivenessMs() !== 0;
4388
+ const when = `during the ctl handshake (up to ${defaultSpawnCtlMs()}ms after spawn)`;
4389
+ let lastScrollback;
4390
+ const assertPaneAlive = async () => {
4391
+ const mux = await activeMux();
4392
+ const sb = await mux.capturePane(pid, { lines: 50 }).catch(() => void 0);
4393
+ lastScrollback = sb ?? lastScrollback;
4394
+ if (await mux.paneExists(pid)) return;
4395
+ throw new AgentDiedOnSpawnError(opts.name, pid, lastScrollback, command, when);
4396
+ };
4397
+ ctl = await awaitCtlHandshake(ctlSocket, check ? assertPaneAlive : void 0);
4398
+ if (check) await checkSpawnHealth(pid, opts.name, command, when);
4399
+ } else {
4400
+ await awaitSpawnLiveness(paneId, opts.name, command);
4401
+ }
4381
4402
  } else {
4382
4403
  await startAgent({ paneId, name: opts.name, cli, command, commandSource });
4383
4404
  }
@@ -4386,14 +4407,13 @@ async function spawnAgent(db, opts) {
4386
4407
  if (hasWorkspace) attachOrphanCleanupHint(err, opts.name, opts.workstream);
4387
4408
  throw err;
4388
4409
  }
4389
- const ctl = opts.ctl === false || !speaksMuCtl(cli, command) || defaultSpawnCtlMs() === 0 ? "skipped" : await awaitCtlHandshake(ctlSocket);
4410
+ ctl ??= wantsCtl ? await awaitCtlHandshake(ctlSocket) : "skipped";
4390
4411
  emitEvent(
4391
4412
  db,
4392
4413
  opts.workstream,
4393
4414
  "agent.spawn",
4394
4415
  `agent spawn ${opts.name} (cli=${cli}, role=${opts.role ?? "full-access"}, pane=${paneId})`
4395
4416
  );
4396
- await refreshAgentTitle(db, opts.name, opts.workstream);
4397
4417
  maybeWarnNonConventionalAgentName(opts.name);
4398
4418
  return { ...agent, ctl, ctlSocket };
4399
4419
  }
@@ -4490,15 +4510,18 @@ async function awaitSpawnLiveness(paneId, agentName, command) {
4490
4510
  const ms = defaultSpawnLivenessMs();
4491
4511
  if (ms === 0) return;
4492
4512
  await sleep(ms);
4513
+ await checkSpawnHealth(paneId, agentName, command);
4514
+ }
4515
+ async function checkSpawnHealth(paneId, agentName, command, when) {
4493
4516
  const mux = await activeMux();
4494
4517
  const scrollback = await mux.capturePane(paneId, { lines: 50 }).catch(() => void 0);
4495
4518
  if (!await mux.paneExists(paneId)) {
4496
- throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command);
4519
+ throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command, when);
4497
4520
  }
4498
4521
  if (scrollback !== void 0) {
4499
4522
  const matchedLine = detectSpawnStartupError(scrollback);
4500
4523
  if (matchedLine !== void 0) {
4501
- throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback);
4524
+ throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback, when);
4502
4525
  }
4503
4526
  }
4504
4527
  }
@@ -4709,7 +4732,7 @@ var init_errors = __esm({
4709
4732
  }
4710
4733
  };
4711
4734
  AgentDiedOnSpawnError = class extends Error {
4712
- constructor(agentName, paneId, scrollback, command) {
4735
+ constructor(agentName, paneId, scrollback, command, when = `within ${defaultSpawnLivenessMs()}ms of spawn`) {
4713
4736
  const tail = scrollback?.trim();
4714
4737
  const detail = tail ? `
4715
4738
 
@@ -4718,9 +4741,7 @@ ${tail}
4718
4741
  --- end scrollback ---` : "";
4719
4742
  const remote = /^\s*ssh\s/.test(command ?? "");
4720
4743
  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).";
4721
- super(
4722
- `agent ${agentName} died within ${defaultSpawnLivenessMs()}ms of spawn (pane ${paneId}). ${cause}${detail}`
4723
- );
4744
+ super(`agent ${agentName} died ${when} (pane ${paneId}). ${cause}${detail}`);
4724
4745
  this.agentName = agentName;
4725
4746
  this.paneId = paneId;
4726
4747
  this.scrollback = scrollback;
@@ -4779,9 +4800,9 @@ ${tail}
4779
4800
  }
4780
4801
  };
4781
4802
  AgentSpawnStartupError = class extends Error {
4782
- constructor(agentName, paneId, matchedLine, scrollback) {
4803
+ constructor(agentName, paneId, matchedLine, scrollback, when = `within ${defaultSpawnLivenessMs()}ms of spawn`) {
4783
4804
  super(
4784
- `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.
4805
+ `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.
4785
4806
 
4786
4807
  Matched line: ${matchedLine.trim()}
4787
4808
 
@@ -4867,7 +4888,14 @@ ${scrollback.trim()}
4867
4888
  kind;
4868
4889
  name = "AgentCtlUnreachableError";
4869
4890
  errorNextSteps() {
4891
+ const trust = this.kind === "missing" ? [
4892
+ {
4893
+ intent: "Just spawned? pi may sit at its project trust prompt: answer /trust in the pane, or respawn with --command 'pi --approve'",
4894
+ command: `mu agent read ${this.agentName} -w ${this.workstream}`
4895
+ }
4896
+ ] : [];
4870
4897
  return [
4898
+ ...trust,
4871
4899
  { intent: "Check whether the mu pi extension is linked", command: "mu doctor" },
4872
4900
  {
4873
4901
  intent: "Stop a runaway tool without the socket",
@@ -4950,7 +4978,7 @@ ${scrollback.trim()}
4950
4978
  }
4951
4979
  };
4952
4980
  AgentExtensionOutdatedError = class extends Error {
4953
- constructor(agentName, workstream, op, extVersion) {
4981
+ constructor(agentName, workstream, op, extVersion, ops) {
4954
4982
  super(
4955
4983
  `agent ${agentName}'s mu pi extension (${extVersion ?? "version unknown"}) predates op ${op}: its pi loaded an older mu build; reload or respawn it`
4956
4984
  );
@@ -4958,18 +4986,21 @@ ${scrollback.trim()}
4958
4986
  this.workstream = workstream;
4959
4987
  this.op = op;
4960
4988
  this.extVersion = extVersion;
4989
+ this.ops = ops;
4961
4990
  }
4962
4991
  agentName;
4963
4992
  workstream;
4964
4993
  op;
4965
4994
  extVersion;
4995
+ ops;
4966
4996
  name = "AgentExtensionOutdatedError";
4967
4997
  errorNextSteps() {
4968
4998
  const w = `-w ${this.workstream}`;
4999
+ const via = this.ops?.includes("command") ? "" : " --via mux";
4969
5000
  return [
4970
5001
  {
4971
- intent: "Reload pi's extensions in the pane (keeps the session and context)",
4972
- command: `mu agent send ${this.agentName} '/reload' --via mux ${w}`
5002
+ intent: "Reload pi's extensions (keeps the session and context)",
5003
+ command: `mu agent send ${this.agentName} '/reload'${via} ${w}`
4973
5004
  },
4974
5005
  {
4975
5006
  intent: "Or respawn the agent (loses its context)",
@@ -5058,7 +5089,7 @@ async function extensionOutdated(agent, sock, e) {
5058
5089
  if (hello.ok) extVersion = hello.extVersion;
5059
5090
  } catch {
5060
5091
  }
5061
- return new AgentExtensionOutdatedError(agent.name, agent.workstreamName, e.op, extVersion);
5092
+ return new AgentExtensionOutdatedError(agent.name, agent.workstreamName, e.op, extVersion, e.ops);
5062
5093
  }
5063
5094
  async function ctlRequestFor(agent, sock, req) {
5064
5095
  try {
@@ -5107,7 +5138,11 @@ async function sendViaTransport(agent, text2, opts) {
5107
5138
  }
5108
5139
  throw new Error(`control socket refused the send: ${reply.error}`);
5109
5140
  }
5110
- return reply.state === void 0 ? { transport } : { transport, state: reply.state };
5141
+ return {
5142
+ transport,
5143
+ ...reply.state !== void 0 ? { state: reply.state } : {},
5144
+ ...reply.runs !== void 0 ? { runs: reply.runs } : {}
5145
+ };
5111
5146
  }
5112
5147
  async function runSessionCommand(agent, sock, cmd, force) {
5113
5148
  const reply = await ctlRequestFor(agent, sock, {
@@ -5301,7 +5336,8 @@ async function readAgentStates(agents, opts = {}) {
5301
5336
  since: probe.status.since,
5302
5337
  alive: true,
5303
5338
  ctl: "ok",
5304
- pending: probe.status.pending
5339
+ pending: probe.status.pending,
5340
+ runs: probe.status.runs
5305
5341
  });
5306
5342
  } else {
5307
5343
  failed.set(agentKey(agent), probe?.kind === "missing" ? "missing" : "refused");
@@ -5857,6 +5893,15 @@ var init_id = __esm({
5857
5893
  });
5858
5894
 
5859
5895
  // src/tasks/errors.ts
5896
+ function stallWaitHint(owner, ws, runs) {
5897
+ if (runs === void 0 || runs < 1) return [];
5898
+ return [
5899
+ {
5900
+ intent: "Read the worker's last answer (its final text, at once)",
5901
+ command: `mu agent wait ${owner} --after-runs ${runs - 1} --json -w ${ws}`
5902
+ }
5903
+ ];
5904
+ }
5860
5905
  var TaskNotFoundError, TaskIdInvalidError, TaskExistsError, TaskNotInWorkstreamError, TaskAlreadyOwnedError, ClaimerNotRegisteredError, TaskClaimStaleWorkspaceError, CycleError, ReaperDetectedDuringWaitError, StallDetectedDuringWaitError, CrossWorkstreamEdgeError, SubstateReasonRequiredError, TaskParkStateError, TaskInTriageError, TaskParkedError, InvalidSubstateError;
5861
5906
  var init_errors2 = __esm({
5862
5907
  "src/tasks/errors.ts"() {
@@ -6079,7 +6124,7 @@ var init_errors2 = __esm({
6079
6124
  }
6080
6125
  };
6081
6126
  StallDetectedDuringWaitError = class extends Error {
6082
- constructor(taskName, owner, workstream, ageSecs, ownerExpectsCtl = false) {
6127
+ constructor(taskName, owner, workstream, ageSecs, ownerExpectsCtl = false, ownerRuns) {
6083
6128
  const ownerBit = owner !== null ? owner : "<unknown>";
6084
6129
  super(
6085
6130
  `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.`
@@ -6089,12 +6134,14 @@ var init_errors2 = __esm({
6089
6134
  this.workstream = workstream;
6090
6135
  this.ageSecs = ageSecs;
6091
6136
  this.ownerExpectsCtl = ownerExpectsCtl;
6137
+ this.ownerRuns = ownerRuns;
6092
6138
  }
6093
6139
  taskName;
6094
6140
  owner;
6095
6141
  workstream;
6096
6142
  ageSecs;
6097
6143
  ownerExpectsCtl;
6144
+ ownerRuns;
6098
6145
  name = "StallDetectedDuringWaitError";
6099
6146
  /**
6100
6147
  * Diagnose before acting. `mu agent read` comes FIRST because the
@@ -6109,6 +6156,7 @@ var init_errors2 = __esm({
6109
6156
  const ws = this.workstream;
6110
6157
  const ownerBit = this.owner !== null ? this.owner : "<owner>";
6111
6158
  return [
6159
+ ...stallWaitHint(ownerBit, ws, this.ownerRuns),
6112
6160
  {
6113
6161
  intent: "Read the worker's pane to see what it is waiting on",
6114
6162
  command: `mu agent read ${ownerBit} -w ${ws} --lines 60`
@@ -6460,7 +6508,7 @@ var init_edges = __esm({
6460
6508
 
6461
6509
  // src/workstream.ts
6462
6510
  import { existsSync as existsSync9, readdirSync as readdirSync2, rmdirSync } from "fs";
6463
- import { join as join8 } from "path";
6511
+ import { dirname as dirname6, join as join8 } from "path";
6464
6512
  async function listMuxSessions() {
6465
6513
  try {
6466
6514
  return await (await activeMux()).listSessions();
@@ -6614,6 +6662,7 @@ async function teardownWorkstream(db, opts) {
6614
6662
  const notesBefore = countNotes(db, opts.workstream);
6615
6663
  const edgesBefore = countEdges(db, opts.workstream);
6616
6664
  const workspacesBefore = listWorkspaces2(db, opts.workstream);
6665
+ const agentNames = listAgentNames(db, opts.workstream);
6617
6666
  const mux = await activeMux();
6618
6667
  const muxAliveBefore = await mux.sessionExists(muxSession);
6619
6668
  if (muxAliveBefore) {
@@ -6651,6 +6700,14 @@ async function teardownWorkstream(db, opts) {
6651
6700
  } catch {
6652
6701
  }
6653
6702
  }
6703
+ for (const name of agentNames) unlinkCtlSocket(db, name, opts.workstream);
6704
+ if (!db.memory) {
6705
+ const sockDir = join8(dirname6(db.name), "sock", opts.workstream);
6706
+ try {
6707
+ if (readdirSync2(sockDir).length === 0) rmdirSync(sockDir);
6708
+ } catch {
6709
+ }
6710
+ }
6654
6711
  withOpContext(
6655
6712
  db,
6656
6713
  { intent: "workstream.teardown", group: "new" },
@@ -6667,6 +6724,14 @@ async function teardownWorkstream(db, opts) {
6667
6724
  failedWorkspaces
6668
6725
  };
6669
6726
  }
6727
+ function listAgentNames(db, workstream) {
6728
+ const rows = db.prepare(
6729
+ `SELECT a.name FROM agents a
6730
+ JOIN workstreams ws ON ws.id = a.workstream_id
6731
+ WHERE ws.name = ?`
6732
+ ).all(workstream);
6733
+ return rows.map((r) => r.name);
6734
+ }
6670
6735
  function countAgents(db, workstream) {
6671
6736
  const row2 = db.prepare(
6672
6737
  `SELECT COUNT(*) AS n FROM agents a
@@ -6707,6 +6772,7 @@ var WORKSTREAM_NAME_RE, RESERVED_WORKSTREAM_PREFIX, RESERVED_WORKSTREAM_NAMES, W
6707
6772
  var init_workstream = __esm({
6708
6773
  "src/workstream.ts"() {
6709
6774
  "use strict";
6775
+ init_path();
6710
6776
  init_db();
6711
6777
  init_legacy_ops();
6712
6778
  init_mux2();
@@ -7360,12 +7426,15 @@ async function waitForTasks(db, input, opts) {
7360
7426
  const ctlBrokenSince = /* @__PURE__ */ new Map();
7361
7427
  const stuckLabel = /* @__PURE__ */ new Map();
7362
7428
  const ctlOwners = /* @__PURE__ */ new Set();
7429
+ const ownerRuns = /* @__PURE__ */ new Map();
7363
7430
  const stuckAgeMs = async (status, owner, workstreamName) => {
7364
7431
  if (stuckAfterMs <= 0 || status !== "IN_PROGRESS" || owner === null || opts.readOwnerState === void 0)
7365
7432
  return null;
7366
7433
  const reading = await opts.readOwnerState({ name: owner, workstreamName });
7367
7434
  const ownerKey = `${workstreamName}/${owner}`;
7368
7435
  if (reading?.ctl !== void 0) ctlOwners.add(ownerKey);
7436
+ if (reading?.runs !== void 0) ownerRuns.set(ownerKey, reading.runs);
7437
+ else ownerRuns.delete(ownerKey);
7369
7438
  if (reading?.alive === true && (reading.ctl === "missing" || reading.ctl === "refused")) {
7370
7439
  const firstSeen = ctlBrokenSince.get(ownerKey) ?? Date.now();
7371
7440
  ctlBrokenSince.set(ownerKey, firstSeen);
@@ -7395,10 +7464,13 @@ async function waitForTasks(db, input, opts) {
7395
7464
  stuckWarned.add(key);
7396
7465
  const ageSecs = Math.round(ageMs2 / 1e3);
7397
7466
  const ownerBit = owner ?? "<none>";
7467
+ const runs = ownerRuns.get(`${ref.workstreamName}/${ownerBit}`);
7398
7468
  const stateBit = stuckLabel.get(`${ref.workstreamName}/${ownerBit}`) ?? "needs_input";
7469
+ const waitLine = stallWaitHint(ownerBit, ref.workstreamName, runs).map((s) => ` ${s.command}
7470
+ `).join("");
7399
7471
  currentStuckWarn(
7400
7472
  `\x1B[33mmu task wait: ${key} needs attention \u2014 owner=${ownerBit} has been in ${stateBit} for ${formatStallAge(ageMs2)}. It may have finished without closing, be waiting on an answer, or be sitting at a prompt.\x1B[0m
7401
- mu agent read ${ownerBit} -w ${ref.workstreamName} --lines 60
7473
+ ` + waitLine + ` mu agent read ${ownerBit} -w ${ref.workstreamName} --lines 60
7402
7474
  `
7403
7475
  );
7404
7476
  emitEvent(
@@ -7414,7 +7486,8 @@ async function waitForTasks(db, input, opts) {
7414
7486
  owner,
7415
7487
  ref.workstreamName,
7416
7488
  ageSecs,
7417
- ctlOwners.has(`${ref.workstreamName}/${owner ?? ""}`)
7489
+ ctlOwners.has(`${ref.workstreamName}/${owner ?? ""}`),
7490
+ runs
7418
7491
  );
7419
7492
  }
7420
7493
  }
@@ -7930,8 +8003,7 @@ var init_wait2 = __esm({
7930
8003
  });
7931
8004
 
7932
8005
  // src/agents.ts
7933
- import { rmSync as rmSync4 } from "fs";
7934
- import { dirname as dirname6 } from "path";
8006
+ import { dirname as dirname7 } from "path";
7935
8007
  function idleThresholdMs() {
7936
8008
  const env = process.env.MU_IDLE_THRESHOLD_MS;
7937
8009
  if (env === void 0 || env === "") return DEFAULT_IDLE_THRESHOLD_MS;
@@ -8050,13 +8122,6 @@ function deleteAgent(db, name, workstream) {
8050
8122
  if (deleted) unlinkCtlSocket(db, name, workstream);
8051
8123
  return deleted;
8052
8124
  }
8053
- function unlinkCtlSocket(db, name, workstream) {
8054
- if (db.memory) return;
8055
- try {
8056
- rmSync4(ctlSocketPath(workstream, name, dirname6(db.name)), { force: true });
8057
- } catch {
8058
- }
8059
- }
8060
8125
  function deleteAgentRow(db, name, workstream) {
8061
8126
  return withOpContext(
8062
8127
  db,
@@ -8151,7 +8216,7 @@ async function listLiveAgents(db, opts) {
8151
8216
  ...opts.mode !== void 0 ? { mode: opts.mode } : {}
8152
8217
  });
8153
8218
  const baseAgents = listAgents(db, { workstream: opts.workstream });
8154
- const readings = await readAgentStates(baseAgents, { stateDir: dirname6(db.name) });
8219
+ const readings = await readAgentStates(baseAgents, { stateDir: dirname7(db.name) });
8155
8220
  const now = Date.now();
8156
8221
  const agents = baseAgents.map((agent) => {
8157
8222
  const reading = readings.get(agentKey(agent)) ?? {
@@ -8708,7 +8773,7 @@ var init_apply = __esm({
8708
8773
 
8709
8774
  // src/rebuild.ts
8710
8775
  import { existsSync as existsSync10, mkdirSync as mkdirSync3, statSync } from "fs";
8711
- import { dirname as dirname8 } from "path";
8776
+ import { dirname as dirname9 } from "path";
8712
8777
  function isProjectable(entity, intent) {
8713
8778
  if (isLegacyLogOnlyIntent(intent)) return false;
8714
8779
  if (LOG_ONLY_ENTITIES.has(entity)) return false;
@@ -8739,7 +8804,7 @@ function rebuildInto(source, opts) {
8739
8804
  if (existsSync10(targetPath) && opts.force !== true) {
8740
8805
  throw new RebuildTargetExistsError(targetPath);
8741
8806
  }
8742
- mkdirSync3(dirname8(targetPath), { recursive: true });
8807
+ mkdirSync3(dirname9(targetPath), { recursive: true });
8743
8808
  const identity = source.prepare(
8744
8809
  "SELECT machine_id, hostname, created_at, last_wall, last_counter FROM machine_identity WHERE id = 1"
8745
8810
  ).get();
@@ -9321,13 +9386,13 @@ import {
9321
9386
  writeFileSync
9322
9387
  } from "fs";
9323
9388
  import { homedir as homedir4 } from "os";
9324
- import { dirname as dirname9, join as join10, resolve as resolve5 } from "path";
9389
+ import { dirname as dirname10, join as join10, resolve as resolve5 } from "path";
9325
9390
  import { fileURLToPath, pathToFileURL } from "url";
9326
9391
  function resolveHome(opts) {
9327
9392
  return opts?.home ?? process.env.MU_PI_HOME ?? homedir4();
9328
9393
  }
9329
9394
  function packageRoot() {
9330
- return resolve5(dirname9(fileURLToPath(import.meta.url)), "..");
9395
+ return resolve5(dirname10(fileURLToPath(import.meta.url)), "..");
9331
9396
  }
9332
9397
  function extensionEntry() {
9333
9398
  return process.env.MU_EXTENSION_ENTRY ?? join10(packageRoot(), "dist", "extension", "mu-pi.js");
@@ -9363,7 +9428,7 @@ function linkPi(opts) {
9363
9428
  replacedCopy = !readFileSync(path2, "utf8").includes(MU_SHIM_MARKER);
9364
9429
  } catch {
9365
9430
  }
9366
- mkdirSync4(dirname9(path2), { recursive: true });
9431
+ mkdirSync4(dirname10(path2), { recursive: true });
9367
9432
  writeFileSync(path2, opts?.copy === true ? readFileSync(entry, "utf8") : shim(entry));
9368
9433
  return { path: path2, replacedCopy };
9369
9434
  }
@@ -9374,7 +9439,7 @@ function linkSkill(opts) {
9374
9439
  const st = lstatOrUndefined(path2);
9375
9440
  if (st) {
9376
9441
  if (!st.isSymbolicLink()) throw new LinkConflictError(path2, "not-a-symlink");
9377
- const current = resolve5(dirname9(path2), readlinkSync(path2));
9442
+ const current = resolve5(dirname10(path2), readlinkSync(path2));
9378
9443
  if (current === target) return { path: path2, target };
9379
9444
  if (existsSync12(current) && opts?.force !== true) {
9380
9445
  throw new LinkConflictError(path2, "foreign-symlink", current);
@@ -9382,7 +9447,7 @@ function linkSkill(opts) {
9382
9447
  previous = current;
9383
9448
  rmSync6(path2);
9384
9449
  }
9385
- mkdirSync4(dirname9(path2), { recursive: true });
9450
+ mkdirSync4(dirname10(path2), { recursive: true });
9386
9451
  symlinkSync(target, path2, "dir");
9387
9452
  return previous === void 0 ? { path: path2, target } : { path: path2, target, previous };
9388
9453
  }
@@ -9416,7 +9481,7 @@ function inspectSkill(path2) {
9416
9481
  try {
9417
9482
  target = realpathSync(path2);
9418
9483
  } catch {
9419
- return { path: path2, state: "dangling", target: resolve5(dirname9(path2), readlinkSync(path2)) };
9484
+ return { path: path2, state: "dangling", target: resolve5(dirname10(path2), readlinkSync(path2)) };
9420
9485
  }
9421
9486
  let skillMd;
9422
9487
  try {
@@ -9467,7 +9532,7 @@ var init_link = __esm({
9467
9532
 
9468
9533
  // src/doctor-summary.ts
9469
9534
  import { accessSync as accessSync2, constants as constants2, existsSync as existsSync13, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
9470
- import { delimiter as delimiter2, dirname as dirname10, join as join11, parse } from "path";
9535
+ import { delimiter as delimiter2, dirname as dirname11, join as join11, parse } from "path";
9471
9536
  import { fileURLToPath as fileURLToPath2 } from "url";
9472
9537
  function loadDoctorSummary(db, snapshot2, agentStateSource = "murmur") {
9473
9538
  const checks = [];
@@ -9628,7 +9693,7 @@ function resolveExecutable(name) {
9628
9693
  return null;
9629
9694
  }
9630
9695
  function murmurVersion(executable) {
9631
- let dir = dirname10(executable);
9696
+ let dir = dirname11(executable);
9632
9697
  const root = parse(dir).root;
9633
9698
  while (dir !== root) {
9634
9699
  const packagePath = join11(dir, "package.json");
@@ -9641,7 +9706,7 @@ function murmurVersion(executable) {
9641
9706
  } catch {
9642
9707
  }
9643
9708
  }
9644
- dir = dirname10(dir);
9709
+ dir = dirname11(dir);
9645
9710
  }
9646
9711
  return null;
9647
9712
  }
@@ -9676,7 +9741,7 @@ function murmurDoctorCheck(agentStateSource, opts = {}) {
9676
9741
  return {
9677
9742
  name: "murmur",
9678
9743
  status: "warn",
9679
- detail: `murmur ${version} is older than 1.0.0; @murmur_pane_since missing, idle/stall timing unknown`
9744
+ 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`
9680
9745
  };
9681
9746
  }
9682
9747
  return {
@@ -9730,7 +9795,7 @@ function skillDoctorCheck(opts) {
9730
9795
  }
9731
9796
  function installedMuVersion() {
9732
9797
  try {
9733
- const pkgPath = join11(dirname10(fileURLToPath2(import.meta.url)), "..", "package.json");
9798
+ const pkgPath = join11(dirname11(fileURLToPath2(import.meta.url)), "..", "package.json");
9734
9799
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
9735
9800
  const v = typeof pkg === "object" && pkg !== null ? pkg.version : null;
9736
9801
  return typeof v === "string" ? v : null;
@@ -9774,7 +9839,10 @@ async function ctlSocketsDoctorCheck(agents, opts = {}) {
9774
9839
  socket,
9775
9840
  probe: probe.kind,
9776
9841
  ...probe.kind === "ok" && probe.extVersion !== void 0 ? { extVersion: probe.extVersion } : {},
9777
- ...older || missingOps.length > 0 ? { outdated: true } : {},
9842
+ ...older || missingOps.length > 0 ? {
9843
+ outdated: true,
9844
+ reloadVia: probe.kind === "ok" && probe.ops?.includes("command") ? "ctl" : "mux"
9845
+ } : {},
9778
9846
  ...missingOps.length > 0 ? { missingOps } : {}
9779
9847
  };
9780
9848
  })
@@ -9860,11 +9928,14 @@ function remediationParagraph(check) {
9860
9928
  return [
9861
9929
  "mu talks to pi agents over a per-agent control socket served by",
9862
9930
  "the mu pi extension. `missing`: no socket (extension not loaded,",
9863
- "or the agent predates `mu link pi`). `refused`: socket present but",
9864
- "not answering. `version`: protocol mismatch; upgrade mu or pi.",
9931
+ "the agent predates `mu link pi`, or a new pi still waits at its",
9932
+ "project trust prompt: answer /trust in the pane, or spawn with",
9933
+ "--command 'pi --approve'). `refused`: socket present but not",
9934
+ "answering. `version`: protocol mismatch; upgrade mu or pi.",
9865
9935
  "Run `mu link pi`, then respawn the agent. `extension X older than",
9866
- "installed Y`: the agent's pi loaded an older mu build; type /reload",
9867
- "in its pane (`mu agent send <a> '/reload' --via mux`) or respawn it."
9936
+ "installed Y` or `lacks ops`: the agent's pi loaded an older mu",
9937
+ "build; reload it with `mu agent send <a> '/reload'` (add",
9938
+ "`--via mux` when the extension lacks op command) or respawn it."
9868
9939
  ];
9869
9940
  case "agents":
9870
9941
  return [
@@ -18608,14 +18679,14 @@ var init_tui = __esm({
18608
18679
  // src/cli.ts
18609
18680
  init_agents();
18610
18681
  import { readFileSync as readFileSync4, realpathSync as realpathSync4 } from "fs";
18611
- import { dirname as dirname16, join as join14 } from "path";
18682
+ import { dirname as dirname17, join as join14 } from "path";
18612
18683
  import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL2 } from "url";
18613
18684
  import { Command, InvalidArgumentError } from "commander";
18614
18685
 
18615
18686
  // src/cli/agents.ts
18616
18687
  init_agent_state();
18617
18688
  init_agents();
18618
- import { dirname as dirname12 } from "path";
18689
+ import { dirname as dirname13 } from "path";
18619
18690
  init_client();
18620
18691
  init_mux2();
18621
18692
  init_output();
@@ -18715,7 +18786,7 @@ function formatStaleWorkspaceWarning(staleness) {
18715
18786
 
18716
18787
  // src/cli/agents-remote.ts
18717
18788
  init_agents();
18718
- import { dirname as dirname7 } from "path";
18789
+ import { dirname as dirname8 } from "path";
18719
18790
  init_path();
18720
18791
  init_output();
18721
18792
  init_workstream();
@@ -18731,7 +18802,7 @@ function buildRemoteEnv(db, agent, workstream, remoteSock) {
18731
18802
  if (!isValidWorkstreamName(workstream)) {
18732
18803
  throw new UsageError(`invalid workstream name ${JSON.stringify(workstream)}`);
18733
18804
  }
18734
- const localSock = ctlSocketPath(workstream, agent, dirname7(db.name));
18805
+ const localSock = ctlSocketPath(workstream, agent, dirname8(db.name));
18735
18806
  const remote = remoteSock ?? remoteCtlSocketPath(workstream, agent, process.getuid?.() ?? 0);
18736
18807
  if (!remote.startsWith("/") || !SAFE_WORD.test(remote)) {
18737
18808
  throw new UsageError(
@@ -19210,12 +19281,13 @@ init_tasks();
19210
19281
  init_agent_state();
19211
19282
  init_errors();
19212
19283
  init_agents();
19213
- import { dirname as dirname11 } from "path";
19284
+ import { dirname as dirname12 } from "path";
19214
19285
  init_db();
19215
19286
  init_glyphs();
19216
19287
  init_output();
19217
19288
  init_reconcile();
19218
19289
  init_state();
19290
+ init_errors2();
19219
19291
  init_tasks();
19220
19292
  init_vcs2();
19221
19293
  init_workspace();
@@ -19446,7 +19518,7 @@ async function cmdTaskWait(db, ids, opts) {
19446
19518
  const agent = getAgent(db, owner, ref.workstreamName);
19447
19519
  return agent === void 0 ? [] : [agent];
19448
19520
  });
19449
- ownerReadings = await readAgentStates(owners, { stateDir: dirname11(db.name) });
19521
+ ownerReadings = await readAgentStates(owners, { stateDir: dirname12(db.name) });
19450
19522
  if (!reaperExitEnabled) return;
19451
19523
  for (const ref of refs) {
19452
19524
  const key = qualifiedId(ref);
@@ -19498,6 +19570,10 @@ async function cmdTaskWait(db, ids, opts) {
19498
19570
  }
19499
19571
  for (const t of unmetRefs) {
19500
19572
  if (t.stuck && t.owner !== null) {
19573
+ const runs = ownerReadings.get(
19574
+ agentKey({ name: t.owner, workstreamName: t.workstreamName })
19575
+ )?.runs;
19576
+ nextSteps3.push(...stallWaitHint(t.owner, t.workstreamName, runs));
19501
19577
  nextSteps3.push({
19502
19578
  intent: `Read ${t.owner}'s pane \u2014 ${qualifiedId(t)} is IN_PROGRESS but its owner needs attention`,
19503
19579
  command: `mu agent read ${t.owner} -w ${t.workstreamName} --lines 60`
@@ -20305,8 +20381,6 @@ async function cmdSend(db, rawName, text2, opts = {}) {
20305
20381
  });
20306
20382
  let undelivered;
20307
20383
  const agentRow = getAgent(db, name, ws);
20308
- const plainCtl = agentRow !== void 0 && !opts.fresh && !opts.steer && parseSessionCommand(text2) === void 0 && (via ?? chooseTransport(agentRow, agentCtlSocket(db, agentRow))) === "ctl";
20309
- const before = plainCtl && agentRow !== void 0 ? await ctlProbe(agentCtlSocket(db, agentRow)) : void 0;
20310
20384
  const sent = await sendToAgent(db, name, text2, {
20311
20385
  workstream: ws,
20312
20386
  ...opts.steer ? { mode: "steer" } : {},
@@ -20318,11 +20392,15 @@ async function cmdSend(db, rawName, text2, opts = {}) {
20318
20392
  }
20319
20393
  });
20320
20394
  const nextSteps3 = [
20321
- { intent: "Read response", command: `mu agent read ${name} -n 50 -w ${ws}` },
20395
+ sent.transport === "ctl" && sent.runs !== void 0 ? {
20396
+ intent: "Wait for the response (returns its final text)",
20397
+ command: `mu agent wait ${name} --after-runs ${sent.runs} --json -w ${ws}`
20398
+ } : { intent: "Read response", command: `mu agent read ${name} -n 50 -w ${ws}` },
20322
20399
  { intent: "Watch live events", command: `mu log -w ${ws} --tail` }
20323
20400
  ];
20324
- if (agentRow !== void 0 && before?.kind === "ok") {
20325
- nextSteps3.push(...plainSendHints(agentRow, before.status));
20401
+ const plainCtl = sent.transport === "ctl" && !opts.fresh && !opts.steer && sent.command === void 0;
20402
+ if (agentRow !== void 0 && plainCtl && sent.state !== void 0 && sent.runs !== void 0) {
20403
+ nextSteps3.push(...plainSendHints(agentRow, { state: sent.state, runs: sent.runs }));
20326
20404
  }
20327
20405
  if (stalenessCheck.warned && stalenessCheck.nextStep !== null) {
20328
20406
  nextSteps3.push(stalenessCheck.nextStep);
@@ -20341,6 +20419,7 @@ async function cmdSend(db, rawName, text2, opts = {}) {
20341
20419
  fresh: opts.fresh === true,
20342
20420
  ...sent.command !== void 0 ? { command: sent.command } : {},
20343
20421
  ...sent.state !== void 0 ? { state: sent.state } : {},
20422
+ ...sent.runs !== void 0 ? { runs: sent.runs } : {},
20344
20423
  delivered: undelivered === void 0,
20345
20424
  ...undelivered !== void 0 ? { undelivered: { reason: undelivered.reason, message: undelivered.message } } : {},
20346
20425
  staleness: stalenessCheck.staleness,
@@ -20351,7 +20430,7 @@ async function cmdSend(db, rawName, text2, opts = {}) {
20351
20430
  if (undelivered !== void 0) {
20352
20431
  console.error(pc.yellow(`warning: ${undelivered.message}`));
20353
20432
  } else {
20354
- const how = sent.transport === "mux" ? "via mux paste" : opts.fresh ? "via ctl, fresh session" : sent.command !== void 0 ? `via ctl, pi ran /${sent.command}` : `via ctl, pi ${sent.state ?? "?"}`;
20433
+ const how = sent.transport === "mux" ? "via mux paste" : opts.fresh ? "via ctl, fresh session" : sent.command !== void 0 ? `via ctl, pi ran /${sent.command}` : sent.runs !== void 0 ? `via ctl, pi was ${sent.state ?? "?"}, runs ${sent.runs}` : `via ctl, pi ${sent.state ?? "?"}`;
20355
20434
  console.log(pc.dim(`sent ${text2.length} bytes to ${name} (${how})`));
20356
20435
  }
20357
20436
  printNextSteps(nextSteps3);
@@ -20414,7 +20493,7 @@ async function cmdAgentShow(db, rawName, opts) {
20414
20493
  } catch {
20415
20494
  scrollback = "";
20416
20495
  }
20417
- const reading = (await readAgentStates([agent], { stateDir: dirname12(db.name) })).get(
20496
+ const reading = (await readAgentStates([agent], { stateDir: dirname13(db.name) })).get(
20418
20497
  agentKey(agent)
20419
20498
  );
20420
20499
  const displayed = {
@@ -20449,7 +20528,7 @@ async function cmdAgentShow(db, rawName, opts) {
20449
20528
  }
20450
20529
  async function cmdMe(db, opts = {}) {
20451
20530
  const self = resolveSelf(db);
20452
- const reading = (await readAgentStates([self], { stateDir: dirname12(db.name) })).get(
20531
+ const reading = (await readAgentStates([self], { stateDir: dirname13(db.name) })).get(
20453
20532
  agentKey(self)
20454
20533
  );
20455
20534
  const displayed = {
@@ -20642,7 +20721,7 @@ async function cmdAbort(db, rawName, opts = {}) {
20642
20721
  intent: "Steer it with the next instruction",
20643
20722
  command: `mu agent send ${name} '...' -w ${ws}`
20644
20723
  },
20645
- { intent: "Read the pane", command: `mu agent read ${name} -n 30 -w ${ws}` }
20724
+ ...result.after === "idle" ? [] : [{ intent: "Read the pane", command: `mu agent read ${name} -n 30 -w ${ws}` }]
20646
20725
  ];
20647
20726
  if (opts.json) {
20648
20727
  emitJson({ ...result, nextSteps: nextSteps3 });
@@ -20667,12 +20746,22 @@ async function cmdAgentWait(db, rawNames, opts) {
20667
20746
  return { name, workstreamName };
20668
20747
  })
20669
20748
  );
20749
+ if (opts.afterRuns !== void 0) {
20750
+ for (const ref of refs) {
20751
+ const agent = getAgent(db, ref.name, ref.workstreamName);
20752
+ if (agent !== void 0 && !expectsCtl(agent, agentCtlSocket(db, agent))) {
20753
+ throw new UsageError(
20754
+ `--after-runs needs a pi agent (control socket); ${ref.workstreamName}/${ref.name} runs ${agent.cli}. Drop --after-runs to wait on its pane state.`
20755
+ );
20756
+ }
20757
+ }
20758
+ }
20670
20759
  const readStatuses = async (pending2) => {
20671
20760
  const agents = pending2.flatMap((ref) => {
20672
20761
  const agent = getAgent(db, ref.name, ref.workstreamName);
20673
20762
  return agent === void 0 ? [] : [agent];
20674
20763
  });
20675
- const readings = await readAgentStates(agents, { stateDir: dirname12(db.name) });
20764
+ const readings = await readAgentStates(agents, { stateDir: dirname13(db.name) });
20676
20765
  const snapshots = /* @__PURE__ */ new Map();
20677
20766
  for (const ref of pending2) {
20678
20767
  const agent = getAgent(db, ref.name, ref.workstreamName);
@@ -20689,9 +20778,15 @@ async function cmdAgentWait(db, rawNames, opts) {
20689
20778
  if (agent === void 0) return void 0;
20690
20779
  const sock = agentCtlSocket(db, agent);
20691
20780
  if (!expectsCtl(agent, sock)) return void 0;
20692
- const probe = await ctlProbe(sock);
20693
- if (probe.kind !== "ok") return void 0;
20694
- const settled = ctlRequest(sock, { op: "wait", afterRuns: probe.status.runs }, { signal }).then(
20781
+ let initial = { status: "busy" };
20782
+ let afterRuns = opts.afterRuns;
20783
+ if (afterRuns === void 0) {
20784
+ const probe = await ctlProbe(sock);
20785
+ if (probe.kind !== "ok") return void 0;
20786
+ afterRuns = probe.status.runs;
20787
+ initial = { status: ctlRuntimeState(probe.status.state) };
20788
+ }
20789
+ const settled = ctlRequest(sock, { op: "wait", afterRuns }, { signal }).then(
20695
20790
  (reply) => reply.ok && reply.state !== void 0 ? {
20696
20791
  status: ctlRuntimeState(reply.state),
20697
20792
  ...reply.lastText !== void 0 ? { lastText: reply.lastText } : {},
@@ -20706,7 +20801,7 @@ async function cmdAgentWait(db, rawNames, opts) {
20706
20801
  return again.kind === "ok" ? { status: "unknown", unknownReason: "ctl wait interrupted" } : { status: null };
20707
20802
  }
20708
20803
  );
20709
- return { initial: { status: ctlRuntimeState(probe.status.state) }, settled };
20804
+ return { initial, settled };
20710
20805
  };
20711
20806
  const timeoutMs = (opts.timeout ?? 600) * 1e3;
20712
20807
  const result = await waitForAgents(db, refs, {
@@ -20729,6 +20824,12 @@ async function cmdAgentWait(db, rawNames, opts) {
20729
20824
  const fired = result.agents.filter((a) => a.fired);
20730
20825
  const firing = wantFirstShape && fired.length > 0 ? fired[0] : null;
20731
20826
  const qualified = (a) => `${a.workstreamName}/${a.name}`;
20827
+ const readHint = firing && (!firing.lastText || firing.lastError !== void 0) ? [
20828
+ {
20829
+ intent: "Read the finished agent",
20830
+ command: `mu agent read ${firing.name} -w ${firing.workstreamName}`
20831
+ }
20832
+ ] : [];
20732
20833
  if (opts.json) {
20733
20834
  emitJson({
20734
20835
  agents: result.agents.map((a) => ({
@@ -20738,12 +20839,7 @@ async function cmdAgentWait(db, rawNames, opts) {
20738
20839
  timedOut: result.timedOut,
20739
20840
  ...firing ? { firing: { workstream: firing.workstreamName, name: firing.name } } : {},
20740
20841
  ...dead.length > 0 ? { dead: dead.map(qualified) } : {},
20741
- nextSteps: firing ? [
20742
- {
20743
- intent: "Read the finished agent",
20744
- command: `mu agent read ${firing.name} -w ${firing.workstreamName}`
20745
- }
20746
- ] : []
20842
+ nextSteps: readHint
20747
20843
  });
20748
20844
  } else if (result.timedOut) {
20749
20845
  console.log(
@@ -20755,12 +20851,7 @@ async function cmdAgentWait(db, rawNames, opts) {
20755
20851
  console.log(pc.red(`Agent pane(s) died: ${dead.map(qualified).join(", ")}`));
20756
20852
  } else if (firing) {
20757
20853
  console.log(`${pc.bold(qualified(firing))} finished`);
20758
- printNextSteps([
20759
- {
20760
- intent: "Read the finished agent",
20761
- command: `mu agent read ${firing.name} -w ${firing.workstreamName}`
20762
- }
20763
- ]);
20854
+ printNextSteps(readHint);
20764
20855
  } else {
20765
20856
  console.log(`All ${result.agents.length} agent(s) finished`);
20766
20857
  }
@@ -20855,7 +20946,11 @@ function wireAgentCommands(program) {
20855
20946
  "--timeout <seconds>",
20856
20947
  "max seconds to wait (0 = forever, default 600)",
20857
20948
  parseNonNegativeInt
20858
- ).option("--lines <n>", "scrollback lines to scan per poll (default 100)", parseLines).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(names) {
20949
+ ).option(
20950
+ "--after-runs <n>",
20951
+ "pi agents only: fire on the first run settled past N (the `runs` that `mu agent send --json` printed), even one that settled before this wait started",
20952
+ parseNonNegativeInt
20953
+ ).option(...WORKSTREAM_OPT).option(...JSON_OPT).action(function(names) {
20859
20954
  const opts = this.optsWithGlobals();
20860
20955
  return handle(
20861
20956
  (db) => cmdAgentWait(db, names, {
@@ -20905,7 +21000,7 @@ function wireSelfCommands(program) {
20905
21000
 
20906
21001
  // src/cli/db.ts
20907
21002
  import { existsSync as existsSync16, mkdirSync as mkdirSync6, statSync as statSync5 } from "fs";
20908
- import { dirname as dirname13, resolve as resolve7 } from "path";
21003
+ import { dirname as dirname14, resolve as resolve7 } from "path";
20909
21004
 
20910
21005
  // src/compact.ts
20911
21006
  init_legacy_ops();
@@ -21997,7 +22092,7 @@ function cmdDbBackup(db, file, opts = {}) {
21997
22092
  if (existsSync16(target)) {
21998
22093
  throw new UsageError(`${target} already exists: mu db backup never overwrites`);
21999
22094
  }
22000
- mkdirSync6(dirname13(target), { recursive: true });
22095
+ mkdirSync6(dirname14(target), { recursive: true });
22001
22096
  db.prepare("VACUUM INTO ?").run(target);
22002
22097
  const nextSteps3 = [
22003
22098
  {
@@ -22141,7 +22236,7 @@ function wireDbCommands(program) {
22141
22236
 
22142
22237
  // src/cli/doctor.ts
22143
22238
  init_agents();
22144
- import { dirname as dirname14 } from "path";
22239
+ import { dirname as dirname15 } from "path";
22145
22240
  init_db();
22146
22241
  init_doctor_summary();
22147
22242
 
@@ -22307,7 +22402,7 @@ async function cmdDoctor(db, opts = {}) {
22307
22402
  printCheck("mu skill", skillDoctorCheck());
22308
22403
  printCheck(
22309
22404
  "ctl",
22310
- (await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname14(db.name) })).check
22405
+ (await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname15(db.name) })).check
22311
22406
  );
22312
22407
  console.log(pc.bold("\ndb"));
22313
22408
  console.log(` path : ${pc.dim(defaultDbPath())}`);
@@ -22505,7 +22600,7 @@ function opsSizeHint(db) {
22505
22600
  async function cmdDoctorJson(db, opts = {}) {
22506
22601
  const health = await muxHealth();
22507
22602
  const muExt = ctlExtensionDoctorCheck();
22508
- const ctlSockets = await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname14(db.name) });
22603
+ const ctlSockets = await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname15(db.name) });
22509
22604
  const env = {
22510
22605
  // `mux` is the backend-agnostic key. `tmux` is kept as an alias for
22511
22606
  // back-compat with scripts that grew around the pre-MuxBackend
@@ -23209,7 +23304,6 @@ function wireSqlCommand(program) {
23209
23304
  }
23210
23305
 
23211
23306
  // src/cli/state.ts
23212
- init_agents();
23213
23307
  init_db();
23214
23308
  init_glyphs();
23215
23309
  init_output();
@@ -23224,7 +23318,7 @@ import path from "path";
23224
23318
  // src/project-root.ts
23225
23319
  import { execFile as execFile3 } from "child_process";
23226
23320
  import { realpath } from "fs/promises";
23227
- import { dirname as dirname15, resolve as resolve8 } from "path";
23321
+ import { dirname as dirname16, resolve as resolve8 } from "path";
23228
23322
  import { promisify as promisify3 } from "util";
23229
23323
  var exec2 = promisify3(execFile3);
23230
23324
  async function run2(bin, args, cwd) {
@@ -23246,7 +23340,7 @@ async function gitProjectRoot(workspacePath2) {
23246
23340
  workspacePath2
23247
23341
  );
23248
23342
  if (commonDir.length === 0) return null;
23249
- return realpathOrResolve(dirname15(commonDir));
23343
+ return realpathOrResolve(dirname16(commonDir));
23250
23344
  } catch {
23251
23345
  return null;
23252
23346
  }
@@ -23266,7 +23360,7 @@ async function workspaceProjectRoot(workspacePath2, backend) {
23266
23360
  if (backend === "sl") return slProjectRoot(workspacePath2);
23267
23361
  const gitRoot = await gitProjectRoot(workspacePath2);
23268
23362
  if (gitRoot !== null) return gitRoot;
23269
- return realpathOrResolve(dirname15(workspacePath2));
23363
+ return realpathOrResolve(dirname16(workspacePath2));
23270
23364
  }
23271
23365
 
23272
23366
  // src/cli/tui-launch-focus.ts
@@ -23455,6 +23549,13 @@ async function cmdState(db, opts) {
23455
23549
  renderFullMode(perWs);
23456
23550
  }
23457
23551
  var BIG_WORKSTREAM_TASKS = 300;
23552
+ function scratchIdleNudge(workstreamName, agents) {
23553
+ if (!isScratchWorkstream(workstreamName)) return null;
23554
+ const lingering = agents.filter((a) => a.idle === true);
23555
+ if (lingering.length === 0) return null;
23556
+ const names = lingering.map((a) => a.name).join(", ");
23557
+ return `\u26A0 ${lingering.length} idle scratch agent(s): ${names}. Close when done: mu agent close ${lingering[0]?.name ?? "<name>"} -w scratch`;
23558
+ }
23458
23559
  function renderFullMode(perWs) {
23459
23560
  perWs.forEach((d, i) => {
23460
23561
  if (i > 0) console.log("");
@@ -23475,18 +23576,8 @@ function renderFullCard(d) {
23475
23576
  );
23476
23577
  }
23477
23578
  }
23478
- if (isScratchWorkstream(workstreamName)) {
23479
- const threshold = idleThresholdMs();
23480
- const lingering = view.agents.filter((a) => isLingeringScratchAgent(a.updatedAt, threshold));
23481
- if (lingering.length > 0) {
23482
- const names = lingering.map((a) => a.name).join(", ");
23483
- console.log(
23484
- pc.yellow(
23485
- `\u26A0 ${lingering.length} idle scratch agent(s): ${names}. Close when done: mu agent close ${lingering[0]?.name ?? "<name>"} -w scratch`
23486
- )
23487
- );
23488
- }
23489
- }
23579
+ const nudge = scratchIdleNudge(workstreamName, view.agents);
23580
+ if (nudge !== null) console.log(pc.yellow(nudge));
23490
23581
  const total = d.taskCount;
23491
23582
  const open = ready.length + inProgress.length + blocked.length + d.triage.length;
23492
23583
  if (total >= BIG_WORKSTREAM_TASKS && open * 10 < total) {
@@ -24759,7 +24850,7 @@ async function cmdBareTui(db, program, requestedWorkstreams) {
24759
24850
  }
24760
24851
  function readPackageVersion() {
24761
24852
  try {
24762
- const here = dirname16(fileURLToPath3(import.meta.url));
24853
+ const here = dirname17(fileURLToPath3(import.meta.url));
24763
24854
  const pkgPath = join14(here, "..", "package.json");
24764
24855
  const raw = readFileSync4(pkgPath, "utf8");
24765
24856
  const parsed = JSON.parse(raw);