lobstah 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -6
- package/dist/main.js +3564 -758
- package/dist/runner.js +389 -107
- package/docs/assets/favicon.png +0 -0
- package/docs/assets/lob-sprite.png +0 -0
- package/docs/assets/lob.png +0 -0
- package/docs/assets/star.png +0 -0
- package/docs/configuration.md +22 -0
- package/docs/lobsterman.md +153 -14
- package/docs/pickup.md +13 -0
- package/docs/vocabulary.md +42 -15
- package/package.json +1 -1
package/dist/runner.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// packages/runner/src/index.ts
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
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
|
|
@@ -37,18 +37,11 @@ function evidencePath(id, lane) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// packages/core/dist/queue.js
|
|
40
|
-
import * as
|
|
40
|
+
import * as fs2 from "node:fs";
|
|
41
41
|
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
42
|
|
|
50
43
|
// packages/core/dist/status.js
|
|
51
|
-
import * as
|
|
44
|
+
import * as fs from "node:fs";
|
|
52
45
|
function isVerb(v) {
|
|
53
46
|
return VERBS.includes(v);
|
|
54
47
|
}
|
|
@@ -57,15 +50,15 @@ function appendStatus(id, lane, verb, note) {
|
|
|
57
50
|
throw new Error(`invalid status verb "${verb}" \u2014 must be one of: ${VERBS.join(", ")}`);
|
|
58
51
|
}
|
|
59
52
|
const entry = { at: (/* @__PURE__ */ new Date()).toISOString(), verb, ...note ? { note } : {} };
|
|
60
|
-
|
|
53
|
+
fs.appendFileSync(statusPath(id, lane), `${JSON.stringify(entry)}
|
|
61
54
|
`);
|
|
62
55
|
return entry;
|
|
63
56
|
}
|
|
64
57
|
function readStatusLog(id, lane) {
|
|
65
58
|
const file = statusPath(id, lane);
|
|
66
|
-
if (!
|
|
59
|
+
if (!fs.existsSync(file))
|
|
67
60
|
return [];
|
|
68
|
-
return
|
|
61
|
+
return fs.readFileSync(file, "utf8").split("\n").filter(Boolean).flatMap((line) => {
|
|
69
62
|
try {
|
|
70
63
|
const parsed = JSON.parse(line);
|
|
71
64
|
return isVerb(parsed.verb) ? [parsed] : [];
|
|
@@ -75,6 +68,15 @@ function readStatusLog(id, lane) {
|
|
|
75
68
|
});
|
|
76
69
|
}
|
|
77
70
|
|
|
71
|
+
// packages/core/dist/queue.js
|
|
72
|
+
function complete(id, lane) {
|
|
73
|
+
const dirs = laneDirs(lane);
|
|
74
|
+
fs2.renameSync(path2.join(dirs.active, id), path2.join(dirs.done, id));
|
|
75
|
+
}
|
|
76
|
+
function cancelRequested(id, lane) {
|
|
77
|
+
return fs2.existsSync(path2.join(laneDirs(lane).active, id, "cancel"));
|
|
78
|
+
}
|
|
79
|
+
|
|
78
80
|
// packages/core/dist/events.js
|
|
79
81
|
import * as fs3 from "node:fs";
|
|
80
82
|
function appendEvent(id, lane, ev) {
|
|
@@ -771,6 +773,10 @@ var DEFAULT_SOAK = {
|
|
|
771
773
|
deferSecs: 90,
|
|
772
774
|
ttlSecs: 1800
|
|
773
775
|
};
|
|
776
|
+
var DEFAULT_HELM = {
|
|
777
|
+
ttlSecs: 1800,
|
|
778
|
+
reportSecs: 900
|
|
779
|
+
};
|
|
774
780
|
var DEFAULT_LIMITS = {
|
|
775
781
|
maxConcurrent: 2,
|
|
776
782
|
choreConcurrent: 1,
|
|
@@ -801,11 +807,18 @@ function loadConfig() {
|
|
|
801
807
|
pickup: r.pickup === void 0 ? void 0 : Boolean(r.pickup)
|
|
802
808
|
};
|
|
803
809
|
}
|
|
810
|
+
const groundsRaw = raw.grounds ?? {};
|
|
811
|
+
const grounds = {};
|
|
812
|
+
for (const [key, g] of Object.entries(groundsRaw)) {
|
|
813
|
+
grounds[key] = { repos: Array.isArray(g.repos) ? g.repos.map(String) : [] };
|
|
814
|
+
}
|
|
804
815
|
return {
|
|
805
816
|
repos,
|
|
806
817
|
harness: raw.harness ?? {},
|
|
807
818
|
limits: { ...DEFAULT_LIMITS, ...raw.limits ?? {} },
|
|
808
819
|
soak: { ...DEFAULT_SOAK, ...raw.soak ?? {} },
|
|
820
|
+
helm: { ...DEFAULT_HELM, ...raw.helm ?? {} },
|
|
821
|
+
grounds,
|
|
809
822
|
notifyCommand: raw.notifyCommand ? String(raw.notifyCommand) : void 0,
|
|
810
823
|
notifyVerbs: Array.isArray(raw.notifyVerbs) ? raw.notifyVerbs.map(String) : void 0,
|
|
811
824
|
remindSecs: raw.remindSecs !== void 0 ? Number(raw.remindSecs) : void 0
|
|
@@ -830,6 +843,58 @@ function resolveDispatch(d, cfg) {
|
|
|
830
843
|
};
|
|
831
844
|
}
|
|
832
845
|
|
|
846
|
+
// packages/core/dist/harness.js
|
|
847
|
+
import * as fs7 from "node:fs";
|
|
848
|
+
import * as path5 from "node:path";
|
|
849
|
+
import { createRequire } from "node:module";
|
|
850
|
+
function resolveOnPath(bin, pathVar = process.env.PATH) {
|
|
851
|
+
if (!pathVar)
|
|
852
|
+
return void 0;
|
|
853
|
+
const names = process.platform === "win32" ? [bin, `${bin}.exe`, `${bin}.cmd`, `${bin}.bat`] : [bin];
|
|
854
|
+
for (const dir of pathVar.split(path5.delimiter)) {
|
|
855
|
+
if (!dir)
|
|
856
|
+
continue;
|
|
857
|
+
for (const name of names) {
|
|
858
|
+
const candidate = path5.join(dir, name);
|
|
859
|
+
try {
|
|
860
|
+
fs7.accessSync(candidate, fs7.constants.X_OK);
|
|
861
|
+
if (fs7.statSync(candidate).isFile())
|
|
862
|
+
return candidate;
|
|
863
|
+
} catch {
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return void 0;
|
|
868
|
+
}
|
|
869
|
+
function onPath(bin, pathVar = process.env.PATH) {
|
|
870
|
+
return resolveOnPath(bin, pathVar) !== void 0;
|
|
871
|
+
}
|
|
872
|
+
function codexInvocation(argv) {
|
|
873
|
+
if (onPath("codex"))
|
|
874
|
+
return { file: "codex", argv };
|
|
875
|
+
const require_ = createRequire(import.meta.url);
|
|
876
|
+
try {
|
|
877
|
+
const cli = require_.resolve("@openai/codex/bin/codex.js");
|
|
878
|
+
return { file: process.execPath, argv: [cli, ...argv] };
|
|
879
|
+
} catch {
|
|
880
|
+
}
|
|
881
|
+
for (const dir of require_.resolve.paths("@openai/codex") ?? []) {
|
|
882
|
+
const cli = path5.join(dir, "@openai", "codex", "bin", "codex.js");
|
|
883
|
+
if (fs7.existsSync(cli))
|
|
884
|
+
return { file: process.execPath, argv: [cli, ...argv] };
|
|
885
|
+
}
|
|
886
|
+
return void 0;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// packages/core/dist/compiled.js
|
|
890
|
+
var COMPILED_BINARY = (() => {
|
|
891
|
+
const bun = globalThis.Bun;
|
|
892
|
+
return bun !== void 0 && String(bun.main ?? "").includes("$bunfs");
|
|
893
|
+
})();
|
|
894
|
+
|
|
895
|
+
// packages/adapters/dist/claude.js
|
|
896
|
+
import { spawn } from "node:child_process";
|
|
897
|
+
|
|
833
898
|
// packages/adapters/dist/types.js
|
|
834
899
|
var AsyncQueue = class {
|
|
835
900
|
buffer = [];
|
|
@@ -908,29 +973,154 @@ function flagsToExtraArgs(flags) {
|
|
|
908
973
|
}
|
|
909
974
|
return extra;
|
|
910
975
|
}
|
|
976
|
+
function pumpClaudeMessage(msg, push, onSession) {
|
|
977
|
+
const at = now();
|
|
978
|
+
if (msg.type === "system" && msg.subtype === "init") {
|
|
979
|
+
if (msg.session_id)
|
|
980
|
+
onSession(msg.session_id);
|
|
981
|
+
push({ at, type: "session", data: { sessionId: msg.session_id } });
|
|
982
|
+
} else if (msg.type === "assistant") {
|
|
983
|
+
for (const block of msg.message?.content ?? []) {
|
|
984
|
+
if (block.type === "tool_use") {
|
|
985
|
+
push({ at, type: "tool-start", data: { name: block.name } });
|
|
986
|
+
} else if (block.type === "text" && block.text) {
|
|
987
|
+
push({ at, type: "text", data: { text: String(block.text).slice(0, 2e3) } });
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
} else if (msg.type === "user") {
|
|
991
|
+
const content = msg.message?.content;
|
|
992
|
+
if (Array.isArray(content)) {
|
|
993
|
+
for (const block of content) {
|
|
994
|
+
if (block.type === "tool_result")
|
|
995
|
+
push({ at, type: "tool-end", data: {} });
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
} else if (msg.type === "result") {
|
|
999
|
+
push({ at, type: "turn-end", data: { subtype: msg.subtype, costUsd: msg.total_cost_usd } });
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
var userMessage = (content) => ({
|
|
1003
|
+
type: "user",
|
|
1004
|
+
message: { role: "user", content },
|
|
1005
|
+
parent_tool_use_id: null,
|
|
1006
|
+
session_id: ""
|
|
1007
|
+
});
|
|
1008
|
+
function claudeCliArgs(opts) {
|
|
1009
|
+
const args = [
|
|
1010
|
+
"-p",
|
|
1011
|
+
"--output-format",
|
|
1012
|
+
"stream-json",
|
|
1013
|
+
"--input-format",
|
|
1014
|
+
"stream-json",
|
|
1015
|
+
"--verbose",
|
|
1016
|
+
"--permission-mode",
|
|
1017
|
+
"bypassPermissions"
|
|
1018
|
+
];
|
|
1019
|
+
if (opts.model)
|
|
1020
|
+
args.push("--model", opts.model);
|
|
1021
|
+
if (opts.limits.maxTurns)
|
|
1022
|
+
args.push("--max-turns", String(opts.limits.maxTurns));
|
|
1023
|
+
if (opts.resumeSession)
|
|
1024
|
+
args.push("--resume", opts.resumeSession, "--fork-session");
|
|
1025
|
+
args.push(...opts.flags);
|
|
1026
|
+
return args;
|
|
1027
|
+
}
|
|
1028
|
+
function startClaudeCli(invocation, opts) {
|
|
1029
|
+
const child = spawn(invocation.file, [...invocation.argvPrefix, ...claudeCliArgs(opts)], {
|
|
1030
|
+
cwd: opts.cwd,
|
|
1031
|
+
env: { ...process.env, ...opts.env },
|
|
1032
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1033
|
+
shell: process.platform === "win32"
|
|
1034
|
+
// .cmd shims need a shell to exec
|
|
1035
|
+
});
|
|
1036
|
+
const events = new AsyncQueue();
|
|
1037
|
+
const input = new InputGate();
|
|
1038
|
+
let sessionId;
|
|
1039
|
+
let stderrTail = "";
|
|
1040
|
+
child.stdin.on("error", () => {
|
|
1041
|
+
});
|
|
1042
|
+
child.stderr.on("data", (d) => {
|
|
1043
|
+
stderrTail = (stderrTail + d.toString()).slice(-2e3);
|
|
1044
|
+
});
|
|
1045
|
+
child.stdin.write(`${JSON.stringify(userMessage(opts.prompt))}
|
|
1046
|
+
`);
|
|
1047
|
+
void (async () => {
|
|
1048
|
+
while (true) {
|
|
1049
|
+
const text = await input.next();
|
|
1050
|
+
if (text === void 0)
|
|
1051
|
+
break;
|
|
1052
|
+
child.stdin.write(`${JSON.stringify(userMessage(text))}
|
|
1053
|
+
`);
|
|
1054
|
+
}
|
|
1055
|
+
child.stdin.end();
|
|
1056
|
+
})();
|
|
1057
|
+
let buf = "";
|
|
1058
|
+
child.stdout.on("data", (d) => {
|
|
1059
|
+
buf += d.toString();
|
|
1060
|
+
let i;
|
|
1061
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
1062
|
+
const line = buf.slice(0, i).trim();
|
|
1063
|
+
buf = buf.slice(i + 1);
|
|
1064
|
+
if (!line)
|
|
1065
|
+
continue;
|
|
1066
|
+
try {
|
|
1067
|
+
pumpClaudeMessage(JSON.parse(line), (e) => events.push(e), (id) => sessionId = id);
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
const done = new Promise((resolve2) => {
|
|
1073
|
+
child.on("error", (err) => {
|
|
1074
|
+
events.push({ at: now(), type: "error", data: { message: err.message } });
|
|
1075
|
+
events.close();
|
|
1076
|
+
resolve2({ sessionId, error: err.message });
|
|
1077
|
+
});
|
|
1078
|
+
child.on("exit", (code) => {
|
|
1079
|
+
events.close();
|
|
1080
|
+
if (code === 0)
|
|
1081
|
+
resolve2({ sessionId });
|
|
1082
|
+
else {
|
|
1083
|
+
const detail = stderrTail.trim().split("\n").at(-1);
|
|
1084
|
+
const error = `claude exited ${code}${detail ? ` \u2014 ${detail}` : ""}`;
|
|
1085
|
+
resolve2({ sessionId, error });
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
return {
|
|
1090
|
+
events,
|
|
1091
|
+
send: (text) => input.send(text),
|
|
1092
|
+
end: () => input.end(),
|
|
1093
|
+
done,
|
|
1094
|
+
kill: () => {
|
|
1095
|
+
try {
|
|
1096
|
+
child.kill("SIGKILL");
|
|
1097
|
+
} catch {
|
|
1098
|
+
}
|
|
1099
|
+
input.end();
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
911
1103
|
function createClaudeAdapter() {
|
|
912
1104
|
return {
|
|
913
1105
|
name: "claude",
|
|
914
1106
|
async start(opts) {
|
|
915
|
-
|
|
1107
|
+
let sdk;
|
|
1108
|
+
try {
|
|
1109
|
+
sdk = await import("@anthropic-ai/claude-agent-sdk");
|
|
1110
|
+
} catch (err) {
|
|
1111
|
+
if (onPath("claude"))
|
|
1112
|
+
return startClaudeCli({ file: "claude", argvPrefix: [] }, opts);
|
|
1113
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1114
|
+
throw new Error(`claude adapter: agent SDK unavailable (${detail}) and no \`claude\` CLI on PATH`);
|
|
1115
|
+
}
|
|
916
1116
|
const input = new InputGate();
|
|
917
1117
|
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
|
-
};
|
|
1118
|
+
yield userMessage(opts.prompt);
|
|
924
1119
|
while (true) {
|
|
925
1120
|
const text = await input.next();
|
|
926
1121
|
if (text === void 0)
|
|
927
1122
|
return;
|
|
928
|
-
yield
|
|
929
|
-
type: "user",
|
|
930
|
-
message: { role: "user", content: text },
|
|
931
|
-
parent_tool_use_id: null,
|
|
932
|
-
session_id: ""
|
|
933
|
-
};
|
|
1123
|
+
yield userMessage(text);
|
|
934
1124
|
}
|
|
935
1125
|
}
|
|
936
1126
|
const q = sdk.query({
|
|
@@ -951,29 +1141,7 @@ function createClaudeAdapter() {
|
|
|
951
1141
|
const done = (async () => {
|
|
952
1142
|
try {
|
|
953
1143
|
for await (const msg of q) {
|
|
954
|
-
|
|
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
|
-
}
|
|
1144
|
+
pumpClaudeMessage(msg, (e) => events.push(e), (id) => sessionId = id);
|
|
977
1145
|
}
|
|
978
1146
|
return { sessionId };
|
|
979
1147
|
} catch (err) {
|
|
@@ -1002,32 +1170,159 @@ function createClaudeAdapter() {
|
|
|
1002
1170
|
}
|
|
1003
1171
|
|
|
1004
1172
|
// packages/adapters/dist/codex.js
|
|
1005
|
-
import * as
|
|
1173
|
+
import * as fs8 from "node:fs";
|
|
1006
1174
|
import * as os3 from "node:os";
|
|
1007
|
-
import * as
|
|
1175
|
+
import * as path6 from "node:path";
|
|
1176
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1008
1177
|
function isolatedCodexHome() {
|
|
1009
|
-
const dir =
|
|
1010
|
-
|
|
1011
|
-
const auth =
|
|
1012
|
-
const realAuth =
|
|
1013
|
-
if (!
|
|
1178
|
+
const dir = path6.join(lobstahHome(), "codex-home");
|
|
1179
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
1180
|
+
const auth = path6.join(dir, "auth.json");
|
|
1181
|
+
const realAuth = path6.join(os3.homedir(), ".codex", "auth.json");
|
|
1182
|
+
if (!fs8.existsSync(auth) && fs8.existsSync(realAuth)) {
|
|
1014
1183
|
try {
|
|
1015
|
-
|
|
1184
|
+
fs8.symlinkSync(realAuth, auth);
|
|
1016
1185
|
} catch {
|
|
1017
|
-
|
|
1186
|
+
fs8.copyFileSync(realAuth, auth);
|
|
1018
1187
|
}
|
|
1019
1188
|
}
|
|
1020
|
-
const cfg =
|
|
1021
|
-
if (!
|
|
1022
|
-
|
|
1189
|
+
const cfg = path6.join(dir, "config.toml");
|
|
1190
|
+
if (!fs8.existsSync(cfg)) {
|
|
1191
|
+
fs8.writeFileSync(cfg, "# lobstah-managed: auth inherited from ~/.codex, interactive config not\n");
|
|
1023
1192
|
}
|
|
1024
1193
|
return dir;
|
|
1025
1194
|
}
|
|
1195
|
+
function pumpCodexEvent(ev, push, onSession) {
|
|
1196
|
+
const at = now();
|
|
1197
|
+
if (ev.type === "thread.started") {
|
|
1198
|
+
if (ev.thread_id)
|
|
1199
|
+
onSession(ev.thread_id);
|
|
1200
|
+
push({ at, type: "session", data: { sessionId: ev.thread_id } });
|
|
1201
|
+
} else if (ev.type === "turn.started") {
|
|
1202
|
+
push({ at, type: "turn-start", data: {} });
|
|
1203
|
+
} else if (ev.type === "item.started") {
|
|
1204
|
+
push({ at, type: "tool-start", data: { name: ev.item?.type } });
|
|
1205
|
+
} else if (ev.type === "item.completed") {
|
|
1206
|
+
if (ev.item?.type === "agent_message" && ev.item?.text) {
|
|
1207
|
+
push({ at, type: "text", data: { text: String(ev.item.text).slice(0, 2e3) } });
|
|
1208
|
+
} else {
|
|
1209
|
+
push({ at, type: "tool-end", data: { name: ev.item?.type } });
|
|
1210
|
+
}
|
|
1211
|
+
} else if (ev.type === "turn.failed") {
|
|
1212
|
+
const message = ev.error?.message ?? "turn failed";
|
|
1213
|
+
push({ at, type: "error", data: { message } });
|
|
1214
|
+
return message;
|
|
1215
|
+
}
|
|
1216
|
+
return void 0;
|
|
1217
|
+
}
|
|
1218
|
+
function codexExecArgs(opts, prompt, resumeThread) {
|
|
1219
|
+
const args = ["exec"];
|
|
1220
|
+
if (resumeThread)
|
|
1221
|
+
args.push("resume", resumeThread);
|
|
1222
|
+
args.push("--json", "--skip-git-repo-check", "--sandbox", "workspace-write", "--cd", opts.cwd);
|
|
1223
|
+
if (opts.model)
|
|
1224
|
+
args.push("-m", opts.model);
|
|
1225
|
+
if (opts.effort)
|
|
1226
|
+
args.push("-c", `model_reasoning_effort=${JSON.stringify(opts.effort)}`);
|
|
1227
|
+
args.push(prompt);
|
|
1228
|
+
return args;
|
|
1229
|
+
}
|
|
1230
|
+
function startCodexCli(invoke, opts) {
|
|
1231
|
+
const input = new InputGate();
|
|
1232
|
+
const events = new AsyncQueue();
|
|
1233
|
+
let sessionId;
|
|
1234
|
+
let current;
|
|
1235
|
+
let killed = false;
|
|
1236
|
+
function runTurn(prompt, resumeThread) {
|
|
1237
|
+
return new Promise((resolve2) => {
|
|
1238
|
+
const inv = invoke(codexExecArgs(opts, prompt, resumeThread));
|
|
1239
|
+
const child = spawn2(inv.file, inv.argv, {
|
|
1240
|
+
cwd: opts.cwd,
|
|
1241
|
+
env: { ...process.env, ...opts.env, CODEX_HOME: isolatedCodexHome() },
|
|
1242
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1243
|
+
shell: process.platform === "win32" && inv.file !== process.execPath
|
|
1244
|
+
});
|
|
1245
|
+
current = child;
|
|
1246
|
+
let turnError;
|
|
1247
|
+
let stderrTail = "";
|
|
1248
|
+
child.stderr?.on("data", (d) => {
|
|
1249
|
+
stderrTail = (stderrTail + d.toString()).slice(-2e3);
|
|
1250
|
+
});
|
|
1251
|
+
let buf = "";
|
|
1252
|
+
child.stdout?.on("data", (d) => {
|
|
1253
|
+
buf += d.toString();
|
|
1254
|
+
let i;
|
|
1255
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
1256
|
+
const line = buf.slice(0, i).trim();
|
|
1257
|
+
buf = buf.slice(i + 1);
|
|
1258
|
+
if (!line)
|
|
1259
|
+
continue;
|
|
1260
|
+
try {
|
|
1261
|
+
const failed = pumpCodexEvent(JSON.parse(line), (e) => events.push(e), (id) => sessionId = id);
|
|
1262
|
+
if (failed)
|
|
1263
|
+
turnError = failed;
|
|
1264
|
+
} catch {
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
child.on("error", (err) => resolve2(err.message));
|
|
1269
|
+
child.on("exit", (code) => {
|
|
1270
|
+
if (turnError)
|
|
1271
|
+
return resolve2(turnError);
|
|
1272
|
+
if (code === 0 || killed)
|
|
1273
|
+
return resolve2(void 0);
|
|
1274
|
+
const detail = stderrTail.trim().split("\n").at(-1);
|
|
1275
|
+
resolve2(`codex exited ${code}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
1276
|
+
});
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
const done = (async () => {
|
|
1280
|
+
try {
|
|
1281
|
+
let next = opts.prompt;
|
|
1282
|
+
let thread = opts.resumeSession;
|
|
1283
|
+
while (next !== void 0 && !killed) {
|
|
1284
|
+
const error = await runTurn(next, thread);
|
|
1285
|
+
thread = sessionId ?? thread;
|
|
1286
|
+
if (error) {
|
|
1287
|
+
events.push({ at: now(), type: "error", data: { message: error } });
|
|
1288
|
+
return { sessionId, error };
|
|
1289
|
+
}
|
|
1290
|
+
events.push({ at: now(), type: "turn-end", data: {} });
|
|
1291
|
+
next = await input.next();
|
|
1292
|
+
}
|
|
1293
|
+
return { sessionId };
|
|
1294
|
+
} finally {
|
|
1295
|
+
events.close();
|
|
1296
|
+
}
|
|
1297
|
+
})();
|
|
1298
|
+
return {
|
|
1299
|
+
events,
|
|
1300
|
+
send: (text) => input.send(text),
|
|
1301
|
+
end: () => input.end(),
|
|
1302
|
+
done,
|
|
1303
|
+
kill: () => {
|
|
1304
|
+
killed = true;
|
|
1305
|
+
try {
|
|
1306
|
+
current?.kill("SIGKILL");
|
|
1307
|
+
} catch {
|
|
1308
|
+
}
|
|
1309
|
+
input.end();
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1026
1313
|
function createCodexAdapter() {
|
|
1027
1314
|
return {
|
|
1028
1315
|
name: "codex",
|
|
1029
1316
|
async start(opts) {
|
|
1030
|
-
|
|
1317
|
+
let mod;
|
|
1318
|
+
try {
|
|
1319
|
+
mod = await import("@openai/codex-sdk");
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
if (codexInvocation([]))
|
|
1322
|
+
return startCodexCli((argv) => codexInvocation(argv), opts);
|
|
1323
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1324
|
+
throw new Error(`codex adapter: SDK unavailable (${detail}) and no codex CLI found (PATH or vendored)`);
|
|
1325
|
+
}
|
|
1031
1326
|
const codex = new mod.Codex({
|
|
1032
1327
|
env: { ...process.env, ...opts.env, CODEX_HOME: isolatedCodexHome() }
|
|
1033
1328
|
});
|
|
@@ -1050,24 +1345,9 @@ function createCodexAdapter() {
|
|
|
1050
1345
|
while (next !== void 0) {
|
|
1051
1346
|
const streamed = await thread.runStreamed(next);
|
|
1052
1347
|
for await (const ev of streamed.events) {
|
|
1053
|
-
const
|
|
1054
|
-
if (
|
|
1055
|
-
|
|
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
|
-
}
|
|
1348
|
+
const failed = pumpCodexEvent(ev, (e) => events.push(e), (id) => sessionId = id);
|
|
1349
|
+
if (failed)
|
|
1350
|
+
lastTurnError = failed;
|
|
1071
1351
|
}
|
|
1072
1352
|
events.push({ at: now(), type: "turn-end", data: {} });
|
|
1073
1353
|
next = await input.next();
|
|
@@ -1105,14 +1385,14 @@ function loadAdapter(name) {
|
|
|
1105
1385
|
}
|
|
1106
1386
|
|
|
1107
1387
|
// packages/worktree/dist/index.js
|
|
1108
|
-
import * as
|
|
1109
|
-
import * as
|
|
1388
|
+
import * as fs9 from "node:fs";
|
|
1389
|
+
import * as path7 from "node:path";
|
|
1110
1390
|
import { exec, execFile } from "node:child_process";
|
|
1111
1391
|
import { promisify } from "node:util";
|
|
1112
1392
|
var run = promisify(execFile);
|
|
1113
1393
|
var shell = promisify(exec);
|
|
1114
1394
|
function worktreePath(id) {
|
|
1115
|
-
return
|
|
1395
|
+
return path7.join(lobstahHome(), "worktrees", id);
|
|
1116
1396
|
}
|
|
1117
1397
|
async function git(cwd, ...args) {
|
|
1118
1398
|
const { stdout } = await run("git", args, { cwd, env: process.env });
|
|
@@ -1120,10 +1400,10 @@ async function git(cwd, ...args) {
|
|
|
1120
1400
|
}
|
|
1121
1401
|
async function allocate(repo, id) {
|
|
1122
1402
|
const dir = worktreePath(id);
|
|
1123
|
-
if (
|
|
1403
|
+
if (fs9.existsSync(dir)) {
|
|
1124
1404
|
throw new Error(`worktree for ${id} already exists at ${dir} \u2014 never allocate a second`);
|
|
1125
1405
|
}
|
|
1126
|
-
if (!
|
|
1406
|
+
if (!fs9.existsSync(repo.path)) {
|
|
1127
1407
|
if (!repo.origin)
|
|
1128
1408
|
throw new Error(`repo path ${repo.path} missing and no origin configured`);
|
|
1129
1409
|
await run("git", ["clone", repo.origin, repo.path], { env: process.env });
|
|
@@ -1167,40 +1447,40 @@ function followUpSession(followUp) {
|
|
|
1167
1447
|
return void 0;
|
|
1168
1448
|
}
|
|
1169
1449
|
async function main(activeDir, lane) {
|
|
1170
|
-
const id =
|
|
1450
|
+
const id = path8.basename(activeDir);
|
|
1171
1451
|
const status = (verb, note) => appendStatus(id, lane, verb, note);
|
|
1172
1452
|
const cfg = loadConfig();
|
|
1173
|
-
const descriptor = JSON.parse(
|
|
1453
|
+
const descriptor = JSON.parse(fs10.readFileSync(path8.join(activeDir, "descriptor.json"), "utf8"));
|
|
1174
1454
|
const resolved = resolveDispatch(descriptor, cfg);
|
|
1175
1455
|
const repo = cfg.repos[descriptor.repo];
|
|
1176
|
-
const briefFile =
|
|
1177
|
-
if (!
|
|
1178
|
-
const brief =
|
|
1456
|
+
const briefFile = path8.join(activeDir, "brief.md");
|
|
1457
|
+
if (!fs10.existsSync(briefFile)) fs10.writeFileSync(briefFile, descriptor.brief);
|
|
1458
|
+
const brief = fs10.readFileSync(briefFile, "utf8");
|
|
1179
1459
|
const attempts = Number(process.env.LOBSTAH_ATTEMPTS ?? "1");
|
|
1180
1460
|
const runnerInfo = {
|
|
1181
1461
|
pid: process.pid,
|
|
1182
1462
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1183
1463
|
attempts
|
|
1184
1464
|
};
|
|
1185
|
-
|
|
1465
|
+
fs10.writeFileSync(path8.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
|
|
1186
1466
|
status("working", attempts > 1 ? `attempt ${attempts}` : void 0);
|
|
1187
1467
|
appendEvent(id, lane, { at: (/* @__PURE__ */ new Date()).toISOString(), type: "runner", data: { pid: process.pid, attempts } });
|
|
1188
|
-
const wtFile =
|
|
1468
|
+
const wtFile = path8.join(activeDir, "worktree.json");
|
|
1189
1469
|
let cwd;
|
|
1190
|
-
if (
|
|
1191
|
-
cwd = JSON.parse(
|
|
1192
|
-
if (!
|
|
1193
|
-
} else if (
|
|
1470
|
+
if (fs10.existsSync(wtFile)) {
|
|
1471
|
+
cwd = JSON.parse(fs10.readFileSync(wtFile, "utf8")).path;
|
|
1472
|
+
if (!fs10.existsSync(cwd)) throw new Error(`recorded worktree ${cwd} is gone`);
|
|
1473
|
+
} else if (fs10.existsSync(worktreePath(id))) {
|
|
1194
1474
|
throw new Error(`unrecorded worktree already exists for ${id} \u2014 refusing to proceed`);
|
|
1195
1475
|
} else {
|
|
1196
1476
|
cwd = await allocate(repo, id);
|
|
1197
|
-
|
|
1477
|
+
fs10.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
|
|
1198
1478
|
}
|
|
1199
1479
|
const resumeSession = process.env.LOBSTAH_RESUME ?? (descriptor.followUp ? followUpSession(descriptor.followUp) : void 0);
|
|
1200
1480
|
const adapter = loadAdapter(resolved.harness);
|
|
1201
1481
|
const prompt = buildPrompt(brief, { id, nudge: process.env.LOBSTAH_NUDGE });
|
|
1202
|
-
const binDir =
|
|
1203
|
-
const workerPath =
|
|
1482
|
+
const binDir = path8.resolve(path8.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "bin");
|
|
1483
|
+
const workerPath = fs10.existsSync(path8.join(binDir, "lobstah")) ? `${binDir}${path8.delimiter}${process.env.PATH ?? ""}` : void 0;
|
|
1204
1484
|
const run2 = await adapter.start({
|
|
1205
1485
|
id,
|
|
1206
1486
|
cwd,
|
|
@@ -1268,21 +1548,23 @@ async function main(activeDir, lane) {
|
|
|
1268
1548
|
}
|
|
1269
1549
|
complete(id, lane);
|
|
1270
1550
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
const id = path7.basename(activeDirArg);
|
|
1551
|
+
function runRunner(activeDirArg2, laneArg2) {
|
|
1552
|
+
const lane = laneArg2 === "chore" ? "chore" : "work";
|
|
1553
|
+
main(activeDirArg2, lane).catch((err) => {
|
|
1554
|
+
const id = path8.basename(activeDirArg2);
|
|
1276
1555
|
try {
|
|
1277
1556
|
appendStatus(id, lane, "failed", String(err instanceof Error ? err.message : err).slice(0, 500));
|
|
1278
|
-
|
|
1557
|
+
fs10.renameSync(activeDirArg2, path8.join(laneDirs(lane).done, id));
|
|
1279
1558
|
} catch {
|
|
1280
1559
|
}
|
|
1281
1560
|
process.exitCode = 1;
|
|
1282
1561
|
});
|
|
1283
1562
|
}
|
|
1563
|
+
var [activeDirArg, laneArg] = process.argv.slice(2);
|
|
1564
|
+
if (activeDirArg && activeDirArg !== "__runner") runRunner(activeDirArg, laneArg);
|
|
1284
1565
|
export {
|
|
1285
|
-
main
|
|
1566
|
+
main,
|
|
1567
|
+
runRunner
|
|
1286
1568
|
};
|
|
1287
1569
|
/*! Bundled license information:
|
|
1288
1570
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|