@mu-crew/mu 3.7.0 → 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.
package/dist/index.js CHANGED
@@ -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) {
@@ -863,1769 +863,1769 @@ function unlinkCtlSocket(db, name, workstream) {
863
863
  }
864
864
  }
865
865
 
866
- // src/mux.ts
867
- var mux_exports = {};
868
- __export(mux_exports, {
869
- HerdrCommandOverrideError: () => HerdrCommandOverrideError,
870
- HerdrError: () => HerdrError,
871
- HerdrNotImplementedError: () => HerdrNotImplementedError,
872
- HerdrSyntaxError: () => HerdrSyntaxError,
873
- HerdrUnsupportedCliError: () => HerdrUnsupportedCliError,
874
- HerdrWorkspaceGroupCloseError: () => HerdrWorkspaceGroupCloseError,
875
- MuxError: () => MuxError,
876
- NoMultiplexerError: () => NoMultiplexerError,
877
- PaneNotFoundError: () => PaneNotFoundError,
878
- activeMux: () => activeMux,
879
- detectMux: () => detectMux,
880
- herdrBackend: () => herdrBackend,
881
- isHerdrStatusUsable: () => isHerdrStatusUsable,
882
- muxByName: () => muxByName,
883
- parseAgentNameFromTitle: () => parseAgentNameFromTitle,
884
- resetHerdrExecutor: () => resetHerdrExecutor,
885
- resetMux: () => resetMux,
886
- setHerdrExecutor: () => setHerdrExecutor,
887
- setMuxForTests: () => setMuxForTests,
888
- tmuxBackend: () => tmuxBackend
889
- });
890
-
891
- // src/mux/herdr.ts
892
- import { execa as execa2 } from "execa";
893
-
894
- // src/mux/tmux.ts
895
- import { execa } from "execa";
896
-
897
- // src/mux/input-timing.ts
898
- var TAIL_WINDOW_LINES = 100;
899
- var TAIL_LINES = 20;
900
- var BUSY_MARKERS = ["to interrupt)"];
901
- var BRAILLE_SPINNER_RE = /[\u2800-\u28ff]/;
902
- function extractTail(scrollback) {
903
- const lines = scrollback.split("\n");
904
- const window = lines.slice(-TAIL_WINDOW_LINES);
905
- let end = window.length;
906
- while (end > 0 && (window[end - 1] ?? "").trim() === "") end--;
907
- return window.slice(Math.max(0, end - TAIL_LINES), end).join("\n");
908
- }
909
- function paneLooksBusy(scrollback) {
910
- const tail = extractTail(scrollback);
911
- return BUSY_MARKERS.some((marker) => tail.includes(marker)) || BRAILLE_SPINNER_RE.test(tail);
912
- }
866
+ // src/logs.ts
867
+ import { randomUUID as randomUUID2 } from "crypto";
913
868
 
914
- // src/mux/types.ts
915
- function parseAgentNameFromTitle(title) {
916
- const idx = title.indexOf(" \xB7 ");
917
- return idx === -1 ? title.trim() : title.slice(0, idx).trim();
918
- }
919
- var MuxError = class extends Error {
920
- name = "MuxError";
921
- errorNextSteps() {
922
- return [{ intent: "Run health check", command: "mu doctor" }];
923
- }
924
- };
925
- var PaneNotFoundError = class extends Error {
926
- constructor(paneId, backend) {
927
- super(`${backend?.name ?? "mux"} pane not found: ${paneId}`);
928
- this.paneId = paneId;
929
- this.backend = backend;
930
- }
931
- paneId;
932
- backend;
933
- name = "PaneNotFoundError";
934
- errorNextSteps() {
935
- return [
936
- ...this.backend?.paneNotFoundNextSteps(this.paneId) ?? [],
937
- {
938
- intent: "List workstreams to choose the right scope",
939
- command: "mu workstream list"
940
- },
941
- {
942
- intent: "List registered agents and orphan panes in that scope",
943
- command: "mu agent list -w <workstream>"
944
- }
945
- ];
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";
946
880
  }
881
+ value;
947
882
  };
948
- var NoMultiplexerError = class extends Error {
949
- constructor(tried) {
883
+ var HlcOverflowError = class extends Error {
884
+ constructor(field, value) {
950
885
  super(
951
- `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`
952
887
  );
953
- this.tried = tried;
954
- }
955
- tried;
956
- name = "NoMultiplexerError";
957
- errorNextSteps() {
958
- return [
959
- { intent: "Run health check", command: "mu doctor" },
960
- { intent: "Verify tmux is installed", command: "tmux -V" },
961
- { intent: "Force a specific backend", command: "MU_MUX=tmux mu <verb>" }
962
- ];
888
+ this.field = field;
889
+ this.value = value;
890
+ this.name = "HlcOverflowError";
963
891
  }
892
+ field;
893
+ value;
964
894
  };
965
-
966
- // src/mux/tmux.ts
967
- var TmuxError = class extends MuxError {
968
- constructor(args, stderr, stdout, exitCode) {
969
- const detail = stderr.trim() || stdout.trim() || "no output";
970
- super(`tmux ${args.join(" ")} failed (exit ${exitCode}): ${detail}`);
971
- this.args = args;
972
- this.stderr = stderr;
973
- this.stdout = stdout;
974
- this.exitCode = exitCode;
975
- this.name = "TmuxError";
976
- }
977
- args;
978
- stderr;
979
- stdout;
980
- exitCode;
981
- errorNextSteps() {
982
- return [
983
- { intent: "Run health check", command: "mu doctor" },
984
- {
985
- intent: "Verify tmux is running and reachable",
986
- command: "tmux info | head"
987
- },
988
- {
989
- intent: "Check the failing tmux command in isolation",
990
- command: `tmux ${this.args.join(" ")}`
991
- }
992
- ];
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";
993
899
  }
994
900
  };
995
- var PANE_ID_RE = /^%\d+$/;
996
- function isValidPaneId(s) {
997
- 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}`;
998
912
  }
999
- function assertValidPaneId(s) {
1000
- if (!isValidPaneId(s)) {
1001
- 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);
1002
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 };
1003
925
  }
1004
- function defaultSendDelayMs() {
1005
- const raw = process.env.MU_SEND_DELAY_MS;
1006
- if (raw === void 0) return 500;
1007
- const parsed = Number.parseInt(raw, 10);
1008
- if (Number.isNaN(parsed) || parsed < 0) return 500;
1009
- return parsed;
1010
- }
1011
- function tmuxGlobalFlags() {
1012
- const socket = process.env.MU_TMUX_SOCKET;
1013
- if (socket === void 0 || socket.length === 0) return [];
1014
- return ["-L", socket, "-f", "/dev/null"];
926
+ function compareHlc(a, b) {
927
+ if (a < b) return -1;
928
+ if (a > b) return 1;
929
+ return 0;
1015
930
  }
1016
- var realExecutor = async (args) => {
1017
- const result = await execa("tmux", [...tmuxGlobalFlags(), ...args], { reject: false });
1018
- return {
1019
- stdout: result.stdout ?? "",
1020
- stderr: result.stderr ?? "",
1021
- exitCode: result.exitCode ?? null
1022
- };
1023
- };
1024
- var currentExecutor = realExecutor;
1025
- function setTmuxExecutor(executor) {
1026
- const previous = currentExecutor;
1027
- currentExecutor = executor;
1028
- return previous;
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 });
1029
941
  }
1030
- function resetTmuxExecutor() {
1031
- currentExecutor = realExecutor;
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();
1032
972
  }
1033
- async function tmux(args) {
1034
- const result = await currentExecutor(args);
1035
- if (result.exitCode !== 0) {
1036
- throw new TmuxError([...args], result.stderr, result.stdout, result.exitCode);
1037
- }
1038
- return result.stdout;
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);
1039
978
  }
1040
- var currentSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
1041
- function setSleepForTests(impl) {
1042
- const previous = currentSleep;
1043
- currentSleep = impl;
1044
- 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];
1045
985
  }
1046
- function resetSleep() {
1047
- 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 '\\')`;
1048
1005
  }
1049
- function sleep(ms) {
1050
- return currentSleep(ms);
1006
+ function workstreamScopeParams(workstream) {
1007
+ const escaped = workstream.replace(/[\\%_]/g, (c) => `\\${c}`);
1008
+ return [workstream, `${escaped}/%`];
1051
1009
  }
1052
- async function listSessions() {
1053
- try {
1054
- const out = await tmux(["list-sessions", "-F", "#{session_name}"]);
1055
- return out.split("\n").filter((line) => line.length > 0).map((name) => ({ name }));
1056
- } catch (err) {
1057
- if (err instanceof TmuxError && /no server running|no sessions/i.test(err.stderr)) {
1058
- return [];
1059
- }
1060
- 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
+ ];
1061
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
+ );
1062
1053
  }
1063
- async function sessionExists(name) {
1064
- const result = await currentExecutor(["has-session", "-t", name]);
1065
- 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";
1066
1057
  }
1067
- async function newSession(name, opts = {}) {
1068
- const args = ["new-session"];
1069
- if (opts.detached !== false) args.push("-d");
1070
- args.push("-s", name);
1071
- if (opts.windowName) args.push("-n", opts.windowName);
1072
- if (opts.cwd) args.push("-c", opts.cwd);
1073
- appendEnvFlags(args, opts.env);
1074
- if (opts.command) args.push(opts.command);
1075
- 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
+ };
1076
1070
  }
1077
- async function newSessionWithPane(name, opts) {
1078
- const args = ["new-session"];
1079
- if (opts.detached !== false) args.push("-d");
1080
- args.push("-s", name, "-n", opts.windowName);
1081
- if (opts.cwd) args.push("-c", opts.cwd);
1082
- appendEnvFlags(args, opts.env);
1083
- args.push("-P", "-F", "#{pane_id}", opts.command);
1084
- const out = (await tmux(args)).trim();
1085
- assertValidPaneId(out);
1086
- 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
+ };
1087
1101
  }
1088
- async function killSession(name) {
1089
- const result = await currentExecutor(["kill-session", "-t", name]);
1090
- if (result.exitCode !== 0 && !/can't find session|session not found|no server running|no current target/i.test(result.stderr)) {
1091
- throw new TmuxError(
1092
- ["kill-session", "-t", name],
1093
- result.stderr,
1094
- result.stdout,
1095
- result.exitCode
1096
- );
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));
1097
1111
  }
1098
- }
1099
- async function listWindows(session) {
1100
- if (session) {
1101
- const out2 = await tmux(["list-windows", "-t", session, "-F", "#{window_id} #{window_name}"]);
1102
- return parseWindows(out2);
1112
+ if (opts.since !== void 0) {
1113
+ conditions.push("l.seq > ?");
1114
+ params.push(opts.since);
1103
1115
  }
1104
- const out = await tmux([
1105
- "list-windows",
1106
- "-a",
1107
- "-F",
1108
- "#{session_name} #{window_id} #{window_name}"
1109
- ]);
1110
- const windows = [];
1111
- for (const line of out.split("\n")) {
1112
- if (line.length === 0) continue;
1113
- const [sessionName, id, name] = line.split(" ");
1114
- if (!sessionName || !id || name === void 0) continue;
1115
- windows.push({ id, name, sessionName });
1116
+ if (opts.source !== void 0) {
1117
+ conditions.push("l.actor = ?");
1118
+ params.push(opts.source);
1116
1119
  }
1117
- return windows;
1118
- }
1119
- function parseWindows(output) {
1120
- const windows = [];
1121
- for (const line of output.split("\n")) {
1122
- if (line.length === 0) continue;
1123
- const [id, name] = line.split(" ");
1124
- if (!id || name === void 0) continue;
1125
- windows.push({ id, name });
1120
+ if (opts.kind !== void 0) {
1121
+ conditions.push("l.entity = ?");
1122
+ params.push(opts.kind);
1126
1123
  }
1127
- return windows;
1128
- }
1129
- async function newWindow(opts) {
1130
- const args = ["new-window"];
1131
- if (opts.detached !== false) args.push("-d");
1132
- if (opts.session) args.push("-t", opts.session);
1133
- args.push("-n", opts.name);
1134
- if (opts.cwd) args.push("-c", opts.cwd);
1135
- appendEnvFlags(args, opts.env);
1136
- args.push("-P", "-F", "#{pane_id}", opts.command);
1137
- const out = (await tmux(args)).trim();
1138
- assertValidPaneId(out);
1139
- return out;
1140
- }
1141
- async function listPanesInSession(session) {
1142
- const args = [
1143
- "list-panes",
1144
- "-s",
1145
- "-t",
1146
- session,
1147
- "-F",
1148
- "#{window_id} #{pane_id} #{pane_title} #{pane_current_command}"
1149
- ];
1150
- const result = await currentExecutor(args);
1151
- if (result.exitCode !== 0) {
1152
- if (/can't find (session|window)|no server running|no sessions|no current target/i.test(
1153
- result.stderr
1154
- )) {
1155
- return [];
1156
- }
1157
- throw new TmuxError(args, result.stderr, result.stdout, result.exitCode);
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);
1158
1128
  }
1159
- const panes = [];
1160
- for (const line of result.stdout.split("\n")) {
1161
- if (line.length === 0) continue;
1162
- const [windowId, paneId, title, command] = line.split(" ");
1163
- if (!windowId || !paneId || command === void 0) continue;
1164
- panes.push({ paneId, title: title ?? "", command, windowId });
1129
+ if (opts.group !== void 0) {
1130
+ conditions.push("l.group_id = ?");
1131
+ params.push(opts.group);
1165
1132
  }
1166
- return panes;
1167
- }
1168
- async function listPanes(target) {
1169
- if (target === "*") {
1170
- const out2 = await tmux([
1171
- "list-panes",
1172
- "-a",
1173
- "-F",
1174
- "#{session_name} #{window_id} #{pane_id} #{pane_title} #{pane_current_command}"
1175
- ]);
1176
- const panes2 = [];
1177
- for (const line of out2.split("\n")) {
1178
- if (line.length === 0) continue;
1179
- const [sessionName, windowId, paneId, title, command] = line.split(" ");
1180
- if (!sessionName || !windowId || !paneId || command === void 0) continue;
1181
- panes2.push({ paneId, title: title ?? "", command, windowId, sessionName });
1182
- }
1183
- return panes2;
1184
- }
1185
- const args = ["list-panes"];
1186
- if (target !== void 0) args.push("-t", target);
1187
- args.push("-F", "#{pane_id} #{pane_title} #{pane_current_command}");
1188
- const out = await tmux(args);
1189
- const panes = [];
1190
- for (const line of out.split("\n")) {
1191
- if (line.length === 0) continue;
1192
- const [paneId, title, command] = line.split(" ");
1193
- if (!paneId || command === void 0) continue;
1194
- panes.push({ paneId, title: title ?? "", command });
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);
1195
1138
  }
1196
- return panes;
1197
- }
1198
- async function splitWindow(opts) {
1199
- const args = ["split-window"];
1200
- if (opts.horizontal !== false) args.push("-h");
1201
- if (opts.detached !== false) args.push("-d");
1202
- args.push("-t", opts.target);
1203
- if (opts.cwd) args.push("-c", opts.cwd);
1204
- appendEnvFlags(args, opts.env);
1205
- args.push("-P", "-F", "#{pane_id}", opts.command);
1206
- const out = (await tmux(args)).trim();
1207
- assertValidPaneId(out);
1208
- return out;
1209
- }
1210
- function appendEnvFlags(args, env) {
1211
- if (!env) return;
1212
- for (const [k, v] of Object.entries(env)) {
1213
- if (k.length === 0) {
1214
- throw new TypeError("tmux env key must be non-empty");
1215
- }
1216
- if (k.includes("=")) {
1217
- throw new TypeError(`tmux env key must not contain '=': ${JSON.stringify(k)}`);
1218
- }
1219
- args.push("-e", `${k}=${v}`);
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);
1220
1143
  }
1144
+ const rows = db.prepare(sql).all(...params);
1145
+ return rows.map(rowFromDb);
1221
1146
  }
1222
- async function killPane(paneId) {
1223
- assertValidPaneId(paneId);
1224
- const result = await currentExecutor(["kill-pane", "-t", paneId]);
1225
- if (result.exitCode !== 0 && !/can't find pane/i.test(result.stderr)) {
1226
- throw new TmuxError(["kill-pane", "-t", paneId], result.stderr, result.stdout, result.exitCode);
1227
- }
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;
1228
1152
  }
1229
- async function paneExists(paneId) {
1230
- if (!isValidPaneId(paneId)) return false;
1231
- const result = await currentExecutor(["display-message", "-t", paneId, "-p", "#{pane_id}"]);
1232
- if (result.exitCode !== 0) return false;
1233
- return result.stdout.trim() === paneId;
1153
+ function emitEvent(db, workstream, intent, payload, source = "system") {
1154
+ appendLog(db, { workstream, source, kind: entityForIntent(intent), payload, intent });
1234
1155
  }
1235
- async function setPaneTitle(paneId, title) {
1236
- assertValidPaneId(paneId);
1237
- await tmux(["select-pane", "-t", paneId, "-T", title]);
1156
+ function entityForIntent(intent) {
1157
+ const dot = intent.indexOf(".");
1158
+ return dot === -1 ? intent : intent.slice(0, dot);
1238
1159
  }
1239
- async function getWindowIdForPane(paneId) {
1240
- if (!isValidPaneId(paneId)) return void 0;
1241
- const result = await currentExecutor(["display-message", "-t", paneId, "-p", "#{window_id}"]);
1242
- if (result.exitCode !== 0) return void 0;
1243
- const id = result.stdout.trim();
1244
- return id.length > 0 ? id : void 0;
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;
1245
1167
  }
1246
- function muBannersDisabled() {
1247
- return process.env.MU_BANNER_QUIET === "1";
1168
+ function lastClaimEventAt(db, workstream, localId) {
1169
+ return lastClaimOp(db, workstream, localId)?.created_at ?? null;
1248
1170
  }
1249
- async function enableMuPaneBordersForSession(session) {
1250
- if (muBannersDisabled()) return 0;
1251
- const windows = await listWindows(session).catch(() => []);
1252
- let n = 0;
1253
- for (const w of windows) {
1254
- try {
1255
- await enableMuPaneBorders(w.id);
1256
- n += 1;
1257
- } catch {
1258
- }
1259
- }
1260
- return n;
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");
1261
1214
  }
1262
- async function enableMuPaneBordersForPane(paneId) {
1263
- if (muBannersDisabled()) return;
1264
- const wid = await getWindowIdForPane(paneId).catch(() => void 0);
1265
- if (wid) await enableMuPaneBorders(wid).catch(() => {
1266
- });
1215
+ function paneLooksBusy(scrollback) {
1216
+ const tail = extractTail(scrollback);
1217
+ return BUSY_MARKERS.some((marker) => tail.includes(marker)) || BRAILLE_SPINNER_RE.test(tail);
1267
1218
  }
1268
- async function enableMuPaneBorders(target) {
1269
- if (muBannersDisabled()) return;
1270
- await tmux(["set-option", "-w", "-t", target, "pane-border-status", "top"]);
1271
- await tmux(["set-option", "-w", "-t", target, "pane-border-lines", "heavy"]);
1272
- await tmux(["set-option", "-w", "-t", target, "pane-active-border-style", "fg=cyan,bold"]);
1273
- await tmux(["set-option", "-w", "-t", target, "pane-border-style", "fg=brightblack"]);
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();
1274
1224
  }
1275
- async function paneTTY(paneId) {
1276
- assertValidPaneId(paneId);
1277
- const result = await currentExecutor(["display-message", "-t", paneId, "-p", "#{pane_tty}"]);
1278
- if (result.exitCode !== 0) {
1279
- if (/can't find pane|pane not found/i.test(result.stderr)) {
1280
- throw new PaneNotFoundError(paneId, tmuxBackend);
1281
- }
1282
- throw new TmuxError(
1283
- ["display-message", "-t", paneId, "-p", "#{pane_tty}"],
1284
- result.stderr,
1285
- result.stdout,
1286
- result.exitCode
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.`
1287
1258
  );
1259
+ this.tried = tried;
1288
1260
  }
1289
- const tty = result.stdout.trim();
1290
- if (tty === "") throw new PaneNotFoundError(paneId, tmuxBackend);
1291
- return tty;
1292
- }
1293
- async function getPaneTitle(paneId) {
1294
- if (!isValidPaneId(paneId)) return void 0;
1295
- const result = await currentExecutor(["display-message", "-t", paneId, "-p", "#{pane_title}"]);
1296
- if (result.exitCode !== 0) return void 0;
1297
- return result.stdout.trimEnd();
1298
- }
1299
- async function currentPaneTitle() {
1300
- const paneId = process.env.TMUX_PANE;
1301
- if (!paneId || !isValidPaneId(paneId)) return void 0;
1302
- return getPaneTitle(paneId);
1303
- }
1304
- async function currentAgentName() {
1305
- const title = await currentPaneTitle();
1306
- if (title === void 0) return void 0;
1307
- return parseAgentNameFromTitle(title);
1308
- }
1309
- async function currentSessionName() {
1310
- if (!process.env.TMUX) return void 0;
1311
- const result = await currentExecutor(["display-message", "-p", "#S"]);
1312
- if (result.exitCode !== 0) return void 0;
1313
- const name = result.stdout.trim();
1314
- return name === "" ? void 0 : name;
1315
- }
1316
- async function selectLayout(window, layout) {
1317
- await tmux(["select-layout", "-t", window, layout]);
1318
- }
1319
- function defaultSendReadinessMs() {
1320
- const raw = process.env.MU_SEND_READINESS_MS;
1321
- if (raw === void 0) return 15e3;
1322
- const parsed = Number.parseInt(raw, 10);
1323
- if (Number.isNaN(parsed) || parsed < 0) return 15e3;
1324
- return parsed;
1325
- }
1326
- var SEND_READINESS_POLL_MS = 250;
1327
- var QUIESCENCE_CONFIRMATIONS = 3;
1328
- var SUBMIT_RETRIES = 2;
1329
- var SUBMIT_SETTLE_MS = 600;
1330
- var SUBMIT_VERIFY_SAMPLES = 4;
1331
- var SUBMIT_VERIFY_INTERVAL_MS = 700;
1332
- var WORK_MARKERS = ["to interrupt)", "Working"];
1333
- function hasWorkMarker(scrollback) {
1334
- const tail = scrollback.split("\n").slice(-40).join("\n");
1335
- return WORK_MARKERS.some((m) => tail.includes(m));
1336
- }
1337
- async function awaitPaneQuiescence(paneId, budgetMs) {
1338
- if (budgetMs <= 0) return true;
1339
- const deadline = Date.now() + budgetMs;
1340
- let calm = 0;
1341
- for (; ; ) {
1342
- const scrollback = await capturePane(paneId, { lines: 50 }).catch(() => void 0);
1343
- const busy = scrollback === void 0 || paneLooksBusy(scrollback);
1344
- if (busy && scrollback !== void 0 && hasWorkMarker(scrollback)) return true;
1345
- if (!busy) {
1346
- calm++;
1347
- if (calm >= QUIESCENCE_CONFIRMATIONS) return true;
1348
- } else {
1349
- calm = 0;
1350
- }
1351
- const remaining = deadline - Date.now();
1352
- if (remaining <= 0) return false;
1353
- await currentSleep(Math.min(SEND_READINESS_POLL_MS, remaining));
1354
- }
1355
- }
1356
- function pasteProbe(text) {
1357
- const firstLine = (text.split("\n")[0] ?? "").trim();
1358
- return firstLine.slice(0, 24);
1359
- }
1360
- async function isTextStranded(paneId, probe) {
1361
- for (let i = 0; i < SUBMIT_VERIFY_SAMPLES; i++) {
1362
- if (i > 0) await currentSleep(SUBMIT_VERIFY_INTERVAL_MS);
1363
- const capture = await capturePane(paneId, { lines: 50 }).catch(() => void 0);
1364
- if (capture === void 0) return false;
1365
- if (paneLooksBusy(capture)) return false;
1366
- let count = 0;
1367
- let idx = capture.indexOf(probe);
1368
- while (idx !== -1 && count < 2) {
1369
- count++;
1370
- idx = capture.indexOf(probe, idx + probe.length);
1371
- }
1372
- if (count !== 1) return false;
1373
- }
1374
- return true;
1375
- }
1376
- function defaultUndeliveredWarning(warning) {
1377
- console.warn(`warning: ${warning.message}`);
1378
- }
1379
- async function sendToPane(paneId, text, opts = {}) {
1380
- assertValidPaneId(paneId);
1381
- const readinessMs = opts.readinessMs ?? defaultSendReadinessMs();
1382
- const quiesced = await awaitPaneQuiescence(paneId, readinessMs);
1383
- const copyResult = await currentExecutor(["copy-mode", "-q", "-t", paneId]);
1384
- if (copyResult.exitCode !== 0 && /can't find pane|no current target/i.test(copyResult.stderr)) {
1385
- throw new TmuxError(
1386
- ["copy-mode", "-q", "-t", paneId],
1387
- copyResult.stderr,
1388
- copyResult.stdout,
1389
- copyResult.exitCode
1390
- );
1391
- }
1392
- const bufferName = `mu-send-${process.pid}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
1393
- await tmux(["set-buffer", "-b", bufferName, text]);
1394
- try {
1395
- await tmux(["paste-buffer", "-p", "-d", "-r", "-b", bufferName, "-t", paneId]);
1396
- } catch (err) {
1397
- await currentExecutor(["delete-buffer", "-b", bufferName]).catch(() => {
1398
- });
1399
- throw err;
1400
- }
1401
- const delay2 = opts.delayMs ?? defaultSendDelayMs();
1402
- if (delay2 > 0) await currentSleep(delay2);
1403
- await tmux(["send-keys", "-t", paneId, "Enter"]);
1404
- if (readinessMs <= 0) return;
1405
- const probe = pasteProbe(text);
1406
- if (probe.length === 0) return;
1407
- for (let attempt = 0; attempt <= SUBMIT_RETRIES; attempt++) {
1408
- if (!await isTextStranded(paneId, probe)) return;
1409
- if (attempt === SUBMIT_RETRIES) break;
1410
- await tmux(["send-keys", "-t", paneId, "Enter"]);
1411
- await currentSleep(SUBMIT_SETTLE_MS);
1412
- }
1413
- const warn2 = opts.onUndelivered ?? defaultUndeliveredWarning;
1414
- warn2({
1415
- paneId,
1416
- reason: quiesced ? "paste-vanished" : "busy-at-deadline",
1417
- message: `send to ${paneId} was NOT submitted: the text is still sitting unsubmitted in the pane's input box after ${SUBMIT_RETRIES + 1} Enter attempts. The agent has NOT seen it. Press Enter in the pane, or re-send.`
1418
- });
1419
- }
1420
- async function capturePane(paneId, opts = {}) {
1421
- assertValidPaneId(paneId);
1422
- const args = ["capture-pane", "-t", paneId, "-p"];
1423
- if (opts.lines === void 0) {
1424
- args.push("-S", "-", "-E", "-");
1425
- } else if (opts.lines > 0) {
1426
- args.push("-S", `-${opts.lines}`);
1427
- }
1428
- return tmux(args);
1429
- }
1430
- async function tmuxAvailable() {
1431
- try {
1432
- await tmux(["-V"]);
1433
- return true;
1434
- } catch {
1435
- return false;
1436
- }
1437
- }
1438
- function attachTargetSpec(target) {
1439
- return target.window === void 0 ? target.session : `${target.session}:${target.window}`;
1440
- }
1441
- function attachHint(target) {
1442
- const spec = attachTargetSpec(target);
1443
- if (target.inside === true) return `tmux switch-client -t ${spec}`;
1444
- if (target.window === void 0) return `tmux attach -t ${target.session}`;
1445
- return `tmux attach -t ${target.session} && tmux select-window -t ${spec}`;
1446
- }
1447
- function attachCommands(target) {
1448
- const spec = attachTargetSpec(target);
1449
- if (target.inside === true) {
1450
- return [{ command: "tmux", args: ["switch-client", "-t", spec] }];
1451
- }
1452
- const steps = [{ command: "tmux", args: ["attach-session", "-t", target.session] }];
1453
- if (target.window !== void 0) {
1454
- steps.push({ command: "tmux", args: ["select-window", "-t", spec], optional: true });
1455
- }
1456
- return steps;
1457
- }
1458
- function paneNotFoundNextSteps(paneId) {
1459
- return [
1460
- {
1461
- intent: `Verify the pane id ${paneId} actually exists`,
1462
- command: `tmux display-message -t ${paneId} -p '#{pane_id} #{pane_title}'`
1463
- },
1464
- {
1465
- intent: "List all live panes across all sessions",
1466
- command: "tmux list-panes -a -F '#{session_name}:#{window_id}.#{pane_id}\\t#{pane_title}\\t#{pane_current_command}'"
1467
- }
1468
- ];
1469
- }
1470
- async function healthCheck() {
1471
- let version = null;
1472
- try {
1473
- version = (await tmux(["-V"])).trim();
1474
- } catch {
1475
- version = null;
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
+ ];
1476
1269
  }
1477
- return {
1478
- name: "tmux",
1479
- ok: version !== null,
1480
- version,
1481
- env: [
1482
- { name: "$TMUX", value: process.env.TMUX ?? null },
1483
- { name: "$TMUX_PANE", value: process.env.TMUX_PANE ?? null }
1484
- ],
1485
- remediation: "install tmux \u2265 3.0"
1486
- };
1487
- }
1488
- var tmuxBackend = Object.freeze({
1489
- name: "tmux",
1490
- available: tmuxAvailable,
1491
- isValidPaneId,
1492
- assertValidPaneId,
1493
- listSessions,
1494
- sessionExists,
1495
- newSession,
1496
- newSessionWithPane,
1497
- killSession,
1498
- listWindows,
1499
- newWindow,
1500
- selectLayout,
1501
- listPanes,
1502
- listPanesInSession,
1503
- splitWindow,
1504
- killPane,
1505
- paneExists,
1506
- paneTTY,
1507
- setPaneTitle,
1508
- getPaneTitle,
1509
- currentAgentName,
1510
- currentSessionName,
1511
- sendToPane,
1512
- capturePane,
1513
- enableMuPaneBordersForSession,
1514
- enableMuPaneBordersForPane,
1515
- attachHint,
1516
- attachCommands,
1517
- paneNotFoundNextSteps,
1518
- healthCheck
1519
- });
1270
+ };
1520
1271
 
1521
- // src/mux/herdr.ts
1522
- var HerdrError = class extends MuxError {
1523
- constructor(args, stderr, stdout, exitCode, code) {
1524
- const detail = parseErrorEnvelope(stderr)?.message ?? stderr.trim() ?? stdout.trim();
1525
- super(`herdr ${args.join(" ")} failed (exit ${exitCode}): ${detail || "no output"}`);
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}`);
1526
1277
  this.args = args;
1527
1278
  this.stderr = stderr;
1528
1279
  this.stdout = stdout;
1529
1280
  this.exitCode = exitCode;
1530
- this.code = code;
1531
- this.name = "HerdrError";
1281
+ this.name = "TmuxError";
1532
1282
  }
1533
1283
  args;
1534
1284
  stderr;
1535
1285
  stdout;
1536
1286
  exitCode;
1537
- code;
1538
1287
  errorNextSteps() {
1539
1288
  return [
1540
1289
  { intent: "Run health check", command: "mu doctor" },
1541
- { intent: "Verify the herdr server is running", command: "herdr status" },
1542
1290
  {
1543
- intent: "Check the failing herdr command in isolation",
1544
- command: `herdr ${this.args.join(" ")}`
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(" ")}`
1545
1297
  }
1546
1298
  ];
1547
1299
  }
1548
1300
  };
1549
- var HerdrSyntaxError = class extends Error {
1550
- constructor(args, output) {
1551
- super(
1552
- `herdr rejected the command line (exit 2) \u2014 this is a bug in mu, not a herdr outage. Ran: herdr ${args.join(" ")}. herdr said: ${output.trim().split("\n")[0] ?? "(no output)"}`
1553
- );
1554
- this.args = args;
1555
- this.output = output;
1556
- }
1557
- args;
1558
- output;
1559
- name = "HerdrSyntaxError";
1560
- };
1561
- var HerdrUnsupportedCliError = class extends Error {
1562
- constructor(cli, detail) {
1563
- super(
1564
- `herdr does not recognise --cli ${cli} as an interactive agent kind, so it cannot classify that pane's status. herdr said: ${detail}`
1565
- );
1566
- this.cli = cli;
1567
- this.detail = detail;
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+$/)`);
1568
1308
  }
1569
- cli;
1570
- detail;
1571
- name = "HerdrUnsupportedCliError";
1572
- errorNextSteps() {
1573
- return [
1574
- {
1575
- intent: "List the agent kinds this herdr knows",
1576
- command: "herdr agent start --help"
1577
- },
1578
- {
1579
- intent: "Spawn with a kind herdr supports",
1580
- command: "mu agent spawn <name> --cli pi -w <workstream>"
1581
- },
1582
- {
1583
- intent: "Or spawn this cli under tmux, which runs any command",
1584
- command: `MU_MUX=tmux mu agent spawn <name> --cli ${this.cli}`
1585
- }
1586
- ];
1587
- }
1588
- };
1589
- var HerdrCommandOverrideError = class extends Error {
1590
- constructor(cli, command, envVar) {
1591
- super(
1592
- (envVar === void 0 ? `--command ${JSON.stringify(command)} cannot be honoured on the herdr backend` : `${envVar}=${command} cannot be honoured on the herdr backend`) + `: 'herdr agent start --kind ${cli}' resolves the executable itself and has no override flag. mu refuses rather than starting a different binary than you asked for.`
1593
- );
1594
- this.cli = cli;
1595
- this.command = command;
1596
- this.envVar = envVar;
1597
- }
1598
- cli;
1599
- command;
1600
- envVar;
1601
- name = "HerdrCommandOverrideError";
1602
- errorNextSteps() {
1603
- return [
1604
- ...this.envVar === void 0 ? [
1605
- {
1606
- intent: "Spawn without --command so herdr resolves the agent itself",
1607
- command: `mu agent spawn <name> --cli ${this.cli} -w <workstream>`
1608
- }
1609
- ] : [
1610
- {
1611
- intent: `Unset the override so herdr resolves ${this.cli} itself`,
1612
- command: `unset ${this.envVar}`
1613
- }
1614
- ],
1615
- {
1616
- intent: "Or spawn under tmux, which runs the command you name",
1617
- command: `MU_MUX=tmux mu agent spawn <name> --cli ${this.cli}`
1618
- }
1619
- ];
1620
- }
1621
- };
1622
- var GROUP_CLOSE_REQUIRED_CODE = "workspace_group_close_required";
1623
- var HerdrWorkspaceGroupCloseError = class extends Error {
1624
- constructor(session, workspaceId, detail) {
1625
- super(
1626
- `herdr refused to close workspace ${workspaceId} (${session}): it has linked worktree workspaces, which mu did not create and will not close on your behalf. herdr said: ${detail}`
1627
- );
1628
- this.session = session;
1629
- this.workspaceId = workspaceId;
1630
- this.detail = detail;
1631
- }
1632
- session;
1633
- workspaceId;
1634
- detail;
1635
- name = "HerdrWorkspaceGroupCloseError";
1636
- errorNextSteps() {
1637
- return [
1638
- {
1639
- intent: "See which workspaces are linked as a group",
1640
- command: "herdr workspace list"
1641
- },
1642
- {
1643
- intent: "Remove the worktree workspaces you no longer want",
1644
- command: "herdr worktree remove <workspace-id>"
1645
- },
1646
- {
1647
- intent: "Or close the whole group yourself, then re-run the teardown",
1648
- command: `herdr workspace close ${this.workspaceId} --group`
1649
- }
1650
- ];
1651
- }
1652
- };
1653
- var HerdrNotImplementedError = class extends Error {
1654
- name = "HerdrNotImplementedError";
1655
- constructor(method, owner) {
1656
- super(`herdr backend: ${method} is not implemented yet (owned by task ${owner})`);
1657
- }
1658
- };
1659
- var HERDR_ORDINAL = "[0-9A-Z]+";
1660
- var PANE_ID_RE2 = new RegExp(`^w${HERDR_ORDINAL}:p${HERDR_ORDINAL}$`);
1661
- var WORKSPACE_ID_RE = new RegExp(`^w${HERDR_ORDINAL}$`);
1662
- var TAB_ID_RE = new RegExp(`^w${HERDR_ORDINAL}:t${HERDR_ORDINAL}$`);
1663
- function isValidPaneId2(s) {
1664
- return PANE_ID_RE2.test(s);
1665
1309
  }
1666
- function assertValidPaneId2(s) {
1667
- if (!isValidPaneId2(s)) {
1668
- throw new TypeError(
1669
- `invalid herdr pane id: ${JSON.stringify(s)} (expected ${PANE_ID_RE2.source})`
1670
- );
1671
- }
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;
1672
1316
  }
1673
- function herdrGlobalFlags() {
1674
- const session = process.env.MU_HERDR_SESSION;
1675
- if (session === void 0 || session.length === 0) return [];
1676
- return ["--session", session];
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"];
1677
1321
  }
1678
- var realExecutor2 = async (args) => {
1679
- const result = await execa2("herdr", [...herdrGlobalFlags(), ...args], { reject: false });
1322
+ var realExecutor = async (args) => {
1323
+ const result = await execa("tmux", [...tmuxGlobalFlags(), ...args], { reject: false });
1680
1324
  return {
1681
1325
  stdout: result.stdout ?? "",
1682
1326
  stderr: result.stderr ?? "",
1683
1327
  exitCode: result.exitCode ?? null
1684
1328
  };
1685
1329
  };
1686
- var currentExecutor2 = realExecutor2;
1687
- function setHerdrExecutor(executor) {
1688
- const previous = currentExecutor2;
1689
- currentExecutor2 = executor;
1330
+ var currentExecutor = realExecutor;
1331
+ function setTmuxExecutor(executor) {
1332
+ const previous = currentExecutor;
1333
+ currentExecutor = executor;
1690
1334
  return previous;
1691
1335
  }
1692
- function resetHerdrExecutor() {
1693
- currentExecutor2 = realExecutor2;
1336
+ function resetTmuxExecutor() {
1337
+ currentExecutor = realExecutor;
1694
1338
  }
1695
- function isRecord(v) {
1696
- return typeof v === "object" && v !== null && !Array.isArray(v);
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;
1697
1345
  }
1698
- function asString(v) {
1699
- return typeof v === "string" ? v : void 0;
1346
+ var currentSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
1347
+ function setSleepForTests(impl) {
1348
+ const previous = currentSleep;
1349
+ currentSleep = impl;
1350
+ return previous;
1700
1351
  }
1701
- function parseErrorEnvelope(stderr) {
1702
- const trimmed = stderr.trim();
1703
- if (trimmed.length === 0) return void 0;
1704
- let parsed;
1705
- try {
1706
- parsed = JSON.parse(trimmed);
1707
- } catch {
1708
- return void 0;
1709
- }
1710
- if (!isRecord(parsed)) return void 0;
1711
- const err = parsed.error;
1712
- if (!isRecord(err)) return void 0;
1713
- const code = asString(err.code);
1714
- const message = asString(err.message);
1715
- if (code === void 0) return void 0;
1716
- return { code, message: message ?? code };
1352
+ function resetSleep() {
1353
+ currentSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
1717
1354
  }
1718
- async function herdr(args) {
1719
- const result = await currentExecutor2(args);
1720
- if (result.exitCode === 2) {
1721
- throw new HerdrSyntaxError([...args], result.stderr || result.stdout);
1722
- }
1723
- if (result.exitCode !== 0) {
1724
- throw new HerdrError(
1725
- [...args],
1726
- result.stderr,
1727
- result.stdout,
1728
- result.exitCode,
1729
- parseErrorEnvelope(result.stderr)?.code
1730
- );
1731
- }
1732
- const trimmed = result.stdout.trim();
1733
- if (trimmed.length === 0) return {};
1734
- let parsed;
1735
- try {
1736
- parsed = JSON.parse(trimmed);
1737
- } catch {
1738
- throw new HerdrError([...args], result.stderr, result.stdout, result.exitCode);
1739
- }
1740
- if (!isRecord(parsed)) return {};
1741
- const inner = parsed.result;
1742
- return isRecord(inner) ? inner : {};
1355
+ function sleep(ms) {
1356
+ return currentSleep(ms);
1743
1357
  }
1744
- async function herdrTolerating(args, tolerate) {
1358
+ async function listSessions() {
1745
1359
  try {
1746
- return await herdr(args);
1360
+ const out = await tmux(["list-sessions", "-F", "#{session_name}"]);
1361
+ return out.split("\n").filter((line) => line.length > 0).map((name) => ({ name }));
1747
1362
  } catch (err) {
1748
- if (err instanceof HerdrError && err.code !== void 0 && tolerate.includes(err.code)) {
1749
- return void 0;
1363
+ if (err instanceof TmuxError && /no server running|no sessions/i.test(err.stderr)) {
1364
+ return [];
1750
1365
  }
1751
1366
  throw err;
1752
1367
  }
1753
1368
  }
1754
- function readWorkspace(v) {
1755
- if (!isRecord(v)) return void 0;
1756
- const workspaceId = asString(v.workspace_id);
1757
- if (workspaceId === void 0) return void 0;
1758
- return { workspaceId, label: asString(v.label) ?? "" };
1759
- }
1760
- function readTab(v) {
1761
- if (!isRecord(v)) return void 0;
1762
- const id = asString(v.tab_id);
1763
- if (id === void 0) return void 0;
1764
- return { id, name: asString(v.label) ?? "" };
1765
- }
1766
- function readPane(v) {
1767
- if (!isRecord(v)) return void 0;
1768
- const paneId = asString(v.pane_id);
1769
- if (paneId === void 0) return void 0;
1770
- return {
1771
- paneId,
1772
- // herdr's optional `label` is what `pane rename` writes and is
1773
- // therefore mu's pane-title equivalent.
1774
- title: asString(v.label) ?? "",
1775
- // herdr does not report a foreground command on `pane list`; the
1776
- // authoritative answer costs a `pane process-info` round trip per
1777
- // pane. Callers that need it use `paneCommand()`.
1778
- command: "",
1779
- windowId: asString(v.tab_id)
1780
- };
1369
+ async function sessionExists(name) {
1370
+ const result = await currentExecutor(["has-session", "-t", name]);
1371
+ return result.exitCode === 0;
1781
1372
  }
1782
- function readArray(result, key) {
1783
- const v = result[key];
1784
- return Array.isArray(v) ? v : [];
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);
1785
1382
  }
1786
- async function listWorkspaces() {
1787
- const result = await herdr(["workspace", "list"]);
1788
- const out = [];
1789
- for (const raw of readArray(result, "workspaces")) {
1790
- const ws = readWorkspace(raw);
1791
- if (ws) out.push(ws);
1792
- }
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);
1793
1392
  return out;
1794
1393
  }
1795
- async function listSessions2() {
1796
- return (await listWorkspaces()).map((ws) => ({ name: ws.label }));
1797
- }
1798
- async function resolveWorkspaceId(name) {
1799
- for (const ws of await listWorkspaces()) {
1800
- if (ws.label === name) return ws.workspaceId;
1801
- }
1802
- return void 0;
1803
- }
1804
- async function requireWorkspaceId(name) {
1805
- const id = await resolveWorkspaceId(name);
1806
- if (id === void 0) {
1807
- throw new HerdrError(
1808
- ["workspace", "list"],
1809
- `{"error":{"code":"workspace_not_found","message":"no herdr workspace labelled ${name}"}}`,
1810
- "",
1811
- 1,
1812
- "workspace_not_found"
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
1813
1402
  );
1814
1403
  }
1815
- return id;
1816
- }
1817
- async function sessionExists2(name) {
1818
- return await resolveWorkspaceId(name) !== void 0;
1819
- }
1820
- async function newSession2(name, opts = {}) {
1821
- rejectCommand("newSession", opts.command);
1822
- const args = ["workspace", "create", "--label", name, "--no-focus"];
1823
- if (opts.cwd) args.push("--cwd", opts.cwd);
1824
- appendEnvFlags2(args, opts.env);
1825
- await herdr(args);
1826
- }
1827
- async function newSessionWithPane2(name, opts) {
1828
- rejectCommand("newSessionWithPane", opts.command);
1829
- const args = ["workspace", "create", "--label", name, "--no-focus"];
1830
- if (opts.cwd) args.push("--cwd", opts.cwd);
1831
- appendEnvFlags2(args, opts.env);
1832
- const result = await herdr(args);
1833
- return readCreatedPaneId(result, "root_pane", args);
1834
- }
1835
- async function killSession2(name) {
1836
- const id = await resolveWorkspaceId(name);
1837
- if (id === void 0) return;
1838
- try {
1839
- await herdrTolerating(["workspace", "close", id], ["workspace_not_found"]);
1840
- } catch (err) {
1841
- if (err instanceof HerdrError && err.code === GROUP_CLOSE_REQUIRED_CODE) {
1842
- throw new HerdrWorkspaceGroupCloseError(name, id, err.message);
1843
- }
1844
- throw err;
1845
- }
1846
1404
  }
1847
- async function listWindows2(session) {
1848
- if (session !== void 0) {
1849
- const id = await resolveWorkspaceId(session);
1850
- if (id === void 0) return [];
1851
- return readTabs(await herdr(["tab", "list", "--workspace", id]));
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);
1852
1409
  }
1410
+ const out = await tmux([
1411
+ "list-windows",
1412
+ "-a",
1413
+ "-F",
1414
+ "#{session_name} #{window_id} #{window_name}"
1415
+ ]);
1853
1416
  const windows = [];
1854
- for (const ws of await listWorkspaces()) {
1855
- for (const tab of readTabs(await herdr(["tab", "list", "--workspace", ws.workspaceId]))) {
1856
- windows.push({ ...tab, sessionName: ws.label });
1857
- }
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 });
1858
1422
  }
1859
1423
  return windows;
1860
1424
  }
1861
- function readTabs(result) {
1862
- const out = [];
1863
- for (const raw of readArray(result, "tabs")) {
1864
- const tab = readTab(raw);
1865
- if (tab) out.push(tab);
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 });
1866
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);
1867
1445
  return out;
1868
1446
  }
1869
- async function newWindow2(opts) {
1870
- rejectCommand("newWindow", opts.command);
1871
- const args = ["tab", "create"];
1872
- if (opts.session !== void 0) {
1873
- args.push("--workspace", await requireWorkspaceId(opts.session));
1447
+ async function listPanesInSession(session) {
1448
+ const args = [
1449
+ "list-panes",
1450
+ "-s",
1451
+ "-t",
1452
+ session,
1453
+ "-F",
1454
+ "#{window_id} #{pane_id} #{pane_title} #{pane_current_command}"
1455
+ ];
1456
+ const result = await currentExecutor(args);
1457
+ if (result.exitCode !== 0) {
1458
+ if (/can't find (session|window)|no server running|no sessions|no current target/i.test(
1459
+ result.stderr
1460
+ )) {
1461
+ return [];
1462
+ }
1463
+ throw new TmuxError(args, result.stderr, result.stdout, result.exitCode);
1874
1464
  }
1875
- args.push("--label", opts.name, "--no-focus");
1876
- if (opts.cwd) args.push("--cwd", opts.cwd);
1877
- appendEnvFlags2(args, opts.env);
1878
- return readCreatedPaneId(await herdr(args), "root_pane", args);
1879
- }
1880
- async function selectLayout2(_window, _layout) {
1881
- }
1882
- async function listPanesInSession2(session) {
1883
- const id = await resolveWorkspaceId(session);
1884
- if (id === void 0) return [];
1885
- const result = await herdrTolerating(
1886
- ["pane", "list", "--workspace", id],
1887
- ["workspace_not_found"]
1888
- );
1889
- if (result === void 0) return [];
1890
- return readPanes(result).map((p) => ({ ...p, sessionName: session }));
1465
+ const panes = [];
1466
+ for (const line of result.stdout.split("\n")) {
1467
+ if (line.length === 0) continue;
1468
+ const [windowId, paneId, title, command] = line.split(" ");
1469
+ if (!windowId || !paneId || command === void 0) continue;
1470
+ panes.push({ paneId, title: title ?? "", command, windowId });
1471
+ }
1472
+ return panes;
1891
1473
  }
1892
- async function listPanes2(target) {
1893
- if (target !== void 0 && target !== "*") {
1894
- if (TAB_ID_RE.test(target)) {
1895
- const workspaceId = target.split(":")[0] ?? target;
1896
- const panes2 = readPanes(await herdr(["pane", "list", "--workspace", workspaceId]));
1897
- return panes2.filter((p) => p.windowId === target);
1898
- }
1899
- if (WORKSPACE_ID_RE.test(target)) {
1900
- return readPanes(await herdr(["pane", "list", "--workspace", target]));
1474
+ async function listPanes(target) {
1475
+ if (target === "*") {
1476
+ const out2 = await tmux([
1477
+ "list-panes",
1478
+ "-a",
1479
+ "-F",
1480
+ "#{session_name} #{window_id} #{pane_id} #{pane_title} #{pane_current_command}"
1481
+ ]);
1482
+ const panes2 = [];
1483
+ for (const line of out2.split("\n")) {
1484
+ if (line.length === 0) continue;
1485
+ const [sessionName, windowId, paneId, title, command] = line.split(" ");
1486
+ if (!sessionName || !windowId || !paneId || command === void 0) continue;
1487
+ panes2.push({ paneId, title: title ?? "", command, windowId, sessionName });
1901
1488
  }
1902
- return listPanesInSession2(target);
1903
- }
1904
- const ambient = process.env.HERDR_WORKSPACE_ID;
1905
- if (target === void 0 && ambient !== void 0 && WORKSPACE_ID_RE.test(ambient)) {
1906
- return readPanes(await herdr(["pane", "list", "--workspace", ambient]));
1489
+ return panes2;
1907
1490
  }
1491
+ const args = ["list-panes"];
1492
+ if (target !== void 0) args.push("-t", target);
1493
+ args.push("-F", "#{pane_id} #{pane_title} #{pane_current_command}");
1494
+ const out = await tmux(args);
1908
1495
  const panes = [];
1909
- for (const ws of await listWorkspaces()) {
1910
- for (const pane of readPanes(await herdr(["pane", "list", "--workspace", ws.workspaceId]))) {
1911
- panes.push({ ...pane, sessionName: ws.label });
1912
- }
1496
+ for (const line of out.split("\n")) {
1497
+ if (line.length === 0) continue;
1498
+ const [paneId, title, command] = line.split(" ");
1499
+ if (!paneId || command === void 0) continue;
1500
+ panes.push({ paneId, title: title ?? "", command });
1913
1501
  }
1914
1502
  return panes;
1915
1503
  }
1916
- function readPanes(result) {
1917
- const out = [];
1918
- for (const raw of readArray(result, "panes")) {
1919
- const pane = readPane(raw);
1920
- if (pane) out.push(pane);
1921
- }
1504
+ async function splitWindow(opts) {
1505
+ const args = ["split-window"];
1506
+ if (opts.horizontal !== false) args.push("-h");
1507
+ if (opts.detached !== false) args.push("-d");
1508
+ args.push("-t", opts.target);
1509
+ if (opts.cwd) args.push("-c", opts.cwd);
1510
+ appendEnvFlags(args, opts.env);
1511
+ args.push("-P", "-F", "#{pane_id}", opts.command);
1512
+ const out = (await tmux(args)).trim();
1513
+ assertValidPaneId(out);
1922
1514
  return out;
1923
1515
  }
1924
- async function splitWindow2(opts) {
1925
- rejectCommand("splitWindow", opts.command);
1926
- assertValidPaneId2(opts.target);
1927
- const args = [
1928
- "pane",
1929
- "split",
1930
- opts.target,
1931
- "--direction",
1932
- opts.horizontal === false ? "down" : "right",
1933
- "--no-focus"
1934
- ];
1935
- if (opts.cwd) args.push("--cwd", opts.cwd);
1936
- appendEnvFlags2(args, opts.env);
1937
- return readCreatedPaneId(await herdr(args), "pane", args);
1516
+ function appendEnvFlags(args, env) {
1517
+ if (!env) return;
1518
+ for (const [k, v] of Object.entries(env)) {
1519
+ if (k.length === 0) {
1520
+ throw new TypeError("tmux env key must be non-empty");
1521
+ }
1522
+ if (k.includes("=")) {
1523
+ throw new TypeError(`tmux env key must not contain '=': ${JSON.stringify(k)}`);
1524
+ }
1525
+ args.push("-e", `${k}=${v}`);
1526
+ }
1938
1527
  }
1939
- async function killPane2(paneId) {
1940
- assertValidPaneId2(paneId);
1941
- await herdrTolerating(["pane", "close", paneId], ["pane_not_found", "workspace_not_found"]);
1528
+ async function killPane(paneId) {
1529
+ assertValidPaneId(paneId);
1530
+ const result = await currentExecutor(["kill-pane", "-t", paneId]);
1531
+ if (result.exitCode !== 0 && !/can't find pane/i.test(result.stderr)) {
1532
+ throw new TmuxError(["kill-pane", "-t", paneId], result.stderr, result.stdout, result.exitCode);
1533
+ }
1942
1534
  }
1943
- async function paneExists2(paneId) {
1944
- if (!isValidPaneId2(paneId)) return false;
1945
- const pane = await getPane(paneId);
1946
- return pane !== void 0;
1535
+ async function paneExists(paneId) {
1536
+ if (!isValidPaneId(paneId)) return false;
1537
+ const result = await currentExecutor(["display-message", "-t", paneId, "-p", "#{pane_id}"]);
1538
+ if (result.exitCode !== 0) return false;
1539
+ return result.stdout.trim() === paneId;
1947
1540
  }
1948
- async function getPane(paneId) {
1949
- const result = await herdrTolerating(
1950
- ["pane", "get", paneId],
1951
- ["pane_not_found", "workspace_not_found"]
1952
- );
1953
- if (result === void 0) return void 0;
1954
- return readPane(result.pane);
1955
- }
1956
- async function paneTTY2(paneId) {
1957
- assertValidPaneId2(paneId);
1958
- const result = await herdrTolerating(
1959
- ["pane", "process-info", "--pane", paneId],
1960
- ["pane_not_found", "workspace_not_found"]
1961
- );
1962
- if (result === void 0) throw new PaneNotFoundError(paneId);
1963
- const info = result.process_info;
1964
- const pid = isRecord(info) ? info.shell_pid : void 0;
1965
- if (typeof pid !== "number") throw new PaneNotFoundError(paneId);
1966
- const { readlink } = await import("fs/promises");
1967
- try {
1968
- return await readlink(`/proc/${pid}/fd/0`);
1969
- } catch (err) {
1970
- throw new HerdrError(
1971
- ["pane", "process-info", "--pane", paneId],
1972
- `cannot resolve the TTY for herdr pane ${paneId} (shell pid ${pid}) via /proc: ${String(err)}`,
1973
- "",
1974
- 1
1975
- );
1976
- }
1977
- }
1978
- async function setPaneTitle2(paneId, title) {
1979
- assertValidPaneId2(paneId);
1980
- await herdr(["pane", "rename", paneId, title]);
1981
- }
1982
- async function getPaneTitle2(paneId) {
1983
- if (!isValidPaneId2(paneId)) return void 0;
1984
- const pane = await getPane(paneId).catch(() => void 0);
1985
- if (pane === void 0) return void 0;
1986
- return pane.title.length > 0 ? pane.title : void 0;
1541
+ async function setPaneTitle(paneId, title) {
1542
+ assertValidPaneId(paneId);
1543
+ await tmux(["select-pane", "-t", paneId, "-T", title]);
1987
1544
  }
1988
- async function currentAgentName2() {
1989
- const paneId = process.env.HERDR_PANE_ID;
1990
- if (paneId === void 0 || !isValidPaneId2(paneId)) return void 0;
1991
- const title = await getPaneTitle2(paneId);
1992
- if (title === void 0) return void 0;
1993
- const idx = title.indexOf(" \xB7 ");
1994
- return idx === -1 ? title.trim() : title.slice(0, idx).trim();
1545
+ async function getWindowIdForPane(paneId) {
1546
+ if (!isValidPaneId(paneId)) return void 0;
1547
+ const result = await currentExecutor(["display-message", "-t", paneId, "-p", "#{window_id}"]);
1548
+ if (result.exitCode !== 0) return void 0;
1549
+ const id = result.stdout.trim();
1550
+ return id.length > 0 ? id : void 0;
1995
1551
  }
1996
- async function currentSessionName2() {
1997
- const workspaceId = process.env.HERDR_WORKSPACE_ID;
1998
- if (workspaceId === void 0 || !WORKSPACE_ID_RE.test(workspaceId)) return void 0;
1999
- for (const ws of await listWorkspaces()) {
2000
- if (ws.workspaceId === workspaceId) return ws.label;
2001
- }
2002
- return void 0;
1552
+ function muBannersDisabled() {
1553
+ return process.env.MU_BANNER_QUIET === "1";
2003
1554
  }
2004
- var PANE_READY_RETRY_INTERVAL_MS = 50;
2005
- var PANE_READY_MAX_ATTEMPTS = 60;
2006
- async function startAgentInPane(opts) {
2007
- assertValidPaneId2(opts.paneId);
2008
- if (opts.commandSource !== "cli-key") {
2009
- throw new HerdrCommandOverrideError(
2010
- opts.cli,
2011
- opts.command,
2012
- // Name the env var only when that is actually where the override
2013
- // came from; "unset MU_PI_COMMAND" is useless advice to someone who
2014
- // typed --command.
2015
- opts.commandSource === "env" ? envVarNameForCli(opts.cli) : void 0
2016
- );
2017
- }
2018
- const args = ["agent", "start", opts.name, "--kind", opts.cli, "--pane", opts.paneId];
2019
- for (let attempt = 1; ; attempt += 1) {
1555
+ async function enableMuPaneBordersForSession(session) {
1556
+ if (muBannersDisabled()) return 0;
1557
+ const windows = await listWindows(session).catch(() => []);
1558
+ let n = 0;
1559
+ for (const w of windows) {
2020
1560
  try {
2021
- await herdr(args);
2022
- return;
2023
- } catch (err) {
2024
- if (err instanceof HerdrSyntaxError) {
2025
- throw new HerdrUnsupportedCliError(opts.cli, err.output.trim().split("\n")[0] ?? "");
2026
- }
2027
- const busy = err instanceof HerdrError && err.code === "agent_pane_busy";
2028
- if (!busy || attempt >= PANE_READY_MAX_ATTEMPTS) throw err;
2029
- await sleep(PANE_READY_RETRY_INTERVAL_MS);
1561
+ await enableMuPaneBorders(w.id);
1562
+ n += 1;
1563
+ } catch {
2030
1564
  }
2031
1565
  }
1566
+ return n;
2032
1567
  }
2033
- function envVarNameForCli(cli) {
2034
- return `MU_${cli.toUpperCase().replace(/-/g, "_")}_COMMAND`;
2035
- }
2036
- var PROMPT_STALLED_CODE = "agent_prompt_stalled";
2037
- var NO_AGENT_CODES = ["agent_not_found", "agent_not_recognized"];
2038
- function defaultUndeliveredWarning2(warning) {
2039
- console.warn(`warning: ${warning.message}`);
1568
+ async function enableMuPaneBordersForPane(paneId) {
1569
+ if (muBannersDisabled()) return;
1570
+ const wid = await getWindowIdForPane(paneId).catch(() => void 0);
1571
+ if (wid) await enableMuPaneBorders(wid).catch(() => {
1572
+ });
2040
1573
  }
2041
- async function sendToPane2(paneId, text, opts = {}) {
2042
- assertValidPaneId2(paneId);
2043
- const wait = opts.readinessMs === 0 ? [] : ["--wait"];
2044
- try {
2045
- await herdr(["agent", "prompt", paneId, text, ...wait]);
2046
- return;
2047
- } catch (err) {
2048
- if (!(err instanceof HerdrError) || err.code === void 0) throw err;
2049
- if (err.code === PROMPT_STALLED_CODE) {
2050
- const warn2 = opts.onUndelivered ?? defaultUndeliveredWarning2;
2051
- warn2({
2052
- paneId,
2053
- reason: "paste-vanished",
2054
- 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.`
2055
- });
2056
- return;
2057
- }
2058
- if (!NO_AGENT_CODES.includes(err.code)) throw err;
2059
- }
2060
- await herdr(["pane", "run", paneId, text]);
1574
+ async function enableMuPaneBorders(target) {
1575
+ if (muBannersDisabled()) return;
1576
+ await tmux(["set-option", "-w", "-t", target, "pane-border-status", "top"]);
1577
+ await tmux(["set-option", "-w", "-t", target, "pane-border-lines", "heavy"]);
1578
+ await tmux(["set-option", "-w", "-t", target, "pane-active-border-style", "fg=cyan,bold"]);
1579
+ await tmux(["set-option", "-w", "-t", target, "pane-border-style", "fg=brightblack"]);
2061
1580
  }
2062
- async function capturePane2(paneId, opts = {}) {
2063
- assertValidPaneId2(paneId);
2064
- const args = ["pane", "read", paneId];
2065
- if (opts.lines === 0) {
2066
- args.push("--source", "visible");
2067
- } else {
2068
- args.push("--source", "recent-unwrapped");
2069
- if (opts.lines !== void 0) args.push("--lines", String(opts.lines));
2070
- }
2071
- const result = await currentExecutor2(args);
2072
- if (result.exitCode === 2) throw new HerdrSyntaxError(args, result.stderr || result.stdout);
1581
+ async function paneTTY(paneId) {
1582
+ assertValidPaneId(paneId);
1583
+ const result = await currentExecutor(["display-message", "-t", paneId, "-p", "#{pane_tty}"]);
2073
1584
  if (result.exitCode !== 0) {
2074
- throw new HerdrError(
2075
- args,
1585
+ if (/can't find pane|pane not found/i.test(result.stderr)) {
1586
+ throw new PaneNotFoundError(paneId, tmuxBackend);
1587
+ }
1588
+ throw new TmuxError(
1589
+ ["display-message", "-t", paneId, "-p", "#{pane_tty}"],
2076
1590
  result.stderr,
2077
1591
  result.stdout,
2078
- result.exitCode,
2079
- parseErrorEnvelope(result.stderr)?.code
1592
+ result.exitCode
2080
1593
  );
2081
1594
  }
2082
- return result.stdout;
1595
+ const tty = result.stdout.trim();
1596
+ if (tty === "") throw new PaneNotFoundError(paneId, tmuxBackend);
1597
+ return tty;
2083
1598
  }
2084
- function mapAgentStatus(agentStatus) {
2085
- switch (agentStatus) {
2086
- case "working":
2087
- return "busy";
2088
- case "blocked":
2089
- return "needs_permission";
2090
- default:
2091
- return "needs_input";
2092
- }
1599
+ async function getPaneTitle(paneId) {
1600
+ if (!isValidPaneId(paneId)) return void 0;
1601
+ const result = await currentExecutor(["display-message", "-t", paneId, "-p", "#{pane_title}"]);
1602
+ if (result.exitCode !== 0) return void 0;
1603
+ return result.stdout.trimEnd();
2093
1604
  }
2094
- async function paneStatus(paneId) {
2095
- if (!isValidPaneId2(paneId)) return void 0;
2096
- const result = await herdrTolerating(
2097
- ["pane", "get", paneId],
2098
- ["pane_not_found", "workspace_not_found"]
2099
- );
2100
- if (result === void 0) return void 0;
2101
- const pane = result.pane;
2102
- const status = isRecord(pane) ? asString(pane.agent_status) : void 0;
2103
- if (status === void 0) return void 0;
2104
- return mapAgentStatus(status);
1605
+ async function currentPaneTitle() {
1606
+ const paneId = process.env.TMUX_PANE;
1607
+ if (!paneId || !isValidPaneId(paneId)) return void 0;
1608
+ return getPaneTitle(paneId);
2105
1609
  }
2106
- async function enableMuPaneBordersForSession2(_session) {
2107
- return 0;
1610
+ async function currentAgentName() {
1611
+ const title = await currentPaneTitle();
1612
+ if (title === void 0) return void 0;
1613
+ return parseAgentNameFromTitle(title);
2108
1614
  }
2109
- async function enableMuPaneBordersForPane2(_paneId) {
1615
+ async function currentSessionName() {
1616
+ if (!process.env.TMUX) return void 0;
1617
+ const result = await currentExecutor(["display-message", "-p", "#S"]);
1618
+ if (result.exitCode !== 0) return void 0;
1619
+ const name = result.stdout.trim();
1620
+ return name === "" ? void 0 : name;
2110
1621
  }
2111
- function appendEnvFlags2(args, env) {
2112
- if (!env) return;
2113
- for (const [k, v] of Object.entries(env)) {
2114
- if (k.length === 0) throw new TypeError("herdr env key must be non-empty");
2115
- if (k.includes("=")) {
2116
- throw new TypeError(`herdr env key must not contain '=': ${JSON.stringify(k)}`);
1622
+ async function selectLayout(window, layout) {
1623
+ await tmux(["select-layout", "-t", window, layout]);
1624
+ }
1625
+ function defaultSendReadinessMs() {
1626
+ const raw = process.env.MU_SEND_READINESS_MS;
1627
+ if (raw === void 0) return 15e3;
1628
+ const parsed = Number.parseInt(raw, 10);
1629
+ if (Number.isNaN(parsed) || parsed < 0) return 15e3;
1630
+ return parsed;
1631
+ }
1632
+ var SEND_READINESS_POLL_MS = 250;
1633
+ var QUIESCENCE_CONFIRMATIONS = 3;
1634
+ var SUBMIT_RETRIES = 2;
1635
+ var SUBMIT_SETTLE_MS = 600;
1636
+ var SUBMIT_VERIFY_SAMPLES = 4;
1637
+ var SUBMIT_VERIFY_INTERVAL_MS = 700;
1638
+ var WORK_MARKERS = ["to interrupt)", "Working"];
1639
+ function hasWorkMarker(scrollback) {
1640
+ const tail = scrollback.split("\n").slice(-40).join("\n");
1641
+ return WORK_MARKERS.some((m) => tail.includes(m));
1642
+ }
1643
+ async function awaitPaneQuiescence(paneId, budgetMs) {
1644
+ if (budgetMs <= 0) return true;
1645
+ const deadline = Date.now() + budgetMs;
1646
+ let calm = 0;
1647
+ for (; ; ) {
1648
+ const scrollback = await capturePane(paneId, { lines: 50 }).catch(() => void 0);
1649
+ const busy = scrollback === void 0 || paneLooksBusy(scrollback);
1650
+ if (busy && scrollback !== void 0 && hasWorkMarker(scrollback)) return true;
1651
+ if (!busy) {
1652
+ calm++;
1653
+ if (calm >= QUIESCENCE_CONFIRMATIONS) return true;
1654
+ } else {
1655
+ calm = 0;
2117
1656
  }
2118
- args.push("--env", `${k}=${v}`);
1657
+ const remaining = deadline - Date.now();
1658
+ if (remaining <= 0) return false;
1659
+ await currentSleep(Math.min(SEND_READINESS_POLL_MS, remaining));
2119
1660
  }
2120
1661
  }
2121
- function rejectCommand(method, command) {
2122
- if (command === void 0 || command.trim().length === 0) return;
2123
- throw new HerdrNotImplementedError(`${method} with a command`, "mux-herdr-spawn");
1662
+ function pasteProbe(text) {
1663
+ const firstLine = (text.split("\n")[0] ?? "").trim();
1664
+ return firstLine.slice(0, 24);
2124
1665
  }
2125
- function readCreatedPaneId(result, key, args) {
2126
- const pane = readPane(result[key]);
2127
- if (pane === void 0) {
2128
- throw new HerdrError(
2129
- args,
2130
- `herdr response had no ${key}.pane_id: ${JSON.stringify(result)}`,
2131
- "",
2132
- 0
1666
+ async function isTextStranded(paneId, probe) {
1667
+ for (let i = 0; i < SUBMIT_VERIFY_SAMPLES; i++) {
1668
+ if (i > 0) await currentSleep(SUBMIT_VERIFY_INTERVAL_MS);
1669
+ const capture = await capturePane(paneId, { lines: 50 }).catch(() => void 0);
1670
+ if (capture === void 0) return false;
1671
+ if (paneLooksBusy(capture)) return false;
1672
+ let count = 0;
1673
+ let idx = capture.indexOf(probe);
1674
+ while (idx !== -1 && count < 2) {
1675
+ count++;
1676
+ idx = capture.indexOf(probe, idx + probe.length);
1677
+ }
1678
+ if (count !== 1) return false;
1679
+ }
1680
+ return true;
1681
+ }
1682
+ function defaultUndeliveredWarning(warning) {
1683
+ console.warn(`warning: ${warning.message}`);
1684
+ }
1685
+ async function sendToPane(paneId, text, opts = {}) {
1686
+ assertValidPaneId(paneId);
1687
+ const readinessMs = opts.readinessMs ?? defaultSendReadinessMs();
1688
+ const quiesced = await awaitPaneQuiescence(paneId, readinessMs);
1689
+ const copyResult = await currentExecutor(["copy-mode", "-q", "-t", paneId]);
1690
+ if (copyResult.exitCode !== 0 && /can't find pane|no current target/i.test(copyResult.stderr)) {
1691
+ throw new TmuxError(
1692
+ ["copy-mode", "-q", "-t", paneId],
1693
+ copyResult.stderr,
1694
+ copyResult.stdout,
1695
+ copyResult.exitCode
2133
1696
  );
2134
1697
  }
2135
- assertValidPaneId2(pane.paneId);
2136
- return pane.paneId;
1698
+ const bufferName = `mu-send-${process.pid}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
1699
+ await tmux(["set-buffer", "-b", bufferName, text]);
1700
+ try {
1701
+ await tmux(["paste-buffer", "-p", "-d", "-r", "-b", bufferName, "-t", paneId]);
1702
+ } catch (err) {
1703
+ await currentExecutor(["delete-buffer", "-b", bufferName]).catch(() => {
1704
+ });
1705
+ throw err;
1706
+ }
1707
+ const delay2 = opts.delayMs ?? defaultSendDelayMs();
1708
+ if (delay2 > 0) await currentSleep(delay2);
1709
+ await tmux(["send-keys", "-t", paneId, "Enter"]);
1710
+ if (readinessMs <= 0) return;
1711
+ const probe = pasteProbe(text);
1712
+ if (probe.length === 0) return;
1713
+ for (let attempt = 0; attempt <= SUBMIT_RETRIES; attempt++) {
1714
+ if (!await isTextStranded(paneId, probe)) return;
1715
+ if (attempt === SUBMIT_RETRIES) break;
1716
+ await tmux(["send-keys", "-t", paneId, "Enter"]);
1717
+ await currentSleep(SUBMIT_SETTLE_MS);
1718
+ }
1719
+ const warn2 = opts.onUndelivered ?? defaultUndeliveredWarning;
1720
+ warn2({
1721
+ paneId,
1722
+ reason: quiesced ? "paste-vanished" : "busy-at-deadline",
1723
+ message: `send to ${paneId} was NOT submitted: the text is still sitting unsubmitted in the pane's input box after ${SUBMIT_RETRIES + 1} Enter attempts. The agent has NOT seen it. Press Enter in the pane, or re-send.`
1724
+ });
2137
1725
  }
2138
- function isHerdrStatusUsable(stdout) {
2139
- if (!/^\s*status:\s*running\s*$/m.test(stdout)) return false;
2140
- if (/^\s*endpoint_compatible:\s*no\s*$/m.test(stdout)) return false;
2141
- return !/^\s*compatible:\s*no\s*$/m.test(stdout);
1726
+ async function capturePane(paneId, opts = {}) {
1727
+ assertValidPaneId(paneId);
1728
+ const args = ["capture-pane", "-t", paneId, "-p"];
1729
+ if (opts.lines === void 0) {
1730
+ args.push("-S", "-", "-E", "-");
1731
+ } else if (opts.lines > 0) {
1732
+ args.push("-S", `-${opts.lines}`);
1733
+ }
1734
+ return tmux(args);
2142
1735
  }
2143
- async function herdrAvailable() {
2144
- const result = await currentExecutor2(["status"]).catch(() => void 0);
2145
- if (result === void 0 || result.exitCode !== 0) return false;
2146
- return isHerdrStatusUsable(result.stdout);
1736
+ async function tmuxAvailable() {
1737
+ try {
1738
+ await tmux(["-V"]);
1739
+ return true;
1740
+ } catch {
1741
+ return false;
1742
+ }
2147
1743
  }
2148
- function attachHint2(target) {
1744
+ function attachTargetSpec(target) {
1745
+ return target.window === void 0 ? target.session : `${target.session}:${target.window}`;
1746
+ }
1747
+ function attachHint(target) {
1748
+ const spec = attachTargetSpec(target);
1749
+ if (target.inside === true) return `tmux switch-client -t ${spec}`;
1750
+ if (target.window === void 0) return `tmux attach -t ${target.session}`;
1751
+ return `tmux attach -t ${target.session} && tmux select-window -t ${spec}`;
1752
+ }
1753
+ function attachCommands(target) {
1754
+ const spec = attachTargetSpec(target);
1755
+ if (target.inside === true) {
1756
+ return [{ command: "tmux", args: ["switch-client", "-t", spec] }];
1757
+ }
1758
+ const steps = [{ command: "tmux", args: ["attach-session", "-t", target.session] }];
2149
1759
  if (target.window !== void 0) {
2150
- return `herdr session attach ${target.session} # then focus tab ${target.window}`;
1760
+ steps.push({ command: "tmux", args: ["select-window", "-t", spec], optional: true });
2151
1761
  }
2152
- return `herdr session attach ${target.session}`;
2153
- }
2154
- function attachCommands2(target) {
2155
- return [{ command: "herdr", args: ["session", "attach", target.session] }];
1762
+ return steps;
2156
1763
  }
2157
- function paneNotFoundNextSteps2(paneId) {
1764
+ function paneNotFoundNextSteps(paneId) {
2158
1765
  return [
2159
1766
  {
2160
1767
  intent: `Verify the pane id ${paneId} actually exists`,
2161
- command: `herdr pane get ${paneId}`
1768
+ command: `tmux display-message -t ${paneId} -p '#{pane_id} #{pane_title}'`
2162
1769
  },
2163
1770
  {
2164
- intent: "List panes in the current workspace",
2165
- command: 'herdr pane list --workspace "$HERDR_WORKSPACE_ID"'
1771
+ intent: "List all live panes across all sessions",
1772
+ command: "tmux list-panes -a -F '#{session_name}:#{window_id}.#{pane_id}\\t#{pane_title}\\t#{pane_current_command}'"
2166
1773
  }
2167
1774
  ];
2168
1775
  }
2169
- async function healthCheck2() {
1776
+ async function healthCheck() {
2170
1777
  let version = null;
2171
1778
  try {
2172
- const result = await currentExecutor2(["status"]);
2173
- if (result.exitCode === 0) {
2174
- const match = /^\s*version:\s*(\S+)\s*$/m.exec(result.stdout);
2175
- version = match?.[1] ?? null;
2176
- }
1779
+ version = (await tmux(["-V"])).trim();
2177
1780
  } catch {
2178
1781
  version = null;
2179
1782
  }
2180
1783
  return {
2181
- name: "herdr",
1784
+ name: "tmux",
2182
1785
  ok: version !== null,
2183
1786
  version,
2184
1787
  env: [
2185
- { name: "$HERDR_ENV", value: process.env.HERDR_ENV ?? null },
2186
- { name: "$HERDR_WORKSPACE_ID", value: process.env.HERDR_WORKSPACE_ID ?? null },
2187
- { name: "$HERDR_PANE_ID", value: process.env.HERDR_PANE_ID ?? null }
1788
+ { name: "$TMUX", value: process.env.TMUX ?? null },
1789
+ { name: "$TMUX_PANE", value: process.env.TMUX_PANE ?? null }
2188
1790
  ],
2189
- remediation: "start the herdr server (herdr status) or install herdr"
1791
+ remediation: "install tmux \u2265 3.0"
2190
1792
  };
2191
1793
  }
2192
- var herdrBackend = Object.freeze({
2193
- name: "herdr",
2194
- available: herdrAvailable,
2195
- isValidPaneId: isValidPaneId2,
2196
- assertValidPaneId: assertValidPaneId2,
2197
- listSessions: listSessions2,
2198
- sessionExists: sessionExists2,
2199
- newSession: newSession2,
2200
- newSessionWithPane: newSessionWithPane2,
2201
- killSession: killSession2,
2202
- listWindows: listWindows2,
2203
- newWindow: newWindow2,
2204
- selectLayout: selectLayout2,
2205
- listPanes: listPanes2,
2206
- listPanesInSession: listPanesInSession2,
2207
- splitWindow: splitWindow2,
2208
- killPane: killPane2,
2209
- paneExists: paneExists2,
2210
- paneTTY: paneTTY2,
2211
- setPaneTitle: setPaneTitle2,
2212
- getPaneTitle: getPaneTitle2,
2213
- currentAgentName: currentAgentName2,
2214
- currentSessionName: currentSessionName2,
2215
- startAgentInPane,
2216
- sendToPane: sendToPane2,
2217
- capturePane: capturePane2,
2218
- paneStatus,
2219
- enableMuPaneBordersForSession: enableMuPaneBordersForSession2,
2220
- enableMuPaneBordersForPane: enableMuPaneBordersForPane2,
2221
- attachHint: attachHint2,
2222
- attachCommands: attachCommands2,
2223
- paneNotFoundNextSteps: paneNotFoundNextSteps2,
2224
- healthCheck: healthCheck2
1794
+ var tmuxBackend = Object.freeze({
1795
+ name: "tmux",
1796
+ available: tmuxAvailable,
1797
+ isValidPaneId,
1798
+ assertValidPaneId,
1799
+ listSessions,
1800
+ sessionExists,
1801
+ newSession,
1802
+ newSessionWithPane,
1803
+ killSession,
1804
+ listWindows,
1805
+ newWindow,
1806
+ selectLayout,
1807
+ listPanes,
1808
+ listPanesInSession,
1809
+ splitWindow,
1810
+ killPane,
1811
+ paneExists,
1812
+ paneTTY,
1813
+ setPaneTitle,
1814
+ getPaneTitle,
1815
+ currentAgentName,
1816
+ currentSessionName,
1817
+ sendToPane,
1818
+ capturePane,
1819
+ enableMuPaneBordersForSession,
1820
+ enableMuPaneBordersForPane,
1821
+ attachHint,
1822
+ attachCommands,
1823
+ paneNotFoundNextSteps,
1824
+ healthCheck
2225
1825
  });
2226
1826
 
2227
- // src/mux/detect.ts
2228
- var BACKENDS = [tmuxBackend, herdrBackend];
2229
- function muxByName(name) {
2230
- for (const backend of BACKENDS) {
2231
- if (backend.name === name) return backend;
1827
+ // src/mux/herdr.ts
1828
+ var HerdrError = class extends MuxError {
1829
+ constructor(args, stderr, stdout, exitCode, code) {
1830
+ const detail = parseErrorEnvelope(stderr)?.message ?? stderr.trim() ?? stdout.trim();
1831
+ super(`herdr ${args.join(" ")} failed (exit ${exitCode}): ${detail || "no output"}`);
1832
+ this.args = args;
1833
+ this.stderr = stderr;
1834
+ this.stdout = stdout;
1835
+ this.exitCode = exitCode;
1836
+ this.code = code;
1837
+ this.name = "HerdrError";
2232
1838
  }
2233
- throw new Error(`unknown mux backend: ${name}`);
2234
- }
2235
- var KNOWN_NAMES = BACKENDS.map((b) => b.name);
2236
- async function detectMux() {
2237
- const override = process.env.MU_MUX;
2238
- if (override !== void 0 && override.length > 0) {
2239
- return muxByName(override);
1839
+ args;
1840
+ stderr;
1841
+ stdout;
1842
+ exitCode;
1843
+ code;
1844
+ errorNextSteps() {
1845
+ return [
1846
+ { intent: "Run health check", command: "mu doctor" },
1847
+ { intent: "Verify the herdr server is running", command: "herdr status" },
1848
+ {
1849
+ intent: "Check the failing herdr command in isolation",
1850
+ command: `herdr ${this.args.join(" ")}`
1851
+ }
1852
+ ];
2240
1853
  }
2241
- if (process.env.HERDR_ENV === "1") return herdrBackend;
2242
- if (process.env.TMUX || process.env.TMUX_PANE) return tmuxBackend;
2243
- for (const backend of BACKENDS) {
2244
- if (await backend.available()) return backend;
1854
+ };
1855
+ var HerdrSyntaxError = class extends Error {
1856
+ constructor(args, output) {
1857
+ super(
1858
+ `herdr rejected the command line (exit 2) \u2014 this is a bug in mu, not a herdr outage. Ran: herdr ${args.join(" ")}. herdr said: ${output.trim().split("\n")[0] ?? "(no output)"}`
1859
+ );
1860
+ this.args = args;
1861
+ this.output = output;
2245
1862
  }
2246
- throw new NoMultiplexerError(KNOWN_NAMES);
2247
- }
2248
- var cached;
2249
- async function activeMux() {
2250
- if (cached === void 0) cached = await detectMux();
2251
- return cached;
2252
- }
2253
- function setMuxForTests(backend) {
2254
- const previous = cached;
2255
- cached = backend;
2256
- return previous;
2257
- }
2258
- function resetMux() {
2259
- cached = void 0;
2260
- }
2261
-
2262
- // src/agents/spawn.ts
2263
- import { execFile as execFile2 } from "child_process";
2264
- import { mkdirSync as mkdirSync2, rmSync as rmSync4 } from "fs";
2265
- import { basename, dirname as dirname6 } from "path";
2266
- import { setTimeout as delay } from "timers/promises";
2267
- import { promisify as promisify2 } from "util";
2268
-
2269
- // src/agents.ts
2270
- import { dirname as dirname5 } from "path";
2271
-
2272
- // src/glyphs.ts
2273
- var AGENT_STATE_GLYPH = {
2274
- busy: "\uF04B",
2275
- // nf-fa-play
2276
- needs_input: "\uF186",
2277
- // nf-fa-moon_o
2278
- needs_permission: "\uF023",
2279
- // nf-fa-lock
2280
- unknown: "\uF059"
2281
- // nf-fa-question_circle
1863
+ args;
1864
+ output;
1865
+ name = "HerdrSyntaxError";
2282
1866
  };
2283
- function agentStateGlyph(state) {
2284
- return AGENT_STATE_GLYPH[state] ?? GLYPH.unknown;
2285
- }
2286
- var GLYPH = {
2287
- /** Healthy, clean, passing, closed-successfully. */
2288
- ok: "\uF058",
2289
- // nf-fa-check_circle
2290
- /** Failed check. */
2291
- fail: "\uF057",
2292
- // nf-fa-times_circle
2293
- /** Needs attention but not broken (idle-but-assigned, stale warning). */
2294
- warn: "\uF071",
2295
- // nf-fa-warning
2296
- /** Workspace has uncommitted edits. */
2297
- dirty: "\uF005",
2298
- // nf-fa-star
2299
- /** Workspace is ≥ WORKSPACE_STALE_THRESHOLD commits behind main. */
2300
- stale: "\uF017",
2301
- // nf-fa-clock_o
2302
- /** Task is blocked by an incoming edge. */
2303
- blocked: "\uF0C1",
2304
- // nf-fa-link (chain)
2305
- /** Task is parked (OPEN/parked): open, but set aside on purpose. */
2306
- parked: "\uF04C",
2307
- // nf-fa-pause
2308
- /** Track whose roots merged (diamond dependency). */
2309
- merge: "\uF074",
2310
- // nf-fa-random
2311
- /** Agent owns more than one task (pane-title multi-count slot). */
2312
- multi: "\uF055",
2313
- // nf-fa-plus_circle
2314
- /** Filter toggle: enabled / disabled. */
2315
- on: "\uF111",
2316
- // nf-fa-circle
2317
- off: "\uF10C",
2318
- // nf-fa-circle_o
2319
- /** Status outside the known enum. */
2320
- unknown: "\uF059"
2321
- // nf-fa-question_circle
1867
+ var HerdrUnsupportedCliError = class extends Error {
1868
+ constructor(cli, detail) {
1869
+ super(
1870
+ `herdr does not recognise --cli ${cli} as an interactive agent kind, so it cannot classify that pane's status. herdr said: ${detail}`
1871
+ );
1872
+ this.cli = cli;
1873
+ this.detail = detail;
1874
+ }
1875
+ cli;
1876
+ detail;
1877
+ name = "HerdrUnsupportedCliError";
1878
+ errorNextSteps() {
1879
+ return [
1880
+ {
1881
+ intent: "List the agent kinds this herdr knows",
1882
+ command: "herdr agent start --help"
1883
+ },
1884
+ {
1885
+ intent: "Spawn with a kind herdr supports",
1886
+ command: "mu agent spawn <name> --cli pi -w <workstream>"
1887
+ },
1888
+ {
1889
+ intent: "Or spawn this cli under tmux, which runs any command",
1890
+ command: `MU_MUX=tmux mu agent spawn <name> --cli ${this.cli}`
1891
+ }
1892
+ ];
1893
+ }
2322
1894
  };
2323
-
2324
- // src/logs.ts
2325
- import { randomUUID as randomUUID2 } from "crypto";
2326
-
2327
- // src/hlc.ts
2328
- var WALL_WIDTH = 15;
2329
- var COUNTER_WIDTH = 6;
2330
- var SEP = ".";
2331
- var MAX_WALL = 10 ** WALL_WIDTH - 1;
2332
- var MAX_COUNTER = 10 ** COUNTER_WIDTH - 1;
2333
- var HlcParseError = class extends Error {
2334
- constructor(value) {
2335
- super(`Malformed HLC: ${JSON.stringify(value)}`);
2336
- this.value = value;
2337
- this.name = "HlcParseError";
1895
+ var HerdrCommandOverrideError = class extends Error {
1896
+ constructor(cli, command, envVar) {
1897
+ super(
1898
+ (envVar === void 0 ? `--command ${JSON.stringify(command)} cannot be honoured on the herdr backend` : `${envVar}=${command} cannot be honoured on the herdr backend`) + `: 'herdr agent start --kind ${cli}' resolves the executable itself and has no override flag. mu refuses rather than starting a different binary than you asked for.`
1899
+ );
1900
+ this.cli = cli;
1901
+ this.command = command;
1902
+ this.envVar = envVar;
1903
+ }
1904
+ cli;
1905
+ command;
1906
+ envVar;
1907
+ name = "HerdrCommandOverrideError";
1908
+ errorNextSteps() {
1909
+ return [
1910
+ ...this.envVar === void 0 ? [
1911
+ {
1912
+ intent: "Spawn without --command so herdr resolves the agent itself",
1913
+ command: `mu agent spawn <name> --cli ${this.cli} -w <workstream>`
1914
+ }
1915
+ ] : [
1916
+ {
1917
+ intent: `Unset the override so herdr resolves ${this.cli} itself`,
1918
+ command: `unset ${this.envVar}`
1919
+ }
1920
+ ],
1921
+ {
1922
+ intent: "Or spawn under tmux, which runs the command you name",
1923
+ command: `MU_MUX=tmux mu agent spawn <name> --cli ${this.cli}`
1924
+ }
1925
+ ];
2338
1926
  }
2339
- value;
2340
1927
  };
2341
- var HlcOverflowError = class extends Error {
2342
- constructor(field, value) {
1928
+ var GROUP_CLOSE_REQUIRED_CODE = "workspace_group_close_required";
1929
+ var HerdrWorkspaceGroupCloseError = class extends Error {
1930
+ constructor(session, workspaceId, detail) {
2343
1931
  super(
2344
- 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`
1932
+ `herdr refused to close workspace ${workspaceId} (${session}): it has linked worktree workspaces, which mu did not create and will not close on your behalf. herdr said: ${detail}`
1933
+ );
1934
+ this.session = session;
1935
+ this.workspaceId = workspaceId;
1936
+ this.detail = detail;
1937
+ }
1938
+ session;
1939
+ workspaceId;
1940
+ detail;
1941
+ name = "HerdrWorkspaceGroupCloseError";
1942
+ errorNextSteps() {
1943
+ return [
1944
+ {
1945
+ intent: "See which workspaces are linked as a group",
1946
+ command: "herdr workspace list"
1947
+ },
1948
+ {
1949
+ intent: "Remove the worktree workspaces you no longer want",
1950
+ command: "herdr worktree remove <workspace-id>"
1951
+ },
1952
+ {
1953
+ intent: "Or close the whole group yourself, then re-run the teardown",
1954
+ command: `herdr workspace close ${this.workspaceId} --group`
1955
+ }
1956
+ ];
1957
+ }
1958
+ };
1959
+ var HerdrNotImplementedError = class extends Error {
1960
+ name = "HerdrNotImplementedError";
1961
+ constructor(method, owner) {
1962
+ super(`herdr backend: ${method} is not implemented yet (owned by task ${owner})`);
1963
+ }
1964
+ };
1965
+ var HERDR_ORDINAL = "[0-9A-Z]+";
1966
+ var PANE_ID_RE2 = new RegExp(`^w${HERDR_ORDINAL}:p${HERDR_ORDINAL}$`);
1967
+ var WORKSPACE_ID_RE = new RegExp(`^w${HERDR_ORDINAL}$`);
1968
+ var TAB_ID_RE = new RegExp(`^w${HERDR_ORDINAL}:t${HERDR_ORDINAL}$`);
1969
+ function isValidPaneId2(s) {
1970
+ return PANE_ID_RE2.test(s);
1971
+ }
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
+ );
1977
+ }
1978
+ }
1979
+ function herdrGlobalFlags() {
1980
+ const session = process.env.MU_HERDR_SESSION;
1981
+ if (session === void 0 || session.length === 0) return [];
1982
+ return ["--session", session];
1983
+ }
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;
1997
+ }
1998
+ function resetHerdrExecutor() {
1999
+ currentExecutor2 = realExecutor2;
2000
+ }
2001
+ function isRecord(v) {
2002
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2003
+ }
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;
2011
+ try {
2012
+ parsed = JSON.parse(trimmed);
2013
+ } catch {
2014
+ return void 0;
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 };
2023
+ }
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);
2028
+ }
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
2036
+ );
2037
+ }
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);
2045
+ }
2046
+ if (!isRecord(parsed)) return {};
2047
+ const inner = parsed.result;
2048
+ return isRecord(inner) ? inner : {};
2049
+ }
2050
+ async function herdrTolerating(args, tolerate) {
2051
+ try {
2052
+ return await herdr(args);
2053
+ } catch (err) {
2054
+ if (err instanceof HerdrError && err.code !== void 0 && tolerate.includes(err.code)) {
2055
+ return void 0;
2056
+ }
2057
+ throw err;
2058
+ }
2059
+ }
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);
2098
+ }
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;
2107
+ }
2108
+ return void 0;
2109
+ }
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
+ );
2120
+ }
2121
+ return id;
2122
+ }
2123
+ async function sessionExists2(name) {
2124
+ return await resolveWorkspaceId(name) !== void 0;
2125
+ }
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);
2132
+ }
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);
2140
+ }
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);
2149
+ }
2150
+ throw err;
2151
+ }
2152
+ }
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]));
2158
+ }
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;
2166
+ }
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;
2174
+ }
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));
2180
+ }
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);
2185
+ }
2186
+ async function selectLayout2(_window, _layout) {
2187
+ }
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 }));
2197
+ }
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);
2204
+ }
2205
+ if (WORKSPACE_ID_RE.test(target)) {
2206
+ return readPanes(await herdr(["pane", "list", "--workspace", target]));
2207
+ }
2208
+ return listPanesInSession2(target);
2209
+ }
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;
2221
+ }
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);
2227
+ }
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"]);
2248
+ }
2249
+ async function paneExists2(paneId) {
2250
+ if (!isValidPaneId2(paneId)) return false;
2251
+ const pane = await getPane(paneId);
2252
+ return pane !== void 0;
2253
+ }
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);
2261
+ }
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
+ }
2283
+ }
2284
+ async function setPaneTitle2(paneId, title) {
2285
+ assertValidPaneId2(paneId);
2286
+ await herdr(["pane", "rename", paneId, title]);
2287
+ }
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;
2293
+ }
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;
2307
+ }
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
2322
+ );
2323
+ }
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
+ }
2337
+ }
2338
+ }
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
2345
2386
  );
2346
- this.field = field;
2347
- this.value = value;
2348
- this.name = "HlcOverflowError";
2349
- }
2350
- field;
2351
- value;
2352
- };
2353
- var MachineIdentityMissingError = class extends Error {
2354
- constructor() {
2355
- super("machine_identity row (id = 1) is missing; the DB was not opened via openDb()");
2356
- this.name = "MachineIdentityMissingError";
2357
2387
  }
2358
- };
2359
- function formatHlc(hlc) {
2360
- const { wallMs, counter, machineId: machineId2 } = hlc;
2361
- if (!Number.isInteger(wallMs) || wallMs < 0) throw new HlcParseError(String(wallMs));
2362
- if (!Number.isInteger(counter) || counter < 0) throw new HlcParseError(String(counter));
2363
- if (wallMs > MAX_WALL) throw new HlcOverflowError("wall", wallMs);
2364
- if (counter > MAX_COUNTER) throw new HlcOverflowError("counter", counter);
2365
- if (machineId2.length === 0 || machineId2.includes(SEP)) throw new HlcParseError(machineId2);
2366
- return `${String(wallMs).padStart(WALL_WIDTH, "0")}${SEP}${String(counter).padStart(
2367
- COUNTER_WIDTH,
2368
- "0"
2369
- )}${SEP}${machineId2}`;
2388
+ return result.stdout;
2370
2389
  }
2371
- function parseHlc(value) {
2372
- const parts = value.split(SEP);
2373
- if (parts.length !== 3) throw new HlcParseError(value);
2374
- const [wall, counter, machineId2] = parts;
2375
- if (wall === void 0 || counter === void 0 || machineId2 === void 0) {
2376
- throw new HlcParseError(value);
2377
- }
2378
- if (wall.length !== WALL_WIDTH || counter.length !== COUNTER_WIDTH || machineId2.length === 0) {
2379
- throw new HlcParseError(value);
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";
2380
2398
  }
2381
- if (!/^\d+$/.test(wall) || !/^\d+$/.test(counter)) throw new HlcParseError(value);
2382
- return { wallMs: Number(wall), counter: Number(counter), machineId: machineId2 };
2383
2399
  }
2384
- function compareHlc(a, b) {
2385
- if (a < b) return -1;
2386
- 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) {
2387
2413
  return 0;
2388
2414
  }
2389
- function nextHlc(db, now = Date.now()) {
2390
- const row = db.prepare(
2391
- `UPDATE machine_identity
2392
- SET last_wall = MAX(last_wall, @now),
2393
- last_counter = CASE WHEN @now > last_wall THEN 0 ELSE last_counter + 1 END
2394
- WHERE id = 1
2395
- RETURNING machine_id, last_wall, last_counter`
2396
- ).get({ now: Math.floor(now) });
2397
- if (!row) throw new MachineIdentityMissingError();
2398
- return formatHlc({ wallMs: row.last_wall, counter: row.last_counter, machineId: row.machine_id });
2415
+ async function enableMuPaneBordersForPane2(_paneId) {
2399
2416
  }
2400
- function receiveHlc(db, remoteHlc, now = Date.now()) {
2401
- const remote = parseHlc(remoteHlc);
2402
- const wallNow = Math.floor(now);
2403
- const read = db.prepare(
2404
- "SELECT machine_id, last_wall, last_counter FROM machine_identity WHERE id = 1"
2405
- );
2406
- const write = db.prepare(
2407
- "UPDATE machine_identity SET last_wall = @wall, last_counter = @counter WHERE id = 1"
2408
- );
2409
- const advance = db.transaction(() => {
2410
- const row = read.get();
2411
- if (!row) throw new MachineIdentityMissingError();
2412
- const wall = Math.max(row.last_wall, remote.wallMs, wallNow);
2413
- const localAtMax = row.last_wall === wall;
2414
- const remoteAtMax = remote.wallMs === wall;
2415
- let counter;
2416
- if (!localAtMax && !remoteAtMax) {
2417
- counter = 0;
2418
- } else if (localAtMax && remoteAtMax) {
2419
- counter = Math.max(row.last_counter, remote.counter) + 1;
2420
- } else if (localAtMax) {
2421
- counter = row.last_counter + 1;
2422
- } else {
2423
- 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)}`);
2424
2423
  }
2425
- if (counter > MAX_COUNTER) throw new HlcOverflowError("counter", counter);
2426
- write.run({ wall, counter });
2427
- return formatHlc({ wallMs: wall, counter, machineId: row.machine_id });
2428
- });
2429
- return advance.immediate();
2424
+ args.push("--env", `${k}=${v}`);
2425
+ }
2430
2426
  }
2431
-
2432
- // src/legacy-ops.ts
2433
- var LEGACY_LOG_ONLY_INTENTS = /* @__PURE__ */ new Set(["workstream.export"]);
2434
- function isLegacyLogOnlyIntent(intent) {
2435
- 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");
2436
2430
  }
2437
- var LEGACY_INTENT_SYNONYMS = /* @__PURE__ */ new Map([
2438
- ["workstream.teardown", ["workstream.destroy"]]
2439
- ]);
2440
- function intentSpellings(intent) {
2441
- const legacy = LEGACY_INTENT_SYNONYMS.get(intent);
2442
- 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;
2443
2443
  }
2444
- var LEGACY_LOG_ONLY_SQL_EXCLUSION = `(intent IS NULL OR intent NOT IN (${[
2445
- ...LEGACY_LOG_ONLY_INTENTS
2446
- ].map((i) => `'${i}'`).join(", ")}))`;
2447
-
2448
- // src/logs.ts
2449
- var SELECT_LOG_COLS = `
2450
- l.seq AS seq,
2451
- l.key AS workstream,
2452
- l.actor AS source,
2453
- l.intent AS intent,
2454
- l.group_id AS group_id,
2455
- l.op AS op,
2456
- l.entity AS kind,
2457
- l.payload AS payload,
2458
- l.created_at AS created_at
2459
- `;
2460
- var LOG_FROM_JOIN = "FROM ops l";
2461
- function workstreamScopeSql(column = "l.key") {
2462
- 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);
2463
2448
  }
2464
- function workstreamScopeParams(workstream) {
2465
- const escaped = workstream.replace(/[\\%_]/g, (c) => `\\${c}`);
2466
- 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);
2467
2453
  }
2468
- var TOUCH_OP_FILTER = `NOT (
2469
- l.intent IS NOT NULL
2470
- AND l.op = 'put'
2471
- AND l.payload LIKE '{"updated_at":%'
2472
- AND LENGTH(l.payload) < 45
2473
- )`;
2474
- var MACHINE_WIDE_KEY = "";
2475
- var GroupIdAmbiguousError = class extends Error {
2476
- constructor(prefix, candidates) {
2477
- super(
2478
- `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" : ""})`
2479
- );
2480
- this.prefix = prefix;
2481
- this.candidates = candidates;
2482
- this.name = "GroupIdAmbiguousError";
2483
- }
2484
- prefix;
2485
- candidates;
2486
- errorNextSteps() {
2487
- return [
2488
- { intent: "List recent groups with their ids", command: "mu undo" },
2489
- {
2490
- intent: "Retry with more characters",
2491
- command: `mu log --group ${this.candidates[0] ?? "<full-id>"}`
2492
- }
2493
- ];
2454
+ function attachHint2(target) {
2455
+ if (target.window !== void 0) {
2456
+ return `herdr session attach ${target.session} # then focus tab ${target.window}`;
2494
2457
  }
2495
- };
2496
- function groupIdFromPrefix(db, prefix) {
2497
- if (prefix === "") return null;
2498
- const rows = db.prepare(
2499
- `SELECT DISTINCT group_id AS groupId FROM ops
2500
- WHERE group_id = @exact OR group_id LIKE @prefix || '%' ESCAPE '\\'`
2501
- ).all({ exact: prefix, prefix: prefix.replace(/[\\%_]/g, (c) => `\\${c}`) });
2502
- const exact = rows.find((r) => r.groupId === prefix);
2503
- if (exact !== void 0) return exact.groupId;
2504
- const first = rows[0];
2505
- if (rows.length === 1 && first !== void 0) return first.groupId;
2506
- if (rows.length === 0) return null;
2507
- throw new GroupIdAmbiguousError(
2508
- prefix,
2509
- rows.map((r) => r.groupId)
2510
- );
2511
- }
2512
- function machineId(db) {
2513
- const row = db.prepare("SELECT machine_id FROM machine_identity WHERE id = 1").get();
2514
- return row?.machine_id ?? "unknown";
2458
+ return `herdr session attach ${target.session}`;
2515
2459
  }
2516
- function rowFromDb(row) {
2517
- return {
2518
- seq: row.seq,
2519
- workstreamName: row.workstream === MACHINE_WIDE_KEY ? null : row.workstream,
2520
- source: row.source ?? "system",
2521
- intent: row.intent,
2522
- group: row.group_id,
2523
- op: row.op,
2524
- kind: row.kind,
2525
- payload: row.payload,
2526
- createdAt: row.created_at
2527
- };
2460
+ function attachCommands2(target) {
2461
+ return [{ command: "herdr", args: ["session", "attach", target.session] }];
2528
2462
  }
2529
- function appendLog(db, opts) {
2530
- const kind = opts.kind ?? "message";
2531
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
2532
- const key = opts.workstream ?? MACHINE_WIDE_KEY;
2533
- const group = randomUUID2();
2534
- const result = db.prepare(
2535
- `INSERT INTO ops (hlc, machine_id, group_id, actor, intent, entity, key, op, payload, created_at)
2536
- VALUES (?, ?, ?, ?, ?, ?, ?, 'put', ?, ?)`
2537
- ).run(
2538
- nextHlc(db),
2539
- machineId(db),
2540
- group,
2541
- opts.source,
2542
- opts.intent ?? null,
2543
- kind,
2544
- key,
2545
- opts.payload,
2546
- createdAt
2547
- );
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
+ ];
2474
+ }
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
+ }
2548
2486
  return {
2549
- seq: Number(result.lastInsertRowid),
2550
- workstreamName: opts.workstream,
2551
- source: opts.source,
2552
- intent: opts.intent ?? null,
2553
- group,
2554
- op: "put",
2555
- kind,
2556
- payload: opts.payload,
2557
- 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"
2558
2496
  };
2559
2497
  }
2560
- function listLogs(db, opts = {}) {
2561
- const conditions = [];
2562
- const params = [];
2563
- if (opts.workstream === null) {
2564
- conditions.push("l.key = ?");
2565
- params.push(MACHINE_WIDE_KEY);
2566
- } else if (opts.workstream !== void 0) {
2567
- conditions.push(workstreamScopeSql());
2568
- params.push(...workstreamScopeParams(opts.workstream));
2569
- }
2570
- if (opts.since !== void 0) {
2571
- conditions.push("l.seq > ?");
2572
- params.push(opts.since);
2573
- }
2574
- if (opts.source !== void 0) {
2575
- conditions.push("l.actor = ?");
2576
- params.push(opts.source);
2577
- }
2578
- if (opts.kind !== void 0) {
2579
- conditions.push("l.entity = ?");
2580
- params.push(opts.kind);
2581
- }
2582
- if (opts.intent !== void 0) {
2583
- const spellings = intentSpellings(opts.intent);
2584
- conditions.push(`l.intent IN (${spellings.map(() => "?").join(", ")})`);
2585
- params.push(...spellings);
2586
- }
2587
- if (opts.group !== void 0) {
2588
- conditions.push("l.group_id = ?");
2589
- 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;
2590
2538
  }
2591
- conditions.push(TOUCH_OP_FILTER);
2592
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2593
- if (opts.limit !== void 0 && opts.since === void 0) {
2594
- const rowsDesc = db.prepare(`SELECT ${SELECT_LOG_COLS} ${LOG_FROM_JOIN} ${where} ORDER BY l.seq DESC LIMIT ?`).all(...params, opts.limit);
2595
- 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);
2596
2546
  }
2597
- let sql = `SELECT ${SELECT_LOG_COLS} ${LOG_FROM_JOIN} ${where} ORDER BY l.seq ASC`;
2598
- if (opts.limit !== void 0) {
2599
- sql += " LIMIT ?";
2600
- 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;
2601
2551
  }
2602
- const rows = db.prepare(sql).all(...params);
2603
- return rows.map(rowFromDb);
2604
- }
2605
- function latestSeq(db, workstream) {
2606
- const row = workstream === void 0 ? db.prepare(`SELECT MAX(seq) AS s FROM ops l WHERE ${TOUCH_OP_FILTER}`).get() : db.prepare(
2607
- `SELECT MAX(seq) AS s FROM ops l WHERE ${workstreamScopeSql()} AND ${TOUCH_OP_FILTER}`
2608
- ).get(...workstreamScopeParams(workstream));
2609
- return row.s ?? 0;
2552
+ throw new NoMultiplexerError(KNOWN_NAMES);
2610
2553
  }
2611
- function emitEvent(db, workstream, intent, payload, source = "system") {
2612
- 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;
2613
2558
  }
2614
- function entityForIntent(intent) {
2615
- const dot = intent.indexOf(".");
2616
- return dot === -1 ? intent : intent.slice(0, dot);
2559
+ function setMuxForTests(backend) {
2560
+ const previous = cached;
2561
+ cached = backend;
2562
+ return previous;
2617
2563
  }
2618
- function lastClaimOp(db, workstream, localId) {
2619
- const row = db.prepare(
2620
- `SELECT actor, created_at FROM ops
2621
- WHERE key = ? AND intent = 'task.claim'
2622
- ORDER BY seq DESC LIMIT 1`
2623
- ).get(`${workstream}/${localId}`);
2624
- return row ?? null;
2564
+ function resetMux() {
2565
+ cached = void 0;
2625
2566
  }
2626
- function lastClaimEventAt(db, workstream, localId) {
2627
- 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;
2628
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
+ };
2629
2629
 
2630
2630
  // src/op-context.ts
2631
2631
  import { randomUUID as randomUUID3 } from "crypto";
@@ -5332,12 +5332,32 @@ function recordReasonNote(db, localId, workstream, substate, why, author) {
5332
5332
  if (why === void 0 || why.trim() === "") return;
5333
5333
  const noteOpts = { workstream };
5334
5334
  if (author !== void 0 && author !== "") noteOpts.author = author;
5335
- 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
+ });
5336
5353
  }
5337
5354
  function openTask(db, localId, opts) {
5338
- return withOpContext(db, { intent: "task.open", group: "new" }, () => {
5355
+ return withOpContext(db, { intent: "task.open", actor: opts.author, group: "new" }, () => {
5339
5356
  const before = getTask(db, localId, opts.workstream);
5340
- 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
+ });
5341
5361
  if (r.changed && before) recordEvidenceNote(db, localId, before.workstreamName, "OPEN", opts);
5342
5362
  return r;
5343
5363
  });
@@ -5368,7 +5388,7 @@ function parkTask(db, localId, opts) {
5368
5388
  function acceptTask(db, localId, opts) {
5369
5389
  return withOpContext(
5370
5390
  db,
5371
- { intent: "task.accept", group: "new" },
5391
+ { intent: "task.accept", actor: opts.author, group: "new" },
5372
5392
  () => db.transaction(() => {
5373
5393
  const before = getTask(db, localId, opts.workstream);
5374
5394
  if (!before) throw new TaskNotFoundError(localId);
@@ -5390,7 +5410,7 @@ function acceptTask(db, localId, opts) {
5390
5410
  function unparkTask(db, localId, opts) {
5391
5411
  return withOpContext(
5392
5412
  db,
5393
- { intent: "task.unpark", group: "new" },
5413
+ { intent: "task.unpark", actor: opts.author, group: "new" },
5394
5414
  () => db.transaction(() => {
5395
5415
  const before = getTask(db, localId, opts.workstream);
5396
5416
  if (!before) throw new TaskNotFoundError(localId);
@@ -5414,7 +5434,7 @@ function unparkTask(db, localId, opts) {
5414
5434
  function releaseTask(db, localId, opts) {
5415
5435
  return withOpContext(
5416
5436
  db,
5417
- { intent: "task.release", group: "new" },
5437
+ { intent: "task.release", actor: opts.author, group: "new" },
5418
5438
  () => releaseTaskImpl(db, localId, opts)
5419
5439
  );
5420
5440
  }
@@ -5502,7 +5522,10 @@ async function claimTaskImpl(db, localId, opts) {
5502
5522
  }
5503
5523
  const after = getTask(db, localId, opts.workstream);
5504
5524
  if (!after) throw new Error(`claimTask: row missing after update: ${localId}`);
5505
- recordEvidenceNote(db, localId, opts.workstream, "CLAIM", opts);
5525
+ recordEvidenceNote(db, localId, opts.workstream, "CLAIM", {
5526
+ ...opts,
5527
+ author: opts.author ?? agentName
5528
+ });
5506
5529
  return {
5507
5530
  ownerName: agentName,
5508
5531
  actorName: agentName,
@@ -5562,7 +5585,10 @@ async function claimSelf(db, localId, opts) {
5562
5585
  }
5563
5586
  const after = getTask(db, localId, before.workstreamName);
5564
5587
  if (!after) throw new Error(`claimTask: row missing after update: ${localId}`);
5565
- recordEvidenceNote(db, localId, before.workstreamName, "CLAIM", opts);
5588
+ recordEvidenceNote(db, localId, before.workstreamName, "CLAIM", {
5589
+ ...opts,
5590
+ author: opts.author ?? actor
5591
+ });
5566
5592
  return {
5567
5593
  ownerName: null,
5568
5594
  actorName: actor,
@@ -5753,6 +5779,7 @@ async function abortAgent(db, name, opts) {
5753
5779
  const st = await ctlRequestFor(agent, sock, { op: "status" });
5754
5780
  if (!st.ok) throw new Error(`control socket refused status: ${st.error}`);
5755
5781
  const before = st.state ?? "idle";
5782
+ const pending = st.pending ?? false;
5756
5783
  if (before !== "busy") {
5757
5784
  return {
5758
5785
  ...base,
@@ -5760,6 +5787,7 @@ async function abortAgent(db, name, opts) {
5760
5787
  after: before,
5761
5788
  settled: true,
5762
5789
  aborted: false,
5790
+ pending,
5763
5791
  elapsedMs: Date.now() - started
5764
5792
  };
5765
5793
  }
@@ -5776,9 +5804,50 @@ async function abortAgent(db, name, opts) {
5776
5804
  after: w.state ?? "idle",
5777
5805
  settled: true,
5778
5806
  aborted: true,
5807
+ pending,
5779
5808
  elapsedMs: Date.now() - started
5780
5809
  };
5781
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
+ }
5782
5851
 
5783
5852
  // src/agents/adopt.ts
5784
5853
  var ADOPT_CTL_PROBE_MS = 1e3;
@@ -6171,10 +6240,23 @@ function isValidAgentName(name) {
6171
6240
  async function sendToAgent(db, name, text, opts) {
6172
6241
  const agent = getAgent(db, name, opts.workstream);
6173
6242
  if (!agent) throw new AgentNotFoundError(name);
6174
- return sendViaTransport(agent, text, {
6243
+ const sent = await sendViaTransport(agent, text, {
6175
6244
  ...opts,
6176
6245
  socket: opts.socket ?? agentCtlSocket(db, agent)
6177
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;
6178
6260
  }
6179
6261
  async function readAgent(db, name, opts) {
6180
6262
  const agent = getAgent(db, name, opts.workstream);
@@ -7247,16 +7329,28 @@ async function ctlRequestFor(agent, sock, req) {
7247
7329
  try {
7248
7330
  return await ctlRequest(sock, req);
7249
7331
  } catch (e) {
7250
- if (e instanceof CtlVersionError) throw e;
7251
- if (e instanceof CtlUnknownOpError) throw await extensionOutdated(agent, sock, e);
7252
- throw new AgentCtlUnreachableError(
7253
- agent.name,
7254
- agent.workstreamName,
7255
- sock,
7256
- errCode(e) === "ENOENT" ? "missing" : "refused"
7257
- );
7332
+ throw await ctlFailure(agent, sock, e);
7258
7333
  }
7259
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
+ }
7260
7354
  async function sendViaTransport(agent, text, opts) {
7261
7355
  const sock = opts.socket;
7262
7356
  if (opts.fresh && (opts.via === "mux" || !expectsCtl(agent, sock))) {
@@ -7293,7 +7387,8 @@ async function sendViaTransport(agent, text, opts) {
7293
7387
  return {
7294
7388
  transport,
7295
7389
  ...reply.state !== void 0 ? { state: reply.state } : {},
7296
- ...reply.runs !== void 0 ? { runs: reply.runs } : {}
7390
+ ...reply.runs !== void 0 ? { runs: reply.runs } : {},
7391
+ ...reply.since !== void 0 ? { since: reply.since } : {}
7297
7392
  };
7298
7393
  }
7299
7394
  async function runSessionCommand(agent, sock, cmd, force) {
@@ -9365,6 +9460,7 @@ var VERBS = {
9365
9460
  "agent.close": "agent close",
9366
9461
  "agent.adopt": "agent adopt",
9367
9462
  "agent.kick": "agent kick",
9463
+ "agent.send": "agent send",
9368
9464
  // Payload prose reads "agent stalled ..."; keep the verb matching it
9369
9465
  // so the emitted text is not printed with a second, different verb.
9370
9466
  "agent.stall": "agent stalled",
@@ -9487,6 +9583,7 @@ function renderKnown(row, intent) {
9487
9583
  case "agent.close":
9488
9584
  case "agent.adopt":
9489
9585
  case "agent.kick":
9586
+ case "agent.send":
9490
9587
  case "agent.stall":
9491
9588
  case "workspace.create":
9492
9589
  case "workspace.free":
@@ -11240,6 +11337,7 @@ export {
11240
11337
  insertAgent,
11241
11338
  inspectLinks,
11242
11339
  installCapture,
11340
+ interruptAgent,
11243
11341
  isKickSignal,
11244
11342
  isPathInside,
11245
11343
  isTaskStatus,