@mu-crew/mu 3.6.2 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cli.js +12652 -12264
  2. package/dist/cli.js.map +1 -1
  3. package/dist/extension/mu-pi.js +302 -48
  4. package/dist/extension/mu-pi.js.map +1 -1
  5. package/dist/index.d.ts +59 -13
  6. package/dist/index.js +1376 -1205
  7. package/dist/index.js.map +1 -1
  8. package/docs/ARCHITECTURE.md +4 -3
  9. package/docs/architecture/control-socket.md +10 -5
  10. package/docs/architecture/mux.md +4 -1
  11. package/docs/guide/delegate.md +25 -3
  12. package/docs/guide/dispatch.md +22 -5
  13. package/docs/guide/getting-started.md +2 -2
  14. package/docs/guide/recovery.md +11 -6
  15. package/docs/guide/upgrade.md +4 -4
  16. package/docs/reference/env.md +2 -2
  17. package/docs/specs/2026-10-03-keep-driving-nudge.md +21 -0
  18. package/package.json +1 -1
  19. package/skills/mu/SKILL.md +40 -16
  20. package/skills/mu/recipes/adversarial-review.md +14 -6
  21. package/skills/mu/recipes/brief.md +29 -1
  22. package/skills/mu/recipes/codemode-driver.md +19 -9
  23. package/skills/mu/recipes/deep-research.md +6 -5
  24. package/skills/mu/recipes/drift-audit.md +72 -0
  25. package/skills/mu/recipes/findings.md +24 -15
  26. package/skills/mu/recipes/hypothesis-panel.md +5 -3
  27. package/skills/mu/recipes/long-run.md +13 -3
  28. package/skills/mu/recipes/loop-until-done.md +4 -3
  29. package/skills/mu/recipes/orchestrator-loop.md +29 -16
  30. package/skills/mu/recipes/plan.md +10 -6
  31. package/skills/mu/recipes/refute.md +3 -2
  32. package/skills/mu/recipes/remote-workers.md +23 -6
  33. package/skills/mu/recipes/review-panel.md +2 -2
  34. package/skills/mu/recipes/rules-audit.md +10 -5
  35. package/skills/mu/recipes/tasks-or-calls.md +40 -23
  36. package/skills/mu/recipes/tournament.md +4 -3
  37. package/skills/mu/recipes/ultrathink.md +2 -2
  38. package/skills/mu/recipes/worker.md +3 -2
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { execa as execa3 } from "execa";
12
12
 
13
13
  // src/agents/transport.ts
14
14
  import { existsSync as existsSync8 } from "fs";
15
- import { dirname as dirname6 } from "path";
15
+ import { dirname as dirname7 } from "path";
16
16
 
17
17
  // src/ctl/client.ts
18
18
  import { createConnection } from "net";
@@ -68,7 +68,7 @@ var CtlUnknownOpError = class extends Error {
68
68
  function clientTimeout(req, override) {
69
69
  if (override !== void 0) return override;
70
70
  if (req.op === "fresh" || req.op === "command") return CTL_FRESH_TIMEOUT_MS;
71
- if (req.op !== "wait") return CTL_DEFAULT_TIMEOUT_MS;
71
+ if (req.op !== "wait" && req.op !== "interrupt") return CTL_DEFAULT_TIMEOUT_MS;
72
72
  return req.timeoutMs === void 0 ? void 0 : req.timeoutMs + CTL_WAIT_SLACK_MS;
73
73
  }
74
74
  function parseReply(line) {
@@ -144,6 +144,7 @@ async function ctlProbe(sock, timeoutMs = CTL_DEFAULT_TIMEOUT_MS) {
144
144
 
145
145
  // src/ctl/path.ts
146
146
  import { createHash } from "crypto";
147
+ import { rmSync } from "fs";
147
148
  import { dirname as dirname2, join as join2 } from "path";
148
149
 
149
150
  // src/db.ts
@@ -854,285 +855,598 @@ function ctlSocketPath(workstream, agent, stateDir) {
854
855
  const base = stateDir ?? (dbPath ? dirname2(dbPath) : defaultStateDir());
855
856
  return sockPathUnder(join2(base, "sock"), workstream, agent);
856
857
  }
857
-
858
- // src/mux.ts
859
- var mux_exports = {};
860
- __export(mux_exports, {
861
- HerdrCommandOverrideError: () => HerdrCommandOverrideError,
862
- HerdrError: () => HerdrError,
863
- HerdrNotImplementedError: () => HerdrNotImplementedError,
864
- HerdrSyntaxError: () => HerdrSyntaxError,
865
- HerdrUnsupportedCliError: () => HerdrUnsupportedCliError,
866
- HerdrWorkspaceGroupCloseError: () => HerdrWorkspaceGroupCloseError,
867
- MuxError: () => MuxError,
868
- NoMultiplexerError: () => NoMultiplexerError,
869
- PaneNotFoundError: () => PaneNotFoundError,
870
- activeMux: () => activeMux,
871
- detectMux: () => detectMux,
872
- herdrBackend: () => herdrBackend,
873
- isHerdrStatusUsable: () => isHerdrStatusUsable,
874
- muxByName: () => muxByName,
875
- parseAgentNameFromTitle: () => parseAgentNameFromTitle,
876
- resetHerdrExecutor: () => resetHerdrExecutor,
877
- resetMux: () => resetMux,
878
- setHerdrExecutor: () => setHerdrExecutor,
879
- setMuxForTests: () => setMuxForTests,
880
- tmuxBackend: () => tmuxBackend
881
- });
882
-
883
- // src/mux/herdr.ts
884
- import { execa as execa2 } from "execa";
885
-
886
- // src/mux/tmux.ts
887
- import { execa } from "execa";
888
-
889
- // src/mux/input-timing.ts
890
- var TAIL_WINDOW_LINES = 100;
891
- var TAIL_LINES = 20;
892
- var BUSY_MARKERS = ["to interrupt)"];
893
- var BRAILLE_SPINNER_RE = /[\u2800-\u28ff]/;
894
- function extractTail(scrollback) {
895
- const lines = scrollback.split("\n");
896
- const window = lines.slice(-TAIL_WINDOW_LINES);
897
- let end = window.length;
898
- while (end > 0 && (window[end - 1] ?? "").trim() === "") end--;
899
- return window.slice(Math.max(0, end - TAIL_LINES), end).join("\n");
900
- }
901
- function paneLooksBusy(scrollback) {
902
- const tail = extractTail(scrollback);
903
- return BUSY_MARKERS.some((marker) => tail.includes(marker)) || BRAILLE_SPINNER_RE.test(tail);
858
+ function unlinkCtlSocket(db, name, workstream) {
859
+ if (db.memory) return;
860
+ try {
861
+ rmSync(ctlSocketPath(workstream, name, dirname2(db.name)), { force: true });
862
+ } catch {
863
+ }
904
864
  }
905
865
 
906
- // src/mux/types.ts
907
- function parseAgentNameFromTitle(title) {
908
- const idx = title.indexOf(" \xB7 ");
909
- return idx === -1 ? title.trim() : title.slice(0, idx).trim();
910
- }
911
- var MuxError = class extends Error {
912
- name = "MuxError";
913
- errorNextSteps() {
914
- return [{ intent: "Run health check", command: "mu doctor" }];
915
- }
916
- };
917
- var PaneNotFoundError = class extends Error {
918
- constructor(paneId, backend) {
919
- super(`${backend?.name ?? "mux"} pane not found: ${paneId}`);
920
- this.paneId = paneId;
921
- this.backend = backend;
922
- }
923
- paneId;
924
- backend;
925
- name = "PaneNotFoundError";
926
- errorNextSteps() {
927
- return [
928
- ...this.backend?.paneNotFoundNextSteps(this.paneId) ?? [],
929
- {
930
- intent: "List workstreams to choose the right scope",
931
- command: "mu workstream list"
932
- },
933
- {
934
- intent: "List registered agents and orphan panes in that scope",
935
- command: "mu agent list -w <workstream>"
936
- }
937
- ];
866
+ // src/logs.ts
867
+ import { randomUUID as randomUUID2 } from "crypto";
868
+
869
+ // src/hlc.ts
870
+ var WALL_WIDTH = 15;
871
+ var COUNTER_WIDTH = 6;
872
+ var SEP = ".";
873
+ var MAX_WALL = 10 ** WALL_WIDTH - 1;
874
+ var MAX_COUNTER = 10 ** COUNTER_WIDTH - 1;
875
+ var HlcParseError = class extends Error {
876
+ constructor(value) {
877
+ super(`Malformed HLC: ${JSON.stringify(value)}`);
878
+ this.value = value;
879
+ this.name = "HlcParseError";
938
880
  }
881
+ value;
939
882
  };
940
- var NoMultiplexerError = class extends Error {
941
- constructor(tried) {
883
+ var HlcOverflowError = class extends Error {
884
+ constructor(field, value) {
942
885
  super(
943
- `no supported multiplexer found (tried: ${tried.join(", ")}). mu drives agents through tmux panes.`
886
+ field === "counter" ? `HLC counter overflow (${value} > ${MAX_COUNTER}): more than ${MAX_COUNTER + 1} ops in one millisecond` : `HLC wall-clock overflow (${value} > ${MAX_WALL}): system clock is far in the future`
944
887
  );
945
- this.tried = tried;
946
- }
947
- tried;
948
- name = "NoMultiplexerError";
949
- errorNextSteps() {
950
- return [
951
- { intent: "Run health check", command: "mu doctor" },
952
- { intent: "Verify tmux is installed", command: "tmux -V" },
953
- { intent: "Force a specific backend", command: "MU_MUX=tmux mu <verb>" }
954
- ];
888
+ this.field = field;
889
+ this.value = value;
890
+ this.name = "HlcOverflowError";
955
891
  }
892
+ field;
893
+ value;
956
894
  };
957
-
958
- // src/mux/tmux.ts
959
- var TmuxError = class extends MuxError {
960
- constructor(args, stderr, stdout, exitCode) {
961
- const detail = stderr.trim() || stdout.trim() || "no output";
962
- super(`tmux ${args.join(" ")} failed (exit ${exitCode}): ${detail}`);
963
- this.args = args;
964
- this.stderr = stderr;
965
- this.stdout = stdout;
966
- this.exitCode = exitCode;
967
- this.name = "TmuxError";
968
- }
969
- args;
970
- stderr;
971
- stdout;
972
- exitCode;
973
- errorNextSteps() {
974
- return [
975
- { intent: "Run health check", command: "mu doctor" },
976
- {
977
- intent: "Verify tmux is running and reachable",
978
- command: "tmux info | head"
979
- },
980
- {
981
- intent: "Check the failing tmux command in isolation",
982
- command: `tmux ${this.args.join(" ")}`
983
- }
984
- ];
895
+ var MachineIdentityMissingError = class extends Error {
896
+ constructor() {
897
+ super("machine_identity row (id = 1) is missing; the DB was not opened via openDb()");
898
+ this.name = "MachineIdentityMissingError";
985
899
  }
986
900
  };
987
- var PANE_ID_RE = /^%\d+$/;
988
- function isValidPaneId(s) {
989
- return PANE_ID_RE.test(s);
901
+ function formatHlc(hlc) {
902
+ const { wallMs, counter, machineId: machineId2 } = hlc;
903
+ if (!Number.isInteger(wallMs) || wallMs < 0) throw new HlcParseError(String(wallMs));
904
+ if (!Number.isInteger(counter) || counter < 0) throw new HlcParseError(String(counter));
905
+ if (wallMs > MAX_WALL) throw new HlcOverflowError("wall", wallMs);
906
+ if (counter > MAX_COUNTER) throw new HlcOverflowError("counter", counter);
907
+ if (machineId2.length === 0 || machineId2.includes(SEP)) throw new HlcParseError(machineId2);
908
+ return `${String(wallMs).padStart(WALL_WIDTH, "0")}${SEP}${String(counter).padStart(
909
+ COUNTER_WIDTH,
910
+ "0"
911
+ )}${SEP}${machineId2}`;
990
912
  }
991
- function assertValidPaneId(s) {
992
- if (!isValidPaneId(s)) {
993
- throw new TypeError(`invalid tmux pane id: ${JSON.stringify(s)} (expected /^%\\d+$/)`);
913
+ function parseHlc(value) {
914
+ const parts = value.split(SEP);
915
+ if (parts.length !== 3) throw new HlcParseError(value);
916
+ const [wall, counter, machineId2] = parts;
917
+ if (wall === void 0 || counter === void 0 || machineId2 === void 0) {
918
+ throw new HlcParseError(value);
994
919
  }
920
+ if (wall.length !== WALL_WIDTH || counter.length !== COUNTER_WIDTH || machineId2.length === 0) {
921
+ throw new HlcParseError(value);
922
+ }
923
+ if (!/^\d+$/.test(wall) || !/^\d+$/.test(counter)) throw new HlcParseError(value);
924
+ return { wallMs: Number(wall), counter: Number(counter), machineId: machineId2 };
995
925
  }
996
- function defaultSendDelayMs() {
997
- const raw = process.env.MU_SEND_DELAY_MS;
998
- if (raw === void 0) return 500;
999
- const parsed = Number.parseInt(raw, 10);
1000
- if (Number.isNaN(parsed) || parsed < 0) return 500;
1001
- return parsed;
926
+ function compareHlc(a, b) {
927
+ if (a < b) return -1;
928
+ if (a > b) return 1;
929
+ return 0;
1002
930
  }
1003
- function tmuxGlobalFlags() {
1004
- const socket = process.env.MU_TMUX_SOCKET;
1005
- if (socket === void 0 || socket.length === 0) return [];
1006
- return ["-L", socket, "-f", "/dev/null"];
931
+ function nextHlc(db, now = Date.now()) {
932
+ const row = db.prepare(
933
+ `UPDATE machine_identity
934
+ SET last_wall = MAX(last_wall, @now),
935
+ last_counter = CASE WHEN @now > last_wall THEN 0 ELSE last_counter + 1 END
936
+ WHERE id = 1
937
+ RETURNING machine_id, last_wall, last_counter`
938
+ ).get({ now: Math.floor(now) });
939
+ if (!row) throw new MachineIdentityMissingError();
940
+ return formatHlc({ wallMs: row.last_wall, counter: row.last_counter, machineId: row.machine_id });
1007
941
  }
1008
- var realExecutor = async (args) => {
1009
- const result = await execa("tmux", [...tmuxGlobalFlags(), ...args], { reject: false });
1010
- return {
1011
- stdout: result.stdout ?? "",
1012
- stderr: result.stderr ?? "",
1013
- exitCode: result.exitCode ?? null
1014
- };
1015
- };
1016
- var currentExecutor = realExecutor;
1017
- function setTmuxExecutor(executor) {
1018
- const previous = currentExecutor;
1019
- currentExecutor = executor;
1020
- return previous;
942
+ function receiveHlc(db, remoteHlc, now = Date.now()) {
943
+ const remote = parseHlc(remoteHlc);
944
+ const wallNow = Math.floor(now);
945
+ const read = db.prepare(
946
+ "SELECT machine_id, last_wall, last_counter FROM machine_identity WHERE id = 1"
947
+ );
948
+ const write = db.prepare(
949
+ "UPDATE machine_identity SET last_wall = @wall, last_counter = @counter WHERE id = 1"
950
+ );
951
+ const advance = db.transaction(() => {
952
+ const row = read.get();
953
+ if (!row) throw new MachineIdentityMissingError();
954
+ const wall = Math.max(row.last_wall, remote.wallMs, wallNow);
955
+ const localAtMax = row.last_wall === wall;
956
+ const remoteAtMax = remote.wallMs === wall;
957
+ let counter;
958
+ if (!localAtMax && !remoteAtMax) {
959
+ counter = 0;
960
+ } else if (localAtMax && remoteAtMax) {
961
+ counter = Math.max(row.last_counter, remote.counter) + 1;
962
+ } else if (localAtMax) {
963
+ counter = row.last_counter + 1;
964
+ } else {
965
+ counter = remote.counter + 1;
966
+ }
967
+ if (counter > MAX_COUNTER) throw new HlcOverflowError("counter", counter);
968
+ write.run({ wall, counter });
969
+ return formatHlc({ wallMs: wall, counter, machineId: row.machine_id });
970
+ });
971
+ return advance.immediate();
1021
972
  }
1022
- function resetTmuxExecutor() {
1023
- currentExecutor = realExecutor;
973
+
974
+ // src/legacy-ops.ts
975
+ var LEGACY_LOG_ONLY_INTENTS = /* @__PURE__ */ new Set(["workstream.export"]);
976
+ function isLegacyLogOnlyIntent(intent) {
977
+ return intent !== null && LEGACY_LOG_ONLY_INTENTS.has(intent);
1024
978
  }
1025
- async function tmux(args) {
1026
- const result = await currentExecutor(args);
1027
- if (result.exitCode !== 0) {
1028
- throw new TmuxError([...args], result.stderr, result.stdout, result.exitCode);
1029
- }
1030
- return result.stdout;
1031
- }
1032
- var currentSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
1033
- function setSleepForTests(impl) {
1034
- const previous = currentSleep;
1035
- currentSleep = impl;
1036
- return previous;
979
+ var LEGACY_INTENT_SYNONYMS = /* @__PURE__ */ new Map([
980
+ ["workstream.teardown", ["workstream.destroy"]]
981
+ ]);
982
+ function intentSpellings(intent) {
983
+ const legacy = LEGACY_INTENT_SYNONYMS.get(intent);
984
+ return legacy === void 0 ? [intent] : [intent, ...legacy];
1037
985
  }
1038
- function resetSleep() {
1039
- currentSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
986
+ var LEGACY_LOG_ONLY_SQL_EXCLUSION = `(intent IS NULL OR intent NOT IN (${[
987
+ ...LEGACY_LOG_ONLY_INTENTS
988
+ ].map((i) => `'${i}'`).join(", ")}))`;
989
+
990
+ // src/logs.ts
991
+ var SELECT_LOG_COLS = `
992
+ l.seq AS seq,
993
+ l.key AS workstream,
994
+ l.actor AS source,
995
+ l.intent AS intent,
996
+ l.group_id AS group_id,
997
+ l.op AS op,
998
+ l.entity AS kind,
999
+ l.payload AS payload,
1000
+ l.created_at AS created_at
1001
+ `;
1002
+ var LOG_FROM_JOIN = "FROM ops l";
1003
+ function workstreamScopeSql(column = "l.key") {
1004
+ return `(${column} = ? OR ${column} LIKE ? ESCAPE '\\')`;
1040
1005
  }
1041
- function sleep(ms) {
1042
- return currentSleep(ms);
1006
+ function workstreamScopeParams(workstream) {
1007
+ const escaped = workstream.replace(/[\\%_]/g, (c) => `\\${c}`);
1008
+ return [workstream, `${escaped}/%`];
1043
1009
  }
1044
- async function listSessions() {
1045
- try {
1046
- const out = await tmux(["list-sessions", "-F", "#{session_name}"]);
1047
- return out.split("\n").filter((line) => line.length > 0).map((name) => ({ name }));
1048
- } catch (err) {
1049
- if (err instanceof TmuxError && /no server running|no sessions/i.test(err.stderr)) {
1050
- return [];
1051
- }
1052
- throw err;
1010
+ var TOUCH_OP_FILTER = `NOT (
1011
+ l.intent IS NOT NULL
1012
+ AND l.op = 'put'
1013
+ AND l.payload LIKE '{"updated_at":%'
1014
+ AND LENGTH(l.payload) < 45
1015
+ )`;
1016
+ var MACHINE_WIDE_KEY = "";
1017
+ var GroupIdAmbiguousError = class extends Error {
1018
+ constructor(prefix, candidates) {
1019
+ super(
1020
+ `group id ${JSON.stringify(prefix)} is ambiguous: matches ${candidates.length} groups (${candidates.slice(0, 4).map((c) => c.slice(0, 12)).join(", ")}${candidates.length > 4 ? ", \u2026" : ""})`
1021
+ );
1022
+ this.prefix = prefix;
1023
+ this.candidates = candidates;
1024
+ this.name = "GroupIdAmbiguousError";
1025
+ }
1026
+ prefix;
1027
+ candidates;
1028
+ errorNextSteps() {
1029
+ return [
1030
+ { intent: "List recent groups with their ids", command: "mu undo" },
1031
+ {
1032
+ intent: "Retry with more characters",
1033
+ command: `mu log --group ${this.candidates[0] ?? "<full-id>"}`
1034
+ }
1035
+ ];
1053
1036
  }
1037
+ };
1038
+ function groupIdFromPrefix(db, prefix) {
1039
+ if (prefix === "") return null;
1040
+ const rows = db.prepare(
1041
+ `SELECT DISTINCT group_id AS groupId FROM ops
1042
+ WHERE group_id = @exact OR group_id LIKE @prefix || '%' ESCAPE '\\'`
1043
+ ).all({ exact: prefix, prefix: prefix.replace(/[\\%_]/g, (c) => `\\${c}`) });
1044
+ const exact = rows.find((r) => r.groupId === prefix);
1045
+ if (exact !== void 0) return exact.groupId;
1046
+ const first = rows[0];
1047
+ if (rows.length === 1 && first !== void 0) return first.groupId;
1048
+ if (rows.length === 0) return null;
1049
+ throw new GroupIdAmbiguousError(
1050
+ prefix,
1051
+ rows.map((r) => r.groupId)
1052
+ );
1054
1053
  }
1055
- async function sessionExists(name) {
1056
- const result = await currentExecutor(["has-session", "-t", name]);
1057
- return result.exitCode === 0;
1054
+ function machineId(db) {
1055
+ const row = db.prepare("SELECT machine_id FROM machine_identity WHERE id = 1").get();
1056
+ return row?.machine_id ?? "unknown";
1058
1057
  }
1059
- async function newSession(name, opts = {}) {
1060
- const args = ["new-session"];
1061
- if (opts.detached !== false) args.push("-d");
1062
- args.push("-s", name);
1063
- if (opts.windowName) args.push("-n", opts.windowName);
1064
- if (opts.cwd) args.push("-c", opts.cwd);
1065
- appendEnvFlags(args, opts.env);
1066
- if (opts.command) args.push(opts.command);
1067
- await tmux(args);
1058
+ function rowFromDb(row) {
1059
+ return {
1060
+ seq: row.seq,
1061
+ workstreamName: row.workstream === MACHINE_WIDE_KEY ? null : row.workstream,
1062
+ source: row.source ?? "system",
1063
+ intent: row.intent,
1064
+ group: row.group_id,
1065
+ op: row.op,
1066
+ kind: row.kind,
1067
+ payload: row.payload,
1068
+ createdAt: row.created_at
1069
+ };
1068
1070
  }
1069
- async function newSessionWithPane(name, opts) {
1070
- const args = ["new-session"];
1071
- if (opts.detached !== false) args.push("-d");
1072
- args.push("-s", name, "-n", opts.windowName);
1073
- if (opts.cwd) args.push("-c", opts.cwd);
1074
- appendEnvFlags(args, opts.env);
1075
- args.push("-P", "-F", "#{pane_id}", opts.command);
1076
- const out = (await tmux(args)).trim();
1077
- assertValidPaneId(out);
1078
- return out;
1071
+ function appendLog(db, opts) {
1072
+ const kind = opts.kind ?? "message";
1073
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1074
+ const key = opts.workstream ?? MACHINE_WIDE_KEY;
1075
+ const group = randomUUID2();
1076
+ const result = db.prepare(
1077
+ `INSERT INTO ops (hlc, machine_id, group_id, actor, intent, entity, key, op, payload, created_at)
1078
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'put', ?, ?)`
1079
+ ).run(
1080
+ nextHlc(db),
1081
+ machineId(db),
1082
+ group,
1083
+ opts.source,
1084
+ opts.intent ?? null,
1085
+ kind,
1086
+ key,
1087
+ opts.payload,
1088
+ createdAt
1089
+ );
1090
+ return {
1091
+ seq: Number(result.lastInsertRowid),
1092
+ workstreamName: opts.workstream,
1093
+ source: opts.source,
1094
+ intent: opts.intent ?? null,
1095
+ group,
1096
+ op: "put",
1097
+ kind,
1098
+ payload: opts.payload,
1099
+ createdAt
1100
+ };
1079
1101
  }
1080
- async function killSession(name) {
1081
- const result = await currentExecutor(["kill-session", "-t", name]);
1082
- if (result.exitCode !== 0 && !/can't find session|session not found|no server running|no current target/i.test(result.stderr)) {
1083
- throw new TmuxError(
1084
- ["kill-session", "-t", name],
1085
- result.stderr,
1086
- result.stdout,
1087
- result.exitCode
1088
- );
1102
+ function listLogs(db, opts = {}) {
1103
+ const conditions = [];
1104
+ const params = [];
1105
+ if (opts.workstream === null) {
1106
+ conditions.push("l.key = ?");
1107
+ params.push(MACHINE_WIDE_KEY);
1108
+ } else if (opts.workstream !== void 0) {
1109
+ conditions.push(workstreamScopeSql());
1110
+ params.push(...workstreamScopeParams(opts.workstream));
1089
1111
  }
1090
- }
1091
- async function listWindows(session) {
1092
- if (session) {
1093
- const out2 = await tmux(["list-windows", "-t", session, "-F", "#{window_id} #{window_name}"]);
1094
- return parseWindows(out2);
1112
+ if (opts.since !== void 0) {
1113
+ conditions.push("l.seq > ?");
1114
+ params.push(opts.since);
1095
1115
  }
1096
- const out = await tmux([
1097
- "list-windows",
1098
- "-a",
1099
- "-F",
1100
- "#{session_name} #{window_id} #{window_name}"
1101
- ]);
1102
- const windows = [];
1103
- for (const line of out.split("\n")) {
1104
- if (line.length === 0) continue;
1105
- const [sessionName, id, name] = line.split(" ");
1106
- if (!sessionName || !id || name === void 0) continue;
1107
- windows.push({ id, name, sessionName });
1116
+ if (opts.source !== void 0) {
1117
+ conditions.push("l.actor = ?");
1118
+ params.push(opts.source);
1108
1119
  }
1109
- return windows;
1110
- }
1111
- function parseWindows(output) {
1112
- const windows = [];
1113
- for (const line of output.split("\n")) {
1114
- if (line.length === 0) continue;
1115
- const [id, name] = line.split(" ");
1116
- if (!id || name === void 0) continue;
1117
- windows.push({ id, name });
1120
+ if (opts.kind !== void 0) {
1121
+ conditions.push("l.entity = ?");
1122
+ params.push(opts.kind);
1118
1123
  }
1119
- return windows;
1124
+ if (opts.intent !== void 0) {
1125
+ const spellings = intentSpellings(opts.intent);
1126
+ conditions.push(`l.intent IN (${spellings.map(() => "?").join(", ")})`);
1127
+ params.push(...spellings);
1128
+ }
1129
+ if (opts.group !== void 0) {
1130
+ conditions.push("l.group_id = ?");
1131
+ params.push(opts.group);
1132
+ }
1133
+ conditions.push(TOUCH_OP_FILTER);
1134
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1135
+ if (opts.limit !== void 0 && opts.since === void 0) {
1136
+ const rowsDesc = db.prepare(`SELECT ${SELECT_LOG_COLS} ${LOG_FROM_JOIN} ${where} ORDER BY l.seq DESC LIMIT ?`).all(...params, opts.limit);
1137
+ return rowsDesc.reverse().map(rowFromDb);
1138
+ }
1139
+ let sql = `SELECT ${SELECT_LOG_COLS} ${LOG_FROM_JOIN} ${where} ORDER BY l.seq ASC`;
1140
+ if (opts.limit !== void 0) {
1141
+ sql += " LIMIT ?";
1142
+ params.push(opts.limit);
1143
+ }
1144
+ const rows = db.prepare(sql).all(...params);
1145
+ return rows.map(rowFromDb);
1120
1146
  }
1121
- async function newWindow(opts) {
1122
- const args = ["new-window"];
1123
- if (opts.detached !== false) args.push("-d");
1124
- if (opts.session) args.push("-t", opts.session);
1125
- args.push("-n", opts.name);
1126
- if (opts.cwd) args.push("-c", opts.cwd);
1127
- appendEnvFlags(args, opts.env);
1128
- args.push("-P", "-F", "#{pane_id}", opts.command);
1129
- const out = (await tmux(args)).trim();
1130
- assertValidPaneId(out);
1131
- return out;
1147
+ function latestSeq(db, workstream) {
1148
+ const row = workstream === void 0 ? db.prepare(`SELECT MAX(seq) AS s FROM ops l WHERE ${TOUCH_OP_FILTER}`).get() : db.prepare(
1149
+ `SELECT MAX(seq) AS s FROM ops l WHERE ${workstreamScopeSql()} AND ${TOUCH_OP_FILTER}`
1150
+ ).get(...workstreamScopeParams(workstream));
1151
+ return row.s ?? 0;
1132
1152
  }
1133
- async function listPanesInSession(session) {
1134
- const args = [
1135
- "list-panes",
1153
+ function emitEvent(db, workstream, intent, payload, source = "system") {
1154
+ appendLog(db, { workstream, source, kind: entityForIntent(intent), payload, intent });
1155
+ }
1156
+ function entityForIntent(intent) {
1157
+ const dot = intent.indexOf(".");
1158
+ return dot === -1 ? intent : intent.slice(0, dot);
1159
+ }
1160
+ function lastClaimOp(db, workstream, localId) {
1161
+ const row = db.prepare(
1162
+ `SELECT actor, created_at FROM ops
1163
+ WHERE key = ? AND intent = 'task.claim'
1164
+ ORDER BY seq DESC LIMIT 1`
1165
+ ).get(`${workstream}/${localId}`);
1166
+ return row ?? null;
1167
+ }
1168
+ function lastClaimEventAt(db, workstream, localId) {
1169
+ return lastClaimOp(db, workstream, localId)?.created_at ?? null;
1170
+ }
1171
+
1172
+ // src/mux.ts
1173
+ var mux_exports = {};
1174
+ __export(mux_exports, {
1175
+ HerdrCommandOverrideError: () => HerdrCommandOverrideError,
1176
+ HerdrError: () => HerdrError,
1177
+ HerdrNotImplementedError: () => HerdrNotImplementedError,
1178
+ HerdrSyntaxError: () => HerdrSyntaxError,
1179
+ HerdrUnsupportedCliError: () => HerdrUnsupportedCliError,
1180
+ HerdrWorkspaceGroupCloseError: () => HerdrWorkspaceGroupCloseError,
1181
+ MuxError: () => MuxError,
1182
+ NoMultiplexerError: () => NoMultiplexerError,
1183
+ PaneNotFoundError: () => PaneNotFoundError,
1184
+ activeMux: () => activeMux,
1185
+ detectMux: () => detectMux,
1186
+ herdrBackend: () => herdrBackend,
1187
+ isHerdrStatusUsable: () => isHerdrStatusUsable,
1188
+ muxByName: () => muxByName,
1189
+ parseAgentNameFromTitle: () => parseAgentNameFromTitle,
1190
+ resetHerdrExecutor: () => resetHerdrExecutor,
1191
+ resetMux: () => resetMux,
1192
+ setHerdrExecutor: () => setHerdrExecutor,
1193
+ setMuxForTests: () => setMuxForTests,
1194
+ tmuxBackend: () => tmuxBackend
1195
+ });
1196
+
1197
+ // src/mux/herdr.ts
1198
+ import { execa as execa2 } from "execa";
1199
+
1200
+ // src/mux/tmux.ts
1201
+ import { execa } from "execa";
1202
+
1203
+ // src/mux/input-timing.ts
1204
+ var TAIL_WINDOW_LINES = 100;
1205
+ var TAIL_LINES = 20;
1206
+ var BUSY_MARKERS = ["to interrupt)"];
1207
+ var BRAILLE_SPINNER_RE = /[\u2800-\u28ff]/;
1208
+ function extractTail(scrollback) {
1209
+ const lines = scrollback.split("\n");
1210
+ const window = lines.slice(-TAIL_WINDOW_LINES);
1211
+ let end = window.length;
1212
+ while (end > 0 && (window[end - 1] ?? "").trim() === "") end--;
1213
+ return window.slice(Math.max(0, end - TAIL_LINES), end).join("\n");
1214
+ }
1215
+ function paneLooksBusy(scrollback) {
1216
+ const tail = extractTail(scrollback);
1217
+ return BUSY_MARKERS.some((marker) => tail.includes(marker)) || BRAILLE_SPINNER_RE.test(tail);
1218
+ }
1219
+
1220
+ // src/mux/types.ts
1221
+ function parseAgentNameFromTitle(title) {
1222
+ const idx = title.indexOf(" \xB7 ");
1223
+ return idx === -1 ? title.trim() : title.slice(0, idx).trim();
1224
+ }
1225
+ var MuxError = class extends Error {
1226
+ name = "MuxError";
1227
+ errorNextSteps() {
1228
+ return [{ intent: "Run health check", command: "mu doctor" }];
1229
+ }
1230
+ };
1231
+ var PaneNotFoundError = class extends Error {
1232
+ constructor(paneId, backend) {
1233
+ super(`${backend?.name ?? "mux"} pane not found: ${paneId}`);
1234
+ this.paneId = paneId;
1235
+ this.backend = backend;
1236
+ }
1237
+ paneId;
1238
+ backend;
1239
+ name = "PaneNotFoundError";
1240
+ errorNextSteps() {
1241
+ return [
1242
+ ...this.backend?.paneNotFoundNextSteps(this.paneId) ?? [],
1243
+ {
1244
+ intent: "List workstreams to choose the right scope",
1245
+ command: "mu workstream list"
1246
+ },
1247
+ {
1248
+ intent: "List registered agents and orphan panes in that scope",
1249
+ command: "mu agent list -w <workstream>"
1250
+ }
1251
+ ];
1252
+ }
1253
+ };
1254
+ var NoMultiplexerError = class extends Error {
1255
+ constructor(tried) {
1256
+ super(
1257
+ `no supported multiplexer found (tried: ${tried.join(", ")}). mu drives agents through tmux panes.`
1258
+ );
1259
+ this.tried = tried;
1260
+ }
1261
+ tried;
1262
+ name = "NoMultiplexerError";
1263
+ errorNextSteps() {
1264
+ return [
1265
+ { intent: "Run health check", command: "mu doctor" },
1266
+ { intent: "Verify tmux is installed", command: "tmux -V" },
1267
+ { intent: "Force a specific backend", command: "MU_MUX=tmux mu <verb>" }
1268
+ ];
1269
+ }
1270
+ };
1271
+
1272
+ // src/mux/tmux.ts
1273
+ var TmuxError = class extends MuxError {
1274
+ constructor(args, stderr, stdout, exitCode) {
1275
+ const detail = stderr.trim() || stdout.trim() || "no output";
1276
+ super(`tmux ${args.join(" ")} failed (exit ${exitCode}): ${detail}`);
1277
+ this.args = args;
1278
+ this.stderr = stderr;
1279
+ this.stdout = stdout;
1280
+ this.exitCode = exitCode;
1281
+ this.name = "TmuxError";
1282
+ }
1283
+ args;
1284
+ stderr;
1285
+ stdout;
1286
+ exitCode;
1287
+ errorNextSteps() {
1288
+ return [
1289
+ { intent: "Run health check", command: "mu doctor" },
1290
+ {
1291
+ intent: "Verify tmux is running and reachable",
1292
+ command: "tmux info | head"
1293
+ },
1294
+ {
1295
+ intent: "Check the failing tmux command in isolation",
1296
+ command: `tmux ${this.args.join(" ")}`
1297
+ }
1298
+ ];
1299
+ }
1300
+ };
1301
+ var PANE_ID_RE = /^%\d+$/;
1302
+ function isValidPaneId(s) {
1303
+ return PANE_ID_RE.test(s);
1304
+ }
1305
+ function assertValidPaneId(s) {
1306
+ if (!isValidPaneId(s)) {
1307
+ throw new TypeError(`invalid tmux pane id: ${JSON.stringify(s)} (expected /^%\\d+$/)`);
1308
+ }
1309
+ }
1310
+ function defaultSendDelayMs() {
1311
+ const raw = process.env.MU_SEND_DELAY_MS;
1312
+ if (raw === void 0) return 500;
1313
+ const parsed = Number.parseInt(raw, 10);
1314
+ if (Number.isNaN(parsed) || parsed < 0) return 500;
1315
+ return parsed;
1316
+ }
1317
+ function tmuxGlobalFlags() {
1318
+ const socket = process.env.MU_TMUX_SOCKET;
1319
+ if (socket === void 0 || socket.length === 0) return [];
1320
+ return ["-L", socket, "-f", "/dev/null"];
1321
+ }
1322
+ var realExecutor = async (args) => {
1323
+ const result = await execa("tmux", [...tmuxGlobalFlags(), ...args], { reject: false });
1324
+ return {
1325
+ stdout: result.stdout ?? "",
1326
+ stderr: result.stderr ?? "",
1327
+ exitCode: result.exitCode ?? null
1328
+ };
1329
+ };
1330
+ var currentExecutor = realExecutor;
1331
+ function setTmuxExecutor(executor) {
1332
+ const previous = currentExecutor;
1333
+ currentExecutor = executor;
1334
+ return previous;
1335
+ }
1336
+ function resetTmuxExecutor() {
1337
+ currentExecutor = realExecutor;
1338
+ }
1339
+ async function tmux(args) {
1340
+ const result = await currentExecutor(args);
1341
+ if (result.exitCode !== 0) {
1342
+ throw new TmuxError([...args], result.stderr, result.stdout, result.exitCode);
1343
+ }
1344
+ return result.stdout;
1345
+ }
1346
+ var currentSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
1347
+ function setSleepForTests(impl) {
1348
+ const previous = currentSleep;
1349
+ currentSleep = impl;
1350
+ return previous;
1351
+ }
1352
+ function resetSleep() {
1353
+ currentSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
1354
+ }
1355
+ function sleep(ms) {
1356
+ return currentSleep(ms);
1357
+ }
1358
+ async function listSessions() {
1359
+ try {
1360
+ const out = await tmux(["list-sessions", "-F", "#{session_name}"]);
1361
+ return out.split("\n").filter((line) => line.length > 0).map((name) => ({ name }));
1362
+ } catch (err) {
1363
+ if (err instanceof TmuxError && /no server running|no sessions/i.test(err.stderr)) {
1364
+ return [];
1365
+ }
1366
+ throw err;
1367
+ }
1368
+ }
1369
+ async function sessionExists(name) {
1370
+ const result = await currentExecutor(["has-session", "-t", name]);
1371
+ return result.exitCode === 0;
1372
+ }
1373
+ async function newSession(name, opts = {}) {
1374
+ const args = ["new-session"];
1375
+ if (opts.detached !== false) args.push("-d");
1376
+ args.push("-s", name);
1377
+ if (opts.windowName) args.push("-n", opts.windowName);
1378
+ if (opts.cwd) args.push("-c", opts.cwd);
1379
+ appendEnvFlags(args, opts.env);
1380
+ if (opts.command) args.push(opts.command);
1381
+ await tmux(args);
1382
+ }
1383
+ async function newSessionWithPane(name, opts) {
1384
+ const args = ["new-session"];
1385
+ if (opts.detached !== false) args.push("-d");
1386
+ args.push("-s", name, "-n", opts.windowName);
1387
+ if (opts.cwd) args.push("-c", opts.cwd);
1388
+ appendEnvFlags(args, opts.env);
1389
+ args.push("-P", "-F", "#{pane_id}", opts.command);
1390
+ const out = (await tmux(args)).trim();
1391
+ assertValidPaneId(out);
1392
+ return out;
1393
+ }
1394
+ async function killSession(name) {
1395
+ const result = await currentExecutor(["kill-session", "-t", name]);
1396
+ if (result.exitCode !== 0 && !/can't find session|session not found|no server running|no current target/i.test(result.stderr)) {
1397
+ throw new TmuxError(
1398
+ ["kill-session", "-t", name],
1399
+ result.stderr,
1400
+ result.stdout,
1401
+ result.exitCode
1402
+ );
1403
+ }
1404
+ }
1405
+ async function listWindows(session) {
1406
+ if (session) {
1407
+ const out2 = await tmux(["list-windows", "-t", session, "-F", "#{window_id} #{window_name}"]);
1408
+ return parseWindows(out2);
1409
+ }
1410
+ const out = await tmux([
1411
+ "list-windows",
1412
+ "-a",
1413
+ "-F",
1414
+ "#{session_name} #{window_id} #{window_name}"
1415
+ ]);
1416
+ const windows = [];
1417
+ for (const line of out.split("\n")) {
1418
+ if (line.length === 0) continue;
1419
+ const [sessionName, id, name] = line.split(" ");
1420
+ if (!sessionName || !id || name === void 0) continue;
1421
+ windows.push({ id, name, sessionName });
1422
+ }
1423
+ return windows;
1424
+ }
1425
+ function parseWindows(output) {
1426
+ const windows = [];
1427
+ for (const line of output.split("\n")) {
1428
+ if (line.length === 0) continue;
1429
+ const [id, name] = line.split(" ");
1430
+ if (!id || name === void 0) continue;
1431
+ windows.push({ id, name });
1432
+ }
1433
+ return windows;
1434
+ }
1435
+ async function newWindow(opts) {
1436
+ const args = ["new-window"];
1437
+ if (opts.detached !== false) args.push("-d");
1438
+ if (opts.session) args.push("-t", opts.session);
1439
+ args.push("-n", opts.name);
1440
+ if (opts.cwd) args.push("-c", opts.cwd);
1441
+ appendEnvFlags(args, opts.env);
1442
+ args.push("-P", "-F", "#{pane_id}", opts.command);
1443
+ const out = (await tmux(args)).trim();
1444
+ assertValidPaneId(out);
1445
+ return out;
1446
+ }
1447
+ async function listPanesInSession(session) {
1448
+ const args = [
1449
+ "list-panes",
1136
1450
  "-s",
1137
1451
  "-t",
1138
1452
  session,
@@ -1655,970 +1969,663 @@ var TAB_ID_RE = new RegExp(`^w${HERDR_ORDINAL}:t${HERDR_ORDINAL}$`);
1655
1969
  function isValidPaneId2(s) {
1656
1970
  return PANE_ID_RE2.test(s);
1657
1971
  }
1658
- function assertValidPaneId2(s) {
1659
- if (!isValidPaneId2(s)) {
1660
- throw new TypeError(
1661
- `invalid herdr pane id: ${JSON.stringify(s)} (expected ${PANE_ID_RE2.source})`
1662
- );
1663
- }
1664
- }
1665
- function herdrGlobalFlags() {
1666
- const session = process.env.MU_HERDR_SESSION;
1667
- if (session === void 0 || session.length === 0) return [];
1668
- return ["--session", session];
1669
- }
1670
- var realExecutor2 = async (args) => {
1671
- const result = await execa2("herdr", [...herdrGlobalFlags(), ...args], { reject: false });
1672
- return {
1673
- stdout: result.stdout ?? "",
1674
- stderr: result.stderr ?? "",
1675
- exitCode: result.exitCode ?? null
1676
- };
1677
- };
1678
- var currentExecutor2 = realExecutor2;
1679
- function setHerdrExecutor(executor) {
1680
- const previous = currentExecutor2;
1681
- currentExecutor2 = executor;
1682
- return previous;
1683
- }
1684
- function resetHerdrExecutor() {
1685
- currentExecutor2 = realExecutor2;
1686
- }
1687
- function isRecord(v) {
1688
- return typeof v === "object" && v !== null && !Array.isArray(v);
1689
- }
1690
- function asString(v) {
1691
- return typeof v === "string" ? v : void 0;
1692
- }
1693
- function parseErrorEnvelope(stderr) {
1694
- const trimmed = stderr.trim();
1695
- if (trimmed.length === 0) return void 0;
1696
- let parsed;
1697
- try {
1698
- parsed = JSON.parse(trimmed);
1699
- } catch {
1700
- return void 0;
1701
- }
1702
- if (!isRecord(parsed)) return void 0;
1703
- const err = parsed.error;
1704
- if (!isRecord(err)) return void 0;
1705
- const code = asString(err.code);
1706
- const message = asString(err.message);
1707
- if (code === void 0) return void 0;
1708
- return { code, message: message ?? code };
1709
- }
1710
- async function herdr(args) {
1711
- const result = await currentExecutor2(args);
1712
- if (result.exitCode === 2) {
1713
- throw new HerdrSyntaxError([...args], result.stderr || result.stdout);
1714
- }
1715
- if (result.exitCode !== 0) {
1716
- throw new HerdrError(
1717
- [...args],
1718
- result.stderr,
1719
- result.stdout,
1720
- result.exitCode,
1721
- parseErrorEnvelope(result.stderr)?.code
1722
- );
1723
- }
1724
- const trimmed = result.stdout.trim();
1725
- if (trimmed.length === 0) return {};
1726
- let parsed;
1727
- try {
1728
- parsed = JSON.parse(trimmed);
1729
- } catch {
1730
- throw new HerdrError([...args], result.stderr, result.stdout, result.exitCode);
1731
- }
1732
- if (!isRecord(parsed)) return {};
1733
- const inner = parsed.result;
1734
- return isRecord(inner) ? inner : {};
1735
- }
1736
- async function herdrTolerating(args, tolerate) {
1737
- try {
1738
- return await herdr(args);
1739
- } catch (err) {
1740
- if (err instanceof HerdrError && err.code !== void 0 && tolerate.includes(err.code)) {
1741
- return void 0;
1742
- }
1743
- throw err;
1744
- }
1745
- }
1746
- function readWorkspace(v) {
1747
- if (!isRecord(v)) return void 0;
1748
- const workspaceId = asString(v.workspace_id);
1749
- if (workspaceId === void 0) return void 0;
1750
- return { workspaceId, label: asString(v.label) ?? "" };
1751
- }
1752
- function readTab(v) {
1753
- if (!isRecord(v)) return void 0;
1754
- const id = asString(v.tab_id);
1755
- if (id === void 0) return void 0;
1756
- return { id, name: asString(v.label) ?? "" };
1757
- }
1758
- function readPane(v) {
1759
- if (!isRecord(v)) return void 0;
1760
- const paneId = asString(v.pane_id);
1761
- if (paneId === void 0) return void 0;
1762
- return {
1763
- paneId,
1764
- // herdr's optional `label` is what `pane rename` writes and is
1765
- // therefore mu's pane-title equivalent.
1766
- title: asString(v.label) ?? "",
1767
- // herdr does not report a foreground command on `pane list`; the
1768
- // authoritative answer costs a `pane process-info` round trip per
1769
- // pane. Callers that need it use `paneCommand()`.
1770
- command: "",
1771
- windowId: asString(v.tab_id)
1772
- };
1773
- }
1774
- function readArray(result, key) {
1775
- const v = result[key];
1776
- return Array.isArray(v) ? v : [];
1777
- }
1778
- async function listWorkspaces() {
1779
- const result = await herdr(["workspace", "list"]);
1780
- const out = [];
1781
- for (const raw of readArray(result, "workspaces")) {
1782
- const ws = readWorkspace(raw);
1783
- if (ws) out.push(ws);
1784
- }
1785
- return out;
1786
- }
1787
- async function listSessions2() {
1788
- return (await listWorkspaces()).map((ws) => ({ name: ws.label }));
1789
- }
1790
- async function resolveWorkspaceId(name) {
1791
- for (const ws of await listWorkspaces()) {
1792
- if (ws.label === name) return ws.workspaceId;
1793
- }
1794
- return void 0;
1795
- }
1796
- async function requireWorkspaceId(name) {
1797
- const id = await resolveWorkspaceId(name);
1798
- if (id === void 0) {
1799
- throw new HerdrError(
1800
- ["workspace", "list"],
1801
- `{"error":{"code":"workspace_not_found","message":"no herdr workspace labelled ${name}"}}`,
1802
- "",
1803
- 1,
1804
- "workspace_not_found"
1805
- );
1806
- }
1807
- return id;
1808
- }
1809
- async function sessionExists2(name) {
1810
- return await resolveWorkspaceId(name) !== void 0;
1811
- }
1812
- async function newSession2(name, opts = {}) {
1813
- rejectCommand("newSession", opts.command);
1814
- const args = ["workspace", "create", "--label", name, "--no-focus"];
1815
- if (opts.cwd) args.push("--cwd", opts.cwd);
1816
- appendEnvFlags2(args, opts.env);
1817
- await herdr(args);
1818
- }
1819
- async function newSessionWithPane2(name, opts) {
1820
- rejectCommand("newSessionWithPane", opts.command);
1821
- const args = ["workspace", "create", "--label", name, "--no-focus"];
1822
- if (opts.cwd) args.push("--cwd", opts.cwd);
1823
- appendEnvFlags2(args, opts.env);
1824
- const result = await herdr(args);
1825
- return readCreatedPaneId(result, "root_pane", args);
1826
- }
1827
- async function killSession2(name) {
1828
- const id = await resolveWorkspaceId(name);
1829
- if (id === void 0) return;
1830
- try {
1831
- await herdrTolerating(["workspace", "close", id], ["workspace_not_found"]);
1832
- } catch (err) {
1833
- if (err instanceof HerdrError && err.code === GROUP_CLOSE_REQUIRED_CODE) {
1834
- throw new HerdrWorkspaceGroupCloseError(name, id, err.message);
1835
- }
1836
- throw err;
1837
- }
1838
- }
1839
- async function listWindows2(session) {
1840
- if (session !== void 0) {
1841
- const id = await resolveWorkspaceId(session);
1842
- if (id === void 0) return [];
1843
- return readTabs(await herdr(["tab", "list", "--workspace", id]));
1844
- }
1845
- const windows = [];
1846
- for (const ws of await listWorkspaces()) {
1847
- for (const tab of readTabs(await herdr(["tab", "list", "--workspace", ws.workspaceId]))) {
1848
- windows.push({ ...tab, sessionName: ws.label });
1849
- }
1850
- }
1851
- return windows;
1852
- }
1853
- function readTabs(result) {
1854
- const out = [];
1855
- for (const raw of readArray(result, "tabs")) {
1856
- const tab = readTab(raw);
1857
- if (tab) out.push(tab);
1858
- }
1859
- return out;
1860
- }
1861
- async function newWindow2(opts) {
1862
- rejectCommand("newWindow", opts.command);
1863
- const args = ["tab", "create"];
1864
- if (opts.session !== void 0) {
1865
- args.push("--workspace", await requireWorkspaceId(opts.session));
1866
- }
1867
- args.push("--label", opts.name, "--no-focus");
1868
- if (opts.cwd) args.push("--cwd", opts.cwd);
1869
- appendEnvFlags2(args, opts.env);
1870
- return readCreatedPaneId(await herdr(args), "root_pane", args);
1871
- }
1872
- async function selectLayout2(_window, _layout) {
1873
- }
1874
- async function listPanesInSession2(session) {
1875
- const id = await resolveWorkspaceId(session);
1876
- if (id === void 0) return [];
1877
- const result = await herdrTolerating(
1878
- ["pane", "list", "--workspace", id],
1879
- ["workspace_not_found"]
1880
- );
1881
- if (result === void 0) return [];
1882
- return readPanes(result).map((p) => ({ ...p, sessionName: session }));
1883
- }
1884
- async function listPanes2(target) {
1885
- if (target !== void 0 && target !== "*") {
1886
- if (TAB_ID_RE.test(target)) {
1887
- const workspaceId = target.split(":")[0] ?? target;
1888
- const panes2 = readPanes(await herdr(["pane", "list", "--workspace", workspaceId]));
1889
- return panes2.filter((p) => p.windowId === target);
1890
- }
1891
- if (WORKSPACE_ID_RE.test(target)) {
1892
- return readPanes(await herdr(["pane", "list", "--workspace", target]));
1893
- }
1894
- return listPanesInSession2(target);
1895
- }
1896
- const ambient = process.env.HERDR_WORKSPACE_ID;
1897
- if (target === void 0 && ambient !== void 0 && WORKSPACE_ID_RE.test(ambient)) {
1898
- return readPanes(await herdr(["pane", "list", "--workspace", ambient]));
1899
- }
1900
- const panes = [];
1901
- for (const ws of await listWorkspaces()) {
1902
- for (const pane of readPanes(await herdr(["pane", "list", "--workspace", ws.workspaceId]))) {
1903
- panes.push({ ...pane, sessionName: ws.label });
1904
- }
1905
- }
1906
- return panes;
1907
- }
1908
- function readPanes(result) {
1909
- const out = [];
1910
- for (const raw of readArray(result, "panes")) {
1911
- const pane = readPane(raw);
1912
- if (pane) out.push(pane);
1972
+ function assertValidPaneId2(s) {
1973
+ if (!isValidPaneId2(s)) {
1974
+ throw new TypeError(
1975
+ `invalid herdr pane id: ${JSON.stringify(s)} (expected ${PANE_ID_RE2.source})`
1976
+ );
1913
1977
  }
1914
- return out;
1915
1978
  }
1916
- async function splitWindow2(opts) {
1917
- rejectCommand("splitWindow", opts.command);
1918
- assertValidPaneId2(opts.target);
1919
- const args = [
1920
- "pane",
1921
- "split",
1922
- opts.target,
1923
- "--direction",
1924
- opts.horizontal === false ? "down" : "right",
1925
- "--no-focus"
1926
- ];
1927
- if (opts.cwd) args.push("--cwd", opts.cwd);
1928
- appendEnvFlags2(args, opts.env);
1929
- return readCreatedPaneId(await herdr(args), "pane", args);
1979
+ function herdrGlobalFlags() {
1980
+ const session = process.env.MU_HERDR_SESSION;
1981
+ if (session === void 0 || session.length === 0) return [];
1982
+ return ["--session", session];
1930
1983
  }
1931
- async function killPane2(paneId) {
1932
- assertValidPaneId2(paneId);
1933
- await herdrTolerating(["pane", "close", paneId], ["pane_not_found", "workspace_not_found"]);
1984
+ var realExecutor2 = async (args) => {
1985
+ const result = await execa2("herdr", [...herdrGlobalFlags(), ...args], { reject: false });
1986
+ return {
1987
+ stdout: result.stdout ?? "",
1988
+ stderr: result.stderr ?? "",
1989
+ exitCode: result.exitCode ?? null
1990
+ };
1991
+ };
1992
+ var currentExecutor2 = realExecutor2;
1993
+ function setHerdrExecutor(executor) {
1994
+ const previous = currentExecutor2;
1995
+ currentExecutor2 = executor;
1996
+ return previous;
1934
1997
  }
1935
- async function paneExists2(paneId) {
1936
- if (!isValidPaneId2(paneId)) return false;
1937
- const pane = await getPane(paneId);
1938
- return pane !== void 0;
1998
+ function resetHerdrExecutor() {
1999
+ currentExecutor2 = realExecutor2;
1939
2000
  }
1940
- async function getPane(paneId) {
1941
- const result = await herdrTolerating(
1942
- ["pane", "get", paneId],
1943
- ["pane_not_found", "workspace_not_found"]
1944
- );
1945
- if (result === void 0) return void 0;
1946
- return readPane(result.pane);
2001
+ function isRecord(v) {
2002
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1947
2003
  }
1948
- async function paneTTY2(paneId) {
1949
- assertValidPaneId2(paneId);
1950
- const result = await herdrTolerating(
1951
- ["pane", "process-info", "--pane", paneId],
1952
- ["pane_not_found", "workspace_not_found"]
1953
- );
1954
- if (result === void 0) throw new PaneNotFoundError(paneId);
1955
- const info = result.process_info;
1956
- const pid = isRecord(info) ? info.shell_pid : void 0;
1957
- if (typeof pid !== "number") throw new PaneNotFoundError(paneId);
1958
- const { readlink } = await import("fs/promises");
2004
+ function asString(v) {
2005
+ return typeof v === "string" ? v : void 0;
2006
+ }
2007
+ function parseErrorEnvelope(stderr) {
2008
+ const trimmed = stderr.trim();
2009
+ if (trimmed.length === 0) return void 0;
2010
+ let parsed;
1959
2011
  try {
1960
- return await readlink(`/proc/${pid}/fd/0`);
1961
- } catch (err) {
1962
- throw new HerdrError(
1963
- ["pane", "process-info", "--pane", paneId],
1964
- `cannot resolve the TTY for herdr pane ${paneId} (shell pid ${pid}) via /proc: ${String(err)}`,
1965
- "",
1966
- 1
1967
- );
2012
+ parsed = JSON.parse(trimmed);
2013
+ } catch {
2014
+ return void 0;
1968
2015
  }
2016
+ if (!isRecord(parsed)) return void 0;
2017
+ const err = parsed.error;
2018
+ if (!isRecord(err)) return void 0;
2019
+ const code = asString(err.code);
2020
+ const message = asString(err.message);
2021
+ if (code === void 0) return void 0;
2022
+ return { code, message: message ?? code };
1969
2023
  }
1970
- async function setPaneTitle2(paneId, title) {
1971
- assertValidPaneId2(paneId);
1972
- await herdr(["pane", "rename", paneId, title]);
1973
- }
1974
- async function getPaneTitle2(paneId) {
1975
- if (!isValidPaneId2(paneId)) return void 0;
1976
- const pane = await getPane(paneId).catch(() => void 0);
1977
- if (pane === void 0) return void 0;
1978
- return pane.title.length > 0 ? pane.title : void 0;
1979
- }
1980
- async function currentAgentName2() {
1981
- const paneId = process.env.HERDR_PANE_ID;
1982
- if (paneId === void 0 || !isValidPaneId2(paneId)) return void 0;
1983
- const title = await getPaneTitle2(paneId);
1984
- if (title === void 0) return void 0;
1985
- const idx = title.indexOf(" \xB7 ");
1986
- return idx === -1 ? title.trim() : title.slice(0, idx).trim();
1987
- }
1988
- async function currentSessionName2() {
1989
- const workspaceId = process.env.HERDR_WORKSPACE_ID;
1990
- if (workspaceId === void 0 || !WORKSPACE_ID_RE.test(workspaceId)) return void 0;
1991
- for (const ws of await listWorkspaces()) {
1992
- if (ws.workspaceId === workspaceId) return ws.label;
2024
+ async function herdr(args) {
2025
+ const result = await currentExecutor2(args);
2026
+ if (result.exitCode === 2) {
2027
+ throw new HerdrSyntaxError([...args], result.stderr || result.stdout);
1993
2028
  }
1994
- return void 0;
1995
- }
1996
- var PANE_READY_RETRY_INTERVAL_MS = 50;
1997
- var PANE_READY_MAX_ATTEMPTS = 60;
1998
- async function startAgentInPane(opts) {
1999
- assertValidPaneId2(opts.paneId);
2000
- if (opts.commandSource !== "cli-key") {
2001
- throw new HerdrCommandOverrideError(
2002
- opts.cli,
2003
- opts.command,
2004
- // Name the env var only when that is actually where the override
2005
- // came from; "unset MU_PI_COMMAND" is useless advice to someone who
2006
- // typed --command.
2007
- opts.commandSource === "env" ? envVarNameForCli(opts.cli) : void 0
2029
+ if (result.exitCode !== 0) {
2030
+ throw new HerdrError(
2031
+ [...args],
2032
+ result.stderr,
2033
+ result.stdout,
2034
+ result.exitCode,
2035
+ parseErrorEnvelope(result.stderr)?.code
2008
2036
  );
2009
2037
  }
2010
- const args = ["agent", "start", opts.name, "--kind", opts.cli, "--pane", opts.paneId];
2011
- for (let attempt = 1; ; attempt += 1) {
2012
- try {
2013
- await herdr(args);
2014
- return;
2015
- } catch (err) {
2016
- if (err instanceof HerdrSyntaxError) {
2017
- throw new HerdrUnsupportedCliError(opts.cli, err.output.trim().split("\n")[0] ?? "");
2018
- }
2019
- const busy = err instanceof HerdrError && err.code === "agent_pane_busy";
2020
- if (!busy || attempt >= PANE_READY_MAX_ATTEMPTS) throw err;
2021
- await sleep(PANE_READY_RETRY_INTERVAL_MS);
2022
- }
2038
+ const trimmed = result.stdout.trim();
2039
+ if (trimmed.length === 0) return {};
2040
+ let parsed;
2041
+ try {
2042
+ parsed = JSON.parse(trimmed);
2043
+ } catch {
2044
+ throw new HerdrError([...args], result.stderr, result.stdout, result.exitCode);
2023
2045
  }
2046
+ if (!isRecord(parsed)) return {};
2047
+ const inner = parsed.result;
2048
+ return isRecord(inner) ? inner : {};
2024
2049
  }
2025
- function envVarNameForCli(cli) {
2026
- return `MU_${cli.toUpperCase().replace(/-/g, "_")}_COMMAND`;
2027
- }
2028
- var PROMPT_STALLED_CODE = "agent_prompt_stalled";
2029
- var NO_AGENT_CODES = ["agent_not_found", "agent_not_recognized"];
2030
- function defaultUndeliveredWarning2(warning) {
2031
- console.warn(`warning: ${warning.message}`);
2032
- }
2033
- async function sendToPane2(paneId, text, opts = {}) {
2034
- assertValidPaneId2(paneId);
2035
- const wait = opts.readinessMs === 0 ? [] : ["--wait"];
2050
+ async function herdrTolerating(args, tolerate) {
2036
2051
  try {
2037
- await herdr(["agent", "prompt", paneId, text, ...wait]);
2038
- return;
2052
+ return await herdr(args);
2039
2053
  } catch (err) {
2040
- if (!(err instanceof HerdrError) || err.code === void 0) throw err;
2041
- if (err.code === PROMPT_STALLED_CODE) {
2042
- const warn2 = opts.onUndelivered ?? defaultUndeliveredWarning2;
2043
- warn2({
2044
- paneId,
2045
- reason: "paste-vanished",
2046
- message: `send to ${paneId} was submitted but the agent's state never changed (herdr: ${PROMPT_STALLED_CODE}). The agent may NOT have seen it. Read the pane, or re-send.`
2047
- });
2048
- return;
2054
+ if (err instanceof HerdrError && err.code !== void 0 && tolerate.includes(err.code)) {
2055
+ return void 0;
2049
2056
  }
2050
- if (!NO_AGENT_CODES.includes(err.code)) throw err;
2057
+ throw err;
2051
2058
  }
2052
- await herdr(["pane", "run", paneId, text]);
2053
2059
  }
2054
- async function capturePane2(paneId, opts = {}) {
2055
- assertValidPaneId2(paneId);
2056
- const args = ["pane", "read", paneId];
2057
- if (opts.lines === 0) {
2058
- args.push("--source", "visible");
2059
- } else {
2060
- args.push("--source", "recent-unwrapped");
2061
- if (opts.lines !== void 0) args.push("--lines", String(opts.lines));
2060
+ function readWorkspace(v) {
2061
+ if (!isRecord(v)) return void 0;
2062
+ const workspaceId = asString(v.workspace_id);
2063
+ if (workspaceId === void 0) return void 0;
2064
+ return { workspaceId, label: asString(v.label) ?? "" };
2065
+ }
2066
+ function readTab(v) {
2067
+ if (!isRecord(v)) return void 0;
2068
+ const id = asString(v.tab_id);
2069
+ if (id === void 0) return void 0;
2070
+ return { id, name: asString(v.label) ?? "" };
2071
+ }
2072
+ function readPane(v) {
2073
+ if (!isRecord(v)) return void 0;
2074
+ const paneId = asString(v.pane_id);
2075
+ if (paneId === void 0) return void 0;
2076
+ return {
2077
+ paneId,
2078
+ // herdr's optional `label` is what `pane rename` writes and is
2079
+ // therefore mu's pane-title equivalent.
2080
+ title: asString(v.label) ?? "",
2081
+ // herdr does not report a foreground command on `pane list`; the
2082
+ // authoritative answer costs a `pane process-info` round trip per
2083
+ // pane. Callers that need it use `paneCommand()`.
2084
+ command: "",
2085
+ windowId: asString(v.tab_id)
2086
+ };
2087
+ }
2088
+ function readArray(result, key) {
2089
+ const v = result[key];
2090
+ return Array.isArray(v) ? v : [];
2091
+ }
2092
+ async function listWorkspaces() {
2093
+ const result = await herdr(["workspace", "list"]);
2094
+ const out = [];
2095
+ for (const raw of readArray(result, "workspaces")) {
2096
+ const ws = readWorkspace(raw);
2097
+ if (ws) out.push(ws);
2062
2098
  }
2063
- const result = await currentExecutor2(args);
2064
- if (result.exitCode === 2) throw new HerdrSyntaxError(args, result.stderr || result.stdout);
2065
- if (result.exitCode !== 0) {
2066
- throw new HerdrError(
2067
- args,
2068
- result.stderr,
2069
- result.stdout,
2070
- result.exitCode,
2071
- parseErrorEnvelope(result.stderr)?.code
2072
- );
2099
+ return out;
2100
+ }
2101
+ async function listSessions2() {
2102
+ return (await listWorkspaces()).map((ws) => ({ name: ws.label }));
2103
+ }
2104
+ async function resolveWorkspaceId(name) {
2105
+ for (const ws of await listWorkspaces()) {
2106
+ if (ws.label === name) return ws.workspaceId;
2073
2107
  }
2074
- return result.stdout;
2108
+ return void 0;
2075
2109
  }
2076
- function mapAgentStatus(agentStatus) {
2077
- switch (agentStatus) {
2078
- case "working":
2079
- return "busy";
2080
- case "blocked":
2081
- return "needs_permission";
2082
- default:
2083
- return "needs_input";
2110
+ async function requireWorkspaceId(name) {
2111
+ const id = await resolveWorkspaceId(name);
2112
+ if (id === void 0) {
2113
+ throw new HerdrError(
2114
+ ["workspace", "list"],
2115
+ `{"error":{"code":"workspace_not_found","message":"no herdr workspace labelled ${name}"}}`,
2116
+ "",
2117
+ 1,
2118
+ "workspace_not_found"
2119
+ );
2084
2120
  }
2121
+ return id;
2085
2122
  }
2086
- async function paneStatus(paneId) {
2087
- if (!isValidPaneId2(paneId)) return void 0;
2088
- const result = await herdrTolerating(
2089
- ["pane", "get", paneId],
2090
- ["pane_not_found", "workspace_not_found"]
2091
- );
2092
- if (result === void 0) return void 0;
2093
- const pane = result.pane;
2094
- const status = isRecord(pane) ? asString(pane.agent_status) : void 0;
2095
- if (status === void 0) return void 0;
2096
- return mapAgentStatus(status);
2123
+ async function sessionExists2(name) {
2124
+ return await resolveWorkspaceId(name) !== void 0;
2097
2125
  }
2098
- async function enableMuPaneBordersForSession2(_session) {
2099
- return 0;
2126
+ async function newSession2(name, opts = {}) {
2127
+ rejectCommand("newSession", opts.command);
2128
+ const args = ["workspace", "create", "--label", name, "--no-focus"];
2129
+ if (opts.cwd) args.push("--cwd", opts.cwd);
2130
+ appendEnvFlags2(args, opts.env);
2131
+ await herdr(args);
2100
2132
  }
2101
- async function enableMuPaneBordersForPane2(_paneId) {
2133
+ async function newSessionWithPane2(name, opts) {
2134
+ rejectCommand("newSessionWithPane", opts.command);
2135
+ const args = ["workspace", "create", "--label", name, "--no-focus"];
2136
+ if (opts.cwd) args.push("--cwd", opts.cwd);
2137
+ appendEnvFlags2(args, opts.env);
2138
+ const result = await herdr(args);
2139
+ return readCreatedPaneId(result, "root_pane", args);
2102
2140
  }
2103
- function appendEnvFlags2(args, env) {
2104
- if (!env) return;
2105
- for (const [k, v] of Object.entries(env)) {
2106
- if (k.length === 0) throw new TypeError("herdr env key must be non-empty");
2107
- if (k.includes("=")) {
2108
- throw new TypeError(`herdr env key must not contain '=': ${JSON.stringify(k)}`);
2141
+ async function killSession2(name) {
2142
+ const id = await resolveWorkspaceId(name);
2143
+ if (id === void 0) return;
2144
+ try {
2145
+ await herdrTolerating(["workspace", "close", id], ["workspace_not_found"]);
2146
+ } catch (err) {
2147
+ if (err instanceof HerdrError && err.code === GROUP_CLOSE_REQUIRED_CODE) {
2148
+ throw new HerdrWorkspaceGroupCloseError(name, id, err.message);
2109
2149
  }
2110
- args.push("--env", `${k}=${v}`);
2150
+ throw err;
2111
2151
  }
2112
2152
  }
2113
- function rejectCommand(method, command) {
2114
- if (command === void 0 || command.trim().length === 0) return;
2115
- throw new HerdrNotImplementedError(`${method} with a command`, "mux-herdr-spawn");
2116
- }
2117
- function readCreatedPaneId(result, key, args) {
2118
- const pane = readPane(result[key]);
2119
- if (pane === void 0) {
2120
- throw new HerdrError(
2121
- args,
2122
- `herdr response had no ${key}.pane_id: ${JSON.stringify(result)}`,
2123
- "",
2124
- 0
2125
- );
2153
+ async function listWindows2(session) {
2154
+ if (session !== void 0) {
2155
+ const id = await resolveWorkspaceId(session);
2156
+ if (id === void 0) return [];
2157
+ return readTabs(await herdr(["tab", "list", "--workspace", id]));
2126
2158
  }
2127
- assertValidPaneId2(pane.paneId);
2128
- return pane.paneId;
2129
- }
2130
- function isHerdrStatusUsable(stdout) {
2131
- if (!/^\s*status:\s*running\s*$/m.test(stdout)) return false;
2132
- if (/^\s*endpoint_compatible:\s*no\s*$/m.test(stdout)) return false;
2133
- return !/^\s*compatible:\s*no\s*$/m.test(stdout);
2159
+ const windows = [];
2160
+ for (const ws of await listWorkspaces()) {
2161
+ for (const tab of readTabs(await herdr(["tab", "list", "--workspace", ws.workspaceId]))) {
2162
+ windows.push({ ...tab, sessionName: ws.label });
2163
+ }
2164
+ }
2165
+ return windows;
2134
2166
  }
2135
- async function herdrAvailable() {
2136
- const result = await currentExecutor2(["status"]).catch(() => void 0);
2137
- if (result === void 0 || result.exitCode !== 0) return false;
2138
- return isHerdrStatusUsable(result.stdout);
2167
+ function readTabs(result) {
2168
+ const out = [];
2169
+ for (const raw of readArray(result, "tabs")) {
2170
+ const tab = readTab(raw);
2171
+ if (tab) out.push(tab);
2172
+ }
2173
+ return out;
2139
2174
  }
2140
- function attachHint2(target) {
2141
- if (target.window !== void 0) {
2142
- return `herdr session attach ${target.session} # then focus tab ${target.window}`;
2175
+ async function newWindow2(opts) {
2176
+ rejectCommand("newWindow", opts.command);
2177
+ const args = ["tab", "create"];
2178
+ if (opts.session !== void 0) {
2179
+ args.push("--workspace", await requireWorkspaceId(opts.session));
2143
2180
  }
2144
- return `herdr session attach ${target.session}`;
2181
+ args.push("--label", opts.name, "--no-focus");
2182
+ if (opts.cwd) args.push("--cwd", opts.cwd);
2183
+ appendEnvFlags2(args, opts.env);
2184
+ return readCreatedPaneId(await herdr(args), "root_pane", args);
2145
2185
  }
2146
- function attachCommands2(target) {
2147
- return [{ command: "herdr", args: ["session", "attach", target.session] }];
2186
+ async function selectLayout2(_window, _layout) {
2148
2187
  }
2149
- function paneNotFoundNextSteps2(paneId) {
2150
- return [
2151
- {
2152
- intent: `Verify the pane id ${paneId} actually exists`,
2153
- command: `herdr pane get ${paneId}`
2154
- },
2155
- {
2156
- intent: "List panes in the current workspace",
2157
- command: 'herdr pane list --workspace "$HERDR_WORKSPACE_ID"'
2158
- }
2159
- ];
2188
+ async function listPanesInSession2(session) {
2189
+ const id = await resolveWorkspaceId(session);
2190
+ if (id === void 0) return [];
2191
+ const result = await herdrTolerating(
2192
+ ["pane", "list", "--workspace", id],
2193
+ ["workspace_not_found"]
2194
+ );
2195
+ if (result === void 0) return [];
2196
+ return readPanes(result).map((p) => ({ ...p, sessionName: session }));
2160
2197
  }
2161
- async function healthCheck2() {
2162
- let version = null;
2163
- try {
2164
- const result = await currentExecutor2(["status"]);
2165
- if (result.exitCode === 0) {
2166
- const match = /^\s*version:\s*(\S+)\s*$/m.exec(result.stdout);
2167
- version = match?.[1] ?? null;
2198
+ async function listPanes2(target) {
2199
+ if (target !== void 0 && target !== "*") {
2200
+ if (TAB_ID_RE.test(target)) {
2201
+ const workspaceId = target.split(":")[0] ?? target;
2202
+ const panes2 = readPanes(await herdr(["pane", "list", "--workspace", workspaceId]));
2203
+ return panes2.filter((p) => p.windowId === target);
2168
2204
  }
2169
- } catch {
2170
- version = null;
2205
+ if (WORKSPACE_ID_RE.test(target)) {
2206
+ return readPanes(await herdr(["pane", "list", "--workspace", target]));
2207
+ }
2208
+ return listPanesInSession2(target);
2171
2209
  }
2172
- return {
2173
- name: "herdr",
2174
- ok: version !== null,
2175
- version,
2176
- env: [
2177
- { name: "$HERDR_ENV", value: process.env.HERDR_ENV ?? null },
2178
- { name: "$HERDR_WORKSPACE_ID", value: process.env.HERDR_WORKSPACE_ID ?? null },
2179
- { name: "$HERDR_PANE_ID", value: process.env.HERDR_PANE_ID ?? null }
2180
- ],
2181
- remediation: "start the herdr server (herdr status) or install herdr"
2182
- };
2210
+ const ambient = process.env.HERDR_WORKSPACE_ID;
2211
+ if (target === void 0 && ambient !== void 0 && WORKSPACE_ID_RE.test(ambient)) {
2212
+ return readPanes(await herdr(["pane", "list", "--workspace", ambient]));
2213
+ }
2214
+ const panes = [];
2215
+ for (const ws of await listWorkspaces()) {
2216
+ for (const pane of readPanes(await herdr(["pane", "list", "--workspace", ws.workspaceId]))) {
2217
+ panes.push({ ...pane, sessionName: ws.label });
2218
+ }
2219
+ }
2220
+ return panes;
2183
2221
  }
2184
- var herdrBackend = Object.freeze({
2185
- name: "herdr",
2186
- available: herdrAvailable,
2187
- isValidPaneId: isValidPaneId2,
2188
- assertValidPaneId: assertValidPaneId2,
2189
- listSessions: listSessions2,
2190
- sessionExists: sessionExists2,
2191
- newSession: newSession2,
2192
- newSessionWithPane: newSessionWithPane2,
2193
- killSession: killSession2,
2194
- listWindows: listWindows2,
2195
- newWindow: newWindow2,
2196
- selectLayout: selectLayout2,
2197
- listPanes: listPanes2,
2198
- listPanesInSession: listPanesInSession2,
2199
- splitWindow: splitWindow2,
2200
- killPane: killPane2,
2201
- paneExists: paneExists2,
2202
- paneTTY: paneTTY2,
2203
- setPaneTitle: setPaneTitle2,
2204
- getPaneTitle: getPaneTitle2,
2205
- currentAgentName: currentAgentName2,
2206
- currentSessionName: currentSessionName2,
2207
- startAgentInPane,
2208
- sendToPane: sendToPane2,
2209
- capturePane: capturePane2,
2210
- paneStatus,
2211
- enableMuPaneBordersForSession: enableMuPaneBordersForSession2,
2212
- enableMuPaneBordersForPane: enableMuPaneBordersForPane2,
2213
- attachHint: attachHint2,
2214
- attachCommands: attachCommands2,
2215
- paneNotFoundNextSteps: paneNotFoundNextSteps2,
2216
- healthCheck: healthCheck2
2217
- });
2218
-
2219
- // src/mux/detect.ts
2220
- var BACKENDS = [tmuxBackend, herdrBackend];
2221
- function muxByName(name) {
2222
- for (const backend of BACKENDS) {
2223
- if (backend.name === name) return backend;
2222
+ function readPanes(result) {
2223
+ const out = [];
2224
+ for (const raw of readArray(result, "panes")) {
2225
+ const pane = readPane(raw);
2226
+ if (pane) out.push(pane);
2224
2227
  }
2225
- throw new Error(`unknown mux backend: ${name}`);
2228
+ return out;
2229
+ }
2230
+ async function splitWindow2(opts) {
2231
+ rejectCommand("splitWindow", opts.command);
2232
+ assertValidPaneId2(opts.target);
2233
+ const args = [
2234
+ "pane",
2235
+ "split",
2236
+ opts.target,
2237
+ "--direction",
2238
+ opts.horizontal === false ? "down" : "right",
2239
+ "--no-focus"
2240
+ ];
2241
+ if (opts.cwd) args.push("--cwd", opts.cwd);
2242
+ appendEnvFlags2(args, opts.env);
2243
+ return readCreatedPaneId(await herdr(args), "pane", args);
2244
+ }
2245
+ async function killPane2(paneId) {
2246
+ assertValidPaneId2(paneId);
2247
+ await herdrTolerating(["pane", "close", paneId], ["pane_not_found", "workspace_not_found"]);
2226
2248
  }
2227
- var KNOWN_NAMES = BACKENDS.map((b) => b.name);
2228
- async function detectMux() {
2229
- const override = process.env.MU_MUX;
2230
- if (override !== void 0 && override.length > 0) {
2231
- return muxByName(override);
2232
- }
2233
- if (process.env.HERDR_ENV === "1") return herdrBackend;
2234
- if (process.env.TMUX || process.env.TMUX_PANE) return tmuxBackend;
2235
- for (const backend of BACKENDS) {
2236
- if (await backend.available()) return backend;
2237
- }
2238
- throw new NoMultiplexerError(KNOWN_NAMES);
2249
+ async function paneExists2(paneId) {
2250
+ if (!isValidPaneId2(paneId)) return false;
2251
+ const pane = await getPane(paneId);
2252
+ return pane !== void 0;
2239
2253
  }
2240
- var cached;
2241
- async function activeMux() {
2242
- if (cached === void 0) cached = await detectMux();
2243
- return cached;
2254
+ async function getPane(paneId) {
2255
+ const result = await herdrTolerating(
2256
+ ["pane", "get", paneId],
2257
+ ["pane_not_found", "workspace_not_found"]
2258
+ );
2259
+ if (result === void 0) return void 0;
2260
+ return readPane(result.pane);
2244
2261
  }
2245
- function setMuxForTests(backend) {
2246
- const previous = cached;
2247
- cached = backend;
2248
- return previous;
2262
+ async function paneTTY2(paneId) {
2263
+ assertValidPaneId2(paneId);
2264
+ const result = await herdrTolerating(
2265
+ ["pane", "process-info", "--pane", paneId],
2266
+ ["pane_not_found", "workspace_not_found"]
2267
+ );
2268
+ if (result === void 0) throw new PaneNotFoundError(paneId);
2269
+ const info = result.process_info;
2270
+ const pid = isRecord(info) ? info.shell_pid : void 0;
2271
+ if (typeof pid !== "number") throw new PaneNotFoundError(paneId);
2272
+ const { readlink } = await import("fs/promises");
2273
+ try {
2274
+ return await readlink(`/proc/${pid}/fd/0`);
2275
+ } catch (err) {
2276
+ throw new HerdrError(
2277
+ ["pane", "process-info", "--pane", paneId],
2278
+ `cannot resolve the TTY for herdr pane ${paneId} (shell pid ${pid}) via /proc: ${String(err)}`,
2279
+ "",
2280
+ 1
2281
+ );
2282
+ }
2249
2283
  }
2250
- function resetMux() {
2251
- cached = void 0;
2284
+ async function setPaneTitle2(paneId, title) {
2285
+ assertValidPaneId2(paneId);
2286
+ await herdr(["pane", "rename", paneId, title]);
2252
2287
  }
2253
-
2254
- // src/agents/spawn.ts
2255
- import { execFile as execFile2 } from "child_process";
2256
- import { mkdirSync as mkdirSync2, rmSync as rmSync4 } from "fs";
2257
- import { basename, dirname as dirname5 } from "path";
2258
- import { setTimeout as delay } from "timers/promises";
2259
- import { promisify as promisify2 } from "util";
2260
-
2261
- // src/agents.ts
2262
- import { rmSync as rmSync3 } from "fs";
2263
- import { dirname as dirname4 } from "path";
2264
-
2265
- // src/glyphs.ts
2266
- var AGENT_STATE_GLYPH = {
2267
- busy: "\uF04B",
2268
- // nf-fa-play
2269
- needs_input: "\uF186",
2270
- // nf-fa-moon_o
2271
- needs_permission: "\uF023",
2272
- // nf-fa-lock
2273
- unknown: "\uF059"
2274
- // nf-fa-question_circle
2275
- };
2276
- function agentStateGlyph(state) {
2277
- return AGENT_STATE_GLYPH[state] ?? GLYPH.unknown;
2288
+ async function getPaneTitle2(paneId) {
2289
+ if (!isValidPaneId2(paneId)) return void 0;
2290
+ const pane = await getPane(paneId).catch(() => void 0);
2291
+ if (pane === void 0) return void 0;
2292
+ return pane.title.length > 0 ? pane.title : void 0;
2278
2293
  }
2279
- var GLYPH = {
2280
- /** Healthy, clean, passing, closed-successfully. */
2281
- ok: "\uF058",
2282
- // nf-fa-check_circle
2283
- /** Failed check. */
2284
- fail: "\uF057",
2285
- // nf-fa-times_circle
2286
- /** Needs attention but not broken (idle-but-assigned, stale warning). */
2287
- warn: "\uF071",
2288
- // nf-fa-warning
2289
- /** Workspace has uncommitted edits. */
2290
- dirty: "\uF005",
2291
- // nf-fa-star
2292
- /** Workspace is ≥ WORKSPACE_STALE_THRESHOLD commits behind main. */
2293
- stale: "\uF017",
2294
- // nf-fa-clock_o
2295
- /** Task is blocked by an incoming edge. */
2296
- blocked: "\uF0C1",
2297
- // nf-fa-link (chain)
2298
- /** Task is parked (OPEN/parked): open, but set aside on purpose. */
2299
- parked: "\uF04C",
2300
- // nf-fa-pause
2301
- /** Track whose roots merged (diamond dependency). */
2302
- merge: "\uF074",
2303
- // nf-fa-random
2304
- /** Agent owns more than one task (pane-title multi-count slot). */
2305
- multi: "\uF055",
2306
- // nf-fa-plus_circle
2307
- /** Filter toggle: enabled / disabled. */
2308
- on: "\uF111",
2309
- // nf-fa-circle
2310
- off: "\uF10C",
2311
- // nf-fa-circle_o
2312
- /** Status outside the known enum. */
2313
- unknown: "\uF059"
2314
- // nf-fa-question_circle
2315
- };
2316
-
2317
- // src/logs.ts
2318
- import { randomUUID as randomUUID2 } from "crypto";
2319
-
2320
- // src/hlc.ts
2321
- var WALL_WIDTH = 15;
2322
- var COUNTER_WIDTH = 6;
2323
- var SEP = ".";
2324
- var MAX_WALL = 10 ** WALL_WIDTH - 1;
2325
- var MAX_COUNTER = 10 ** COUNTER_WIDTH - 1;
2326
- var HlcParseError = class extends Error {
2327
- constructor(value) {
2328
- super(`Malformed HLC: ${JSON.stringify(value)}`);
2329
- this.value = value;
2330
- this.name = "HlcParseError";
2294
+ async function currentAgentName2() {
2295
+ const paneId = process.env.HERDR_PANE_ID;
2296
+ if (paneId === void 0 || !isValidPaneId2(paneId)) return void 0;
2297
+ const title = await getPaneTitle2(paneId);
2298
+ if (title === void 0) return void 0;
2299
+ const idx = title.indexOf(" \xB7 ");
2300
+ return idx === -1 ? title.trim() : title.slice(0, idx).trim();
2301
+ }
2302
+ async function currentSessionName2() {
2303
+ const workspaceId = process.env.HERDR_WORKSPACE_ID;
2304
+ if (workspaceId === void 0 || !WORKSPACE_ID_RE.test(workspaceId)) return void 0;
2305
+ for (const ws of await listWorkspaces()) {
2306
+ if (ws.workspaceId === workspaceId) return ws.label;
2331
2307
  }
2332
- value;
2333
- };
2334
- var HlcOverflowError = class extends Error {
2335
- constructor(field, value) {
2336
- super(
2337
- field === "counter" ? `HLC counter overflow (${value} > ${MAX_COUNTER}): more than ${MAX_COUNTER + 1} ops in one millisecond` : `HLC wall-clock overflow (${value} > ${MAX_WALL}): system clock is far in the future`
2308
+ return void 0;
2309
+ }
2310
+ var PANE_READY_RETRY_INTERVAL_MS = 50;
2311
+ var PANE_READY_MAX_ATTEMPTS = 60;
2312
+ async function startAgentInPane(opts) {
2313
+ assertValidPaneId2(opts.paneId);
2314
+ if (opts.commandSource !== "cli-key") {
2315
+ throw new HerdrCommandOverrideError(
2316
+ opts.cli,
2317
+ opts.command,
2318
+ // Name the env var only when that is actually where the override
2319
+ // came from; "unset MU_PI_COMMAND" is useless advice to someone who
2320
+ // typed --command.
2321
+ opts.commandSource === "env" ? envVarNameForCli(opts.cli) : void 0
2338
2322
  );
2339
- this.field = field;
2340
- this.value = value;
2341
- this.name = "HlcOverflowError";
2342
2323
  }
2343
- field;
2344
- value;
2345
- };
2346
- var MachineIdentityMissingError = class extends Error {
2347
- constructor() {
2348
- super("machine_identity row (id = 1) is missing; the DB was not opened via openDb()");
2349
- this.name = "MachineIdentityMissingError";
2324
+ const args = ["agent", "start", opts.name, "--kind", opts.cli, "--pane", opts.paneId];
2325
+ for (let attempt = 1; ; attempt += 1) {
2326
+ try {
2327
+ await herdr(args);
2328
+ return;
2329
+ } catch (err) {
2330
+ if (err instanceof HerdrSyntaxError) {
2331
+ throw new HerdrUnsupportedCliError(opts.cli, err.output.trim().split("\n")[0] ?? "");
2332
+ }
2333
+ const busy = err instanceof HerdrError && err.code === "agent_pane_busy";
2334
+ if (!busy || attempt >= PANE_READY_MAX_ATTEMPTS) throw err;
2335
+ await sleep(PANE_READY_RETRY_INTERVAL_MS);
2336
+ }
2350
2337
  }
2351
- };
2352
- function formatHlc(hlc) {
2353
- const { wallMs, counter, machineId: machineId2 } = hlc;
2354
- if (!Number.isInteger(wallMs) || wallMs < 0) throw new HlcParseError(String(wallMs));
2355
- if (!Number.isInteger(counter) || counter < 0) throw new HlcParseError(String(counter));
2356
- if (wallMs > MAX_WALL) throw new HlcOverflowError("wall", wallMs);
2357
- if (counter > MAX_COUNTER) throw new HlcOverflowError("counter", counter);
2358
- if (machineId2.length === 0 || machineId2.includes(SEP)) throw new HlcParseError(machineId2);
2359
- return `${String(wallMs).padStart(WALL_WIDTH, "0")}${SEP}${String(counter).padStart(
2360
- COUNTER_WIDTH,
2361
- "0"
2362
- )}${SEP}${machineId2}`;
2363
2338
  }
2364
- function parseHlc(value) {
2365
- const parts = value.split(SEP);
2366
- if (parts.length !== 3) throw new HlcParseError(value);
2367
- const [wall, counter, machineId2] = parts;
2368
- if (wall === void 0 || counter === void 0 || machineId2 === void 0) {
2369
- throw new HlcParseError(value);
2339
+ function envVarNameForCli(cli) {
2340
+ return `MU_${cli.toUpperCase().replace(/-/g, "_")}_COMMAND`;
2341
+ }
2342
+ var PROMPT_STALLED_CODE = "agent_prompt_stalled";
2343
+ var NO_AGENT_CODES = ["agent_not_found", "agent_not_recognized"];
2344
+ function defaultUndeliveredWarning2(warning) {
2345
+ console.warn(`warning: ${warning.message}`);
2346
+ }
2347
+ async function sendToPane2(paneId, text, opts = {}) {
2348
+ assertValidPaneId2(paneId);
2349
+ const wait = opts.readinessMs === 0 ? [] : ["--wait"];
2350
+ try {
2351
+ await herdr(["agent", "prompt", paneId, text, ...wait]);
2352
+ return;
2353
+ } catch (err) {
2354
+ if (!(err instanceof HerdrError) || err.code === void 0) throw err;
2355
+ if (err.code === PROMPT_STALLED_CODE) {
2356
+ const warn2 = opts.onUndelivered ?? defaultUndeliveredWarning2;
2357
+ warn2({
2358
+ paneId,
2359
+ reason: "paste-vanished",
2360
+ message: `send to ${paneId} was submitted but the agent's state never changed (herdr: ${PROMPT_STALLED_CODE}). The agent may NOT have seen it. Read the pane, or re-send.`
2361
+ });
2362
+ return;
2363
+ }
2364
+ if (!NO_AGENT_CODES.includes(err.code)) throw err;
2365
+ }
2366
+ await herdr(["pane", "run", paneId, text]);
2367
+ }
2368
+ async function capturePane2(paneId, opts = {}) {
2369
+ assertValidPaneId2(paneId);
2370
+ const args = ["pane", "read", paneId];
2371
+ if (opts.lines === 0) {
2372
+ args.push("--source", "visible");
2373
+ } else {
2374
+ args.push("--source", "recent-unwrapped");
2375
+ if (opts.lines !== void 0) args.push("--lines", String(opts.lines));
2376
+ }
2377
+ const result = await currentExecutor2(args);
2378
+ if (result.exitCode === 2) throw new HerdrSyntaxError(args, result.stderr || result.stdout);
2379
+ if (result.exitCode !== 0) {
2380
+ throw new HerdrError(
2381
+ args,
2382
+ result.stderr,
2383
+ result.stdout,
2384
+ result.exitCode,
2385
+ parseErrorEnvelope(result.stderr)?.code
2386
+ );
2370
2387
  }
2371
- if (wall.length !== WALL_WIDTH || counter.length !== COUNTER_WIDTH || machineId2.length === 0) {
2372
- throw new HlcParseError(value);
2388
+ return result.stdout;
2389
+ }
2390
+ function mapAgentStatus(agentStatus) {
2391
+ switch (agentStatus) {
2392
+ case "working":
2393
+ return "busy";
2394
+ case "blocked":
2395
+ return "needs_permission";
2396
+ default:
2397
+ return "needs_input";
2373
2398
  }
2374
- if (!/^\d+$/.test(wall) || !/^\d+$/.test(counter)) throw new HlcParseError(value);
2375
- return { wallMs: Number(wall), counter: Number(counter), machineId: machineId2 };
2376
2399
  }
2377
- function compareHlc(a, b) {
2378
- if (a < b) return -1;
2379
- if (a > b) return 1;
2400
+ async function paneStatus(paneId) {
2401
+ if (!isValidPaneId2(paneId)) return void 0;
2402
+ const result = await herdrTolerating(
2403
+ ["pane", "get", paneId],
2404
+ ["pane_not_found", "workspace_not_found"]
2405
+ );
2406
+ if (result === void 0) return void 0;
2407
+ const pane = result.pane;
2408
+ const status = isRecord(pane) ? asString(pane.agent_status) : void 0;
2409
+ if (status === void 0) return void 0;
2410
+ return mapAgentStatus(status);
2411
+ }
2412
+ async function enableMuPaneBordersForSession2(_session) {
2380
2413
  return 0;
2381
2414
  }
2382
- function nextHlc(db, now = Date.now()) {
2383
- const row = db.prepare(
2384
- `UPDATE machine_identity
2385
- SET last_wall = MAX(last_wall, @now),
2386
- last_counter = CASE WHEN @now > last_wall THEN 0 ELSE last_counter + 1 END
2387
- WHERE id = 1
2388
- RETURNING machine_id, last_wall, last_counter`
2389
- ).get({ now: Math.floor(now) });
2390
- if (!row) throw new MachineIdentityMissingError();
2391
- return formatHlc({ wallMs: row.last_wall, counter: row.last_counter, machineId: row.machine_id });
2415
+ async function enableMuPaneBordersForPane2(_paneId) {
2392
2416
  }
2393
- function receiveHlc(db, remoteHlc, now = Date.now()) {
2394
- const remote = parseHlc(remoteHlc);
2395
- const wallNow = Math.floor(now);
2396
- const read = db.prepare(
2397
- "SELECT machine_id, last_wall, last_counter FROM machine_identity WHERE id = 1"
2398
- );
2399
- const write = db.prepare(
2400
- "UPDATE machine_identity SET last_wall = @wall, last_counter = @counter WHERE id = 1"
2401
- );
2402
- const advance = db.transaction(() => {
2403
- const row = read.get();
2404
- if (!row) throw new MachineIdentityMissingError();
2405
- const wall = Math.max(row.last_wall, remote.wallMs, wallNow);
2406
- const localAtMax = row.last_wall === wall;
2407
- const remoteAtMax = remote.wallMs === wall;
2408
- let counter;
2409
- if (!localAtMax && !remoteAtMax) {
2410
- counter = 0;
2411
- } else if (localAtMax && remoteAtMax) {
2412
- counter = Math.max(row.last_counter, remote.counter) + 1;
2413
- } else if (localAtMax) {
2414
- counter = row.last_counter + 1;
2415
- } else {
2416
- counter = remote.counter + 1;
2417
+ function appendEnvFlags2(args, env) {
2418
+ if (!env) return;
2419
+ for (const [k, v] of Object.entries(env)) {
2420
+ if (k.length === 0) throw new TypeError("herdr env key must be non-empty");
2421
+ if (k.includes("=")) {
2422
+ throw new TypeError(`herdr env key must not contain '=': ${JSON.stringify(k)}`);
2417
2423
  }
2418
- if (counter > MAX_COUNTER) throw new HlcOverflowError("counter", counter);
2419
- write.run({ wall, counter });
2420
- return formatHlc({ wallMs: wall, counter, machineId: row.machine_id });
2421
- });
2422
- return advance.immediate();
2424
+ args.push("--env", `${k}=${v}`);
2425
+ }
2423
2426
  }
2424
-
2425
- // src/legacy-ops.ts
2426
- var LEGACY_LOG_ONLY_INTENTS = /* @__PURE__ */ new Set(["workstream.export"]);
2427
- function isLegacyLogOnlyIntent(intent) {
2428
- return intent !== null && LEGACY_LOG_ONLY_INTENTS.has(intent);
2427
+ function rejectCommand(method, command) {
2428
+ if (command === void 0 || command.trim().length === 0) return;
2429
+ throw new HerdrNotImplementedError(`${method} with a command`, "mux-herdr-spawn");
2429
2430
  }
2430
- var LEGACY_INTENT_SYNONYMS = /* @__PURE__ */ new Map([
2431
- ["workstream.teardown", ["workstream.destroy"]]
2432
- ]);
2433
- function intentSpellings(intent) {
2434
- const legacy = LEGACY_INTENT_SYNONYMS.get(intent);
2435
- return legacy === void 0 ? [intent] : [intent, ...legacy];
2431
+ function readCreatedPaneId(result, key, args) {
2432
+ const pane = readPane(result[key]);
2433
+ if (pane === void 0) {
2434
+ throw new HerdrError(
2435
+ args,
2436
+ `herdr response had no ${key}.pane_id: ${JSON.stringify(result)}`,
2437
+ "",
2438
+ 0
2439
+ );
2440
+ }
2441
+ assertValidPaneId2(pane.paneId);
2442
+ return pane.paneId;
2436
2443
  }
2437
- var LEGACY_LOG_ONLY_SQL_EXCLUSION = `(intent IS NULL OR intent NOT IN (${[
2438
- ...LEGACY_LOG_ONLY_INTENTS
2439
- ].map((i) => `'${i}'`).join(", ")}))`;
2440
-
2441
- // src/logs.ts
2442
- var SELECT_LOG_COLS = `
2443
- l.seq AS seq,
2444
- l.key AS workstream,
2445
- l.actor AS source,
2446
- l.intent AS intent,
2447
- l.group_id AS group_id,
2448
- l.op AS op,
2449
- l.entity AS kind,
2450
- l.payload AS payload,
2451
- l.created_at AS created_at
2452
- `;
2453
- var LOG_FROM_JOIN = "FROM ops l";
2454
- function workstreamScopeSql(column = "l.key") {
2455
- return `(${column} = ? OR ${column} LIKE ? ESCAPE '\\')`;
2444
+ function isHerdrStatusUsable(stdout) {
2445
+ if (!/^\s*status:\s*running\s*$/m.test(stdout)) return false;
2446
+ if (/^\s*endpoint_compatible:\s*no\s*$/m.test(stdout)) return false;
2447
+ return !/^\s*compatible:\s*no\s*$/m.test(stdout);
2456
2448
  }
2457
- function workstreamScopeParams(workstream) {
2458
- const escaped = workstream.replace(/[\\%_]/g, (c) => `\\${c}`);
2459
- return [workstream, `${escaped}/%`];
2449
+ async function herdrAvailable() {
2450
+ const result = await currentExecutor2(["status"]).catch(() => void 0);
2451
+ if (result === void 0 || result.exitCode !== 0) return false;
2452
+ return isHerdrStatusUsable(result.stdout);
2460
2453
  }
2461
- var TOUCH_OP_FILTER = `NOT (
2462
- l.intent IS NOT NULL
2463
- AND l.op = 'put'
2464
- AND l.payload LIKE '{"updated_at":%'
2465
- AND LENGTH(l.payload) < 45
2466
- )`;
2467
- var MACHINE_WIDE_KEY = "";
2468
- var GroupIdAmbiguousError = class extends Error {
2469
- constructor(prefix, candidates) {
2470
- super(
2471
- `group id ${JSON.stringify(prefix)} is ambiguous: matches ${candidates.length} groups (${candidates.slice(0, 4).map((c) => c.slice(0, 12)).join(", ")}${candidates.length > 4 ? ", \u2026" : ""})`
2472
- );
2473
- this.prefix = prefix;
2474
- this.candidates = candidates;
2475
- this.name = "GroupIdAmbiguousError";
2476
- }
2477
- prefix;
2478
- candidates;
2479
- errorNextSteps() {
2480
- return [
2481
- { intent: "List recent groups with their ids", command: "mu undo" },
2482
- {
2483
- intent: "Retry with more characters",
2484
- command: `mu log --group ${this.candidates[0] ?? "<full-id>"}`
2485
- }
2486
- ];
2454
+ function attachHint2(target) {
2455
+ if (target.window !== void 0) {
2456
+ return `herdr session attach ${target.session} # then focus tab ${target.window}`;
2487
2457
  }
2488
- };
2489
- function groupIdFromPrefix(db, prefix) {
2490
- if (prefix === "") return null;
2491
- const rows = db.prepare(
2492
- `SELECT DISTINCT group_id AS groupId FROM ops
2493
- WHERE group_id = @exact OR group_id LIKE @prefix || '%' ESCAPE '\\'`
2494
- ).all({ exact: prefix, prefix: prefix.replace(/[\\%_]/g, (c) => `\\${c}`) });
2495
- const exact = rows.find((r) => r.groupId === prefix);
2496
- if (exact !== void 0) return exact.groupId;
2497
- const first = rows[0];
2498
- if (rows.length === 1 && first !== void 0) return first.groupId;
2499
- if (rows.length === 0) return null;
2500
- throw new GroupIdAmbiguousError(
2501
- prefix,
2502
- rows.map((r) => r.groupId)
2503
- );
2458
+ return `herdr session attach ${target.session}`;
2504
2459
  }
2505
- function machineId(db) {
2506
- const row = db.prepare("SELECT machine_id FROM machine_identity WHERE id = 1").get();
2507
- return row?.machine_id ?? "unknown";
2460
+ function attachCommands2(target) {
2461
+ return [{ command: "herdr", args: ["session", "attach", target.session] }];
2508
2462
  }
2509
- function rowFromDb(row) {
2510
- return {
2511
- seq: row.seq,
2512
- workstreamName: row.workstream === MACHINE_WIDE_KEY ? null : row.workstream,
2513
- source: row.source ?? "system",
2514
- intent: row.intent,
2515
- group: row.group_id,
2516
- op: row.op,
2517
- kind: row.kind,
2518
- payload: row.payload,
2519
- createdAt: row.created_at
2520
- };
2463
+ function paneNotFoundNextSteps2(paneId) {
2464
+ return [
2465
+ {
2466
+ intent: `Verify the pane id ${paneId} actually exists`,
2467
+ command: `herdr pane get ${paneId}`
2468
+ },
2469
+ {
2470
+ intent: "List panes in the current workspace",
2471
+ command: 'herdr pane list --workspace "$HERDR_WORKSPACE_ID"'
2472
+ }
2473
+ ];
2521
2474
  }
2522
- function appendLog(db, opts) {
2523
- const kind = opts.kind ?? "message";
2524
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
2525
- const key = opts.workstream ?? MACHINE_WIDE_KEY;
2526
- const group = randomUUID2();
2527
- const result = db.prepare(
2528
- `INSERT INTO ops (hlc, machine_id, group_id, actor, intent, entity, key, op, payload, created_at)
2529
- VALUES (?, ?, ?, ?, ?, ?, ?, 'put', ?, ?)`
2530
- ).run(
2531
- nextHlc(db),
2532
- machineId(db),
2533
- group,
2534
- opts.source,
2535
- opts.intent ?? null,
2536
- kind,
2537
- key,
2538
- opts.payload,
2539
- createdAt
2540
- );
2475
+ async function healthCheck2() {
2476
+ let version = null;
2477
+ try {
2478
+ const result = await currentExecutor2(["status"]);
2479
+ if (result.exitCode === 0) {
2480
+ const match = /^\s*version:\s*(\S+)\s*$/m.exec(result.stdout);
2481
+ version = match?.[1] ?? null;
2482
+ }
2483
+ } catch {
2484
+ version = null;
2485
+ }
2541
2486
  return {
2542
- seq: Number(result.lastInsertRowid),
2543
- workstreamName: opts.workstream,
2544
- source: opts.source,
2545
- intent: opts.intent ?? null,
2546
- group,
2547
- op: "put",
2548
- kind,
2549
- payload: opts.payload,
2550
- createdAt
2487
+ name: "herdr",
2488
+ ok: version !== null,
2489
+ version,
2490
+ env: [
2491
+ { name: "$HERDR_ENV", value: process.env.HERDR_ENV ?? null },
2492
+ { name: "$HERDR_WORKSPACE_ID", value: process.env.HERDR_WORKSPACE_ID ?? null },
2493
+ { name: "$HERDR_PANE_ID", value: process.env.HERDR_PANE_ID ?? null }
2494
+ ],
2495
+ remediation: "start the herdr server (herdr status) or install herdr"
2551
2496
  };
2552
2497
  }
2553
- function listLogs(db, opts = {}) {
2554
- const conditions = [];
2555
- const params = [];
2556
- if (opts.workstream === null) {
2557
- conditions.push("l.key = ?");
2558
- params.push(MACHINE_WIDE_KEY);
2559
- } else if (opts.workstream !== void 0) {
2560
- conditions.push(workstreamScopeSql());
2561
- params.push(...workstreamScopeParams(opts.workstream));
2562
- }
2563
- if (opts.since !== void 0) {
2564
- conditions.push("l.seq > ?");
2565
- params.push(opts.since);
2566
- }
2567
- if (opts.source !== void 0) {
2568
- conditions.push("l.actor = ?");
2569
- params.push(opts.source);
2570
- }
2571
- if (opts.kind !== void 0) {
2572
- conditions.push("l.entity = ?");
2573
- params.push(opts.kind);
2574
- }
2575
- if (opts.intent !== void 0) {
2576
- const spellings = intentSpellings(opts.intent);
2577
- conditions.push(`l.intent IN (${spellings.map(() => "?").join(", ")})`);
2578
- params.push(...spellings);
2579
- }
2580
- if (opts.group !== void 0) {
2581
- conditions.push("l.group_id = ?");
2582
- params.push(opts.group);
2498
+ var herdrBackend = Object.freeze({
2499
+ name: "herdr",
2500
+ available: herdrAvailable,
2501
+ isValidPaneId: isValidPaneId2,
2502
+ assertValidPaneId: assertValidPaneId2,
2503
+ listSessions: listSessions2,
2504
+ sessionExists: sessionExists2,
2505
+ newSession: newSession2,
2506
+ newSessionWithPane: newSessionWithPane2,
2507
+ killSession: killSession2,
2508
+ listWindows: listWindows2,
2509
+ newWindow: newWindow2,
2510
+ selectLayout: selectLayout2,
2511
+ listPanes: listPanes2,
2512
+ listPanesInSession: listPanesInSession2,
2513
+ splitWindow: splitWindow2,
2514
+ killPane: killPane2,
2515
+ paneExists: paneExists2,
2516
+ paneTTY: paneTTY2,
2517
+ setPaneTitle: setPaneTitle2,
2518
+ getPaneTitle: getPaneTitle2,
2519
+ currentAgentName: currentAgentName2,
2520
+ currentSessionName: currentSessionName2,
2521
+ startAgentInPane,
2522
+ sendToPane: sendToPane2,
2523
+ capturePane: capturePane2,
2524
+ paneStatus,
2525
+ enableMuPaneBordersForSession: enableMuPaneBordersForSession2,
2526
+ enableMuPaneBordersForPane: enableMuPaneBordersForPane2,
2527
+ attachHint: attachHint2,
2528
+ attachCommands: attachCommands2,
2529
+ paneNotFoundNextSteps: paneNotFoundNextSteps2,
2530
+ healthCheck: healthCheck2
2531
+ });
2532
+
2533
+ // src/mux/detect.ts
2534
+ var BACKENDS = [tmuxBackend, herdrBackend];
2535
+ function muxByName(name) {
2536
+ for (const backend of BACKENDS) {
2537
+ if (backend.name === name) return backend;
2583
2538
  }
2584
- conditions.push(TOUCH_OP_FILTER);
2585
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2586
- if (opts.limit !== void 0 && opts.since === void 0) {
2587
- const rowsDesc = db.prepare(`SELECT ${SELECT_LOG_COLS} ${LOG_FROM_JOIN} ${where} ORDER BY l.seq DESC LIMIT ?`).all(...params, opts.limit);
2588
- return rowsDesc.reverse().map(rowFromDb);
2539
+ throw new Error(`unknown mux backend: ${name}`);
2540
+ }
2541
+ var KNOWN_NAMES = BACKENDS.map((b) => b.name);
2542
+ async function detectMux() {
2543
+ const override = process.env.MU_MUX;
2544
+ if (override !== void 0 && override.length > 0) {
2545
+ return muxByName(override);
2589
2546
  }
2590
- let sql = `SELECT ${SELECT_LOG_COLS} ${LOG_FROM_JOIN} ${where} ORDER BY l.seq ASC`;
2591
- if (opts.limit !== void 0) {
2592
- sql += " LIMIT ?";
2593
- params.push(opts.limit);
2547
+ if (process.env.HERDR_ENV === "1") return herdrBackend;
2548
+ if (process.env.TMUX || process.env.TMUX_PANE) return tmuxBackend;
2549
+ for (const backend of BACKENDS) {
2550
+ if (await backend.available()) return backend;
2594
2551
  }
2595
- const rows = db.prepare(sql).all(...params);
2596
- return rows.map(rowFromDb);
2597
- }
2598
- function latestSeq(db, workstream) {
2599
- const row = workstream === void 0 ? db.prepare(`SELECT MAX(seq) AS s FROM ops l WHERE ${TOUCH_OP_FILTER}`).get() : db.prepare(
2600
- `SELECT MAX(seq) AS s FROM ops l WHERE ${workstreamScopeSql()} AND ${TOUCH_OP_FILTER}`
2601
- ).get(...workstreamScopeParams(workstream));
2602
- return row.s ?? 0;
2552
+ throw new NoMultiplexerError(KNOWN_NAMES);
2603
2553
  }
2604
- function emitEvent(db, workstream, intent, payload, source = "system") {
2605
- appendLog(db, { workstream, source, kind: entityForIntent(intent), payload, intent });
2554
+ var cached;
2555
+ async function activeMux() {
2556
+ if (cached === void 0) cached = await detectMux();
2557
+ return cached;
2606
2558
  }
2607
- function entityForIntent(intent) {
2608
- const dot = intent.indexOf(".");
2609
- return dot === -1 ? intent : intent.slice(0, dot);
2559
+ function setMuxForTests(backend) {
2560
+ const previous = cached;
2561
+ cached = backend;
2562
+ return previous;
2610
2563
  }
2611
- function lastClaimOp(db, workstream, localId) {
2612
- const row = db.prepare(
2613
- `SELECT actor, created_at FROM ops
2614
- WHERE key = ? AND intent = 'task.claim'
2615
- ORDER BY seq DESC LIMIT 1`
2616
- ).get(`${workstream}/${localId}`);
2617
- return row ?? null;
2564
+ function resetMux() {
2565
+ cached = void 0;
2618
2566
  }
2619
- function lastClaimEventAt(db, workstream, localId) {
2620
- return lastClaimOp(db, workstream, localId)?.created_at ?? null;
2567
+
2568
+ // src/agents/spawn.ts
2569
+ import { execFile as execFile2 } from "child_process";
2570
+ import { mkdirSync as mkdirSync2, rmSync as rmSync4 } from "fs";
2571
+ import { basename, dirname as dirname6 } from "path";
2572
+ import { setTimeout as delay } from "timers/promises";
2573
+ import { promisify as promisify2 } from "util";
2574
+
2575
+ // src/agents.ts
2576
+ import { dirname as dirname5 } from "path";
2577
+
2578
+ // src/glyphs.ts
2579
+ var AGENT_STATE_GLYPH = {
2580
+ busy: "\uF04B",
2581
+ // nf-fa-play
2582
+ needs_input: "\uF186",
2583
+ // nf-fa-moon_o
2584
+ needs_permission: "\uF023",
2585
+ // nf-fa-lock
2586
+ unknown: "\uF059"
2587
+ // nf-fa-question_circle
2588
+ };
2589
+ function agentStateGlyph(state) {
2590
+ return AGENT_STATE_GLYPH[state] ?? GLYPH.unknown;
2621
2591
  }
2592
+ var GLYPH = {
2593
+ /** Healthy, clean, passing, closed-successfully. */
2594
+ ok: "\uF058",
2595
+ // nf-fa-check_circle
2596
+ /** Failed check. */
2597
+ fail: "\uF057",
2598
+ // nf-fa-times_circle
2599
+ /** Needs attention but not broken (idle-but-assigned, stale warning). */
2600
+ warn: "\uF071",
2601
+ // nf-fa-warning
2602
+ /** Workspace has uncommitted edits. */
2603
+ dirty: "\uF005",
2604
+ // nf-fa-star
2605
+ /** Workspace is ≥ WORKSPACE_STALE_THRESHOLD commits behind main. */
2606
+ stale: "\uF017",
2607
+ // nf-fa-clock_o
2608
+ /** Task is blocked by an incoming edge. */
2609
+ blocked: "\uF0C1",
2610
+ // nf-fa-link (chain)
2611
+ /** Task is parked (OPEN/parked): open, but set aside on purpose. */
2612
+ parked: "\uF04C",
2613
+ // nf-fa-pause
2614
+ /** Track whose roots merged (diamond dependency). */
2615
+ merge: "\uF074",
2616
+ // nf-fa-random
2617
+ /** Agent owns more than one task (pane-title multi-count slot). */
2618
+ multi: "\uF055",
2619
+ // nf-fa-plus_circle
2620
+ /** Filter toggle: enabled / disabled. */
2621
+ on: "\uF111",
2622
+ // nf-fa-circle
2623
+ off: "\uF10C",
2624
+ // nf-fa-circle_o
2625
+ /** Status outside the known enum. */
2626
+ unknown: "\uF059"
2627
+ // nf-fa-question_circle
2628
+ };
2622
2629
 
2623
2630
  // src/op-context.ts
2624
2631
  import { randomUUID as randomUUID3 } from "crypto";
@@ -2856,7 +2863,7 @@ function workspacesRoot(workstream) {
2856
2863
  }
2857
2864
 
2858
2865
  // src/workspace/crud.ts
2859
- import { existsSync as existsSync6, rmSync as rmSync2 } from "fs";
2866
+ import { existsSync as existsSync6, rmSync as rmSync3 } from "fs";
2860
2867
  import { homedir as homedir2 } from "os";
2861
2868
  import { resolve as resolve3 } from "path";
2862
2869
 
@@ -2866,7 +2873,7 @@ import { resolve as resolve2 } from "path";
2866
2873
 
2867
2874
  // src/vcs/helpers.ts
2868
2875
  import { execFile } from "child_process";
2869
- import { existsSync, rmSync } from "fs";
2876
+ import { existsSync, rmSync as rmSync2 } from "fs";
2870
2877
  import { mkdir } from "fs/promises";
2871
2878
  import { dirname as dirname3 } from "path";
2872
2879
  import { promisify } from "util";
@@ -2972,7 +2979,7 @@ async function ensureParent(path) {
2972
2979
  }
2973
2980
  function rmDirSync(path) {
2974
2981
  if (!existsSync(path)) return false;
2975
- rmSync(path, { recursive: true, force: true });
2982
+ rmSync2(path, { recursive: true, force: true });
2976
2983
  return true;
2977
2984
  }
2978
2985
  async function run(bin, args, cwd) {
@@ -3797,7 +3804,7 @@ async function createWorkspace(db, opts) {
3797
3804
  created = await backend.createWorkspace(createOpts);
3798
3805
  } catch (err) {
3799
3806
  try {
3800
- rmSync2(path, { recursive: true, force: true });
3807
+ rmSync3(path, { recursive: true, force: true });
3801
3808
  } catch {
3802
3809
  }
3803
3810
  throw err;
@@ -4438,8 +4445,17 @@ var CycleError = class extends Error {
4438
4445
  ];
4439
4446
  }
4440
4447
  };
4448
+ function stallWaitHint(owner, ws, runs) {
4449
+ if (runs === void 0 || runs < 1) return [];
4450
+ return [
4451
+ {
4452
+ intent: "Read the worker's last answer (its final text, at once)",
4453
+ command: `mu agent wait ${owner} --after-runs ${runs - 1} --json -w ${ws}`
4454
+ }
4455
+ ];
4456
+ }
4441
4457
  var StallDetectedDuringWaitError = class extends Error {
4442
- constructor(taskName, owner, workstream, ageSecs, ownerExpectsCtl = false) {
4458
+ constructor(taskName, owner, workstream, ageSecs, ownerExpectsCtl = false, ownerRuns) {
4443
4459
  const ownerBit = owner !== null ? owner : "<unknown>";
4444
4460
  super(
4445
4461
  `task ${taskName} owned by ${ownerBit} has been needs_input for ${ageSecs}s; exiting per --on-stall exit. Read the pane to see which it is \u2014 finished without closing, waiting on an answer, or sitting at a prompt.`
@@ -4449,12 +4465,14 @@ var StallDetectedDuringWaitError = class extends Error {
4449
4465
  this.workstream = workstream;
4450
4466
  this.ageSecs = ageSecs;
4451
4467
  this.ownerExpectsCtl = ownerExpectsCtl;
4468
+ this.ownerRuns = ownerRuns;
4452
4469
  }
4453
4470
  taskName;
4454
4471
  owner;
4455
4472
  workstream;
4456
4473
  ageSecs;
4457
4474
  ownerExpectsCtl;
4475
+ ownerRuns;
4458
4476
  name = "StallDetectedDuringWaitError";
4459
4477
  /**
4460
4478
  * Diagnose before acting. `mu agent read` comes FIRST because the
@@ -4469,6 +4487,7 @@ var StallDetectedDuringWaitError = class extends Error {
4469
4487
  const ws = this.workstream;
4470
4488
  const ownerBit = this.owner !== null ? this.owner : "<owner>";
4471
4489
  return [
4490
+ ...stallWaitHint(ownerBit, ws, this.ownerRuns),
4472
4491
  {
4473
4492
  intent: "Read the worker's pane to see what it is waiting on",
4474
4493
  command: `mu agent read ${ownerBit} -w ${ws} --lines 60`
@@ -4822,7 +4841,7 @@ function reparentTaskImpl(db, taskLocalId, blockers, scope) {
4822
4841
 
4823
4842
  // src/workstream.ts
4824
4843
  import { existsSync as existsSync7, readdirSync as readdirSync2, rmdirSync } from "fs";
4825
- import { join as join5 } from "path";
4844
+ import { dirname as dirname4, join as join5 } from "path";
4826
4845
  var WORKSTREAM_NAME_RE = /^[a-z][a-z0-9_-]{0,31}$/;
4827
4846
  var RESERVED_WORKSTREAM_PREFIX = "mu-";
4828
4847
  var RESERVED_WORKSTREAM_NAMES = /* @__PURE__ */ new Set(["scratch"]);
@@ -4936,6 +4955,7 @@ async function teardownWorkstream(db, opts) {
4936
4955
  const notesBefore = countNotes(db, opts.workstream);
4937
4956
  const edgesBefore = countEdges(db, opts.workstream);
4938
4957
  const workspacesBefore = listWorkspaces2(db, opts.workstream);
4958
+ const agentNames = listAgentNames(db, opts.workstream);
4939
4959
  const mux = await activeMux();
4940
4960
  const muxAliveBefore = await mux.sessionExists(muxSession);
4941
4961
  if (muxAliveBefore) {
@@ -4973,6 +4993,14 @@ async function teardownWorkstream(db, opts) {
4973
4993
  } catch {
4974
4994
  }
4975
4995
  }
4996
+ for (const name of agentNames) unlinkCtlSocket(db, name, opts.workstream);
4997
+ if (!db.memory) {
4998
+ const sockDir = join5(dirname4(db.name), "sock", opts.workstream);
4999
+ try {
5000
+ if (readdirSync2(sockDir).length === 0) rmdirSync(sockDir);
5001
+ } catch {
5002
+ }
5003
+ }
4976
5004
  withOpContext(
4977
5005
  db,
4978
5006
  { intent: "workstream.teardown", group: "new" },
@@ -4989,6 +5017,14 @@ async function teardownWorkstream(db, opts) {
4989
5017
  failedWorkspaces
4990
5018
  };
4991
5019
  }
5020
+ function listAgentNames(db, workstream) {
5021
+ const rows = db.prepare(
5022
+ `SELECT a.name FROM agents a
5023
+ JOIN workstreams ws ON ws.id = a.workstream_id
5024
+ WHERE ws.name = ?`
5025
+ ).all(workstream);
5026
+ return rows.map((r) => r.name);
5027
+ }
4992
5028
  function countAgents(db, workstream) {
4993
5029
  const row = db.prepare(
4994
5030
  `SELECT COUNT(*) AS n FROM agents a
@@ -5296,12 +5332,32 @@ function recordReasonNote(db, localId, workstream, substate, why, author) {
5296
5332
  if (why === void 0 || why.trim() === "") return;
5297
5333
  const noteOpts = { workstream };
5298
5334
  if (author !== void 0 && author !== "") noteOpts.author = author;
5299
- insertNote(db, localId, `${substate.toUpperCase()}: ${why}`, noteOpts);
5335
+ const text = substate === "rejected" || substate === "superseded" ? withTitles(db, why, workstream, localId) : why;
5336
+ insertNote(db, localId, `${substate.toUpperCase()}: ${text}`, noteOpts);
5337
+ }
5338
+ var TASK_ID_TOKEN = /(?<![\w./-])[a-z][a-z0-9_-]{0,63}(?![\w-]|\.\w|:\d)/g;
5339
+ var TASK_ID_WITH_PAREN = new RegExp(`(?:${TASK_ID_TOKEN.source})(\\s*\\()?`, "g");
5340
+ function namedTaskIds(db, text, workstream, self) {
5341
+ const ids = (text.match(TASK_ID_TOKEN) ?? []).filter((t) => t !== self);
5342
+ return [...new Set(ids)].filter((t) => getTask(db, t, workstream) !== void 0);
5343
+ }
5344
+ function withTitles(db, why, workstream, self) {
5345
+ const named = new Set(namedTaskIds(db, why, workstream, self));
5346
+ if (named.size === 0) return why;
5347
+ return why.replace(TASK_ID_WITH_PAREN, (match, paren) => {
5348
+ const id = paren === void 0 ? match : match.slice(0, -paren.length);
5349
+ if (!named.has(id)) return match;
5350
+ const title = getTask(db, id, workstream)?.title ?? "";
5351
+ return paren === void 0 ? `${id} (${title})` : `${id} (${title}; `;
5352
+ });
5300
5353
  }
5301
5354
  function openTask(db, localId, opts) {
5302
- return withOpContext(db, { intent: "task.open", group: "new" }, () => {
5355
+ return withOpContext(db, { intent: "task.open", actor: opts.author, group: "new" }, () => {
5303
5356
  const before = getTask(db, localId, opts.workstream);
5304
- const r = setTaskStatus(db, localId, "OPEN", opts);
5357
+ const r = setTaskStatus(db, localId, "OPEN", {
5358
+ workstream: opts.workstream,
5359
+ ...opts.evidence !== void 0 ? { evidence: opts.evidence } : {}
5360
+ });
5305
5361
  if (r.changed && before) recordEvidenceNote(db, localId, before.workstreamName, "OPEN", opts);
5306
5362
  return r;
5307
5363
  });
@@ -5332,7 +5388,7 @@ function parkTask(db, localId, opts) {
5332
5388
  function acceptTask(db, localId, opts) {
5333
5389
  return withOpContext(
5334
5390
  db,
5335
- { intent: "task.accept", group: "new" },
5391
+ { intent: "task.accept", actor: opts.author, group: "new" },
5336
5392
  () => db.transaction(() => {
5337
5393
  const before = getTask(db, localId, opts.workstream);
5338
5394
  if (!before) throw new TaskNotFoundError(localId);
@@ -5354,7 +5410,7 @@ function acceptTask(db, localId, opts) {
5354
5410
  function unparkTask(db, localId, opts) {
5355
5411
  return withOpContext(
5356
5412
  db,
5357
- { intent: "task.unpark", group: "new" },
5413
+ { intent: "task.unpark", actor: opts.author, group: "new" },
5358
5414
  () => db.transaction(() => {
5359
5415
  const before = getTask(db, localId, opts.workstream);
5360
5416
  if (!before) throw new TaskNotFoundError(localId);
@@ -5378,7 +5434,7 @@ function unparkTask(db, localId, opts) {
5378
5434
  function releaseTask(db, localId, opts) {
5379
5435
  return withOpContext(
5380
5436
  db,
5381
- { intent: "task.release", group: "new" },
5437
+ { intent: "task.release", actor: opts.author, group: "new" },
5382
5438
  () => releaseTaskImpl(db, localId, opts)
5383
5439
  );
5384
5440
  }
@@ -5466,7 +5522,10 @@ async function claimTaskImpl(db, localId, opts) {
5466
5522
  }
5467
5523
  const after = getTask(db, localId, opts.workstream);
5468
5524
  if (!after) throw new Error(`claimTask: row missing after update: ${localId}`);
5469
- recordEvidenceNote(db, localId, opts.workstream, "CLAIM", opts);
5525
+ recordEvidenceNote(db, localId, opts.workstream, "CLAIM", {
5526
+ ...opts,
5527
+ author: opts.author ?? agentName
5528
+ });
5470
5529
  return {
5471
5530
  ownerName: agentName,
5472
5531
  actorName: agentName,
@@ -5526,7 +5585,10 @@ async function claimSelf(db, localId, opts) {
5526
5585
  }
5527
5586
  const after = getTask(db, localId, before.workstreamName);
5528
5587
  if (!after) throw new Error(`claimTask: row missing after update: ${localId}`);
5529
- recordEvidenceNote(db, localId, before.workstreamName, "CLAIM", opts);
5588
+ recordEvidenceNote(db, localId, before.workstreamName, "CLAIM", {
5589
+ ...opts,
5590
+ author: opts.author ?? actor
5591
+ });
5530
5592
  return {
5531
5593
  ownerName: null,
5532
5594
  actorName: actor,
@@ -5601,12 +5663,15 @@ async function waitForTasks(db, input, opts) {
5601
5663
  const ctlBrokenSince = /* @__PURE__ */ new Map();
5602
5664
  const stuckLabel = /* @__PURE__ */ new Map();
5603
5665
  const ctlOwners = /* @__PURE__ */ new Set();
5666
+ const ownerRuns = /* @__PURE__ */ new Map();
5604
5667
  const stuckAgeMs = async (status, owner, workstreamName) => {
5605
5668
  if (stuckAfterMs <= 0 || status !== "IN_PROGRESS" || owner === null || opts.readOwnerState === void 0)
5606
5669
  return null;
5607
5670
  const reading = await opts.readOwnerState({ name: owner, workstreamName });
5608
5671
  const ownerKey = `${workstreamName}/${owner}`;
5609
5672
  if (reading?.ctl !== void 0) ctlOwners.add(ownerKey);
5673
+ if (reading?.runs !== void 0) ownerRuns.set(ownerKey, reading.runs);
5674
+ else ownerRuns.delete(ownerKey);
5610
5675
  if (reading?.alive === true && (reading.ctl === "missing" || reading.ctl === "refused")) {
5611
5676
  const firstSeen = ctlBrokenSince.get(ownerKey) ?? Date.now();
5612
5677
  ctlBrokenSince.set(ownerKey, firstSeen);
@@ -5636,10 +5701,13 @@ async function waitForTasks(db, input, opts) {
5636
5701
  stuckWarned.add(key);
5637
5702
  const ageSecs = Math.round(ageMs / 1e3);
5638
5703
  const ownerBit = owner ?? "<none>";
5704
+ const runs = ownerRuns.get(`${ref.workstreamName}/${ownerBit}`);
5639
5705
  const stateBit = stuckLabel.get(`${ref.workstreamName}/${ownerBit}`) ?? "needs_input";
5706
+ const waitLine = stallWaitHint(ownerBit, ref.workstreamName, runs).map((s) => ` ${s.command}
5707
+ `).join("");
5640
5708
  currentStuckWarn(
5641
5709
  `\x1B[33mmu task wait: ${key} needs attention \u2014 owner=${ownerBit} has been in ${stateBit} for ${formatStallAge(ageMs)}. It may have finished without closing, be waiting on an answer, or be sitting at a prompt.\x1B[0m
5642
- mu agent read ${ownerBit} -w ${ref.workstreamName} --lines 60
5710
+ ` + waitLine + ` mu agent read ${ownerBit} -w ${ref.workstreamName} --lines 60
5643
5711
  `
5644
5712
  );
5645
5713
  emitEvent(
@@ -5655,7 +5723,8 @@ async function waitForTasks(db, input, opts) {
5655
5723
  owner,
5656
5724
  ref.workstreamName,
5657
5725
  ageSecs,
5658
- ctlOwners.has(`${ref.workstreamName}/${owner ?? ""}`)
5726
+ ctlOwners.has(`${ref.workstreamName}/${owner ?? ""}`),
5727
+ runs
5659
5728
  );
5660
5729
  }
5661
5730
  }
@@ -5710,6 +5779,7 @@ async function abortAgent(db, name, opts) {
5710
5779
  const st = await ctlRequestFor(agent, sock, { op: "status" });
5711
5780
  if (!st.ok) throw new Error(`control socket refused status: ${st.error}`);
5712
5781
  const before = st.state ?? "idle";
5782
+ const pending = st.pending ?? false;
5713
5783
  if (before !== "busy") {
5714
5784
  return {
5715
5785
  ...base,
@@ -5717,6 +5787,7 @@ async function abortAgent(db, name, opts) {
5717
5787
  after: before,
5718
5788
  settled: true,
5719
5789
  aborted: false,
5790
+ pending,
5720
5791
  elapsedMs: Date.now() - started
5721
5792
  };
5722
5793
  }
@@ -5733,9 +5804,50 @@ async function abortAgent(db, name, opts) {
5733
5804
  after: w.state ?? "idle",
5734
5805
  settled: true,
5735
5806
  aborted: true,
5807
+ pending,
5736
5808
  elapsedMs: Date.now() - started
5737
5809
  };
5738
5810
  }
5811
+ async function interruptAgent(db, name, text, opts) {
5812
+ const r = await interruptAgentImpl(db, name, text, opts);
5813
+ recordSend(db, { name, workstreamName: opts.workstream }, "interrupt", text.length, {
5814
+ transport: "ctl",
5815
+ wasBusy: r.wasBusy
5816
+ });
5817
+ return r;
5818
+ }
5819
+ async function interruptAgentImpl(db, name, text, opts) {
5820
+ const agent = getAgent(db, name, opts.workstream);
5821
+ if (!agent) throw new AgentNotFoundError(name, opts.workstream);
5822
+ const sock = opts.socket ?? agentCtlSocket(db, agent);
5823
+ if (!expectsCtl(agent, sock)) throw new AgentAbortNeedsCtlError(name, opts.workstream, agent.cli);
5824
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_ABORT_TIMEOUT_MS;
5825
+ let r;
5826
+ try {
5827
+ r = await ctlRequest(sock, { op: "interrupt", text, timeoutMs });
5828
+ } catch (e) {
5829
+ if (!(e instanceof CtlUnknownOpError)) throw await ctlFailure(agent, sock, e);
5830
+ const ab = await abortAgent(db, name, { ...opts, socket: sock });
5831
+ const sent = await ctlRequestFor(agent, sock, { op: "send", text });
5832
+ if (!sent.ok) throw new Error(`control socket refused the send: ${sent.error}`);
5833
+ return {
5834
+ wasBusy: ab.aborted,
5835
+ pending: ab.pending,
5836
+ ...sent.runs !== void 0 ? { runs: sent.runs } : {},
5837
+ fallback: true
5838
+ };
5839
+ }
5840
+ if (!r.ok) {
5841
+ if (r.error === "timeout") throw new AgentAbortTimeoutError(name, opts.workstream, timeoutMs);
5842
+ throw new Error(`control socket refused the interrupt: ${r.error}`);
5843
+ }
5844
+ return {
5845
+ wasBusy: r.wasBusy ?? false,
5846
+ pending: r.pending ?? false,
5847
+ ...r.runs !== void 0 ? { runs: r.runs } : {},
5848
+ fallback: false
5849
+ };
5850
+ }
5739
5851
 
5740
5852
  // src/agents/adopt.ts
5741
5853
  var ADOPT_CTL_PROBE_MS = 1e3;
@@ -6089,13 +6201,6 @@ function deleteAgent(db, name, workstream) {
6089
6201
  if (deleted) unlinkCtlSocket(db, name, workstream);
6090
6202
  return deleted;
6091
6203
  }
6092
- function unlinkCtlSocket(db, name, workstream) {
6093
- if (db.memory) return;
6094
- try {
6095
- rmSync3(ctlSocketPath(workstream, name, dirname4(db.name)), { force: true });
6096
- } catch {
6097
- }
6098
- }
6099
6204
  function deleteAgentRow(db, name, workstream) {
6100
6205
  return withOpContext(
6101
6206
  db,
@@ -6135,10 +6240,23 @@ function isValidAgentName(name) {
6135
6240
  async function sendToAgent(db, name, text, opts) {
6136
6241
  const agent = getAgent(db, name, opts.workstream);
6137
6242
  if (!agent) throw new AgentNotFoundError(name);
6138
- return sendViaTransport(agent, text, {
6243
+ const sent = await sendViaTransport(agent, text, {
6139
6244
  ...opts,
6140
6245
  socket: opts.socket ?? agentCtlSocket(db, agent)
6141
6246
  });
6247
+ recordSend(
6248
+ db,
6249
+ agent,
6250
+ opts.fresh ? "fresh" : opts.mode === "steer" ? "steer" : "plain",
6251
+ text.length,
6252
+ {
6253
+ transport: sent.transport,
6254
+ command: sent.command,
6255
+ state: sent.state,
6256
+ since: sent.since
6257
+ }
6258
+ );
6259
+ return sent;
6142
6260
  }
6143
6261
  async function readAgent(db, name, opts) {
6144
6262
  const agent = getAgent(db, name, opts.workstream);
@@ -6191,7 +6309,7 @@ async function listLiveAgents(db, opts) {
6191
6309
  ...opts.mode !== void 0 ? { mode: opts.mode } : {}
6192
6310
  });
6193
6311
  const baseAgents = listAgents(db, { workstream: opts.workstream });
6194
- const readings = await readAgentStates(baseAgents, { stateDir: dirname4(db.name) });
6312
+ const readings = await readAgentStates(baseAgents, { stateDir: dirname5(db.name) });
6195
6313
  const now = Date.now();
6196
6314
  const agents = baseAgents.map((agent) => {
6197
6315
  const reading = readings.get(agentKey(agent)) ?? {
@@ -6456,7 +6574,7 @@ function defaultSpawnCtlMs() {
6456
6574
  return parsed;
6457
6575
  }
6458
6576
  var CTL_POLL_INTERVAL_MS = 250;
6459
- async function awaitCtlHandshake(sock) {
6577
+ async function awaitCtlHandshake(sock, onMiss) {
6460
6578
  const budgetMs = defaultSpawnCtlMs();
6461
6579
  const deadline = Date.now() + budgetMs;
6462
6580
  for (; ; ) {
@@ -6464,6 +6582,7 @@ async function awaitCtlHandshake(sock) {
6464
6582
  const probe = await ctlProbe(sock, Math.max(1, Math.min(remaining, CTL_POLL_INTERVAL_MS * 4)));
6465
6583
  if (probe.kind === "ok") return "ok";
6466
6584
  const last = probeToSpawnCtl(probe.kind);
6585
+ if (onMiss) await onMiss();
6467
6586
  const left = deadline - Date.now();
6468
6587
  if (left <= 0) return last;
6469
6588
  await delay(Math.min(CTL_POLL_INTERVAL_MS, left));
@@ -6491,8 +6610,8 @@ async function spawnAgent(db, opts) {
6491
6610
  }
6492
6611
  }
6493
6612
  const workspacePathStr = opts.workspace ? await prestageWorkspace(db, opts, cli) : void 0;
6494
- const ctlSocket = ctlSocketPath(opts.workstream, opts.name, dirname5(db.name));
6495
- mkdirSync2(dirname5(ctlSocket), { recursive: true, mode: 448 });
6613
+ const ctlSocket = ctlSocketPath(opts.workstream, opts.name, dirname6(db.name));
6614
+ mkdirSync2(dirname6(ctlSocket), { recursive: true, mode: 448 });
6496
6615
  rmSync4(ctlSocket, { force: true });
6497
6616
  const paneEnv = {
6498
6617
  MU_MANAGED_AGENT: "1",
@@ -6503,6 +6622,8 @@ async function spawnAgent(db, opts) {
6503
6622
  const hasWorkspace = workspacePathStr !== void 0;
6504
6623
  let paneId;
6505
6624
  let agent;
6625
+ const wantsCtl = opts.ctl !== false && speaksMuCtl(cli, command) && defaultSpawnCtlMs() !== 0;
6626
+ let ctl;
6506
6627
  try {
6507
6628
  const created = await withSpawnLock(session, async () => {
6508
6629
  const pid = await createOrReusePane({
@@ -6523,7 +6644,23 @@ async function spawnAgent(db, opts) {
6523
6644
  agent = created.row;
6524
6645
  const startAgent = (await activeMux()).startAgentInPane;
6525
6646
  if (startAgent === void 0) {
6526
- await awaitSpawnLiveness(paneId, opts.name, command);
6647
+ if (wantsCtl) {
6648
+ const pid = paneId;
6649
+ const check = defaultSpawnLivenessMs() !== 0;
6650
+ const when = `during the ctl handshake (up to ${defaultSpawnCtlMs()}ms after spawn)`;
6651
+ let lastScrollback;
6652
+ const assertPaneAlive = async () => {
6653
+ const mux = await activeMux();
6654
+ const sb = await mux.capturePane(pid, { lines: 50 }).catch(() => void 0);
6655
+ lastScrollback = sb ?? lastScrollback;
6656
+ if (await mux.paneExists(pid)) return;
6657
+ throw new AgentDiedOnSpawnError(opts.name, pid, lastScrollback, command, when);
6658
+ };
6659
+ ctl = await awaitCtlHandshake(ctlSocket, check ? assertPaneAlive : void 0);
6660
+ if (check) await checkSpawnHealth(pid, opts.name, command, when);
6661
+ } else {
6662
+ await awaitSpawnLiveness(paneId, opts.name, command);
6663
+ }
6527
6664
  } else {
6528
6665
  await startAgent({ paneId, name: opts.name, cli, command, commandSource });
6529
6666
  }
@@ -6532,14 +6669,13 @@ async function spawnAgent(db, opts) {
6532
6669
  if (hasWorkspace) attachOrphanCleanupHint(err, opts.name, opts.workstream);
6533
6670
  throw err;
6534
6671
  }
6535
- const ctl = opts.ctl === false || !speaksMuCtl(cli, command) || defaultSpawnCtlMs() === 0 ? "skipped" : await awaitCtlHandshake(ctlSocket);
6672
+ ctl ??= wantsCtl ? await awaitCtlHandshake(ctlSocket) : "skipped";
6536
6673
  emitEvent(
6537
6674
  db,
6538
6675
  opts.workstream,
6539
6676
  "agent.spawn",
6540
6677
  `agent spawn ${opts.name} (cli=${cli}, role=${opts.role ?? "full-access"}, pane=${paneId})`
6541
6678
  );
6542
- await refreshAgentTitle(db, opts.name, opts.workstream);
6543
6679
  maybeWarnNonConventionalAgentName(opts.name);
6544
6680
  return { ...agent, ctl, ctlSocket };
6545
6681
  }
@@ -6667,15 +6803,18 @@ async function awaitSpawnLiveness(paneId, agentName, command) {
6667
6803
  const ms = defaultSpawnLivenessMs();
6668
6804
  if (ms === 0) return;
6669
6805
  await sleep(ms);
6806
+ await checkSpawnHealth(paneId, agentName, command);
6807
+ }
6808
+ async function checkSpawnHealth(paneId, agentName, command, when) {
6670
6809
  const mux = await activeMux();
6671
6810
  const scrollback = await mux.capturePane(paneId, { lines: 50 }).catch(() => void 0);
6672
6811
  if (!await mux.paneExists(paneId)) {
6673
- throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command);
6812
+ throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command, when);
6674
6813
  }
6675
6814
  if (scrollback !== void 0) {
6676
6815
  const matchedLine = detectSpawnStartupError(scrollback);
6677
6816
  if (matchedLine !== void 0) {
6678
- throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback);
6817
+ throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback, when);
6679
6818
  }
6680
6819
  }
6681
6820
  }
@@ -6830,7 +6969,7 @@ var AgentNotInWorkstreamError = class extends Error {
6830
6969
  }
6831
6970
  };
6832
6971
  var AgentDiedOnSpawnError = class extends Error {
6833
- constructor(agentName, paneId, scrollback, command) {
6972
+ constructor(agentName, paneId, scrollback, command, when = `within ${defaultSpawnLivenessMs()}ms of spawn`) {
6834
6973
  const tail = scrollback?.trim();
6835
6974
  const detail = tail ? `
6836
6975
 
@@ -6839,9 +6978,7 @@ ${tail}
6839
6978
  --- end scrollback ---` : "";
6840
6979
  const remote = /^\s*ssh\s/.test(command ?? "");
6841
6980
  const cause = remote ? 'The command is an ssh hop, so the likely causes are remote: the remote binary is missing, or its env (PATH, provider keys) is absent because ssh runs a non-interactive shell that skips ~/.zshrc / ~/.bashrc (wrap it: $SHELL -ilc "pi --approve"); or sshd refused the socket forward (ExitOnForwardFailure=yes kills the connection; check AllowStreamLocalForwarding).' : "Most common cause: the spawned CLI exited immediately (e.g. a wrapper CLI blocking on its instance lock; set MU_<UPPER_CLI>_COMMAND to a non-blocking variant to bypass).";
6842
- super(
6843
- `agent ${agentName} died within ${defaultSpawnLivenessMs()}ms of spawn (pane ${paneId}). ${cause}${detail}`
6844
- );
6981
+ super(`agent ${agentName} died ${when} (pane ${paneId}). ${cause}${detail}`);
6845
6982
  this.agentName = agentName;
6846
6983
  this.paneId = paneId;
6847
6984
  this.scrollback = scrollback;
@@ -6900,9 +7037,9 @@ ${tail}
6900
7037
  }
6901
7038
  };
6902
7039
  var AgentSpawnStartupError = class extends Error {
6903
- constructor(agentName, paneId, matchedLine, scrollback) {
7040
+ constructor(agentName, paneId, matchedLine, scrollback, when = `within ${defaultSpawnLivenessMs()}ms of spawn`) {
6904
7041
  super(
6905
- `agent ${agentName} reported a startup error within ${defaultSpawnLivenessMs()}ms of spawn (pane ${paneId}). The pane is alive but the spawned CLI parked at an error prompt instead of becoming a working agent.
7042
+ `agent ${agentName} reported a startup error ${when} (pane ${paneId}). The pane is alive but the spawned CLI parked at an error prompt instead of becoming a working agent.
6906
7043
 
6907
7044
  Matched line: ${matchedLine.trim()}
6908
7045
 
@@ -6988,7 +7125,14 @@ var AgentCtlUnreachableError = class extends Error {
6988
7125
  kind;
6989
7126
  name = "AgentCtlUnreachableError";
6990
7127
  errorNextSteps() {
7128
+ const trust = this.kind === "missing" ? [
7129
+ {
7130
+ intent: "Just spawned? pi may sit at its project trust prompt: answer /trust in the pane, or respawn with --command 'pi --approve'",
7131
+ command: `mu agent read ${this.agentName} -w ${this.workstream}`
7132
+ }
7133
+ ] : [];
6991
7134
  return [
7135
+ ...trust,
6992
7136
  { intent: "Check whether the mu pi extension is linked", command: "mu doctor" },
6993
7137
  {
6994
7138
  intent: "Stop a runaway tool without the socket",
@@ -7071,7 +7215,7 @@ var AgentFreshNeedsCtlError = class extends Error {
7071
7215
  }
7072
7216
  };
7073
7217
  var AgentExtensionOutdatedError = class extends Error {
7074
- constructor(agentName, workstream, op, extVersion) {
7218
+ constructor(agentName, workstream, op, extVersion, ops) {
7075
7219
  super(
7076
7220
  `agent ${agentName}'s mu pi extension (${extVersion ?? "version unknown"}) predates op ${op}: its pi loaded an older mu build; reload or respawn it`
7077
7221
  );
@@ -7079,18 +7223,21 @@ var AgentExtensionOutdatedError = class extends Error {
7079
7223
  this.workstream = workstream;
7080
7224
  this.op = op;
7081
7225
  this.extVersion = extVersion;
7226
+ this.ops = ops;
7082
7227
  }
7083
7228
  agentName;
7084
7229
  workstream;
7085
7230
  op;
7086
7231
  extVersion;
7232
+ ops;
7087
7233
  name = "AgentExtensionOutdatedError";
7088
7234
  errorNextSteps() {
7089
7235
  const w = `-w ${this.workstream}`;
7236
+ const via = this.ops?.includes("command") ? "" : " --via mux";
7090
7237
  return [
7091
7238
  {
7092
- intent: "Reload pi's extensions in the pane (keeps the session and context)",
7093
- command: `mu agent send ${this.agentName} '/reload' --via mux ${w}`
7239
+ intent: "Reload pi's extensions (keeps the session and context)",
7240
+ command: `mu agent send ${this.agentName} '/reload'${via} ${w}`
7094
7241
  },
7095
7242
  {
7096
7243
  intent: "Or respawn the agent (loses its context)",
@@ -7148,7 +7295,7 @@ var AgentSlashCommandUnsupportedError = class extends Error {
7148
7295
 
7149
7296
  // src/agents/transport.ts
7150
7297
  function agentCtlSocket(db, agent) {
7151
- return ctlSocketPath(agent.workstreamName, agent.name, dirname6(db.name));
7298
+ return ctlSocketPath(agent.workstreamName, agent.name, dirname7(db.name));
7152
7299
  }
7153
7300
  function expectsCtl(agent, sock = ctlSocketPath(agent.workstreamName, agent.name)) {
7154
7301
  return speaksMuCtl(agent.cli, resolveCliCommand(agent.cli)) || existsSync8(sock);
@@ -7176,22 +7323,34 @@ async function extensionOutdated(agent, sock, e) {
7176
7323
  if (hello.ok) extVersion = hello.extVersion;
7177
7324
  } catch {
7178
7325
  }
7179
- return new AgentExtensionOutdatedError(agent.name, agent.workstreamName, e.op, extVersion);
7326
+ return new AgentExtensionOutdatedError(agent.name, agent.workstreamName, e.op, extVersion, e.ops);
7180
7327
  }
7181
7328
  async function ctlRequestFor(agent, sock, req) {
7182
7329
  try {
7183
7330
  return await ctlRequest(sock, req);
7184
7331
  } catch (e) {
7185
- if (e instanceof CtlVersionError) throw e;
7186
- if (e instanceof CtlUnknownOpError) throw await extensionOutdated(agent, sock, e);
7187
- throw new AgentCtlUnreachableError(
7188
- agent.name,
7189
- agent.workstreamName,
7190
- sock,
7191
- errCode(e) === "ENOENT" ? "missing" : "refused"
7192
- );
7332
+ throw await ctlFailure(agent, sock, e);
7193
7333
  }
7194
7334
  }
7335
+ async function ctlFailure(agent, sock, e) {
7336
+ if (e instanceof CtlVersionError) return e;
7337
+ if (e instanceof CtlUnknownOpError) return extensionOutdated(agent, sock, e);
7338
+ return new AgentCtlUnreachableError(
7339
+ agent.name,
7340
+ agent.workstreamName,
7341
+ sock,
7342
+ errCode(e) === "ENOENT" ? "missing" : "refused"
7343
+ );
7344
+ }
7345
+ function recordSend(db, agent, mode, bytes, fields2) {
7346
+ const detail = Object.entries({ mode, ...fields2 }).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${k === "since" ? new Date(Number(v)).toISOString() : v}`).join(" ");
7347
+ emitEvent(
7348
+ db,
7349
+ agent.workstreamName,
7350
+ "agent.send",
7351
+ `agent send ${agent.name} (${detail}, ${bytes} bytes)`
7352
+ );
7353
+ }
7195
7354
  async function sendViaTransport(agent, text, opts) {
7196
7355
  const sock = opts.socket;
7197
7356
  if (opts.fresh && (opts.via === "mux" || !expectsCtl(agent, sock))) {
@@ -7225,7 +7384,12 @@ async function sendViaTransport(agent, text, opts) {
7225
7384
  }
7226
7385
  throw new Error(`control socket refused the send: ${reply.error}`);
7227
7386
  }
7228
- return reply.state === void 0 ? { transport } : { transport, state: reply.state };
7387
+ return {
7388
+ transport,
7389
+ ...reply.state !== void 0 ? { state: reply.state } : {},
7390
+ ...reply.runs !== void 0 ? { runs: reply.runs } : {},
7391
+ ...reply.since !== void 0 ? { since: reply.since } : {}
7392
+ };
7229
7393
  }
7230
7394
  async function runSessionCommand(agent, sock, cmd, force) {
7231
7395
  const reply = await ctlRequestFor(agent, sock, {
@@ -7427,7 +7591,8 @@ async function readAgentStates(agents, opts = {}) {
7427
7591
  since: probe.status.since,
7428
7592
  alive: true,
7429
7593
  ctl: "ok",
7430
- pending: probe.status.pending
7594
+ pending: probe.status.pending,
7595
+ runs: probe.status.runs
7431
7596
  });
7432
7597
  } else {
7433
7598
  failed.set(agentKey(agent), probe?.kind === "missing" ? "missing" : "refused");
@@ -8078,7 +8243,7 @@ function formatTreeNodeLabel(t, statusFn, opts = {}) {
8078
8243
 
8079
8244
  // src/doctor-summary.ts
8080
8245
  import { accessSync as accessSync2, constants as constants2, existsSync as existsSync13, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
8081
- import { delimiter as delimiter2, dirname as dirname9, join as join11, parse } from "path";
8246
+ import { delimiter as delimiter2, dirname as dirname10, join as join11, parse } from "path";
8082
8247
  import { fileURLToPath as fileURLToPath2 } from "url";
8083
8248
 
8084
8249
  // src/drift.ts
@@ -8088,7 +8253,7 @@ import { join as join9 } from "path";
8088
8253
 
8089
8254
  // src/rebuild.ts
8090
8255
  import { existsSync as existsSync10, mkdirSync as mkdirSync3, statSync } from "fs";
8091
- import { dirname as dirname7 } from "path";
8256
+ import { dirname as dirname8 } from "path";
8092
8257
  var RebuildTargetExistsError = class extends Error {
8093
8258
  constructor(path) {
8094
8259
  super(`rebuild target already exists: ${path}`);
@@ -8151,7 +8316,7 @@ function rebuildInto(source, opts) {
8151
8316
  if (existsSync10(targetPath) && opts.force !== true) {
8152
8317
  throw new RebuildTargetExistsError(targetPath);
8153
8318
  }
8154
- mkdirSync3(dirname7(targetPath), { recursive: true });
8319
+ mkdirSync3(dirname8(targetPath), { recursive: true });
8155
8320
  const identity = source.prepare(
8156
8321
  "SELECT machine_id, hostname, created_at, last_wall, last_counter FROM machine_identity WHERE id = 1"
8157
8322
  ).get();
@@ -8673,7 +8838,7 @@ import {
8673
8838
  writeFileSync
8674
8839
  } from "fs";
8675
8840
  import { homedir as homedir4 } from "os";
8676
- import { dirname as dirname8, join as join10, resolve as resolve5 } from "path";
8841
+ import { dirname as dirname9, join as join10, resolve as resolve5 } from "path";
8677
8842
  import { fileURLToPath, pathToFileURL } from "url";
8678
8843
  var MU_SHIM_MARKER = "// mu:shim";
8679
8844
  var LinkConflictError = class extends Error {
@@ -8703,7 +8868,7 @@ function resolveHome(opts) {
8703
8868
  return opts?.home ?? process.env.MU_PI_HOME ?? homedir4();
8704
8869
  }
8705
8870
  function packageRoot() {
8706
- return resolve5(dirname8(fileURLToPath(import.meta.url)), "..");
8871
+ return resolve5(dirname9(fileURLToPath(import.meta.url)), "..");
8707
8872
  }
8708
8873
  function extensionEntry() {
8709
8874
  return process.env.MU_EXTENSION_ENTRY ?? join10(packageRoot(), "dist", "extension", "mu-pi.js");
@@ -8739,7 +8904,7 @@ function linkPi(opts) {
8739
8904
  replacedCopy = !readFileSync(path, "utf8").includes(MU_SHIM_MARKER);
8740
8905
  } catch {
8741
8906
  }
8742
- mkdirSync4(dirname8(path), { recursive: true });
8907
+ mkdirSync4(dirname9(path), { recursive: true });
8743
8908
  writeFileSync(path, opts?.copy === true ? readFileSync(entry, "utf8") : shim(entry));
8744
8909
  return { path, replacedCopy };
8745
8910
  }
@@ -8750,7 +8915,7 @@ function linkSkill(opts) {
8750
8915
  const st = lstatOrUndefined(path);
8751
8916
  if (st) {
8752
8917
  if (!st.isSymbolicLink()) throw new LinkConflictError(path, "not-a-symlink");
8753
- const current = resolve5(dirname8(path), readlinkSync(path));
8918
+ const current = resolve5(dirname9(path), readlinkSync(path));
8754
8919
  if (current === target) return { path, target };
8755
8920
  if (existsSync12(current) && opts?.force !== true) {
8756
8921
  throw new LinkConflictError(path, "foreign-symlink", current);
@@ -8758,7 +8923,7 @@ function linkSkill(opts) {
8758
8923
  previous = current;
8759
8924
  rmSync6(path);
8760
8925
  }
8761
- mkdirSync4(dirname8(path), { recursive: true });
8926
+ mkdirSync4(dirname9(path), { recursive: true });
8762
8927
  symlinkSync(target, path, "dir");
8763
8928
  return previous === void 0 ? { path, target } : { path, target, previous };
8764
8929
  }
@@ -8792,7 +8957,7 @@ function inspectSkill(path) {
8792
8957
  try {
8793
8958
  target = realpathSync(path);
8794
8959
  } catch {
8795
- return { path, state: "dangling", target: resolve5(dirname8(path), readlinkSync(path)) };
8960
+ return { path, state: "dangling", target: resolve5(dirname9(path), readlinkSync(path)) };
8796
8961
  }
8797
8962
  let skillMd;
8798
8963
  try {
@@ -8971,7 +9136,7 @@ function resolveExecutable(name) {
8971
9136
  return null;
8972
9137
  }
8973
9138
  function murmurVersion(executable) {
8974
- let dir = dirname9(executable);
9139
+ let dir = dirname10(executable);
8975
9140
  const root = parse(dir).root;
8976
9141
  while (dir !== root) {
8977
9142
  const packagePath = join11(dir, "package.json");
@@ -8984,7 +9149,7 @@ function murmurVersion(executable) {
8984
9149
  } catch {
8985
9150
  }
8986
9151
  }
8987
- dir = dirname9(dir);
9152
+ dir = dirname10(dir);
8988
9153
  }
8989
9154
  return null;
8990
9155
  }
@@ -9020,7 +9185,7 @@ function murmurDoctorCheck(agentStateSource, opts = {}) {
9020
9185
  return {
9021
9186
  name: "murmur",
9022
9187
  status: "warn",
9023
- detail: `murmur ${version} is older than 1.0.0; @murmur_pane_since missing, idle/stall timing unknown`
9188
+ detail: opts.muExtOk === true ? `murmur ${version} is older than 1.0.0; idle/stall timing unknown for non-pi CLIs (pi agents use ctl)` : `murmur ${version} is older than 1.0.0; @murmur_pane_since missing, idle/stall timing unknown`
9024
9189
  };
9025
9190
  }
9026
9191
  return {
@@ -9129,11 +9294,14 @@ function remediationParagraph(check) {
9129
9294
  return [
9130
9295
  "mu talks to pi agents over a per-agent control socket served by",
9131
9296
  "the mu pi extension. `missing`: no socket (extension not loaded,",
9132
- "or the agent predates `mu link pi`). `refused`: socket present but",
9133
- "not answering. `version`: protocol mismatch; upgrade mu or pi.",
9297
+ "the agent predates `mu link pi`, or a new pi still waits at its",
9298
+ "project trust prompt: answer /trust in the pane, or spawn with",
9299
+ "--command 'pi --approve'). `refused`: socket present but not",
9300
+ "answering. `version`: protocol mismatch; upgrade mu or pi.",
9134
9301
  "Run `mu link pi`, then respawn the agent. `extension X older than",
9135
- "installed Y`: the agent's pi loaded an older mu build; type /reload",
9136
- "in its pane (`mu agent send <a> '/reload' --via mux`) or respawn it."
9302
+ "installed Y` or `lacks ops`: the agent's pi loaded an older mu",
9303
+ "build; reload it with `mu agent send <a> '/reload'` (add",
9304
+ "`--via mux` when the extension lacks op command) or respawn it."
9137
9305
  ];
9138
9306
  case "agents":
9139
9307
  return [
@@ -9292,6 +9460,7 @@ var VERBS = {
9292
9460
  "agent.close": "agent close",
9293
9461
  "agent.adopt": "agent adopt",
9294
9462
  "agent.kick": "agent kick",
9463
+ "agent.send": "agent send",
9295
9464
  // Payload prose reads "agent stalled ..."; keep the verb matching it
9296
9465
  // so the emitted text is not printed with a second, different verb.
9297
9466
  "agent.stall": "agent stalled",
@@ -9414,6 +9583,7 @@ function renderKnown(row, intent) {
9414
9583
  case "agent.close":
9415
9584
  case "agent.adopt":
9416
9585
  case "agent.kick":
9586
+ case "agent.send":
9417
9587
  case "agent.stall":
9418
9588
  case "workspace.create":
9419
9589
  case "workspace.free":
@@ -11167,6 +11337,7 @@ export {
11167
11337
  insertAgent,
11168
11338
  inspectLinks,
11169
11339
  installCapture,
11340
+ interruptAgent,
11170
11341
  isKickSignal,
11171
11342
  isPathInside,
11172
11343
  isTaskStatus,