@mu-crew/mu 3.7.0 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +10797 -10500
- package/dist/cli.js.map +1 -1
- package/dist/extension/mu-pi.js +266 -39
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.d.ts +39 -9
- package/dist/index.js +1678 -1580
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +4 -3
- package/docs/architecture/control-socket.md +8 -3
- package/docs/guide/delegate.md +18 -0
- package/docs/guide/dispatch.md +17 -3
- package/docs/guide/getting-started.md +1 -1
- package/docs/reference/env.md +1 -1
- package/docs/specs/2026-10-03-keep-driving-nudge.md +21 -0
- package/package.json +1 -1
- package/skills/mu/SKILL.md +43 -15
- package/skills/mu/recipes/adversarial-review.md +14 -6
- package/skills/mu/recipes/brief.md +29 -1
- package/skills/mu/recipes/codemode-driver.md +19 -9
- package/skills/mu/recipes/deep-research.md +6 -5
- package/skills/mu/recipes/drift-audit.md +72 -0
- package/skills/mu/recipes/findings.md +24 -15
- package/skills/mu/recipes/hypothesis-panel.md +5 -3
- package/skills/mu/recipes/long-run.md +13 -3
- package/skills/mu/recipes/loop-until-done.md +4 -3
- package/skills/mu/recipes/orchestrator-loop.md +19 -10
- package/skills/mu/recipes/plan.md +10 -6
- package/skills/mu/recipes/refute.md +3 -2
- package/skills/mu/recipes/review-panel.md +2 -2
- package/skills/mu/recipes/rules-audit.md +10 -5
- package/skills/mu/recipes/tasks-or-calls.md +39 -26
- package/skills/mu/recipes/tournament.md +4 -3
- package/skills/mu/recipes/ultrathink.md +2 -2
- package/skills/mu/recipes/worker.md +3 -2
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/
|
|
867
|
-
|
|
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/
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
var
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
|
949
|
-
constructor(
|
|
883
|
+
var HlcOverflowError = class extends Error {
|
|
884
|
+
constructor(field, value) {
|
|
950
885
|
super(
|
|
951
|
-
`
|
|
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.
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
|
1005
|
-
|
|
1006
|
-
if (
|
|
1007
|
-
|
|
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
|
-
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
};
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
|
1031
|
-
|
|
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
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
-
|
|
1047
|
-
|
|
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
|
|
1050
|
-
|
|
1006
|
+
function workstreamScopeParams(workstream) {
|
|
1007
|
+
const escaped = workstream.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
1008
|
+
return [workstream, `${escaped}/%`];
|
|
1051
1009
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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
|
-
|
|
1064
|
-
const
|
|
1065
|
-
return
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
-
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
-
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
1105
|
-
"
|
|
1106
|
-
|
|
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
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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
|
-
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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
|
-
|
|
1230
|
-
|
|
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
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1156
|
+
function entityForIntent(intent) {
|
|
1157
|
+
const dot = intent.indexOf(".");
|
|
1158
|
+
return dot === -1 ? intent : intent.slice(0, dot);
|
|
1238
1159
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
|
1247
|
-
return
|
|
1168
|
+
function lastClaimEventAt(db, workstream, localId) {
|
|
1169
|
+
return lastClaimOp(db, workstream, localId)?.created_at ?? null;
|
|
1248
1170
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
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
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
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
|
-
|
|
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/
|
|
1522
|
-
var
|
|
1523
|
-
constructor(args, stderr, stdout, exitCode
|
|
1524
|
-
const detail =
|
|
1525
|
-
super(`
|
|
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.
|
|
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: "
|
|
1544
|
-
command:
|
|
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
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
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
|
|
1674
|
-
const
|
|
1675
|
-
if (
|
|
1676
|
-
return ["
|
|
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
|
|
1679
|
-
const result = await
|
|
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
|
|
1687
|
-
function
|
|
1688
|
-
const previous =
|
|
1689
|
-
|
|
1330
|
+
var currentExecutor = realExecutor;
|
|
1331
|
+
function setTmuxExecutor(executor) {
|
|
1332
|
+
const previous = currentExecutor;
|
|
1333
|
+
currentExecutor = executor;
|
|
1690
1334
|
return previous;
|
|
1691
1335
|
}
|
|
1692
|
-
function
|
|
1693
|
-
|
|
1336
|
+
function resetTmuxExecutor() {
|
|
1337
|
+
currentExecutor = realExecutor;
|
|
1694
1338
|
}
|
|
1695
|
-
function
|
|
1696
|
-
|
|
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
|
-
|
|
1699
|
-
|
|
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
|
|
1702
|
-
|
|
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
|
-
|
|
1719
|
-
|
|
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
|
|
1358
|
+
async function listSessions() {
|
|
1745
1359
|
try {
|
|
1746
|
-
|
|
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
|
|
1749
|
-
return
|
|
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
|
|
1755
|
-
|
|
1756
|
-
|
|
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
|
|
1783
|
-
const
|
|
1784
|
-
|
|
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
|
|
1787
|
-
const
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
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
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
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
|
|
1848
|
-
if (session
|
|
1849
|
-
const
|
|
1850
|
-
|
|
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
|
|
1855
|
-
|
|
1856
|
-
|
|
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
|
|
1862
|
-
const
|
|
1863
|
-
for (const
|
|
1864
|
-
|
|
1865
|
-
|
|
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
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
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
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
}
|
|
1882
|
-
|
|
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
|
|
1893
|
-
if (target
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
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
|
|
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
|
|
1910
|
-
|
|
1911
|
-
|
|
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
|
|
1917
|
-
const
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
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
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
"
|
|
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
|
|
1940
|
-
|
|
1941
|
-
await
|
|
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
|
|
1944
|
-
if (!
|
|
1945
|
-
const
|
|
1946
|
-
|
|
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
|
|
1949
|
-
|
|
1950
|
-
|
|
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
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
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
|
-
|
|
1997
|
-
|
|
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
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
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
|
|
2022
|
-
|
|
2023
|
-
} catch
|
|
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
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
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
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
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
|
|
2063
|
-
|
|
2064
|
-
const
|
|
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
|
-
|
|
2075
|
-
|
|
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
|
-
|
|
1595
|
+
const tty = result.stdout.trim();
|
|
1596
|
+
if (tty === "") throw new PaneNotFoundError(paneId, tmuxBackend);
|
|
1597
|
+
return tty;
|
|
2083
1598
|
}
|
|
2084
|
-
function
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
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
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
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
|
|
2107
|
-
|
|
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
|
|
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
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
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
|
-
|
|
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
|
|
2122
|
-
|
|
2123
|
-
|
|
1662
|
+
function pasteProbe(text) {
|
|
1663
|
+
const firstLine = (text.split("\n")[0] ?? "").trim();
|
|
1664
|
+
return firstLine.slice(0, 24);
|
|
2124
1665
|
}
|
|
2125
|
-
function
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
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
|
-
|
|
2136
|
-
|
|
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
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
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
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
1736
|
+
async function tmuxAvailable() {
|
|
1737
|
+
try {
|
|
1738
|
+
await tmux(["-V"]);
|
|
1739
|
+
return true;
|
|
1740
|
+
} catch {
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
2147
1743
|
}
|
|
2148
|
-
function
|
|
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
|
-
|
|
1760
|
+
steps.push({ command: "tmux", args: ["select-window", "-t", spec], optional: true });
|
|
2151
1761
|
}
|
|
2152
|
-
return
|
|
2153
|
-
}
|
|
2154
|
-
function attachCommands2(target) {
|
|
2155
|
-
return [{ command: "herdr", args: ["session", "attach", target.session] }];
|
|
1762
|
+
return steps;
|
|
2156
1763
|
}
|
|
2157
|
-
function
|
|
1764
|
+
function paneNotFoundNextSteps(paneId) {
|
|
2158
1765
|
return [
|
|
2159
1766
|
{
|
|
2160
1767
|
intent: `Verify the pane id ${paneId} actually exists`,
|
|
2161
|
-
command: `
|
|
1768
|
+
command: `tmux display-message -t ${paneId} -p '#{pane_id} #{pane_title}'`
|
|
2162
1769
|
},
|
|
2163
1770
|
{
|
|
2164
|
-
intent: "List panes
|
|
2165
|
-
command:
|
|
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
|
|
1776
|
+
async function healthCheck() {
|
|
2170
1777
|
let version = null;
|
|
2171
1778
|
try {
|
|
2172
|
-
|
|
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: "
|
|
1784
|
+
name: "tmux",
|
|
2182
1785
|
ok: version !== null,
|
|
2183
1786
|
version,
|
|
2184
1787
|
env: [
|
|
2185
|
-
{ name: "$
|
|
2186
|
-
{ name: "$
|
|
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: "
|
|
1791
|
+
remediation: "install tmux \u2265 3.0"
|
|
2190
1792
|
};
|
|
2191
1793
|
}
|
|
2192
|
-
var
|
|
2193
|
-
name: "
|
|
2194
|
-
available:
|
|
2195
|
-
isValidPaneId
|
|
2196
|
-
assertValidPaneId
|
|
2197
|
-
listSessions
|
|
2198
|
-
sessionExists
|
|
2199
|
-
newSession
|
|
2200
|
-
newSessionWithPane
|
|
2201
|
-
killSession
|
|
2202
|
-
listWindows
|
|
2203
|
-
newWindow
|
|
2204
|
-
selectLayout
|
|
2205
|
-
listPanes
|
|
2206
|
-
listPanesInSession
|
|
2207
|
-
splitWindow
|
|
2208
|
-
killPane
|
|
2209
|
-
paneExists
|
|
2210
|
-
paneTTY
|
|
2211
|
-
setPaneTitle
|
|
2212
|
-
getPaneTitle
|
|
2213
|
-
currentAgentName
|
|
2214
|
-
currentSessionName
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
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/
|
|
2228
|
-
var
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
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
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
return
|
|
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
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
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
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
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
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
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
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
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
|
|
2342
|
-
|
|
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
|
-
|
|
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
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
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
|
|
2385
|
-
if (
|
|
2386
|
-
|
|
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
|
|
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
|
|
2401
|
-
|
|
2402
|
-
const
|
|
2403
|
-
|
|
2404
|
-
"
|
|
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
|
-
|
|
2426
|
-
|
|
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
|
-
|
|
2433
|
-
|
|
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
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
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
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
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
|
|
2465
|
-
const
|
|
2466
|
-
return
|
|
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
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
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
|
|
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
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
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
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
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
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
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
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
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
|
-
|
|
2598
|
-
if (
|
|
2599
|
-
|
|
2600
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2612
|
-
|
|
2554
|
+
var cached;
|
|
2555
|
+
async function activeMux() {
|
|
2556
|
+
if (cached === void 0) cached = await detectMux();
|
|
2557
|
+
return cached;
|
|
2613
2558
|
}
|
|
2614
|
-
function
|
|
2615
|
-
const
|
|
2616
|
-
|
|
2559
|
+
function setMuxForTests(backend) {
|
|
2560
|
+
const previous = cached;
|
|
2561
|
+
cached = backend;
|
|
2562
|
+
return previous;
|
|
2617
2563
|
}
|
|
2618
|
-
function
|
|
2619
|
-
|
|
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
|
-
|
|
2627
|
-
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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,
|