lobstah 0.5.0 → 0.5.7
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 +5 -2
- package/dist/main.js +3507 -1117
- package/dist/runner.js +416 -102
- package/docs/configuration.md +2 -0
- package/docs/design.md +21 -1
- package/docs/lobsterman.md +91 -87
- package/docs/vocabulary.md +128 -5
- package/package.json +1 -1
package/dist/runner.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// packages/runner/src/index.ts
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
2
|
+
import * as fs13 from "node:fs";
|
|
3
|
+
import * as path11 from "node:path";
|
|
5
4
|
|
|
6
5
|
// packages/core/dist/types.js
|
|
7
6
|
var VERBS = ["working", "needs-decision", "blocked", "paused", "done", "failed"];
|
|
@@ -26,6 +25,9 @@ function laneDirs(lane) {
|
|
|
26
25
|
inbox: path.join(root, "inbox")
|
|
27
26
|
};
|
|
28
27
|
}
|
|
28
|
+
function soakingDir() {
|
|
29
|
+
return path.join(lobstahHome(), "soaking");
|
|
30
|
+
}
|
|
29
31
|
function statusPath(id, lane) {
|
|
30
32
|
return path.join(laneDirs(lane).state, `${id}.status`);
|
|
31
33
|
}
|
|
@@ -36,6 +38,17 @@ function evidencePath(id, lane) {
|
|
|
36
38
|
return path.join(laneDirs(lane).state, `${id}.evidence`);
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
// packages/core/dist/attachments.js
|
|
42
|
+
function attachmentBlock(attachments) {
|
|
43
|
+
if (attachments.length === 0)
|
|
44
|
+
return "";
|
|
45
|
+
return [
|
|
46
|
+
"--- ATTACHMENTS ---",
|
|
47
|
+
...attachments.map((a) => `${a.name} (${a.type}, ${a.bytes} bytes) at ${a.path}`),
|
|
48
|
+
"Images and PDFs can be read directly with your file tools."
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
// packages/core/dist/queue.js
|
|
40
53
|
import * as fs2 from "node:fs";
|
|
41
54
|
import * as path2 from "node:path";
|
|
@@ -69,6 +82,20 @@ function readStatusLog(id, lane) {
|
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
// packages/core/dist/queue.js
|
|
85
|
+
function storedDescriptor(id, lane) {
|
|
86
|
+
const dirs = laneDirs(lane);
|
|
87
|
+
for (const file of [
|
|
88
|
+
path2.join(dirs.queue, `${id}.json`),
|
|
89
|
+
path2.join(dirs.active, id, "descriptor.json"),
|
|
90
|
+
path2.join(dirs.done, id, "descriptor.json")
|
|
91
|
+
]) {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return void 0;
|
|
98
|
+
}
|
|
72
99
|
function complete(id, lane) {
|
|
73
100
|
const dirs = laneDirs(lane);
|
|
74
101
|
fs2.renameSync(path2.join(dirs.active, id), path2.join(dirs.done, id));
|
|
@@ -83,6 +110,15 @@ function appendEvent(id, lane, ev) {
|
|
|
83
110
|
fs3.appendFileSync(eventsPath(id, lane), `${JSON.stringify(ev)}
|
|
84
111
|
`);
|
|
85
112
|
}
|
|
113
|
+
function touchEvents(id, lane, at = /* @__PURE__ */ new Date()) {
|
|
114
|
+
const file = eventsPath(id, lane);
|
|
115
|
+
try {
|
|
116
|
+
fs3.utimesSync(file, at, at);
|
|
117
|
+
} catch {
|
|
118
|
+
fs3.appendFileSync(file, "");
|
|
119
|
+
fs3.utimesSync(file, at, at);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
86
122
|
|
|
87
123
|
// packages/core/dist/evidence.js
|
|
88
124
|
import * as fs4 from "node:fs";
|
|
@@ -103,6 +139,7 @@ function mergeEvidence(id, lane, patch) {
|
|
|
103
139
|
// packages/core/dist/inbox.js
|
|
104
140
|
import * as fs5 from "node:fs";
|
|
105
141
|
import * as path3 from "node:path";
|
|
142
|
+
var metaName = (file) => file.replace(/\.msg$/, ".meta.json");
|
|
106
143
|
function inboxDir(id, lane) {
|
|
107
144
|
return path3.join(laneDirs(lane).inbox, id);
|
|
108
145
|
}
|
|
@@ -116,6 +153,10 @@ function acknowledge(id, lane, file) {
|
|
|
116
153
|
const dir = inboxDir(id, lane);
|
|
117
154
|
fs5.mkdirSync(path3.join(dir, "handled"), { recursive: true });
|
|
118
155
|
fs5.renameSync(path3.join(dir, file), path3.join(dir, "handled", file));
|
|
156
|
+
try {
|
|
157
|
+
fs5.renameSync(path3.join(dir, metaName(file)), path3.join(dir, "handled", metaName(file)));
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
119
160
|
}
|
|
120
161
|
|
|
121
162
|
// packages/core/dist/config.js
|
|
@@ -769,6 +810,21 @@ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
|
|
|
769
810
|
}
|
|
770
811
|
|
|
771
812
|
// packages/core/dist/config.js
|
|
813
|
+
var ATTENTION_KINDS = ["question", "landed", "pr:draft", "pr:review", "pr:checks", "pr:conflict", "pr:ready"];
|
|
814
|
+
var DEFAULT_ATTENTION_KINDS = ["question", "pr:draft", "pr:review", "pr:checks", "pr:conflict", "pr:ready"];
|
|
815
|
+
function parseAttentionKinds(raw) {
|
|
816
|
+
if (raw === void 0)
|
|
817
|
+
return [...DEFAULT_ATTENTION_KINDS];
|
|
818
|
+
if (!Array.isArray(raw)) {
|
|
819
|
+
throw new Error(`attentionKinds must be an array of kinds (${ATTENTION_KINDS.join(", ")}) in ${configPath()}`);
|
|
820
|
+
}
|
|
821
|
+
for (const k of raw) {
|
|
822
|
+
if (!ATTENTION_KINDS.includes(k)) {
|
|
823
|
+
throw new Error(`attentionKinds: unknown kind "${String(k)}" in ${configPath()} \u2014 valid kinds: ${ATTENTION_KINDS.join(", ")}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return [...new Set(raw)];
|
|
827
|
+
}
|
|
772
828
|
var DEFAULT_SOAK = {
|
|
773
829
|
deferSecs: 90,
|
|
774
830
|
ttlSecs: 1800
|
|
@@ -783,7 +839,8 @@ var DEFAULT_LIMITS = {
|
|
|
783
839
|
wedgeThresholdSecs: 600,
|
|
784
840
|
maxRestartAttempts: 2,
|
|
785
841
|
wallClockSecs: 3600,
|
|
786
|
-
choreRetentionDays: 7
|
|
842
|
+
choreRetentionDays: 7,
|
|
843
|
+
attachmentMaxBytes: 25 * 1024 * 1024
|
|
787
844
|
};
|
|
788
845
|
function configPath() {
|
|
789
846
|
return path4.join(lobstahHome(), "config.toml");
|
|
@@ -821,7 +878,8 @@ function loadConfig() {
|
|
|
821
878
|
grounds,
|
|
822
879
|
notifyCommand: raw.notifyCommand ? String(raw.notifyCommand) : void 0,
|
|
823
880
|
notifyVerbs: Array.isArray(raw.notifyVerbs) ? raw.notifyVerbs.map(String) : void 0,
|
|
824
|
-
remindSecs: raw.remindSecs !== void 0 ? Number(raw.remindSecs) : void 0
|
|
881
|
+
remindSecs: raw.remindSecs !== void 0 ? Number(raw.remindSecs) : void 0,
|
|
882
|
+
attentionKinds: parseAttentionKinds(raw.attentionKinds)
|
|
825
883
|
};
|
|
826
884
|
}
|
|
827
885
|
function resolveDispatch(d, cfg) {
|
|
@@ -886,12 +944,127 @@ function codexInvocation(argv) {
|
|
|
886
944
|
return void 0;
|
|
887
945
|
}
|
|
888
946
|
|
|
947
|
+
// packages/core/dist/soak.js
|
|
948
|
+
import * as fs8 from "node:fs";
|
|
949
|
+
import * as path6 from "node:path";
|
|
950
|
+
function regPath(trapId) {
|
|
951
|
+
return path6.join(soakingDir(), `${trapId}.json`);
|
|
952
|
+
}
|
|
953
|
+
function readTrap(trapId) {
|
|
954
|
+
try {
|
|
955
|
+
return JSON.parse(fs8.readFileSync(regPath(trapId), "utf8"));
|
|
956
|
+
} catch {
|
|
957
|
+
return void 0;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
889
961
|
// packages/core/dist/compiled.js
|
|
890
962
|
var COMPILED_BINARY = (() => {
|
|
891
963
|
const bun = globalThis.Bun;
|
|
892
964
|
return bun !== void 0 && String(bun.main ?? "").includes("$bunfs");
|
|
893
965
|
})();
|
|
894
966
|
|
|
967
|
+
// packages/core/dist/resume.js
|
|
968
|
+
import * as fs9 from "node:fs";
|
|
969
|
+
import * as path7 from "node:path";
|
|
970
|
+
import { spawnSync } from "node:child_process";
|
|
971
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-([0-9a-f])[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
972
|
+
function harnessFromSessionId(sessionId) {
|
|
973
|
+
const version = sessionId ? UUID.exec(sessionId)?.[1] : void 0;
|
|
974
|
+
return version === "7" ? "codex" : version === "4" ? "claude" : void 0;
|
|
975
|
+
}
|
|
976
|
+
function claimOf(id, lane) {
|
|
977
|
+
const dirs = laneDirs(lane);
|
|
978
|
+
for (const dir of [dirs.active, dirs.done]) {
|
|
979
|
+
try {
|
|
980
|
+
return JSON.parse(fs9.readFileSync(path7.join(dir, id, "claim.json"), "utf8"));
|
|
981
|
+
} catch {
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return void 0;
|
|
985
|
+
}
|
|
986
|
+
function laneOf(id) {
|
|
987
|
+
for (const lane of ["work", "chore"]) {
|
|
988
|
+
if (storedDescriptor(id, lane) || readEvidence(id, lane).sessionId)
|
|
989
|
+
return lane;
|
|
990
|
+
}
|
|
991
|
+
return void 0;
|
|
992
|
+
}
|
|
993
|
+
function resolveSessionHarness(id, cfg, laneHint) {
|
|
994
|
+
const lane = laneHint ?? laneOf(id);
|
|
995
|
+
if (!lane)
|
|
996
|
+
return {};
|
|
997
|
+
const ev = readEvidence(id, lane);
|
|
998
|
+
const sessionId = ev.sessionId;
|
|
999
|
+
const out = (harness, source) => ({
|
|
1000
|
+
sessionId,
|
|
1001
|
+
harness,
|
|
1002
|
+
source,
|
|
1003
|
+
lane
|
|
1004
|
+
});
|
|
1005
|
+
if (ev.harness)
|
|
1006
|
+
return out(ev.harness, "evidence");
|
|
1007
|
+
const claim = claimOf(id, lane);
|
|
1008
|
+
if (claim?.harness)
|
|
1009
|
+
return out(claim.harness, "trap");
|
|
1010
|
+
if (ev.deliveredTo?.startsWith("wt:")) {
|
|
1011
|
+
const reg = readTrap(ev.deliveredTo.slice("wt:".length));
|
|
1012
|
+
if (reg?.harness)
|
|
1013
|
+
return out(reg.harness, "trap");
|
|
1014
|
+
}
|
|
1015
|
+
const byId = harnessFromSessionId(sessionId);
|
|
1016
|
+
if (byId)
|
|
1017
|
+
return out(byId, "session-id");
|
|
1018
|
+
const d = storedDescriptor(id, lane);
|
|
1019
|
+
const repo = d && cfg?.repos[d.repo];
|
|
1020
|
+
const fromDescriptor = d?.harness ?? repo?.harness?.default ?? cfg?.harness.default;
|
|
1021
|
+
if (fromDescriptor)
|
|
1022
|
+
return out(fromDescriptor, "descriptor");
|
|
1023
|
+
return { sessionId, lane };
|
|
1024
|
+
}
|
|
1025
|
+
var UNRESUMABLE = /no conversation found|no rollout found|(session|thread|conversation)( id)?[^.\n]{0,80}\b(not found|does not exist|unknown|invalid)|(could not|cannot|failed to|unable to) (find|load|resume)[^.\n]{0,40}\b(session|thread|conversation)/i;
|
|
1026
|
+
function isUnresumable(error) {
|
|
1027
|
+
return error !== void 0 && UNRESUMABLE.test(error);
|
|
1028
|
+
}
|
|
1029
|
+
function worktreeProgress(worktree, trunk) {
|
|
1030
|
+
if (!worktree || !fs9.existsSync(worktree))
|
|
1031
|
+
return "No worktree progress recorded.";
|
|
1032
|
+
const git2 = (...a) => spawnSync("git", a, { cwd: worktree, encoding: "utf8" }).stdout?.trim() ?? "";
|
|
1033
|
+
const commits = trunk ? git2("log", "--oneline", `origin/${trunk}..HEAD`) : git2("log", "--oneline", "-15");
|
|
1034
|
+
const status = git2("status", "--short");
|
|
1035
|
+
return `Commits so far:
|
|
1036
|
+
${commits || "(none)"}
|
|
1037
|
+
|
|
1038
|
+
Uncommitted changes:
|
|
1039
|
+
${status || "(clean)"}`;
|
|
1040
|
+
}
|
|
1041
|
+
function originProgress(originId, lane) {
|
|
1042
|
+
const ev = readEvidence(originId, lane);
|
|
1043
|
+
const last = readStatusLog(originId, lane).at(-1);
|
|
1044
|
+
const lines = [`The earlier dispatch ${originId} left:`];
|
|
1045
|
+
if (ev.branch)
|
|
1046
|
+
lines.push(`- branch: ${ev.branch}`);
|
|
1047
|
+
if (ev.prUrl)
|
|
1048
|
+
lines.push(`- PR: ${ev.prUrl}`);
|
|
1049
|
+
if (last)
|
|
1050
|
+
lines.push(`- last status: ${last.verb}${last.note ? ` \u2014 ${last.note}` : ""}`);
|
|
1051
|
+
if (ev.commits?.length)
|
|
1052
|
+
lines.push("- commits:", ...ev.commits.map((c) => ` ${c}`));
|
|
1053
|
+
if (lines.length === 1)
|
|
1054
|
+
lines.push("- (no recorded branch, PR, or commits)");
|
|
1055
|
+
return lines.join("\n");
|
|
1056
|
+
}
|
|
1057
|
+
function handoffNote(fromHarness, progress, why) {
|
|
1058
|
+
return `You are taking over this dispatch from a previous agent session (harness: ${fromHarness}). Its conversation is not available${why ? ` (${why})` : ""} \u2014 the state below is the ground truth. Review it, then continue the brief from where it stops.
|
|
1059
|
+
|
|
1060
|
+
${progress}`;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// packages/runner/src/run.ts
|
|
1064
|
+
import * as fs12 from "node:fs";
|
|
1065
|
+
import * as path10 from "node:path";
|
|
1066
|
+
import { fileURLToPath } from "node:url";
|
|
1067
|
+
|
|
895
1068
|
// packages/adapters/dist/claude.js
|
|
896
1069
|
import { spawn } from "node:child_process";
|
|
897
1070
|
|
|
@@ -1170,25 +1343,25 @@ function createClaudeAdapter() {
|
|
|
1170
1343
|
}
|
|
1171
1344
|
|
|
1172
1345
|
// packages/adapters/dist/codex.js
|
|
1173
|
-
import * as
|
|
1346
|
+
import * as fs10 from "node:fs";
|
|
1174
1347
|
import * as os3 from "node:os";
|
|
1175
|
-
import * as
|
|
1348
|
+
import * as path8 from "node:path";
|
|
1176
1349
|
import { spawn as spawn2 } from "node:child_process";
|
|
1177
1350
|
function isolatedCodexHome() {
|
|
1178
|
-
const dir =
|
|
1179
|
-
|
|
1180
|
-
const auth =
|
|
1181
|
-
const realAuth =
|
|
1182
|
-
if (!
|
|
1351
|
+
const dir = path8.join(lobstahHome(), "codex-home");
|
|
1352
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
1353
|
+
const auth = path8.join(dir, "auth.json");
|
|
1354
|
+
const realAuth = path8.join(os3.homedir(), ".codex", "auth.json");
|
|
1355
|
+
if (!fs10.existsSync(auth) && fs10.existsSync(realAuth)) {
|
|
1183
1356
|
try {
|
|
1184
|
-
|
|
1357
|
+
fs10.symlinkSync(realAuth, auth);
|
|
1185
1358
|
} catch {
|
|
1186
|
-
|
|
1359
|
+
fs10.copyFileSync(realAuth, auth);
|
|
1187
1360
|
}
|
|
1188
1361
|
}
|
|
1189
|
-
const cfg =
|
|
1190
|
-
if (!
|
|
1191
|
-
|
|
1362
|
+
const cfg = path8.join(dir, "config.toml");
|
|
1363
|
+
if (!fs10.existsSync(cfg)) {
|
|
1364
|
+
fs10.writeFileSync(cfg, "# lobstah-managed: auth inherited from ~/.codex, interactive config not\n");
|
|
1192
1365
|
}
|
|
1193
1366
|
return dir;
|
|
1194
1367
|
}
|
|
@@ -1385,14 +1558,14 @@ function loadAdapter(name) {
|
|
|
1385
1558
|
}
|
|
1386
1559
|
|
|
1387
1560
|
// packages/worktree/dist/index.js
|
|
1388
|
-
import * as
|
|
1389
|
-
import * as
|
|
1561
|
+
import * as fs11 from "node:fs";
|
|
1562
|
+
import * as path9 from "node:path";
|
|
1390
1563
|
import { exec, execFile } from "node:child_process";
|
|
1391
1564
|
import { promisify } from "node:util";
|
|
1392
1565
|
var run = promisify(execFile);
|
|
1393
1566
|
var shell = promisify(exec);
|
|
1394
1567
|
function worktreePath(id) {
|
|
1395
|
-
return
|
|
1568
|
+
return path9.join(lobstahHome(), "worktrees", id);
|
|
1396
1569
|
}
|
|
1397
1570
|
async function git(cwd, ...args) {
|
|
1398
1571
|
const { stdout } = await run("git", args, { cwd, env: process.env });
|
|
@@ -1400,10 +1573,10 @@ async function git(cwd, ...args) {
|
|
|
1400
1573
|
}
|
|
1401
1574
|
async function allocate(repo, id) {
|
|
1402
1575
|
const dir = worktreePath(id);
|
|
1403
|
-
if (
|
|
1576
|
+
if (fs11.existsSync(dir)) {
|
|
1404
1577
|
throw new Error(`worktree for ${id} already exists at ${dir} \u2014 never allocate a second`);
|
|
1405
1578
|
}
|
|
1406
|
-
if (!
|
|
1579
|
+
if (!fs11.existsSync(repo.path)) {
|
|
1407
1580
|
if (!repo.origin)
|
|
1408
1581
|
throw new Error(`repo path ${repo.path} missing and no origin configured`);
|
|
1409
1582
|
await run("git", ["clone", repo.origin, repo.path], { env: process.env });
|
|
@@ -1434,100 +1607,247 @@ function buildPrompt(brief, opts) {
|
|
|
1434
1607
|
`--- BRIEF ---`,
|
|
1435
1608
|
brief
|
|
1436
1609
|
];
|
|
1610
|
+
if (opts.attachments?.length) parts.push(attachmentBlock(opts.attachments));
|
|
1437
1611
|
if (opts.nudge) parts.push(`--- SUPERVISOR NOTE ---`, opts.nudge);
|
|
1438
1612
|
return parts.join("\n\n");
|
|
1439
1613
|
}
|
|
1440
1614
|
|
|
1441
|
-
// packages/runner/src/
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1615
|
+
// packages/runner/src/drive.ts
|
|
1616
|
+
var WAITING_VERBS = ["needs-decision", "blocked", "paused"];
|
|
1617
|
+
var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1618
|
+
function deliver(run2, id, lane) {
|
|
1619
|
+
const msgs = unhandled(id, lane);
|
|
1620
|
+
for (const m of msgs) {
|
|
1621
|
+
run2.send(m.text);
|
|
1622
|
+
acknowledge(id, lane, m.file);
|
|
1446
1623
|
}
|
|
1447
|
-
return
|
|
1624
|
+
return msgs.length;
|
|
1625
|
+
}
|
|
1626
|
+
async function drive(run2, opts) {
|
|
1627
|
+
const { id, lane, pollMs = 3e3, stopped = () => false } = opts;
|
|
1628
|
+
let cancelled = false;
|
|
1629
|
+
let activity = 0;
|
|
1630
|
+
let finished = false;
|
|
1631
|
+
void run2.done.then(() => finished = true);
|
|
1632
|
+
const cancel = () => {
|
|
1633
|
+
cancelled = true;
|
|
1634
|
+
run2.kill();
|
|
1635
|
+
};
|
|
1636
|
+
const answered = (verb) => {
|
|
1637
|
+
appendStatus(id, lane, "working", "operator message delivered");
|
|
1638
|
+
appendEvent(id, lane, { at: (/* @__PURE__ */ new Date()).toISOString(), type: "runner", data: { resumed: verb } });
|
|
1639
|
+
};
|
|
1640
|
+
const awaitAnswer = async (verb) => {
|
|
1641
|
+
appendEvent(id, lane, { at: (/* @__PURE__ */ new Date()).toISOString(), type: "runner", data: { waiting: verb } });
|
|
1642
|
+
while (true) {
|
|
1643
|
+
if (stopped() || finished) return;
|
|
1644
|
+
if (cancelRequested(id, lane)) return cancel();
|
|
1645
|
+
if (deliver(run2, id, lane) > 0) return answered(verb);
|
|
1646
|
+
touchEvents(id, lane);
|
|
1647
|
+
await sleep(pollMs);
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
for await (const ev of run2.events) {
|
|
1651
|
+
appendEvent(id, lane, ev);
|
|
1652
|
+
if (ev.type === "tool-start" || ev.type === "text") activity++;
|
|
1653
|
+
if (ev.type === "session" && ev.data?.sessionId) {
|
|
1654
|
+
mergeEvidence(id, lane, { sessionId: String(ev.data.sessionId) });
|
|
1655
|
+
}
|
|
1656
|
+
if (ev.type !== "turn-end" || cancelled || stopped()) continue;
|
|
1657
|
+
if (cancelRequested(id, lane)) {
|
|
1658
|
+
cancel();
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
const lastVerb = readStatusLog(id, lane).at(-1)?.verb;
|
|
1662
|
+
const waiting = lastVerb !== void 0 && WAITING_VERBS.includes(lastVerb);
|
|
1663
|
+
if (deliver(run2, id, lane) > 0) {
|
|
1664
|
+
if (waiting) answered(lastVerb);
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
if (waiting) {
|
|
1668
|
+
await awaitAnswer(lastVerb);
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
run2.end();
|
|
1672
|
+
}
|
|
1673
|
+
return { cancelled, activity };
|
|
1674
|
+
}
|
|
1675
|
+
function settle(id, lane, r) {
|
|
1676
|
+
const lastVerb = readStatusLog(id, lane).at(-1)?.verb;
|
|
1677
|
+
if (r.cancelled) appendStatus(id, lane, "failed", "cancelled by operator");
|
|
1678
|
+
else if (r.wallClockHit) appendStatus(id, lane, "failed", "wall-clock limit exceeded");
|
|
1679
|
+
else if (r.error) appendStatus(id, lane, "failed", r.error.slice(0, 500));
|
|
1680
|
+
else if (!lastVerb || !TERMINAL_VERBS.includes(lastVerb)) appendStatus(id, lane, "done");
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// packages/runner/src/plan.ts
|
|
1684
|
+
var short = (s) => s.slice(0, 8);
|
|
1685
|
+
function askedHarness(id, lane, cfg) {
|
|
1686
|
+
const d = storedDescriptor(id, lane);
|
|
1687
|
+
return d?.harness ?? (d ? cfg.repos[d.repo]?.harness?.default : void 0) ?? cfg.harness.default ?? "claude";
|
|
1688
|
+
}
|
|
1689
|
+
function planStart(input) {
|
|
1690
|
+
const { id, lane, descriptor, cfg, resolvedHarness, envResume } = input;
|
|
1691
|
+
if (envResume) {
|
|
1692
|
+
const own = resolveSessionHarness(id, cfg, lane);
|
|
1693
|
+
const harness = (own.sessionId === envResume ? own.harness : harnessFromSessionId(envResume)) ?? resolvedHarness;
|
|
1694
|
+
return {
|
|
1695
|
+
harness,
|
|
1696
|
+
resume: { sessionId: envResume, own: true },
|
|
1697
|
+
note: harness !== resolvedHarness ? `resuming own ${harness} session ${short(envResume)}` : void 0
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
const prior = readEvidence(id, lane).sessionId;
|
|
1701
|
+
if (prior) {
|
|
1702
|
+
const from = resolveSessionHarness(id, cfg, lane).harness;
|
|
1703
|
+
return {
|
|
1704
|
+
harness: resolvedHarness,
|
|
1705
|
+
cold: { why: `session ${short(prior)} is not resumed on a respawn`, fromHarness: from },
|
|
1706
|
+
note: `respawn: starting cold on ${resolvedHarness}${from ? ` (previous session: ${from})` : ""}`
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
if (!descriptor.followUp) return { harness: resolvedHarness };
|
|
1710
|
+
const originId = descriptor.followUp;
|
|
1711
|
+
const origin = resolveSessionHarness(originId, cfg);
|
|
1712
|
+
const originLane = origin.lane ?? lane;
|
|
1713
|
+
const originRef = { id: originId, lane: originLane };
|
|
1714
|
+
if (!origin.sessionId) {
|
|
1715
|
+
return {
|
|
1716
|
+
harness: resolvedHarness,
|
|
1717
|
+
cold: { why: `follow-up origin ${short(originId)} has no recorded session`, origin: originRef },
|
|
1718
|
+
note: `follow-up of ${short(originId)}: no session recorded \u2014 starting cold on ${resolvedHarness} with a progress note`
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
if (!origin.harness) {
|
|
1722
|
+
return {
|
|
1723
|
+
harness: resolvedHarness,
|
|
1724
|
+
resume: { sessionId: origin.sessionId, own: false, origin: originRef },
|
|
1725
|
+
note: `follow-up of ${short(originId)}: resuming session ${short(origin.sessionId)} on ${resolvedHarness} (origin harness unknown)`
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
const requested = descriptor.harness;
|
|
1729
|
+
const swap = requested !== void 0 && requested !== origin.harness && requested !== askedHarness(originId, originLane, cfg);
|
|
1730
|
+
if (swap) {
|
|
1731
|
+
return {
|
|
1732
|
+
harness: requested,
|
|
1733
|
+
cold: { why: `swap from ${origin.harness} to ${requested}`, fromHarness: origin.harness, origin: originRef },
|
|
1734
|
+
note: `follow-up of ${short(originId)}: swap \u2014 origin session is ${origin.harness}, ${requested} requested; starting cold on ${requested} with a progress note`
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
const ignored = requested !== void 0 && requested !== origin.harness ? ` (--harness ${requested} ignored for the resume)` : "";
|
|
1738
|
+
return {
|
|
1739
|
+
harness: origin.harness,
|
|
1740
|
+
resume: { sessionId: origin.sessionId, own: false, origin: originRef },
|
|
1741
|
+
note: `follow-up of ${short(originId)}: resuming ${origin.harness} session ${short(origin.sessionId)} (harness from ${origin.source})${ignored}`
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// packages/runner/src/run.ts
|
|
1746
|
+
var defaultDeps = { loadAdapter, allocate, collectEvidence };
|
|
1747
|
+
function coldNote(cold, cwd, trunk) {
|
|
1748
|
+
const parts = [worktreeProgress(cwd, trunk)];
|
|
1749
|
+
if (cold.origin) parts.push(originProgress(cold.origin.id, cold.origin.lane));
|
|
1750
|
+
return handoffNote(cold.fromHarness ?? "unknown", parts.join("\n\n"), cold.why);
|
|
1448
1751
|
}
|
|
1449
|
-
async function main(activeDir, lane) {
|
|
1450
|
-
const id =
|
|
1752
|
+
async function main(activeDir, lane, deps = defaultDeps) {
|
|
1753
|
+
const id = path10.basename(activeDir);
|
|
1451
1754
|
const status = (verb, note) => appendStatus(id, lane, verb, note);
|
|
1452
1755
|
const cfg = loadConfig();
|
|
1453
|
-
const descriptor = JSON.parse(
|
|
1756
|
+
const descriptor = JSON.parse(fs12.readFileSync(path10.join(activeDir, "descriptor.json"), "utf8"));
|
|
1454
1757
|
const resolved = resolveDispatch(descriptor, cfg);
|
|
1455
1758
|
const repo = cfg.repos[descriptor.repo];
|
|
1456
|
-
const briefFile =
|
|
1457
|
-
if (!
|
|
1458
|
-
const brief =
|
|
1759
|
+
const briefFile = path10.join(activeDir, "brief.md");
|
|
1760
|
+
if (!fs12.existsSync(briefFile)) fs12.writeFileSync(briefFile, descriptor.brief);
|
|
1761
|
+
const brief = fs12.readFileSync(briefFile, "utf8");
|
|
1459
1762
|
const attempts = Number(process.env.LOBSTAH_ATTEMPTS ?? "1");
|
|
1460
1763
|
const runnerInfo = {
|
|
1461
1764
|
pid: process.pid,
|
|
1462
1765
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1463
1766
|
attempts
|
|
1464
1767
|
};
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1768
|
+
fs12.writeFileSync(path10.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
|
|
1769
|
+
const plan = planStart({
|
|
1770
|
+
id,
|
|
1771
|
+
lane,
|
|
1772
|
+
descriptor,
|
|
1773
|
+
cfg,
|
|
1774
|
+
resolvedHarness: resolved.harness,
|
|
1775
|
+
envResume: process.env.LOBSTAH_RESUME
|
|
1776
|
+
});
|
|
1777
|
+
const firstNote = [attempts > 1 ? `attempt ${attempts}` : void 0, plan.note].filter(Boolean).join("; ");
|
|
1778
|
+
status("working", firstNote || void 0);
|
|
1779
|
+
appendEvent(id, lane, {
|
|
1780
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1781
|
+
type: "runner",
|
|
1782
|
+
data: { pid: process.pid, attempts, harness: plan.harness, ...plan.resume ? { resume: plan.resume.sessionId } : {} }
|
|
1783
|
+
});
|
|
1784
|
+
const own = readEvidence(id, lane).sessionId;
|
|
1785
|
+
mergeEvidence(id, lane, { harness: plan.harness, sessionId: plan.resume?.own ? own : void 0 });
|
|
1786
|
+
const wtFile = path10.join(activeDir, "worktree.json");
|
|
1469
1787
|
let cwd;
|
|
1470
|
-
if (
|
|
1471
|
-
cwd = JSON.parse(
|
|
1472
|
-
if (!
|
|
1473
|
-
} else if (
|
|
1788
|
+
if (fs12.existsSync(wtFile)) {
|
|
1789
|
+
cwd = JSON.parse(fs12.readFileSync(wtFile, "utf8")).path;
|
|
1790
|
+
if (!fs12.existsSync(cwd)) throw new Error(`recorded worktree ${cwd} is gone`);
|
|
1791
|
+
} else if (fs12.existsSync(worktreePath(id))) {
|
|
1474
1792
|
throw new Error(`unrecorded worktree already exists for ${id} \u2014 refusing to proceed`);
|
|
1475
1793
|
} else {
|
|
1476
|
-
cwd = await allocate(repo, id);
|
|
1477
|
-
|
|
1478
|
-
}
|
|
1479
|
-
const
|
|
1480
|
-
const
|
|
1481
|
-
const
|
|
1482
|
-
const
|
|
1483
|
-
const
|
|
1484
|
-
const
|
|
1485
|
-
id,
|
|
1486
|
-
cwd,
|
|
1487
|
-
prompt,
|
|
1488
|
-
model: resolved.model,
|
|
1489
|
-
effort: resolved.effort,
|
|
1490
|
-
limits: resolved.limits,
|
|
1491
|
-
env: {
|
|
1492
|
-
...resolved.env,
|
|
1493
|
-
LOBSTAH_HOME: lobstahHome(),
|
|
1494
|
-
...workerPath ? { PATH: workerPath } : {}
|
|
1495
|
-
},
|
|
1496
|
-
flags: resolved.flags,
|
|
1497
|
-
resumeSession
|
|
1498
|
-
});
|
|
1794
|
+
cwd = await deps.allocate(repo, id);
|
|
1795
|
+
fs12.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
|
|
1796
|
+
}
|
|
1797
|
+
const adapter = deps.loadAdapter(plan.harness);
|
|
1798
|
+
const envNudge = process.env.LOBSTAH_NUDGE;
|
|
1799
|
+
const planNudge = plan.cold && !envNudge ? coldNote(plan.cold, cwd, repo.trunk) : void 0;
|
|
1800
|
+
const promptWith = (nudge) => buildPrompt(brief, { id, nudge, attachments: descriptor.attachments });
|
|
1801
|
+
const binDir = path10.resolve(path10.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "bin");
|
|
1802
|
+
const workerPath = fs12.existsSync(path10.join(binDir, "lobstah")) ? `${binDir}${path10.delimiter}${process.env.PATH ?? ""}` : void 0;
|
|
1499
1803
|
let wallClockHit = false;
|
|
1804
|
+
let current;
|
|
1500
1805
|
const wallTimer = resolved.limits.wallClockSecs ? setTimeout(() => {
|
|
1501
1806
|
wallClockHit = true;
|
|
1502
|
-
|
|
1807
|
+
current?.kill();
|
|
1503
1808
|
}, resolved.limits.wallClockSecs * 1e3) : void 0;
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1809
|
+
const runOnce = async (prompt, resumeSession) => {
|
|
1810
|
+
const run2 = await adapter.start({
|
|
1811
|
+
id,
|
|
1812
|
+
cwd,
|
|
1813
|
+
prompt,
|
|
1814
|
+
model: resolved.model,
|
|
1815
|
+
effort: resolved.effort,
|
|
1816
|
+
limits: resolved.limits,
|
|
1817
|
+
env: {
|
|
1818
|
+
...resolved.env,
|
|
1819
|
+
LOBSTAH_HOME: lobstahHome(),
|
|
1820
|
+
...workerPath ? { PATH: workerPath } : {}
|
|
1821
|
+
},
|
|
1822
|
+
flags: resolved.flags,
|
|
1823
|
+
resumeSession
|
|
1824
|
+
});
|
|
1825
|
+
current = run2;
|
|
1826
|
+
const { cancelled: cancelled2, activity } = await drive(run2, { id, lane, stopped: () => wallClockHit });
|
|
1827
|
+
const result2 = await run2.done;
|
|
1828
|
+
return { cancelled: cancelled2, activity, result: result2 };
|
|
1829
|
+
};
|
|
1830
|
+
let outcome = await runOnce(promptWith(envNudge ?? planNudge), plan.resume?.sessionId);
|
|
1831
|
+
if (plan.resume && !outcome.cancelled && !wallClockHit && outcome.activity === 0 && isUnresumable(outcome.result.error)) {
|
|
1832
|
+
const reason = outcome.result.error.replace(/\s+/g, " ").slice(0, 200);
|
|
1833
|
+
status("working", `resume-fallback: ${reason} \u2014 starting cold on ${plan.harness} with a progress note`);
|
|
1834
|
+
appendEvent(id, lane, { at: (/* @__PURE__ */ new Date()).toISOString(), type: "runner", data: { resumeFallback: reason } });
|
|
1835
|
+
mergeEvidence(id, lane, { resumeFallback: reason, sessionId: void 0 });
|
|
1836
|
+
const note = coldNote(
|
|
1837
|
+
{
|
|
1838
|
+
why: `resume of session ${plan.resume.sessionId} failed`,
|
|
1839
|
+
fromHarness: plan.harness,
|
|
1840
|
+
...plan.resume.origin ? { origin: plan.resume.origin } : {}
|
|
1841
|
+
},
|
|
1842
|
+
cwd,
|
|
1843
|
+
repo.trunk
|
|
1844
|
+
);
|
|
1845
|
+
outcome = await runOnce(promptWith([envNudge, note].filter(Boolean).join("\n\n")), void 0);
|
|
1526
1846
|
}
|
|
1527
|
-
const result = await run2.done;
|
|
1528
1847
|
if (wallTimer) clearTimeout(wallTimer);
|
|
1848
|
+
const { cancelled, result } = outcome;
|
|
1529
1849
|
try {
|
|
1530
|
-
const gitEvidence = await collectEvidence(repo, id);
|
|
1850
|
+
const gitEvidence = await deps.collectEvidence(repo, id);
|
|
1531
1851
|
console.log(`[runner] git evidence: ${JSON.stringify(gitEvidence)}`);
|
|
1532
1852
|
mergeEvidence(id, lane, { ...gitEvidence, sessionId: result.sessionId ?? readEvidence(id, lane).sessionId });
|
|
1533
1853
|
console.log(`[runner] evidence after merge: ${JSON.stringify(readEvidence(id, lane))}`);
|
|
@@ -1536,25 +1856,18 @@ async function main(activeDir, lane) {
|
|
|
1536
1856
|
note: `evidence collection failed: ${err instanceof Error ? err.message : String(err)}`.slice(0, 300)
|
|
1537
1857
|
});
|
|
1538
1858
|
}
|
|
1539
|
-
|
|
1540
|
-
if (cancelled) {
|
|
1541
|
-
status("failed", "cancelled by operator");
|
|
1542
|
-
} else if (wallClockHit) {
|
|
1543
|
-
status("failed", "wall-clock limit exceeded");
|
|
1544
|
-
} else if (result.error) {
|
|
1545
|
-
status("failed", result.error.slice(0, 500));
|
|
1546
|
-
} else if (!lastVerb || !TERMINAL_VERBS.includes(lastVerb)) {
|
|
1547
|
-
status("done");
|
|
1548
|
-
}
|
|
1859
|
+
settle(id, lane, { cancelled, wallClockHit, error: result.error });
|
|
1549
1860
|
complete(id, lane);
|
|
1550
1861
|
}
|
|
1862
|
+
|
|
1863
|
+
// packages/runner/src/index.ts
|
|
1551
1864
|
function runRunner(activeDirArg2, laneArg2) {
|
|
1552
1865
|
const lane = laneArg2 === "chore" ? "chore" : "work";
|
|
1553
1866
|
main(activeDirArg2, lane).catch((err) => {
|
|
1554
|
-
const id =
|
|
1867
|
+
const id = path11.basename(activeDirArg2);
|
|
1555
1868
|
try {
|
|
1556
1869
|
appendStatus(id, lane, "failed", String(err instanceof Error ? err.message : err).slice(0, 500));
|
|
1557
|
-
|
|
1870
|
+
fs13.renameSync(activeDirArg2, path11.join(laneDirs(lane).done, id));
|
|
1558
1871
|
} catch {
|
|
1559
1872
|
}
|
|
1560
1873
|
process.exitCode = 1;
|
|
@@ -1564,6 +1877,7 @@ var [activeDirArg, laneArg] = process.argv.slice(2);
|
|
|
1564
1877
|
if (activeDirArg && activeDirArg !== "__runner") runRunner(activeDirArg, laneArg);
|
|
1565
1878
|
export {
|
|
1566
1879
|
main,
|
|
1880
|
+
planStart,
|
|
1567
1881
|
runRunner
|
|
1568
1882
|
};
|
|
1569
1883
|
/*! Bundled license information:
|