@mu-crew/mu 3.6.1 → 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, {
@@ -5300,7 +5335,9 @@ async function readAgentStates(agents, opts = {}) {
5300
5335
  source: "ctl",
5301
5336
  since: probe.status.since,
5302
5337
  alive: true,
5303
- ctl: "ok"
5338
+ ctl: "ok",
5339
+ pending: probe.status.pending,
5340
+ runs: probe.status.runs
5304
5341
  });
5305
5342
  } else {
5306
5343
  failed.set(agentKey(agent), probe?.kind === "missing" ? "missing" : "refused");
@@ -5856,6 +5893,15 @@ var init_id = __esm({
5856
5893
  });
5857
5894
 
5858
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
+ }
5859
5905
  var TaskNotFoundError, TaskIdInvalidError, TaskExistsError, TaskNotInWorkstreamError, TaskAlreadyOwnedError, ClaimerNotRegisteredError, TaskClaimStaleWorkspaceError, CycleError, ReaperDetectedDuringWaitError, StallDetectedDuringWaitError, CrossWorkstreamEdgeError, SubstateReasonRequiredError, TaskParkStateError, TaskInTriageError, TaskParkedError, InvalidSubstateError;
5860
5906
  var init_errors2 = __esm({
5861
5907
  "src/tasks/errors.ts"() {
@@ -6078,7 +6124,7 @@ var init_errors2 = __esm({
6078
6124
  }
6079
6125
  };
6080
6126
  StallDetectedDuringWaitError = class extends Error {
6081
- constructor(taskName, owner, workstream, ageSecs, ownerExpectsCtl = false) {
6127
+ constructor(taskName, owner, workstream, ageSecs, ownerExpectsCtl = false, ownerRuns) {
6082
6128
  const ownerBit = owner !== null ? owner : "<unknown>";
6083
6129
  super(
6084
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.`
@@ -6088,12 +6134,14 @@ var init_errors2 = __esm({
6088
6134
  this.workstream = workstream;
6089
6135
  this.ageSecs = ageSecs;
6090
6136
  this.ownerExpectsCtl = ownerExpectsCtl;
6137
+ this.ownerRuns = ownerRuns;
6091
6138
  }
6092
6139
  taskName;
6093
6140
  owner;
6094
6141
  workstream;
6095
6142
  ageSecs;
6096
6143
  ownerExpectsCtl;
6144
+ ownerRuns;
6097
6145
  name = "StallDetectedDuringWaitError";
6098
6146
  /**
6099
6147
  * Diagnose before acting. `mu agent read` comes FIRST because the
@@ -6108,6 +6156,7 @@ var init_errors2 = __esm({
6108
6156
  const ws = this.workstream;
6109
6157
  const ownerBit = this.owner !== null ? this.owner : "<owner>";
6110
6158
  return [
6159
+ ...stallWaitHint(ownerBit, ws, this.ownerRuns),
6111
6160
  {
6112
6161
  intent: "Read the worker's pane to see what it is waiting on",
6113
6162
  command: `mu agent read ${ownerBit} -w ${ws} --lines 60`
@@ -6459,7 +6508,7 @@ var init_edges = __esm({
6459
6508
 
6460
6509
  // src/workstream.ts
6461
6510
  import { existsSync as existsSync9, readdirSync as readdirSync2, rmdirSync } from "fs";
6462
- import { join as join8 } from "path";
6511
+ import { dirname as dirname6, join as join8 } from "path";
6463
6512
  async function listMuxSessions() {
6464
6513
  try {
6465
6514
  return await (await activeMux()).listSessions();
@@ -6613,6 +6662,7 @@ async function teardownWorkstream(db, opts) {
6613
6662
  const notesBefore = countNotes(db, opts.workstream);
6614
6663
  const edgesBefore = countEdges(db, opts.workstream);
6615
6664
  const workspacesBefore = listWorkspaces2(db, opts.workstream);
6665
+ const agentNames = listAgentNames(db, opts.workstream);
6616
6666
  const mux = await activeMux();
6617
6667
  const muxAliveBefore = await mux.sessionExists(muxSession);
6618
6668
  if (muxAliveBefore) {
@@ -6650,6 +6700,14 @@ async function teardownWorkstream(db, opts) {
6650
6700
  } catch {
6651
6701
  }
6652
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
+ }
6653
6711
  withOpContext(
6654
6712
  db,
6655
6713
  { intent: "workstream.teardown", group: "new" },
@@ -6666,6 +6724,14 @@ async function teardownWorkstream(db, opts) {
6666
6724
  failedWorkspaces
6667
6725
  };
6668
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
+ }
6669
6735
  function countAgents(db, workstream) {
6670
6736
  const row2 = db.prepare(
6671
6737
  `SELECT COUNT(*) AS n FROM agents a
@@ -6706,6 +6772,7 @@ var WORKSTREAM_NAME_RE, RESERVED_WORKSTREAM_PREFIX, RESERVED_WORKSTREAM_NAMES, W
6706
6772
  var init_workstream = __esm({
6707
6773
  "src/workstream.ts"() {
6708
6774
  "use strict";
6775
+ init_path();
6709
6776
  init_db();
6710
6777
  init_legacy_ops();
6711
6778
  init_mux2();
@@ -7347,6 +7414,7 @@ async function waitForTasks(db, input, opts) {
7347
7414
  const timeoutMs = opts.timeoutMs ?? 6e5;
7348
7415
  const pollMs = opts.pollMs ?? 1e3;
7349
7416
  const stuckAfterMs = opts.stuckAfterMs ?? DEFAULT_STUCK_AFTER_MS;
7417
+ const ctlStuckAfterMs = opts.ctlStuckAfterMs ?? DEFAULT_CTL_STUCK_AFTER_MS;
7350
7418
  const onStall = opts.onStall ?? "warn";
7351
7419
  const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : Number.POSITIVE_INFINITY;
7352
7420
  for (const ref of refs) {
@@ -7358,12 +7426,15 @@ async function waitForTasks(db, input, opts) {
7358
7426
  const ctlBrokenSince = /* @__PURE__ */ new Map();
7359
7427
  const stuckLabel = /* @__PURE__ */ new Map();
7360
7428
  const ctlOwners = /* @__PURE__ */ new Set();
7429
+ const ownerRuns = /* @__PURE__ */ new Map();
7361
7430
  const stuckAgeMs = async (status, owner, workstreamName) => {
7362
7431
  if (stuckAfterMs <= 0 || status !== "IN_PROGRESS" || owner === null || opts.readOwnerState === void 0)
7363
7432
  return null;
7364
7433
  const reading = await opts.readOwnerState({ name: owner, workstreamName });
7365
7434
  const ownerKey = `${workstreamName}/${owner}`;
7366
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);
7367
7438
  if (reading?.alive === true && (reading.ctl === "missing" || reading.ctl === "refused")) {
7368
7439
  const firstSeen = ctlBrokenSince.get(ownerKey) ?? Date.now();
7369
7440
  ctlBrokenSince.set(ownerKey, firstSeen);
@@ -7374,8 +7445,10 @@ async function waitForTasks(db, input, opts) {
7374
7445
  ctlBrokenSince.delete(ownerKey);
7375
7446
  stuckLabel.delete(ownerKey);
7376
7447
  if (reading?.state !== "needs_input" || reading.since === null) return null;
7448
+ const viaCtl = reading.source === "ctl" && reading.ctl === "ok";
7449
+ if (viaCtl && reading.pending === true) return null;
7377
7450
  const ageMs2 = Date.now() - reading.since;
7378
- return ageMs2 >= stuckAfterMs ? ageMs2 : null;
7451
+ return ageMs2 >= (viaCtl ? ctlStuckAfterMs : stuckAfterMs) ? ageMs2 : null;
7379
7452
  };
7380
7453
  const snapshot2 = async () => {
7381
7454
  const refStates = [];
@@ -7391,10 +7464,13 @@ async function waitForTasks(db, input, opts) {
7391
7464
  stuckWarned.add(key);
7392
7465
  const ageSecs = Math.round(ageMs2 / 1e3);
7393
7466
  const ownerBit = owner ?? "<none>";
7467
+ const runs = ownerRuns.get(`${ref.workstreamName}/${ownerBit}`);
7394
7468
  const stateBit = stuckLabel.get(`${ref.workstreamName}/${ownerBit}`) ?? "needs_input";
7469
+ const waitLine = stallWaitHint(ownerBit, ref.workstreamName, runs).map((s) => ` ${s.command}
7470
+ `).join("");
7395
7471
  currentStuckWarn(
7396
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
7397
- mu agent read ${ownerBit} -w ${ref.workstreamName} --lines 60
7473
+ ` + waitLine + ` mu agent read ${ownerBit} -w ${ref.workstreamName} --lines 60
7398
7474
  `
7399
7475
  );
7400
7476
  emitEvent(
@@ -7410,7 +7486,8 @@ async function waitForTasks(db, input, opts) {
7410
7486
  owner,
7411
7487
  ref.workstreamName,
7412
7488
  ageSecs,
7413
- ctlOwners.has(`${ref.workstreamName}/${owner ?? ""}`)
7489
+ ctlOwners.has(`${ref.workstreamName}/${owner ?? ""}`),
7490
+ runs
7414
7491
  );
7415
7492
  }
7416
7493
  }
@@ -7451,7 +7528,7 @@ async function waitForTasks(db, input, opts) {
7451
7528
  if (isDone(snap)) return snap;
7452
7529
  }
7453
7530
  }
7454
- var currentWaitSleep, pollCount, DEFAULT_STUCK_AFTER_MS, defaultStuckWarn, currentStuckWarn;
7531
+ var currentWaitSleep, pollCount, DEFAULT_STUCK_AFTER_MS, DEFAULT_CTL_STUCK_AFTER_MS, defaultStuckWarn, currentStuckWarn;
7455
7532
  var init_wait = __esm({
7456
7533
  "src/tasks/wait.ts"() {
7457
7534
  "use strict";
@@ -7462,6 +7539,7 @@ var init_wait = __esm({
7462
7539
  currentWaitSleep = (ms) => new Promise((resolve9) => setTimeout(resolve9, ms));
7463
7540
  pollCount = 0;
7464
7541
  DEFAULT_STUCK_AFTER_MS = 12e4;
7542
+ DEFAULT_CTL_STUCK_AFTER_MS = 5e3;
7465
7543
  defaultStuckWarn = (msg) => {
7466
7544
  process.stderr.write(msg);
7467
7545
  };
@@ -7925,8 +8003,7 @@ var init_wait2 = __esm({
7925
8003
  });
7926
8004
 
7927
8005
  // src/agents.ts
7928
- import { rmSync as rmSync4 } from "fs";
7929
- import { dirname as dirname6 } from "path";
8006
+ import { dirname as dirname7 } from "path";
7930
8007
  function idleThresholdMs() {
7931
8008
  const env = process.env.MU_IDLE_THRESHOLD_MS;
7932
8009
  if (env === void 0 || env === "") return DEFAULT_IDLE_THRESHOLD_MS;
@@ -8045,13 +8122,6 @@ function deleteAgent(db, name, workstream) {
8045
8122
  if (deleted) unlinkCtlSocket(db, name, workstream);
8046
8123
  return deleted;
8047
8124
  }
8048
- function unlinkCtlSocket(db, name, workstream) {
8049
- if (db.memory) return;
8050
- try {
8051
- rmSync4(ctlSocketPath(workstream, name, dirname6(db.name)), { force: true });
8052
- } catch {
8053
- }
8054
- }
8055
8125
  function deleteAgentRow(db, name, workstream) {
8056
8126
  return withOpContext(
8057
8127
  db,
@@ -8146,7 +8216,7 @@ async function listLiveAgents(db, opts) {
8146
8216
  ...opts.mode !== void 0 ? { mode: opts.mode } : {}
8147
8217
  });
8148
8218
  const baseAgents = listAgents(db, { workstream: opts.workstream });
8149
- const readings = await readAgentStates(baseAgents, { stateDir: dirname6(db.name) });
8219
+ const readings = await readAgentStates(baseAgents, { stateDir: dirname7(db.name) });
8150
8220
  const now = Date.now();
8151
8221
  const agents = baseAgents.map((agent) => {
8152
8222
  const reading = readings.get(agentKey(agent)) ?? {
@@ -8703,7 +8773,7 @@ var init_apply = __esm({
8703
8773
 
8704
8774
  // src/rebuild.ts
8705
8775
  import { existsSync as existsSync10, mkdirSync as mkdirSync3, statSync } from "fs";
8706
- import { dirname as dirname8 } from "path";
8776
+ import { dirname as dirname9 } from "path";
8707
8777
  function isProjectable(entity, intent) {
8708
8778
  if (isLegacyLogOnlyIntent(intent)) return false;
8709
8779
  if (LOG_ONLY_ENTITIES.has(entity)) return false;
@@ -8734,7 +8804,7 @@ function rebuildInto(source, opts) {
8734
8804
  if (existsSync10(targetPath) && opts.force !== true) {
8735
8805
  throw new RebuildTargetExistsError(targetPath);
8736
8806
  }
8737
- mkdirSync3(dirname8(targetPath), { recursive: true });
8807
+ mkdirSync3(dirname9(targetPath), { recursive: true });
8738
8808
  const identity = source.prepare(
8739
8809
  "SELECT machine_id, hostname, created_at, last_wall, last_counter FROM machine_identity WHERE id = 1"
8740
8810
  ).get();
@@ -9316,13 +9386,13 @@ import {
9316
9386
  writeFileSync
9317
9387
  } from "fs";
9318
9388
  import { homedir as homedir4 } from "os";
9319
- 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";
9320
9390
  import { fileURLToPath, pathToFileURL } from "url";
9321
9391
  function resolveHome(opts) {
9322
9392
  return opts?.home ?? process.env.MU_PI_HOME ?? homedir4();
9323
9393
  }
9324
9394
  function packageRoot() {
9325
- return resolve5(dirname9(fileURLToPath(import.meta.url)), "..");
9395
+ return resolve5(dirname10(fileURLToPath(import.meta.url)), "..");
9326
9396
  }
9327
9397
  function extensionEntry() {
9328
9398
  return process.env.MU_EXTENSION_ENTRY ?? join10(packageRoot(), "dist", "extension", "mu-pi.js");
@@ -9358,7 +9428,7 @@ function linkPi(opts) {
9358
9428
  replacedCopy = !readFileSync(path2, "utf8").includes(MU_SHIM_MARKER);
9359
9429
  } catch {
9360
9430
  }
9361
- mkdirSync4(dirname9(path2), { recursive: true });
9431
+ mkdirSync4(dirname10(path2), { recursive: true });
9362
9432
  writeFileSync(path2, opts?.copy === true ? readFileSync(entry, "utf8") : shim(entry));
9363
9433
  return { path: path2, replacedCopy };
9364
9434
  }
@@ -9369,7 +9439,7 @@ function linkSkill(opts) {
9369
9439
  const st = lstatOrUndefined(path2);
9370
9440
  if (st) {
9371
9441
  if (!st.isSymbolicLink()) throw new LinkConflictError(path2, "not-a-symlink");
9372
- const current = resolve5(dirname9(path2), readlinkSync(path2));
9442
+ const current = resolve5(dirname10(path2), readlinkSync(path2));
9373
9443
  if (current === target) return { path: path2, target };
9374
9444
  if (existsSync12(current) && opts?.force !== true) {
9375
9445
  throw new LinkConflictError(path2, "foreign-symlink", current);
@@ -9377,7 +9447,7 @@ function linkSkill(opts) {
9377
9447
  previous = current;
9378
9448
  rmSync6(path2);
9379
9449
  }
9380
- mkdirSync4(dirname9(path2), { recursive: true });
9450
+ mkdirSync4(dirname10(path2), { recursive: true });
9381
9451
  symlinkSync(target, path2, "dir");
9382
9452
  return previous === void 0 ? { path: path2, target } : { path: path2, target, previous };
9383
9453
  }
@@ -9411,7 +9481,7 @@ function inspectSkill(path2) {
9411
9481
  try {
9412
9482
  target = realpathSync(path2);
9413
9483
  } catch {
9414
- return { path: path2, state: "dangling", target: resolve5(dirname9(path2), readlinkSync(path2)) };
9484
+ return { path: path2, state: "dangling", target: resolve5(dirname10(path2), readlinkSync(path2)) };
9415
9485
  }
9416
9486
  let skillMd;
9417
9487
  try {
@@ -9462,7 +9532,7 @@ var init_link = __esm({
9462
9532
 
9463
9533
  // src/doctor-summary.ts
9464
9534
  import { accessSync as accessSync2, constants as constants2, existsSync as existsSync13, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
9465
- 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";
9466
9536
  import { fileURLToPath as fileURLToPath2 } from "url";
9467
9537
  function loadDoctorSummary(db, snapshot2, agentStateSource = "murmur") {
9468
9538
  const checks = [];
@@ -9623,7 +9693,7 @@ function resolveExecutable(name) {
9623
9693
  return null;
9624
9694
  }
9625
9695
  function murmurVersion(executable) {
9626
- let dir = dirname10(executable);
9696
+ let dir = dirname11(executable);
9627
9697
  const root = parse(dir).root;
9628
9698
  while (dir !== root) {
9629
9699
  const packagePath = join11(dir, "package.json");
@@ -9636,7 +9706,7 @@ function murmurVersion(executable) {
9636
9706
  } catch {
9637
9707
  }
9638
9708
  }
9639
- dir = dirname10(dir);
9709
+ dir = dirname11(dir);
9640
9710
  }
9641
9711
  return null;
9642
9712
  }
@@ -9671,7 +9741,7 @@ function murmurDoctorCheck(agentStateSource, opts = {}) {
9671
9741
  return {
9672
9742
  name: "murmur",
9673
9743
  status: "warn",
9674
- 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`
9675
9745
  };
9676
9746
  }
9677
9747
  return {
@@ -9725,7 +9795,7 @@ function skillDoctorCheck(opts) {
9725
9795
  }
9726
9796
  function installedMuVersion() {
9727
9797
  try {
9728
- const pkgPath = join11(dirname10(fileURLToPath2(import.meta.url)), "..", "package.json");
9798
+ const pkgPath = join11(dirname11(fileURLToPath2(import.meta.url)), "..", "package.json");
9729
9799
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
9730
9800
  const v = typeof pkg === "object" && pkg !== null ? pkg.version : null;
9731
9801
  return typeof v === "string" ? v : null;
@@ -9769,7 +9839,10 @@ async function ctlSocketsDoctorCheck(agents, opts = {}) {
9769
9839
  socket,
9770
9840
  probe: probe.kind,
9771
9841
  ...probe.kind === "ok" && probe.extVersion !== void 0 ? { extVersion: probe.extVersion } : {},
9772
- ...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
+ } : {},
9773
9846
  ...missingOps.length > 0 ? { missingOps } : {}
9774
9847
  };
9775
9848
  })
@@ -9855,11 +9928,14 @@ function remediationParagraph(check) {
9855
9928
  return [
9856
9929
  "mu talks to pi agents over a per-agent control socket served by",
9857
9930
  "the mu pi extension. `missing`: no socket (extension not loaded,",
9858
- "or the agent predates `mu link pi`). `refused`: socket present but",
9859
- "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.",
9860
9935
  "Run `mu link pi`, then respawn the agent. `extension X older than",
9861
- "installed Y`: the agent's pi loaded an older mu build; type /reload",
9862
- "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."
9863
9939
  ];
9864
9940
  case "agents":
9865
9941
  return [
@@ -18603,14 +18679,14 @@ var init_tui = __esm({
18603
18679
  // src/cli.ts
18604
18680
  init_agents();
18605
18681
  import { readFileSync as readFileSync4, realpathSync as realpathSync4 } from "fs";
18606
- import { dirname as dirname16, join as join14 } from "path";
18682
+ import { dirname as dirname17, join as join14 } from "path";
18607
18683
  import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL2 } from "url";
18608
18684
  import { Command, InvalidArgumentError } from "commander";
18609
18685
 
18610
18686
  // src/cli/agents.ts
18611
18687
  init_agent_state();
18612
18688
  init_agents();
18613
- import { dirname as dirname12 } from "path";
18689
+ import { dirname as dirname13 } from "path";
18614
18690
  init_client();
18615
18691
  init_mux2();
18616
18692
  init_output();
@@ -18710,7 +18786,7 @@ function formatStaleWorkspaceWarning(staleness) {
18710
18786
 
18711
18787
  // src/cli/agents-remote.ts
18712
18788
  init_agents();
18713
- import { dirname as dirname7 } from "path";
18789
+ import { dirname as dirname8 } from "path";
18714
18790
  init_path();
18715
18791
  init_output();
18716
18792
  init_workstream();
@@ -18726,7 +18802,7 @@ function buildRemoteEnv(db, agent, workstream, remoteSock) {
18726
18802
  if (!isValidWorkstreamName(workstream)) {
18727
18803
  throw new UsageError(`invalid workstream name ${JSON.stringify(workstream)}`);
18728
18804
  }
18729
- const localSock = ctlSocketPath(workstream, agent, dirname7(db.name));
18805
+ const localSock = ctlSocketPath(workstream, agent, dirname8(db.name));
18730
18806
  const remote = remoteSock ?? remoteCtlSocketPath(workstream, agent, process.getuid?.() ?? 0);
18731
18807
  if (!remote.startsWith("/") || !SAFE_WORD.test(remote)) {
18732
18808
  throw new UsageError(
@@ -19205,12 +19281,13 @@ init_tasks();
19205
19281
  init_agent_state();
19206
19282
  init_errors();
19207
19283
  init_agents();
19208
- import { dirname as dirname11 } from "path";
19284
+ import { dirname as dirname12 } from "path";
19209
19285
  init_db();
19210
19286
  init_glyphs();
19211
19287
  init_output();
19212
19288
  init_reconcile();
19213
19289
  init_state();
19290
+ init_errors2();
19214
19291
  init_tasks();
19215
19292
  init_vcs2();
19216
19293
  init_workspace();
@@ -19419,6 +19496,7 @@ async function cmdTaskWait(db, ids, opts) {
19419
19496
  const timeoutMs = opts.timeout !== void 0 ? opts.timeout * 1e3 : 6e5;
19420
19497
  const stuckAfterMs = opts.stuckAfter !== void 0 ? opts.stuckAfter * 1e3 : DEFAULT_STUCK_AFTER_MS;
19421
19498
  const sdkOpts = { timeoutMs, stuckAfterMs };
19499
+ if (opts.stuckAfter !== void 0) sdkOpts.ctlStuckAfterMs = stuckAfterMs;
19422
19500
  if (statusOpt !== void 0) sdkOpts.status = statusOpt;
19423
19501
  if (wantAny) sdkOpts.any = true;
19424
19502
  let ownerReadings = /* @__PURE__ */ new Map();
@@ -19440,7 +19518,7 @@ async function cmdTaskWait(db, ids, opts) {
19440
19518
  const agent = getAgent(db, owner, ref.workstreamName);
19441
19519
  return agent === void 0 ? [] : [agent];
19442
19520
  });
19443
- ownerReadings = await readAgentStates(owners, { stateDir: dirname11(db.name) });
19521
+ ownerReadings = await readAgentStates(owners, { stateDir: dirname12(db.name) });
19444
19522
  if (!reaperExitEnabled) return;
19445
19523
  for (const ref of refs) {
19446
19524
  const key = qualifiedId(ref);
@@ -19492,6 +19570,10 @@ async function cmdTaskWait(db, ids, opts) {
19492
19570
  }
19493
19571
  for (const t of unmetRefs) {
19494
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));
19495
19577
  nextSteps3.push({
19496
19578
  intent: `Read ${t.owner}'s pane \u2014 ${qualifiedId(t)} is IN_PROGRESS but its owner needs attention`,
19497
19579
  command: `mu agent read ${t.owner} -w ${t.workstreamName} --lines 60`
@@ -20184,7 +20266,7 @@ function wireTaskCommands(program) {
20184
20266
  "alias for --any that ALSO prints the firing ref's qualified id to stdout and populates `firing` in --json (which --any leaves null). Use to drive a single-shot dispatch loop: `closed=$(mu task wait a b --first --json | jq -r .firing.qualifiedId)`."
20185
20267
  ).option("--timeout <seconds>", "max seconds to wait (0 = forever, default 600)", parseLines).option(
20186
20268
  "--stuck-after <seconds>",
20187
- "the TRIGGER: mark an IN_PROGRESS task as needing attention when its owner has been in needs_input for >= N seconds since their last status change (0 = disable, default 120). needs_input has several causes \u2014 the worker may have finished without closing, be waiting on an answer, or be sitting at a prompt \u2014 so the warning names the observation and points at `mu agent read <owner>`, which is the next move in every case. The default ACTION is `exit` (exit 7); see --on-stall warn to keep polling instead.",
20269
+ "the TRIGGER: mark an IN_PROGRESS task as needing attention when its owner has been in needs_input for >= N seconds since their last status change (0 = disable; default 120, or 5 for a pi owner read over its control socket, whose idle is exact and comes after the close nudge; an explicit value applies to every owner). needs_input has several causes \u2014 the worker may have finished without closing, be waiting on an answer, or be sitting at a prompt \u2014 so the warning names the observation and points at `mu agent read <owner>`, which is the next move in every case. The default ACTION is `exit` (exit 7); see --on-stall warn to keep polling instead.",
20188
20270
  parseLines
20189
20271
  ).option(
20190
20272
  "--on-stall <action>",
@@ -20299,8 +20381,6 @@ async function cmdSend(db, rawName, text2, opts = {}) {
20299
20381
  });
20300
20382
  let undelivered;
20301
20383
  const agentRow = getAgent(db, name, ws);
20302
- const plainCtl = agentRow !== void 0 && !opts.fresh && !opts.steer && parseSessionCommand(text2) === void 0 && (via ?? chooseTransport(agentRow, agentCtlSocket(db, agentRow))) === "ctl";
20303
- const before = plainCtl && agentRow !== void 0 ? await ctlProbe(agentCtlSocket(db, agentRow)) : void 0;
20304
20384
  const sent = await sendToAgent(db, name, text2, {
20305
20385
  workstream: ws,
20306
20386
  ...opts.steer ? { mode: "steer" } : {},
@@ -20312,11 +20392,15 @@ async function cmdSend(db, rawName, text2, opts = {}) {
20312
20392
  }
20313
20393
  });
20314
20394
  const nextSteps3 = [
20315
- { 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}` },
20316
20399
  { intent: "Watch live events", command: `mu log -w ${ws} --tail` }
20317
20400
  ];
20318
- if (agentRow !== void 0 && before?.kind === "ok") {
20319
- 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 }));
20320
20404
  }
20321
20405
  if (stalenessCheck.warned && stalenessCheck.nextStep !== null) {
20322
20406
  nextSteps3.push(stalenessCheck.nextStep);
@@ -20335,6 +20419,7 @@ async function cmdSend(db, rawName, text2, opts = {}) {
20335
20419
  fresh: opts.fresh === true,
20336
20420
  ...sent.command !== void 0 ? { command: sent.command } : {},
20337
20421
  ...sent.state !== void 0 ? { state: sent.state } : {},
20422
+ ...sent.runs !== void 0 ? { runs: sent.runs } : {},
20338
20423
  delivered: undelivered === void 0,
20339
20424
  ...undelivered !== void 0 ? { undelivered: { reason: undelivered.reason, message: undelivered.message } } : {},
20340
20425
  staleness: stalenessCheck.staleness,
@@ -20345,7 +20430,7 @@ async function cmdSend(db, rawName, text2, opts = {}) {
20345
20430
  if (undelivered !== void 0) {
20346
20431
  console.error(pc.yellow(`warning: ${undelivered.message}`));
20347
20432
  } else {
20348
- 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 ?? "?"}`;
20349
20434
  console.log(pc.dim(`sent ${text2.length} bytes to ${name} (${how})`));
20350
20435
  }
20351
20436
  printNextSteps(nextSteps3);
@@ -20408,7 +20493,7 @@ async function cmdAgentShow(db, rawName, opts) {
20408
20493
  } catch {
20409
20494
  scrollback = "";
20410
20495
  }
20411
- const reading = (await readAgentStates([agent], { stateDir: dirname12(db.name) })).get(
20496
+ const reading = (await readAgentStates([agent], { stateDir: dirname13(db.name) })).get(
20412
20497
  agentKey(agent)
20413
20498
  );
20414
20499
  const displayed = {
@@ -20443,7 +20528,7 @@ async function cmdAgentShow(db, rawName, opts) {
20443
20528
  }
20444
20529
  async function cmdMe(db, opts = {}) {
20445
20530
  const self = resolveSelf(db);
20446
- const reading = (await readAgentStates([self], { stateDir: dirname12(db.name) })).get(
20531
+ const reading = (await readAgentStates([self], { stateDir: dirname13(db.name) })).get(
20447
20532
  agentKey(self)
20448
20533
  );
20449
20534
  const displayed = {
@@ -20636,7 +20721,7 @@ async function cmdAbort(db, rawName, opts = {}) {
20636
20721
  intent: "Steer it with the next instruction",
20637
20722
  command: `mu agent send ${name} '...' -w ${ws}`
20638
20723
  },
20639
- { 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}` }]
20640
20725
  ];
20641
20726
  if (opts.json) {
20642
20727
  emitJson({ ...result, nextSteps: nextSteps3 });
@@ -20661,12 +20746,22 @@ async function cmdAgentWait(db, rawNames, opts) {
20661
20746
  return { name, workstreamName };
20662
20747
  })
20663
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
+ }
20664
20759
  const readStatuses = async (pending2) => {
20665
20760
  const agents = pending2.flatMap((ref) => {
20666
20761
  const agent = getAgent(db, ref.name, ref.workstreamName);
20667
20762
  return agent === void 0 ? [] : [agent];
20668
20763
  });
20669
- const readings = await readAgentStates(agents, { stateDir: dirname12(db.name) });
20764
+ const readings = await readAgentStates(agents, { stateDir: dirname13(db.name) });
20670
20765
  const snapshots = /* @__PURE__ */ new Map();
20671
20766
  for (const ref of pending2) {
20672
20767
  const agent = getAgent(db, ref.name, ref.workstreamName);
@@ -20683,9 +20778,15 @@ async function cmdAgentWait(db, rawNames, opts) {
20683
20778
  if (agent === void 0) return void 0;
20684
20779
  const sock = agentCtlSocket(db, agent);
20685
20780
  if (!expectsCtl(agent, sock)) return void 0;
20686
- const probe = await ctlProbe(sock);
20687
- if (probe.kind !== "ok") return void 0;
20688
- 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(
20689
20790
  (reply) => reply.ok && reply.state !== void 0 ? {
20690
20791
  status: ctlRuntimeState(reply.state),
20691
20792
  ...reply.lastText !== void 0 ? { lastText: reply.lastText } : {},
@@ -20700,7 +20801,7 @@ async function cmdAgentWait(db, rawNames, opts) {
20700
20801
  return again.kind === "ok" ? { status: "unknown", unknownReason: "ctl wait interrupted" } : { status: null };
20701
20802
  }
20702
20803
  );
20703
- return { initial: { status: ctlRuntimeState(probe.status.state) }, settled };
20804
+ return { initial, settled };
20704
20805
  };
20705
20806
  const timeoutMs = (opts.timeout ?? 600) * 1e3;
20706
20807
  const result = await waitForAgents(db, refs, {
@@ -20723,6 +20824,12 @@ async function cmdAgentWait(db, rawNames, opts) {
20723
20824
  const fired = result.agents.filter((a) => a.fired);
20724
20825
  const firing = wantFirstShape && fired.length > 0 ? fired[0] : null;
20725
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
+ ] : [];
20726
20833
  if (opts.json) {
20727
20834
  emitJson({
20728
20835
  agents: result.agents.map((a) => ({
@@ -20732,12 +20839,7 @@ async function cmdAgentWait(db, rawNames, opts) {
20732
20839
  timedOut: result.timedOut,
20733
20840
  ...firing ? { firing: { workstream: firing.workstreamName, name: firing.name } } : {},
20734
20841
  ...dead.length > 0 ? { dead: dead.map(qualified) } : {},
20735
- nextSteps: firing ? [
20736
- {
20737
- intent: "Read the finished agent",
20738
- command: `mu agent read ${firing.name} -w ${firing.workstreamName}`
20739
- }
20740
- ] : []
20842
+ nextSteps: readHint
20741
20843
  });
20742
20844
  } else if (result.timedOut) {
20743
20845
  console.log(
@@ -20749,12 +20851,7 @@ async function cmdAgentWait(db, rawNames, opts) {
20749
20851
  console.log(pc.red(`Agent pane(s) died: ${dead.map(qualified).join(", ")}`));
20750
20852
  } else if (firing) {
20751
20853
  console.log(`${pc.bold(qualified(firing))} finished`);
20752
- printNextSteps([
20753
- {
20754
- intent: "Read the finished agent",
20755
- command: `mu agent read ${firing.name} -w ${firing.workstreamName}`
20756
- }
20757
- ]);
20854
+ printNextSteps(readHint);
20758
20855
  } else {
20759
20856
  console.log(`All ${result.agents.length} agent(s) finished`);
20760
20857
  }
@@ -20849,7 +20946,11 @@ function wireAgentCommands(program) {
20849
20946
  "--timeout <seconds>",
20850
20947
  "max seconds to wait (0 = forever, default 600)",
20851
20948
  parseNonNegativeInt
20852
- ).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) {
20853
20954
  const opts = this.optsWithGlobals();
20854
20955
  return handle(
20855
20956
  (db) => cmdAgentWait(db, names, {
@@ -20899,7 +21000,7 @@ function wireSelfCommands(program) {
20899
21000
 
20900
21001
  // src/cli/db.ts
20901
21002
  import { existsSync as existsSync16, mkdirSync as mkdirSync6, statSync as statSync5 } from "fs";
20902
- import { dirname as dirname13, resolve as resolve7 } from "path";
21003
+ import { dirname as dirname14, resolve as resolve7 } from "path";
20903
21004
 
20904
21005
  // src/compact.ts
20905
21006
  init_legacy_ops();
@@ -21991,7 +22092,7 @@ function cmdDbBackup(db, file, opts = {}) {
21991
22092
  if (existsSync16(target)) {
21992
22093
  throw new UsageError(`${target} already exists: mu db backup never overwrites`);
21993
22094
  }
21994
- mkdirSync6(dirname13(target), { recursive: true });
22095
+ mkdirSync6(dirname14(target), { recursive: true });
21995
22096
  db.prepare("VACUUM INTO ?").run(target);
21996
22097
  const nextSteps3 = [
21997
22098
  {
@@ -22135,7 +22236,7 @@ function wireDbCommands(program) {
22135
22236
 
22136
22237
  // src/cli/doctor.ts
22137
22238
  init_agents();
22138
- import { dirname as dirname14 } from "path";
22239
+ import { dirname as dirname15 } from "path";
22139
22240
  init_db();
22140
22241
  init_doctor_summary();
22141
22242
 
@@ -22301,7 +22402,7 @@ async function cmdDoctor(db, opts = {}) {
22301
22402
  printCheck("mu skill", skillDoctorCheck());
22302
22403
  printCheck(
22303
22404
  "ctl",
22304
- (await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname14(db.name) })).check
22405
+ (await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname15(db.name) })).check
22305
22406
  );
22306
22407
  console.log(pc.bold("\ndb"));
22307
22408
  console.log(` path : ${pc.dim(defaultDbPath())}`);
@@ -22499,7 +22600,7 @@ function opsSizeHint(db) {
22499
22600
  async function cmdDoctorJson(db, opts = {}) {
22500
22601
  const health = await muxHealth();
22501
22602
  const muExt = ctlExtensionDoctorCheck();
22502
- const ctlSockets = await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname14(db.name) });
22603
+ const ctlSockets = await ctlSocketsDoctorCheck(listAgents(db), { stateDir: dirname15(db.name) });
22503
22604
  const env = {
22504
22605
  // `mux` is the backend-agnostic key. `tmux` is kept as an alias for
22505
22606
  // back-compat with scripts that grew around the pre-MuxBackend
@@ -23203,7 +23304,6 @@ function wireSqlCommand(program) {
23203
23304
  }
23204
23305
 
23205
23306
  // src/cli/state.ts
23206
- init_agents();
23207
23307
  init_db();
23208
23308
  init_glyphs();
23209
23309
  init_output();
@@ -23218,7 +23318,7 @@ import path from "path";
23218
23318
  // src/project-root.ts
23219
23319
  import { execFile as execFile3 } from "child_process";
23220
23320
  import { realpath } from "fs/promises";
23221
- import { dirname as dirname15, resolve as resolve8 } from "path";
23321
+ import { dirname as dirname16, resolve as resolve8 } from "path";
23222
23322
  import { promisify as promisify3 } from "util";
23223
23323
  var exec2 = promisify3(execFile3);
23224
23324
  async function run2(bin, args, cwd) {
@@ -23240,7 +23340,7 @@ async function gitProjectRoot(workspacePath2) {
23240
23340
  workspacePath2
23241
23341
  );
23242
23342
  if (commonDir.length === 0) return null;
23243
- return realpathOrResolve(dirname15(commonDir));
23343
+ return realpathOrResolve(dirname16(commonDir));
23244
23344
  } catch {
23245
23345
  return null;
23246
23346
  }
@@ -23260,7 +23360,7 @@ async function workspaceProjectRoot(workspacePath2, backend) {
23260
23360
  if (backend === "sl") return slProjectRoot(workspacePath2);
23261
23361
  const gitRoot = await gitProjectRoot(workspacePath2);
23262
23362
  if (gitRoot !== null) return gitRoot;
23263
- return realpathOrResolve(dirname15(workspacePath2));
23363
+ return realpathOrResolve(dirname16(workspacePath2));
23264
23364
  }
23265
23365
 
23266
23366
  // src/cli/tui-launch-focus.ts
@@ -23449,6 +23549,13 @@ async function cmdState(db, opts) {
23449
23549
  renderFullMode(perWs);
23450
23550
  }
23451
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
+ }
23452
23559
  function renderFullMode(perWs) {
23453
23560
  perWs.forEach((d, i) => {
23454
23561
  if (i > 0) console.log("");
@@ -23469,18 +23576,8 @@ function renderFullCard(d) {
23469
23576
  );
23470
23577
  }
23471
23578
  }
23472
- if (isScratchWorkstream(workstreamName)) {
23473
- const threshold = idleThresholdMs();
23474
- const lingering = view.agents.filter((a) => isLingeringScratchAgent(a.updatedAt, threshold));
23475
- if (lingering.length > 0) {
23476
- const names = lingering.map((a) => a.name).join(", ");
23477
- console.log(
23478
- pc.yellow(
23479
- `\u26A0 ${lingering.length} idle scratch agent(s): ${names}. Close when done: mu agent close ${lingering[0]?.name ?? "<name>"} -w scratch`
23480
- )
23481
- );
23482
- }
23483
- }
23579
+ const nudge = scratchIdleNudge(workstreamName, view.agents);
23580
+ if (nudge !== null) console.log(pc.yellow(nudge));
23484
23581
  const total = d.taskCount;
23485
23582
  const open = ready.length + inProgress.length + blocked.length + d.triage.length;
23486
23583
  if (total >= BIG_WORKSTREAM_TASKS && open * 10 < total) {
@@ -24753,7 +24850,7 @@ async function cmdBareTui(db, program, requestedWorkstreams) {
24753
24850
  }
24754
24851
  function readPackageVersion() {
24755
24852
  try {
24756
- const here = dirname16(fileURLToPath3(import.meta.url));
24853
+ const here = dirname17(fileURLToPath3(import.meta.url));
24757
24854
  const pkgPath = join14(here, "..", "package.json");
24758
24855
  const raw = readFileSync4(pkgPath, "utf8");
24759
24856
  const parsed = JSON.parse(raw);