lobstah 0.3.2 → 0.5.5

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/runner.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // packages/runner/src/index.ts
2
- import * as fs9 from "node:fs";
3
- import * as path7 from "node:path";
2
+ import * as fs10 from "node:fs";
3
+ import * as path8 from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  // packages/core/dist/types.js
@@ -36,19 +36,23 @@ function evidencePath(id, lane) {
36
36
  return path.join(laneDirs(lane).state, `${id}.evidence`);
37
37
  }
38
38
 
39
+ // packages/core/dist/attachments.js
40
+ function attachmentBlock(attachments) {
41
+ if (attachments.length === 0)
42
+ return "";
43
+ return [
44
+ "--- ATTACHMENTS ---",
45
+ ...attachments.map((a) => `${a.name} (${a.type}, ${a.bytes} bytes) at ${a.path}`),
46
+ "Images and PDFs can be read directly with your file tools."
47
+ ].join("\n");
48
+ }
49
+
39
50
  // packages/core/dist/queue.js
40
- import * as fs from "node:fs";
51
+ import * as fs2 from "node:fs";
41
52
  import * as path2 from "node:path";
42
- function complete(id, lane) {
43
- const dirs = laneDirs(lane);
44
- fs.renameSync(path2.join(dirs.active, id), path2.join(dirs.done, id));
45
- }
46
- function cancelRequested(id, lane) {
47
- return fs.existsSync(path2.join(laneDirs(lane).active, id, "cancel"));
48
- }
49
53
 
50
54
  // packages/core/dist/status.js
51
- import * as fs2 from "node:fs";
55
+ import * as fs from "node:fs";
52
56
  function isVerb(v) {
53
57
  return VERBS.includes(v);
54
58
  }
@@ -57,15 +61,15 @@ function appendStatus(id, lane, verb, note) {
57
61
  throw new Error(`invalid status verb "${verb}" \u2014 must be one of: ${VERBS.join(", ")}`);
58
62
  }
59
63
  const entry = { at: (/* @__PURE__ */ new Date()).toISOString(), verb, ...note ? { note } : {} };
60
- fs2.appendFileSync(statusPath(id, lane), `${JSON.stringify(entry)}
64
+ fs.appendFileSync(statusPath(id, lane), `${JSON.stringify(entry)}
61
65
  `);
62
66
  return entry;
63
67
  }
64
68
  function readStatusLog(id, lane) {
65
69
  const file = statusPath(id, lane);
66
- if (!fs2.existsSync(file))
70
+ if (!fs.existsSync(file))
67
71
  return [];
68
- return fs2.readFileSync(file, "utf8").split("\n").filter(Boolean).flatMap((line) => {
72
+ return fs.readFileSync(file, "utf8").split("\n").filter(Boolean).flatMap((line) => {
69
73
  try {
70
74
  const parsed = JSON.parse(line);
71
75
  return isVerb(parsed.verb) ? [parsed] : [];
@@ -75,12 +79,30 @@ function readStatusLog(id, lane) {
75
79
  });
76
80
  }
77
81
 
82
+ // packages/core/dist/queue.js
83
+ function complete(id, lane) {
84
+ const dirs = laneDirs(lane);
85
+ fs2.renameSync(path2.join(dirs.active, id), path2.join(dirs.done, id));
86
+ }
87
+ function cancelRequested(id, lane) {
88
+ return fs2.existsSync(path2.join(laneDirs(lane).active, id, "cancel"));
89
+ }
90
+
78
91
  // packages/core/dist/events.js
79
92
  import * as fs3 from "node:fs";
80
93
  function appendEvent(id, lane, ev) {
81
94
  fs3.appendFileSync(eventsPath(id, lane), `${JSON.stringify(ev)}
82
95
  `);
83
96
  }
97
+ function touchEvents(id, lane, at = /* @__PURE__ */ new Date()) {
98
+ const file = eventsPath(id, lane);
99
+ try {
100
+ fs3.utimesSync(file, at, at);
101
+ } catch {
102
+ fs3.appendFileSync(file, "");
103
+ fs3.utimesSync(file, at, at);
104
+ }
105
+ }
84
106
 
85
107
  // packages/core/dist/evidence.js
86
108
  import * as fs4 from "node:fs";
@@ -101,6 +123,7 @@ function mergeEvidence(id, lane, patch) {
101
123
  // packages/core/dist/inbox.js
102
124
  import * as fs5 from "node:fs";
103
125
  import * as path3 from "node:path";
126
+ var metaName = (file) => file.replace(/\.msg$/, ".meta.json");
104
127
  function inboxDir(id, lane) {
105
128
  return path3.join(laneDirs(lane).inbox, id);
106
129
  }
@@ -114,6 +137,10 @@ function acknowledge(id, lane, file) {
114
137
  const dir = inboxDir(id, lane);
115
138
  fs5.mkdirSync(path3.join(dir, "handled"), { recursive: true });
116
139
  fs5.renameSync(path3.join(dir, file), path3.join(dir, "handled", file));
140
+ try {
141
+ fs5.renameSync(path3.join(dir, metaName(file)), path3.join(dir, "handled", metaName(file)));
142
+ } catch {
143
+ }
117
144
  }
118
145
 
119
146
  // packages/core/dist/config.js
@@ -767,17 +794,37 @@ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
767
794
  }
768
795
 
769
796
  // packages/core/dist/config.js
797
+ var ATTENTION_KINDS = ["question", "landed", "pr:draft", "pr:review", "pr:checks", "pr:ready"];
798
+ var DEFAULT_ATTENTION_KINDS = ["question", "pr:draft", "pr:review", "pr:checks", "pr:ready"];
799
+ function parseAttentionKinds(raw) {
800
+ if (raw === void 0)
801
+ return [...DEFAULT_ATTENTION_KINDS];
802
+ if (!Array.isArray(raw)) {
803
+ throw new Error(`attentionKinds must be an array of kinds (${ATTENTION_KINDS.join(", ")}) in ${configPath()}`);
804
+ }
805
+ for (const k of raw) {
806
+ if (!ATTENTION_KINDS.includes(k)) {
807
+ throw new Error(`attentionKinds: unknown kind "${String(k)}" in ${configPath()} \u2014 valid kinds: ${ATTENTION_KINDS.join(", ")}`);
808
+ }
809
+ }
810
+ return [...new Set(raw)];
811
+ }
770
812
  var DEFAULT_SOAK = {
771
813
  deferSecs: 90,
772
814
  ttlSecs: 1800
773
815
  };
816
+ var DEFAULT_HELM = {
817
+ ttlSecs: 1800,
818
+ reportSecs: 900
819
+ };
774
820
  var DEFAULT_LIMITS = {
775
821
  maxConcurrent: 2,
776
822
  choreConcurrent: 1,
777
823
  wedgeThresholdSecs: 600,
778
824
  maxRestartAttempts: 2,
779
825
  wallClockSecs: 3600,
780
- choreRetentionDays: 7
826
+ choreRetentionDays: 7,
827
+ attachmentMaxBytes: 25 * 1024 * 1024
781
828
  };
782
829
  function configPath() {
783
830
  return path4.join(lobstahHome(), "config.toml");
@@ -801,14 +848,22 @@ function loadConfig() {
801
848
  pickup: r.pickup === void 0 ? void 0 : Boolean(r.pickup)
802
849
  };
803
850
  }
851
+ const groundsRaw = raw.grounds ?? {};
852
+ const grounds = {};
853
+ for (const [key, g] of Object.entries(groundsRaw)) {
854
+ grounds[key] = { repos: Array.isArray(g.repos) ? g.repos.map(String) : [] };
855
+ }
804
856
  return {
805
857
  repos,
806
858
  harness: raw.harness ?? {},
807
859
  limits: { ...DEFAULT_LIMITS, ...raw.limits ?? {} },
808
860
  soak: { ...DEFAULT_SOAK, ...raw.soak ?? {} },
861
+ helm: { ...DEFAULT_HELM, ...raw.helm ?? {} },
862
+ grounds,
809
863
  notifyCommand: raw.notifyCommand ? String(raw.notifyCommand) : void 0,
810
864
  notifyVerbs: Array.isArray(raw.notifyVerbs) ? raw.notifyVerbs.map(String) : void 0,
811
- remindSecs: raw.remindSecs !== void 0 ? Number(raw.remindSecs) : void 0
865
+ remindSecs: raw.remindSecs !== void 0 ? Number(raw.remindSecs) : void 0,
866
+ attentionKinds: parseAttentionKinds(raw.attentionKinds)
812
867
  };
813
868
  }
814
869
  function resolveDispatch(d, cfg) {
@@ -830,6 +885,58 @@ function resolveDispatch(d, cfg) {
830
885
  };
831
886
  }
832
887
 
888
+ // packages/core/dist/harness.js
889
+ import * as fs7 from "node:fs";
890
+ import * as path5 from "node:path";
891
+ import { createRequire } from "node:module";
892
+ function resolveOnPath(bin, pathVar = process.env.PATH) {
893
+ if (!pathVar)
894
+ return void 0;
895
+ const names = process.platform === "win32" ? [bin, `${bin}.exe`, `${bin}.cmd`, `${bin}.bat`] : [bin];
896
+ for (const dir of pathVar.split(path5.delimiter)) {
897
+ if (!dir)
898
+ continue;
899
+ for (const name of names) {
900
+ const candidate = path5.join(dir, name);
901
+ try {
902
+ fs7.accessSync(candidate, fs7.constants.X_OK);
903
+ if (fs7.statSync(candidate).isFile())
904
+ return candidate;
905
+ } catch {
906
+ }
907
+ }
908
+ }
909
+ return void 0;
910
+ }
911
+ function onPath(bin, pathVar = process.env.PATH) {
912
+ return resolveOnPath(bin, pathVar) !== void 0;
913
+ }
914
+ function codexInvocation(argv) {
915
+ if (onPath("codex"))
916
+ return { file: "codex", argv };
917
+ const require_ = createRequire(import.meta.url);
918
+ try {
919
+ const cli = require_.resolve("@openai/codex/bin/codex.js");
920
+ return { file: process.execPath, argv: [cli, ...argv] };
921
+ } catch {
922
+ }
923
+ for (const dir of require_.resolve.paths("@openai/codex") ?? []) {
924
+ const cli = path5.join(dir, "@openai", "codex", "bin", "codex.js");
925
+ if (fs7.existsSync(cli))
926
+ return { file: process.execPath, argv: [cli, ...argv] };
927
+ }
928
+ return void 0;
929
+ }
930
+
931
+ // packages/core/dist/compiled.js
932
+ var COMPILED_BINARY = (() => {
933
+ const bun = globalThis.Bun;
934
+ return bun !== void 0 && String(bun.main ?? "").includes("$bunfs");
935
+ })();
936
+
937
+ // packages/adapters/dist/claude.js
938
+ import { spawn } from "node:child_process";
939
+
833
940
  // packages/adapters/dist/types.js
834
941
  var AsyncQueue = class {
835
942
  buffer = [];
@@ -908,29 +1015,154 @@ function flagsToExtraArgs(flags) {
908
1015
  }
909
1016
  return extra;
910
1017
  }
1018
+ function pumpClaudeMessage(msg, push, onSession) {
1019
+ const at = now();
1020
+ if (msg.type === "system" && msg.subtype === "init") {
1021
+ if (msg.session_id)
1022
+ onSession(msg.session_id);
1023
+ push({ at, type: "session", data: { sessionId: msg.session_id } });
1024
+ } else if (msg.type === "assistant") {
1025
+ for (const block of msg.message?.content ?? []) {
1026
+ if (block.type === "tool_use") {
1027
+ push({ at, type: "tool-start", data: { name: block.name } });
1028
+ } else if (block.type === "text" && block.text) {
1029
+ push({ at, type: "text", data: { text: String(block.text).slice(0, 2e3) } });
1030
+ }
1031
+ }
1032
+ } else if (msg.type === "user") {
1033
+ const content = msg.message?.content;
1034
+ if (Array.isArray(content)) {
1035
+ for (const block of content) {
1036
+ if (block.type === "tool_result")
1037
+ push({ at, type: "tool-end", data: {} });
1038
+ }
1039
+ }
1040
+ } else if (msg.type === "result") {
1041
+ push({ at, type: "turn-end", data: { subtype: msg.subtype, costUsd: msg.total_cost_usd } });
1042
+ }
1043
+ }
1044
+ var userMessage = (content) => ({
1045
+ type: "user",
1046
+ message: { role: "user", content },
1047
+ parent_tool_use_id: null,
1048
+ session_id: ""
1049
+ });
1050
+ function claudeCliArgs(opts) {
1051
+ const args = [
1052
+ "-p",
1053
+ "--output-format",
1054
+ "stream-json",
1055
+ "--input-format",
1056
+ "stream-json",
1057
+ "--verbose",
1058
+ "--permission-mode",
1059
+ "bypassPermissions"
1060
+ ];
1061
+ if (opts.model)
1062
+ args.push("--model", opts.model);
1063
+ if (opts.limits.maxTurns)
1064
+ args.push("--max-turns", String(opts.limits.maxTurns));
1065
+ if (opts.resumeSession)
1066
+ args.push("--resume", opts.resumeSession, "--fork-session");
1067
+ args.push(...opts.flags);
1068
+ return args;
1069
+ }
1070
+ function startClaudeCli(invocation, opts) {
1071
+ const child = spawn(invocation.file, [...invocation.argvPrefix, ...claudeCliArgs(opts)], {
1072
+ cwd: opts.cwd,
1073
+ env: { ...process.env, ...opts.env },
1074
+ stdio: ["pipe", "pipe", "pipe"],
1075
+ shell: process.platform === "win32"
1076
+ // .cmd shims need a shell to exec
1077
+ });
1078
+ const events = new AsyncQueue();
1079
+ const input = new InputGate();
1080
+ let sessionId;
1081
+ let stderrTail = "";
1082
+ child.stdin.on("error", () => {
1083
+ });
1084
+ child.stderr.on("data", (d) => {
1085
+ stderrTail = (stderrTail + d.toString()).slice(-2e3);
1086
+ });
1087
+ child.stdin.write(`${JSON.stringify(userMessage(opts.prompt))}
1088
+ `);
1089
+ void (async () => {
1090
+ while (true) {
1091
+ const text = await input.next();
1092
+ if (text === void 0)
1093
+ break;
1094
+ child.stdin.write(`${JSON.stringify(userMessage(text))}
1095
+ `);
1096
+ }
1097
+ child.stdin.end();
1098
+ })();
1099
+ let buf = "";
1100
+ child.stdout.on("data", (d) => {
1101
+ buf += d.toString();
1102
+ let i;
1103
+ while ((i = buf.indexOf("\n")) >= 0) {
1104
+ const line = buf.slice(0, i).trim();
1105
+ buf = buf.slice(i + 1);
1106
+ if (!line)
1107
+ continue;
1108
+ try {
1109
+ pumpClaudeMessage(JSON.parse(line), (e) => events.push(e), (id) => sessionId = id);
1110
+ } catch {
1111
+ }
1112
+ }
1113
+ });
1114
+ const done = new Promise((resolve2) => {
1115
+ child.on("error", (err) => {
1116
+ events.push({ at: now(), type: "error", data: { message: err.message } });
1117
+ events.close();
1118
+ resolve2({ sessionId, error: err.message });
1119
+ });
1120
+ child.on("exit", (code) => {
1121
+ events.close();
1122
+ if (code === 0)
1123
+ resolve2({ sessionId });
1124
+ else {
1125
+ const detail = stderrTail.trim().split("\n").at(-1);
1126
+ const error = `claude exited ${code}${detail ? ` \u2014 ${detail}` : ""}`;
1127
+ resolve2({ sessionId, error });
1128
+ }
1129
+ });
1130
+ });
1131
+ return {
1132
+ events,
1133
+ send: (text) => input.send(text),
1134
+ end: () => input.end(),
1135
+ done,
1136
+ kill: () => {
1137
+ try {
1138
+ child.kill("SIGKILL");
1139
+ } catch {
1140
+ }
1141
+ input.end();
1142
+ }
1143
+ };
1144
+ }
911
1145
  function createClaudeAdapter() {
912
1146
  return {
913
1147
  name: "claude",
914
1148
  async start(opts) {
915
- const sdk = await import("@anthropic-ai/claude-agent-sdk");
1149
+ let sdk;
1150
+ try {
1151
+ sdk = await import("@anthropic-ai/claude-agent-sdk");
1152
+ } catch (err) {
1153
+ if (onPath("claude"))
1154
+ return startClaudeCli({ file: "claude", argvPrefix: [] }, opts);
1155
+ const detail = err instanceof Error ? err.message : String(err);
1156
+ throw new Error(`claude adapter: agent SDK unavailable (${detail}) and no \`claude\` CLI on PATH`);
1157
+ }
916
1158
  const input = new InputGate();
917
1159
  async function* userMessages() {
918
- yield {
919
- type: "user",
920
- message: { role: "user", content: opts.prompt },
921
- parent_tool_use_id: null,
922
- session_id: ""
923
- };
1160
+ yield userMessage(opts.prompt);
924
1161
  while (true) {
925
1162
  const text = await input.next();
926
1163
  if (text === void 0)
927
1164
  return;
928
- yield {
929
- type: "user",
930
- message: { role: "user", content: text },
931
- parent_tool_use_id: null,
932
- session_id: ""
933
- };
1165
+ yield userMessage(text);
934
1166
  }
935
1167
  }
936
1168
  const q = sdk.query({
@@ -951,29 +1183,7 @@ function createClaudeAdapter() {
951
1183
  const done = (async () => {
952
1184
  try {
953
1185
  for await (const msg of q) {
954
- const at = now();
955
- if (msg.type === "system" && msg.subtype === "init") {
956
- sessionId = msg.session_id;
957
- events.push({ at, type: "session", data: { sessionId } });
958
- } else if (msg.type === "assistant") {
959
- for (const block of msg.message?.content ?? []) {
960
- if (block.type === "tool_use") {
961
- events.push({ at, type: "tool-start", data: { name: block.name } });
962
- } else if (block.type === "text" && block.text) {
963
- events.push({ at, type: "text", data: { text: String(block.text).slice(0, 2e3) } });
964
- }
965
- }
966
- } else if (msg.type === "user") {
967
- const content = msg.message?.content;
968
- if (Array.isArray(content)) {
969
- for (const block of content) {
970
- if (block.type === "tool_result")
971
- events.push({ at, type: "tool-end", data: {} });
972
- }
973
- }
974
- } else if (msg.type === "result") {
975
- events.push({ at, type: "turn-end", data: { subtype: msg.subtype, costUsd: msg.total_cost_usd } });
976
- }
1186
+ pumpClaudeMessage(msg, (e) => events.push(e), (id) => sessionId = id);
977
1187
  }
978
1188
  return { sessionId };
979
1189
  } catch (err) {
@@ -1002,32 +1212,159 @@ function createClaudeAdapter() {
1002
1212
  }
1003
1213
 
1004
1214
  // packages/adapters/dist/codex.js
1005
- import * as fs7 from "node:fs";
1215
+ import * as fs8 from "node:fs";
1006
1216
  import * as os3 from "node:os";
1007
- import * as path5 from "node:path";
1217
+ import * as path6 from "node:path";
1218
+ import { spawn as spawn2 } from "node:child_process";
1008
1219
  function isolatedCodexHome() {
1009
- const dir = path5.join(lobstahHome(), "codex-home");
1010
- fs7.mkdirSync(dir, { recursive: true });
1011
- const auth = path5.join(dir, "auth.json");
1012
- const realAuth = path5.join(os3.homedir(), ".codex", "auth.json");
1013
- if (!fs7.existsSync(auth) && fs7.existsSync(realAuth)) {
1220
+ const dir = path6.join(lobstahHome(), "codex-home");
1221
+ fs8.mkdirSync(dir, { recursive: true });
1222
+ const auth = path6.join(dir, "auth.json");
1223
+ const realAuth = path6.join(os3.homedir(), ".codex", "auth.json");
1224
+ if (!fs8.existsSync(auth) && fs8.existsSync(realAuth)) {
1014
1225
  try {
1015
- fs7.symlinkSync(realAuth, auth);
1226
+ fs8.symlinkSync(realAuth, auth);
1016
1227
  } catch {
1017
- fs7.copyFileSync(realAuth, auth);
1228
+ fs8.copyFileSync(realAuth, auth);
1018
1229
  }
1019
1230
  }
1020
- const cfg = path5.join(dir, "config.toml");
1021
- if (!fs7.existsSync(cfg)) {
1022
- fs7.writeFileSync(cfg, "# lobstah-managed: auth inherited from ~/.codex, interactive config not\n");
1231
+ const cfg = path6.join(dir, "config.toml");
1232
+ if (!fs8.existsSync(cfg)) {
1233
+ fs8.writeFileSync(cfg, "# lobstah-managed: auth inherited from ~/.codex, interactive config not\n");
1023
1234
  }
1024
1235
  return dir;
1025
1236
  }
1237
+ function pumpCodexEvent(ev, push, onSession) {
1238
+ const at = now();
1239
+ if (ev.type === "thread.started") {
1240
+ if (ev.thread_id)
1241
+ onSession(ev.thread_id);
1242
+ push({ at, type: "session", data: { sessionId: ev.thread_id } });
1243
+ } else if (ev.type === "turn.started") {
1244
+ push({ at, type: "turn-start", data: {} });
1245
+ } else if (ev.type === "item.started") {
1246
+ push({ at, type: "tool-start", data: { name: ev.item?.type } });
1247
+ } else if (ev.type === "item.completed") {
1248
+ if (ev.item?.type === "agent_message" && ev.item?.text) {
1249
+ push({ at, type: "text", data: { text: String(ev.item.text).slice(0, 2e3) } });
1250
+ } else {
1251
+ push({ at, type: "tool-end", data: { name: ev.item?.type } });
1252
+ }
1253
+ } else if (ev.type === "turn.failed") {
1254
+ const message = ev.error?.message ?? "turn failed";
1255
+ push({ at, type: "error", data: { message } });
1256
+ return message;
1257
+ }
1258
+ return void 0;
1259
+ }
1260
+ function codexExecArgs(opts, prompt, resumeThread) {
1261
+ const args = ["exec"];
1262
+ if (resumeThread)
1263
+ args.push("resume", resumeThread);
1264
+ args.push("--json", "--skip-git-repo-check", "--sandbox", "workspace-write", "--cd", opts.cwd);
1265
+ if (opts.model)
1266
+ args.push("-m", opts.model);
1267
+ if (opts.effort)
1268
+ args.push("-c", `model_reasoning_effort=${JSON.stringify(opts.effort)}`);
1269
+ args.push(prompt);
1270
+ return args;
1271
+ }
1272
+ function startCodexCli(invoke, opts) {
1273
+ const input = new InputGate();
1274
+ const events = new AsyncQueue();
1275
+ let sessionId;
1276
+ let current;
1277
+ let killed = false;
1278
+ function runTurn(prompt, resumeThread) {
1279
+ return new Promise((resolve2) => {
1280
+ const inv = invoke(codexExecArgs(opts, prompt, resumeThread));
1281
+ const child = spawn2(inv.file, inv.argv, {
1282
+ cwd: opts.cwd,
1283
+ env: { ...process.env, ...opts.env, CODEX_HOME: isolatedCodexHome() },
1284
+ stdio: ["ignore", "pipe", "pipe"],
1285
+ shell: process.platform === "win32" && inv.file !== process.execPath
1286
+ });
1287
+ current = child;
1288
+ let turnError;
1289
+ let stderrTail = "";
1290
+ child.stderr?.on("data", (d) => {
1291
+ stderrTail = (stderrTail + d.toString()).slice(-2e3);
1292
+ });
1293
+ let buf = "";
1294
+ child.stdout?.on("data", (d) => {
1295
+ buf += d.toString();
1296
+ let i;
1297
+ while ((i = buf.indexOf("\n")) >= 0) {
1298
+ const line = buf.slice(0, i).trim();
1299
+ buf = buf.slice(i + 1);
1300
+ if (!line)
1301
+ continue;
1302
+ try {
1303
+ const failed = pumpCodexEvent(JSON.parse(line), (e) => events.push(e), (id) => sessionId = id);
1304
+ if (failed)
1305
+ turnError = failed;
1306
+ } catch {
1307
+ }
1308
+ }
1309
+ });
1310
+ child.on("error", (err) => resolve2(err.message));
1311
+ child.on("exit", (code) => {
1312
+ if (turnError)
1313
+ return resolve2(turnError);
1314
+ if (code === 0 || killed)
1315
+ return resolve2(void 0);
1316
+ const detail = stderrTail.trim().split("\n").at(-1);
1317
+ resolve2(`codex exited ${code}${detail ? ` \u2014 ${detail}` : ""}`);
1318
+ });
1319
+ });
1320
+ }
1321
+ const done = (async () => {
1322
+ try {
1323
+ let next = opts.prompt;
1324
+ let thread = opts.resumeSession;
1325
+ while (next !== void 0 && !killed) {
1326
+ const error = await runTurn(next, thread);
1327
+ thread = sessionId ?? thread;
1328
+ if (error) {
1329
+ events.push({ at: now(), type: "error", data: { message: error } });
1330
+ return { sessionId, error };
1331
+ }
1332
+ events.push({ at: now(), type: "turn-end", data: {} });
1333
+ next = await input.next();
1334
+ }
1335
+ return { sessionId };
1336
+ } finally {
1337
+ events.close();
1338
+ }
1339
+ })();
1340
+ return {
1341
+ events,
1342
+ send: (text) => input.send(text),
1343
+ end: () => input.end(),
1344
+ done,
1345
+ kill: () => {
1346
+ killed = true;
1347
+ try {
1348
+ current?.kill("SIGKILL");
1349
+ } catch {
1350
+ }
1351
+ input.end();
1352
+ }
1353
+ };
1354
+ }
1026
1355
  function createCodexAdapter() {
1027
1356
  return {
1028
1357
  name: "codex",
1029
1358
  async start(opts) {
1030
- const mod = await import("@openai/codex-sdk");
1359
+ let mod;
1360
+ try {
1361
+ mod = await import("@openai/codex-sdk");
1362
+ } catch (err) {
1363
+ if (codexInvocation([]))
1364
+ return startCodexCli((argv) => codexInvocation(argv), opts);
1365
+ const detail = err instanceof Error ? err.message : String(err);
1366
+ throw new Error(`codex adapter: SDK unavailable (${detail}) and no codex CLI found (PATH or vendored)`);
1367
+ }
1031
1368
  const codex = new mod.Codex({
1032
1369
  env: { ...process.env, ...opts.env, CODEX_HOME: isolatedCodexHome() }
1033
1370
  });
@@ -1050,24 +1387,9 @@ function createCodexAdapter() {
1050
1387
  while (next !== void 0) {
1051
1388
  const streamed = await thread.runStreamed(next);
1052
1389
  for await (const ev of streamed.events) {
1053
- const at = now();
1054
- if (ev.type === "thread.started") {
1055
- sessionId = ev.thread_id;
1056
- events.push({ at, type: "session", data: { sessionId } });
1057
- } else if (ev.type === "turn.started") {
1058
- events.push({ at, type: "turn-start", data: {} });
1059
- } else if (ev.type === "item.started") {
1060
- events.push({ at, type: "tool-start", data: { name: ev.item?.type } });
1061
- } else if (ev.type === "item.completed") {
1062
- if (ev.item?.type === "agent_message" && ev.item?.text) {
1063
- events.push({ at, type: "text", data: { text: String(ev.item.text).slice(0, 2e3) } });
1064
- } else {
1065
- events.push({ at, type: "tool-end", data: { name: ev.item?.type } });
1066
- }
1067
- } else if (ev.type === "turn.failed") {
1068
- lastTurnError = ev.error?.message ?? "turn failed";
1069
- events.push({ at, type: "error", data: { message: lastTurnError } });
1070
- }
1390
+ const failed = pumpCodexEvent(ev, (e) => events.push(e), (id) => sessionId = id);
1391
+ if (failed)
1392
+ lastTurnError = failed;
1071
1393
  }
1072
1394
  events.push({ at: now(), type: "turn-end", data: {} });
1073
1395
  next = await input.next();
@@ -1105,14 +1427,14 @@ function loadAdapter(name) {
1105
1427
  }
1106
1428
 
1107
1429
  // packages/worktree/dist/index.js
1108
- import * as fs8 from "node:fs";
1109
- import * as path6 from "node:path";
1430
+ import * as fs9 from "node:fs";
1431
+ import * as path7 from "node:path";
1110
1432
  import { exec, execFile } from "node:child_process";
1111
1433
  import { promisify } from "node:util";
1112
1434
  var run = promisify(execFile);
1113
1435
  var shell = promisify(exec);
1114
1436
  function worktreePath(id) {
1115
- return path6.join(lobstahHome(), "worktrees", id);
1437
+ return path7.join(lobstahHome(), "worktrees", id);
1116
1438
  }
1117
1439
  async function git(cwd, ...args) {
1118
1440
  const { stdout } = await run("git", args, { cwd, env: process.env });
@@ -1120,10 +1442,10 @@ async function git(cwd, ...args) {
1120
1442
  }
1121
1443
  async function allocate(repo, id) {
1122
1444
  const dir = worktreePath(id);
1123
- if (fs8.existsSync(dir)) {
1445
+ if (fs9.existsSync(dir)) {
1124
1446
  throw new Error(`worktree for ${id} already exists at ${dir} \u2014 never allocate a second`);
1125
1447
  }
1126
- if (!fs8.existsSync(repo.path)) {
1448
+ if (!fs9.existsSync(repo.path)) {
1127
1449
  if (!repo.origin)
1128
1450
  throw new Error(`repo path ${repo.path} missing and no origin configured`);
1129
1451
  await run("git", ["clone", repo.origin, repo.path], { env: process.env });
@@ -1154,10 +1476,77 @@ function buildPrompt(brief, opts) {
1154
1476
  `--- BRIEF ---`,
1155
1477
  brief
1156
1478
  ];
1479
+ if (opts.attachments?.length) parts.push(attachmentBlock(opts.attachments));
1157
1480
  if (opts.nudge) parts.push(`--- SUPERVISOR NOTE ---`, opts.nudge);
1158
1481
  return parts.join("\n\n");
1159
1482
  }
1160
1483
 
1484
+ // packages/runner/src/drive.ts
1485
+ var WAITING_VERBS = ["needs-decision", "blocked", "paused"];
1486
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
1487
+ function deliver(run2, id, lane) {
1488
+ const msgs = unhandled(id, lane);
1489
+ for (const m of msgs) {
1490
+ run2.send(m.text);
1491
+ acknowledge(id, lane, m.file);
1492
+ }
1493
+ return msgs.length;
1494
+ }
1495
+ async function drive(run2, opts) {
1496
+ const { id, lane, pollMs = 3e3, stopped = () => false } = opts;
1497
+ let cancelled = false;
1498
+ let finished = false;
1499
+ void run2.done.then(() => finished = true);
1500
+ const cancel = () => {
1501
+ cancelled = true;
1502
+ run2.kill();
1503
+ };
1504
+ const answered = (verb) => {
1505
+ appendStatus(id, lane, "working", "operator message delivered");
1506
+ appendEvent(id, lane, { at: (/* @__PURE__ */ new Date()).toISOString(), type: "runner", data: { resumed: verb } });
1507
+ };
1508
+ const awaitAnswer = async (verb) => {
1509
+ appendEvent(id, lane, { at: (/* @__PURE__ */ new Date()).toISOString(), type: "runner", data: { waiting: verb } });
1510
+ while (true) {
1511
+ if (stopped() || finished) return;
1512
+ if (cancelRequested(id, lane)) return cancel();
1513
+ if (deliver(run2, id, lane) > 0) return answered(verb);
1514
+ touchEvents(id, lane);
1515
+ await sleep(pollMs);
1516
+ }
1517
+ };
1518
+ for await (const ev of run2.events) {
1519
+ appendEvent(id, lane, ev);
1520
+ if (ev.type === "session" && ev.data?.sessionId) {
1521
+ mergeEvidence(id, lane, { sessionId: String(ev.data.sessionId) });
1522
+ }
1523
+ if (ev.type !== "turn-end" || cancelled || stopped()) continue;
1524
+ if (cancelRequested(id, lane)) {
1525
+ cancel();
1526
+ continue;
1527
+ }
1528
+ const lastVerb = readStatusLog(id, lane).at(-1)?.verb;
1529
+ const waiting = lastVerb !== void 0 && WAITING_VERBS.includes(lastVerb);
1530
+ if (deliver(run2, id, lane) > 0) {
1531
+ if (waiting) answered(lastVerb);
1532
+ continue;
1533
+ }
1534
+ if (waiting) {
1535
+ await awaitAnswer(lastVerb);
1536
+ continue;
1537
+ }
1538
+ run2.end();
1539
+ }
1540
+ return { cancelled };
1541
+ }
1542
+ function settle(id, lane, r) {
1543
+ const lastVerb = readStatusLog(id, lane).at(-1)?.verb;
1544
+ if (r.cancelled) appendStatus(id, lane, "failed", "cancelled by operator");
1545
+ else if (r.wallClockHit) appendStatus(id, lane, "failed", "wall-clock limit exceeded");
1546
+ else if (r.error) appendStatus(id, lane, "failed", r.error.slice(0, 500));
1547
+ else if (!lastVerb || !TERMINAL_VERBS.includes(lastVerb)) appendStatus(id, lane, "done");
1548
+ }
1549
+
1161
1550
  // packages/runner/src/index.ts
1162
1551
  function followUpSession(followUp) {
1163
1552
  for (const lane of ["work", "chore"]) {
@@ -1167,40 +1556,40 @@ function followUpSession(followUp) {
1167
1556
  return void 0;
1168
1557
  }
1169
1558
  async function main(activeDir, lane) {
1170
- const id = path7.basename(activeDir);
1559
+ const id = path8.basename(activeDir);
1171
1560
  const status = (verb, note) => appendStatus(id, lane, verb, note);
1172
1561
  const cfg = loadConfig();
1173
- const descriptor = JSON.parse(fs9.readFileSync(path7.join(activeDir, "descriptor.json"), "utf8"));
1562
+ const descriptor = JSON.parse(fs10.readFileSync(path8.join(activeDir, "descriptor.json"), "utf8"));
1174
1563
  const resolved = resolveDispatch(descriptor, cfg);
1175
1564
  const repo = cfg.repos[descriptor.repo];
1176
- const briefFile = path7.join(activeDir, "brief.md");
1177
- if (!fs9.existsSync(briefFile)) fs9.writeFileSync(briefFile, descriptor.brief);
1178
- const brief = fs9.readFileSync(briefFile, "utf8");
1565
+ const briefFile = path8.join(activeDir, "brief.md");
1566
+ if (!fs10.existsSync(briefFile)) fs10.writeFileSync(briefFile, descriptor.brief);
1567
+ const brief = fs10.readFileSync(briefFile, "utf8");
1179
1568
  const attempts = Number(process.env.LOBSTAH_ATTEMPTS ?? "1");
1180
1569
  const runnerInfo = {
1181
1570
  pid: process.pid,
1182
1571
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1183
1572
  attempts
1184
1573
  };
1185
- fs9.writeFileSync(path7.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
1574
+ fs10.writeFileSync(path8.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
1186
1575
  status("working", attempts > 1 ? `attempt ${attempts}` : void 0);
1187
1576
  appendEvent(id, lane, { at: (/* @__PURE__ */ new Date()).toISOString(), type: "runner", data: { pid: process.pid, attempts } });
1188
- const wtFile = path7.join(activeDir, "worktree.json");
1577
+ const wtFile = path8.join(activeDir, "worktree.json");
1189
1578
  let cwd;
1190
- if (fs9.existsSync(wtFile)) {
1191
- cwd = JSON.parse(fs9.readFileSync(wtFile, "utf8")).path;
1192
- if (!fs9.existsSync(cwd)) throw new Error(`recorded worktree ${cwd} is gone`);
1193
- } else if (fs9.existsSync(worktreePath(id))) {
1579
+ if (fs10.existsSync(wtFile)) {
1580
+ cwd = JSON.parse(fs10.readFileSync(wtFile, "utf8")).path;
1581
+ if (!fs10.existsSync(cwd)) throw new Error(`recorded worktree ${cwd} is gone`);
1582
+ } else if (fs10.existsSync(worktreePath(id))) {
1194
1583
  throw new Error(`unrecorded worktree already exists for ${id} \u2014 refusing to proceed`);
1195
1584
  } else {
1196
1585
  cwd = await allocate(repo, id);
1197
- fs9.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
1586
+ fs10.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
1198
1587
  }
1199
1588
  const resumeSession = process.env.LOBSTAH_RESUME ?? (descriptor.followUp ? followUpSession(descriptor.followUp) : void 0);
1200
1589
  const adapter = loadAdapter(resolved.harness);
1201
- const prompt = buildPrompt(brief, { id, nudge: process.env.LOBSTAH_NUDGE });
1202
- const binDir = path7.resolve(path7.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "bin");
1203
- const workerPath = fs9.existsSync(path7.join(binDir, "lobstah")) ? `${binDir}${path7.delimiter}${process.env.PATH ?? ""}` : void 0;
1590
+ const prompt = buildPrompt(brief, { id, nudge: process.env.LOBSTAH_NUDGE, attachments: descriptor.attachments });
1591
+ const binDir = path8.resolve(path8.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "bin");
1592
+ const workerPath = fs10.existsSync(path8.join(binDir, "lobstah")) ? `${binDir}${path8.delimiter}${process.env.PATH ?? ""}` : void 0;
1204
1593
  const run2 = await adapter.start({
1205
1594
  id,
1206
1595
  cwd,
@@ -1221,29 +1610,7 @@ async function main(activeDir, lane) {
1221
1610
  wallClockHit = true;
1222
1611
  run2.kill();
1223
1612
  }, resolved.limits.wallClockSecs * 1e3) : void 0;
1224
- let cancelled = false;
1225
- for await (const ev of run2.events) {
1226
- appendEvent(id, lane, ev);
1227
- if (ev.type === "session" && ev.data?.sessionId) {
1228
- mergeEvidence(id, lane, { sessionId: String(ev.data.sessionId) });
1229
- }
1230
- if (ev.type === "turn-end") {
1231
- if (cancelRequested(id, lane)) {
1232
- cancelled = true;
1233
- run2.kill();
1234
- continue;
1235
- }
1236
- const msgs = unhandled(id, lane);
1237
- if (msgs.length > 0) {
1238
- for (const m of msgs) {
1239
- run2.send(m.text);
1240
- acknowledge(id, lane, m.file);
1241
- }
1242
- } else {
1243
- run2.end();
1244
- }
1245
- }
1246
- }
1613
+ const { cancelled } = await drive(run2, { id, lane, stopped: () => wallClockHit });
1247
1614
  const result = await run2.done;
1248
1615
  if (wallTimer) clearTimeout(wallTimer);
1249
1616
  try {
@@ -1256,33 +1623,26 @@ async function main(activeDir, lane) {
1256
1623
  note: `evidence collection failed: ${err instanceof Error ? err.message : String(err)}`.slice(0, 300)
1257
1624
  });
1258
1625
  }
1259
- const lastVerb = readStatusLog(id, lane).at(-1)?.verb;
1260
- if (cancelled) {
1261
- status("failed", "cancelled by operator");
1262
- } else if (wallClockHit) {
1263
- status("failed", "wall-clock limit exceeded");
1264
- } else if (result.error) {
1265
- status("failed", result.error.slice(0, 500));
1266
- } else if (!lastVerb || !TERMINAL_VERBS.includes(lastVerb)) {
1267
- status("done");
1268
- }
1626
+ settle(id, lane, { cancelled, wallClockHit, error: result.error });
1269
1627
  complete(id, lane);
1270
1628
  }
1271
- var [activeDirArg, laneArg] = process.argv.slice(2);
1272
- if (activeDirArg) {
1273
- const lane = laneArg === "chore" ? "chore" : "work";
1274
- main(activeDirArg, lane).catch((err) => {
1275
- const id = path7.basename(activeDirArg);
1629
+ function runRunner(activeDirArg2, laneArg2) {
1630
+ const lane = laneArg2 === "chore" ? "chore" : "work";
1631
+ main(activeDirArg2, lane).catch((err) => {
1632
+ const id = path8.basename(activeDirArg2);
1276
1633
  try {
1277
1634
  appendStatus(id, lane, "failed", String(err instanceof Error ? err.message : err).slice(0, 500));
1278
- fs9.renameSync(activeDirArg, path7.join(laneDirs(lane).done, id));
1635
+ fs10.renameSync(activeDirArg2, path8.join(laneDirs(lane).done, id));
1279
1636
  } catch {
1280
1637
  }
1281
1638
  process.exitCode = 1;
1282
1639
  });
1283
1640
  }
1641
+ var [activeDirArg, laneArg] = process.argv.slice(2);
1642
+ if (activeDirArg && activeDirArg !== "__runner") runRunner(activeDirArg, laneArg);
1284
1643
  export {
1285
- main
1644
+ main,
1645
+ runRunner
1286
1646
  };
1287
1647
  /*! Bundled license information:
1288
1648