lobstah 0.6.6 → 0.6.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/dist/main.js +1515 -987
- package/dist/runner.js +336 -176
- package/docs/man.md +19 -1
- package/docs/vocabulary.md +1 -1
- 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 fs21 from "node:fs";
|
|
3
|
+
import * as path18 from "node:path";
|
|
4
4
|
|
|
5
5
|
// packages/core/dist/types.js
|
|
6
6
|
var VERBS = ["working", "needs-decision", "blocked", "paused", "done", "failed"];
|
|
@@ -8,6 +8,10 @@ var TERMINAL_VERBS = ["done", "failed"];
|
|
|
8
8
|
var WAITING_ON = ["review", "pr", "deploy", "person", "external"];
|
|
9
9
|
var WAITING_ON_VERBS = ["paused", "needs-decision", "blocked"];
|
|
10
10
|
|
|
11
|
+
// packages/core/dist/stats.js
|
|
12
|
+
import * as fs2 from "node:fs";
|
|
13
|
+
import * as path2 from "node:path";
|
|
14
|
+
|
|
11
15
|
// packages/core/dist/paths.js
|
|
12
16
|
import * as os from "node:os";
|
|
13
17
|
import * as path from "node:path";
|
|
@@ -54,6 +58,156 @@ function evidencePath(id, lane) {
|
|
|
54
58
|
return path.join(laneDirs(lane).state, `${id}.evidence`);
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
// packages/core/dist/stats.js
|
|
62
|
+
function statsPath() {
|
|
63
|
+
return path2.join(lobstahHome(), "stats.json");
|
|
64
|
+
}
|
|
65
|
+
function localDay(at = Date.now()) {
|
|
66
|
+
const d = new Date(at);
|
|
67
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
68
|
+
}
|
|
69
|
+
function readCatchFacts(stateDir, id) {
|
|
70
|
+
let last;
|
|
71
|
+
try {
|
|
72
|
+
for (const line of fs2.readFileSync(path2.join(stateDir, `${id}.status`), "utf8").split("\n")) {
|
|
73
|
+
if (!line.trim())
|
|
74
|
+
continue;
|
|
75
|
+
try {
|
|
76
|
+
last = JSON.parse(line);
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
return { done: false };
|
|
82
|
+
}
|
|
83
|
+
if (last?.verb !== "done")
|
|
84
|
+
return { done: false };
|
|
85
|
+
let trap;
|
|
86
|
+
try {
|
|
87
|
+
const to = JSON.parse(fs2.readFileSync(path2.join(stateDir, `${id}.evidence`), "utf8")).deliveredTo;
|
|
88
|
+
if (typeof to === "string" && to.startsWith("wt:"))
|
|
89
|
+
trap = to;
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
return { done: true, ...typeof last.at === "string" ? { at: last.at } : {}, ...trap ? { trap } : {} };
|
|
93
|
+
}
|
|
94
|
+
function emptyStore(now2) {
|
|
95
|
+
return { version: 1, totalCatches: 0, perTrap: {}, day: localDay(now2), catchesToday: 0, counted: [] };
|
|
96
|
+
}
|
|
97
|
+
function add(store, facts, now2) {
|
|
98
|
+
store.totalCatches++;
|
|
99
|
+
if (facts.trap)
|
|
100
|
+
store.perTrap[facts.trap] = (store.perTrap[facts.trap] ?? 0) + 1;
|
|
101
|
+
const today = localDay(now2);
|
|
102
|
+
if (store.day !== today) {
|
|
103
|
+
store.day = today;
|
|
104
|
+
store.catchesToday = 0;
|
|
105
|
+
}
|
|
106
|
+
const at = facts.at && Number.isFinite(Date.parse(facts.at)) ? facts.at : void 0;
|
|
107
|
+
if ((at ? localDay(at) : today) === today)
|
|
108
|
+
store.catchesToday++;
|
|
109
|
+
}
|
|
110
|
+
function backfillSources() {
|
|
111
|
+
const live = ["work", "chore"].map((lane) => ({ dir: laneDirs(lane).state, live: true }));
|
|
112
|
+
const backups = readDirIfPresent(lobstahHome()).filter((name) => name.startsWith("state-backup-")).sort().map((name) => ({ dir: path2.join(lobstahHome(), name), live: false })).filter((s) => fs2.statSync(s.dir).isDirectory());
|
|
113
|
+
return [...live, ...backups];
|
|
114
|
+
}
|
|
115
|
+
function backfillStats(now2 = Date.now()) {
|
|
116
|
+
const store = emptyStore(now2);
|
|
117
|
+
const seen = /* @__PURE__ */ new Set();
|
|
118
|
+
for (const { dir, live } of backfillSources()) {
|
|
119
|
+
for (const file of readDirIfPresent(dir).sort()) {
|
|
120
|
+
if (!file.endsWith(".status"))
|
|
121
|
+
continue;
|
|
122
|
+
const id = file.slice(0, -".status".length);
|
|
123
|
+
if (seen.has(id))
|
|
124
|
+
continue;
|
|
125
|
+
seen.add(id);
|
|
126
|
+
const facts = readCatchFacts(dir, id);
|
|
127
|
+
if (!facts.done)
|
|
128
|
+
continue;
|
|
129
|
+
add(store, facts, now2);
|
|
130
|
+
if (live)
|
|
131
|
+
store.counted.push(id);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return store;
|
|
135
|
+
}
|
|
136
|
+
function parseStore(text) {
|
|
137
|
+
try {
|
|
138
|
+
const s = JSON.parse(text);
|
|
139
|
+
if (s.version !== 1 || typeof s.totalCatches !== "number" || typeof s.catchesToday !== "number" || typeof s.day !== "string")
|
|
140
|
+
return void 0;
|
|
141
|
+
return { version: 1, totalCatches: s.totalCatches, perTrap: s.perTrap ?? {}, day: s.day, catchesToday: s.catchesToday, counted: s.counted ?? [] };
|
|
142
|
+
} catch {
|
|
143
|
+
return void 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function writeStore(store) {
|
|
147
|
+
const file = statsPath();
|
|
148
|
+
fs2.mkdirSync(path2.dirname(file), { recursive: true });
|
|
149
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
150
|
+
fs2.writeFileSync(tmp, JSON.stringify(store));
|
|
151
|
+
fs2.renameSync(tmp, file);
|
|
152
|
+
}
|
|
153
|
+
function withStatsLock(action) {
|
|
154
|
+
fs2.mkdirSync(lobstahHome(), { recursive: true });
|
|
155
|
+
const lock = `${statsPath()}.lock`;
|
|
156
|
+
const deadline = Date.now() + 1e4;
|
|
157
|
+
for (; ; ) {
|
|
158
|
+
try {
|
|
159
|
+
fs2.mkdirSync(lock);
|
|
160
|
+
break;
|
|
161
|
+
} catch (err) {
|
|
162
|
+
if (err.code !== "EEXIST")
|
|
163
|
+
throw err;
|
|
164
|
+
try {
|
|
165
|
+
if (Date.now() - fs2.statSync(lock).mtimeMs > 12e4)
|
|
166
|
+
fs2.rmdirSync(lock);
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
if (Date.now() >= deadline)
|
|
170
|
+
throw new Error("stats store locked");
|
|
171
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
return action();
|
|
176
|
+
} finally {
|
|
177
|
+
fs2.rmdirSync(lock);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function loadOrBackfill(now2) {
|
|
181
|
+
let text;
|
|
182
|
+
try {
|
|
183
|
+
text = fs2.readFileSync(statsPath(), "utf8");
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (err.code !== "ENOENT")
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
const saved = text === void 0 ? void 0 : parseStore(text);
|
|
189
|
+
if (saved)
|
|
190
|
+
return saved;
|
|
191
|
+
if (text !== void 0)
|
|
192
|
+
fs2.renameSync(statsPath(), `${statsPath()}.unreadable-${now2}`);
|
|
193
|
+
const store = backfillStats(now2);
|
|
194
|
+
writeStore(store);
|
|
195
|
+
return store;
|
|
196
|
+
}
|
|
197
|
+
function recordCatch(id, lane, now2 = Date.now()) {
|
|
198
|
+
withStatsLock(() => {
|
|
199
|
+
const store = loadOrBackfill(now2);
|
|
200
|
+
if (store.counted.includes(id))
|
|
201
|
+
return;
|
|
202
|
+
const facts = readCatchFacts(laneDirs(lane).state, id);
|
|
203
|
+
if (!facts.done)
|
|
204
|
+
return;
|
|
205
|
+
add(store, facts, now2);
|
|
206
|
+
store.counted.push(id);
|
|
207
|
+
writeStore(store);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
57
211
|
// packages/core/dist/attachments.js
|
|
58
212
|
function attachmentBlock(attachments) {
|
|
59
213
|
if (attachments.length === 0)
|
|
@@ -89,9 +243,9 @@ var ANSWER_TYPES = {
|
|
|
89
243
|
var ANSWER_EXTENSIONS = Object.keys(ANSWER_TYPES);
|
|
90
244
|
|
|
91
245
|
// packages/core/dist/config.js
|
|
92
|
-
import * as
|
|
246
|
+
import * as fs3 from "node:fs";
|
|
93
247
|
import * as os2 from "node:os";
|
|
94
|
-
import * as
|
|
248
|
+
import * as path3 from "node:path";
|
|
95
249
|
|
|
96
250
|
// node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/date.js
|
|
97
251
|
var DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[T ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|[-+]\d{2}:\d{2})?$/i;
|
|
@@ -799,14 +953,14 @@ var DEFAULT_LIMITS = {
|
|
|
799
953
|
releaseOnMerge: false
|
|
800
954
|
};
|
|
801
955
|
function configPath() {
|
|
802
|
-
return
|
|
956
|
+
return path3.join(lobstahHome(), "config.toml");
|
|
803
957
|
}
|
|
804
958
|
function expandHome(p) {
|
|
805
|
-
return p.startsWith("~/") ?
|
|
959
|
+
return p.startsWith("~/") ? path3.join(os2.homedir(), p.slice(2)) : p;
|
|
806
960
|
}
|
|
807
961
|
function loadConfig() {
|
|
808
962
|
const file = configPath();
|
|
809
|
-
const raw =
|
|
963
|
+
const raw = fs3.existsSync(file) ? parse(fs3.readFileSync(file, "utf8")) : {};
|
|
810
964
|
const reposRaw = raw.repos ?? {};
|
|
811
965
|
const repos = {};
|
|
812
966
|
for (const [key, r] of Object.entries(reposRaw)) {
|
|
@@ -869,8 +1023,8 @@ function resolveDispatch(d, cfg) {
|
|
|
869
1023
|
var GB = 1024 ** 3;
|
|
870
1024
|
|
|
871
1025
|
// packages/core/dist/watch.js
|
|
872
|
-
import * as
|
|
873
|
-
import * as
|
|
1026
|
+
import * as fs4 from "node:fs";
|
|
1027
|
+
import * as path4 from "node:path";
|
|
874
1028
|
|
|
875
1029
|
// packages/core/dist/gh-errors.js
|
|
876
1030
|
function firstMeaningfulLine(...outputs) {
|
|
@@ -892,40 +1046,40 @@ function firstMeaningfulLine(...outputs) {
|
|
|
892
1046
|
|
|
893
1047
|
// packages/core/dist/watch.js
|
|
894
1048
|
function watchesDir() {
|
|
895
|
-
return
|
|
1049
|
+
return path4.join(lobstahHome(), "watches");
|
|
896
1050
|
}
|
|
897
1051
|
function slug(key) {
|
|
898
1052
|
return key.replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
899
1053
|
}
|
|
900
1054
|
function watchPath(key) {
|
|
901
|
-
return
|
|
1055
|
+
return path4.join(watchesDir(), `${slug(key)}.json`);
|
|
902
1056
|
}
|
|
903
1057
|
function readWatch(key) {
|
|
904
1058
|
try {
|
|
905
|
-
return JSON.parse(
|
|
1059
|
+
return JSON.parse(fs4.readFileSync(watchPath(key), "utf8"));
|
|
906
1060
|
} catch {
|
|
907
1061
|
return void 0;
|
|
908
1062
|
}
|
|
909
1063
|
}
|
|
910
1064
|
|
|
911
1065
|
// packages/core/dist/inbox.js
|
|
912
|
-
import * as
|
|
913
|
-
import * as
|
|
1066
|
+
import * as fs12 from "node:fs";
|
|
1067
|
+
import * as path10 from "node:path";
|
|
914
1068
|
|
|
915
1069
|
// packages/core/dist/soak.js
|
|
916
|
-
import * as
|
|
917
|
-
import * as
|
|
1070
|
+
import * as fs11 from "node:fs";
|
|
1071
|
+
import * as path9 from "node:path";
|
|
918
1072
|
|
|
919
1073
|
// packages/core/dist/queue.js
|
|
920
|
-
import * as
|
|
921
|
-
import * as
|
|
1074
|
+
import * as fs7 from "node:fs";
|
|
1075
|
+
import * as path6 from "node:path";
|
|
922
1076
|
|
|
923
1077
|
// packages/core/dist/status.js
|
|
924
|
-
import * as
|
|
1078
|
+
import * as fs6 from "node:fs";
|
|
925
1079
|
|
|
926
1080
|
// packages/core/dist/activity.js
|
|
927
|
-
import * as
|
|
928
|
-
import * as
|
|
1081
|
+
import * as fs5 from "node:fs";
|
|
1082
|
+
import * as path5 from "node:path";
|
|
929
1083
|
var ACTIVITY_KINDS = ["tool", "message", "thinking", "waiting"];
|
|
930
1084
|
var ACTIVITY_SUMMARY_MAX = 80;
|
|
931
1085
|
var SECRET_PATTERNS = [
|
|
@@ -979,7 +1133,7 @@ function relativeTarget(p, root) {
|
|
|
979
1133
|
if (!root)
|
|
980
1134
|
return baseName(p);
|
|
981
1135
|
const win = isWindowsPath(p) || isWindowsPath(root);
|
|
982
|
-
const lib = win ?
|
|
1136
|
+
const lib = win ? path5.win32 : path5.posix;
|
|
983
1137
|
if (!lib.isAbsolute(p))
|
|
984
1138
|
return p.replace(/\\/g, "/");
|
|
985
1139
|
const rel = lib.relative(win ? root : root.replace(/\\/g, "/"), p);
|
|
@@ -1048,8 +1202,8 @@ function activityFromEvent(ev, root) {
|
|
|
1048
1202
|
}
|
|
1049
1203
|
function atomicWrite(file, content) {
|
|
1050
1204
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
1051
|
-
|
|
1052
|
-
|
|
1205
|
+
fs5.writeFileSync(tmp, content);
|
|
1206
|
+
fs5.renameSync(tmp, file);
|
|
1053
1207
|
}
|
|
1054
1208
|
function writeActivity(id, lane, a) {
|
|
1055
1209
|
const safe = { at: a.at, kind: a.kind, summary: redactSummary(a.summary) };
|
|
@@ -1057,7 +1211,7 @@ function writeActivity(id, lane, a) {
|
|
|
1057
1211
|
}
|
|
1058
1212
|
function readActivity(id, lane) {
|
|
1059
1213
|
try {
|
|
1060
|
-
const parsed = JSON.parse(
|
|
1214
|
+
const parsed = JSON.parse(fs5.readFileSync(activityPath(id, lane), "utf8"));
|
|
1061
1215
|
if (typeof parsed.at !== "string" || typeof parsed.summary !== "string")
|
|
1062
1216
|
return void 0;
|
|
1063
1217
|
if (!ACTIVITY_KINDS.includes(parsed.kind))
|
|
@@ -1189,15 +1343,21 @@ function appendStatus(id, lane, verb, note, at, waiting, reported = false) {
|
|
|
1189
1343
|
...extra,
|
|
1190
1344
|
...reported ? { reported: true } : {}
|
|
1191
1345
|
};
|
|
1192
|
-
|
|
1346
|
+
fs6.appendFileSync(statusPath(id, lane), `${JSON.stringify(entry)}
|
|
1193
1347
|
`);
|
|
1348
|
+
if (verb === "done") {
|
|
1349
|
+
try {
|
|
1350
|
+
recordCatch(id, lane);
|
|
1351
|
+
} catch {
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1194
1354
|
return entry;
|
|
1195
1355
|
}
|
|
1196
1356
|
function readStatusLog(id, lane) {
|
|
1197
1357
|
const file = statusPath(id, lane);
|
|
1198
|
-
if (!
|
|
1358
|
+
if (!fs6.existsSync(file))
|
|
1199
1359
|
return [];
|
|
1200
|
-
return
|
|
1360
|
+
return fs6.readFileSync(file, "utf8").split("\n").filter(Boolean).flatMap((line) => {
|
|
1201
1361
|
try {
|
|
1202
1362
|
const parsed = JSON.parse(line);
|
|
1203
1363
|
return isVerb(parsed.verb) ? [parsed] : [];
|
|
@@ -1214,12 +1374,12 @@ function pausedWaiting(entry) {
|
|
|
1214
1374
|
function storedDescriptor(id, lane) {
|
|
1215
1375
|
const dirs = laneDirs(lane);
|
|
1216
1376
|
for (const file of [
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1377
|
+
path6.join(dirs.queue, `${id}.json`),
|
|
1378
|
+
path6.join(dirs.active, id, "descriptor.json"),
|
|
1379
|
+
path6.join(dirs.done, id, "descriptor.json")
|
|
1220
1380
|
]) {
|
|
1221
1381
|
try {
|
|
1222
|
-
return JSON.parse(
|
|
1382
|
+
return JSON.parse(fs7.readFileSync(file, "utf8"));
|
|
1223
1383
|
} catch {
|
|
1224
1384
|
}
|
|
1225
1385
|
}
|
|
@@ -1230,17 +1390,17 @@ function activeIds(lane) {
|
|
|
1230
1390
|
}
|
|
1231
1391
|
function complete(id, lane) {
|
|
1232
1392
|
const dirs = laneDirs(lane);
|
|
1233
|
-
|
|
1393
|
+
fs7.renameSync(path6.join(dirs.active, id), path6.join(dirs.done, id));
|
|
1234
1394
|
}
|
|
1235
1395
|
function cancelRequested(id, lane) {
|
|
1236
|
-
return
|
|
1396
|
+
return fs7.existsSync(path6.join(laneDirs(lane).active, id, "cancel"));
|
|
1237
1397
|
}
|
|
1238
1398
|
|
|
1239
1399
|
// packages/core/dist/evidence.js
|
|
1240
|
-
import * as
|
|
1400
|
+
import * as fs8 from "node:fs";
|
|
1241
1401
|
function readEvidence(id, lane) {
|
|
1242
1402
|
try {
|
|
1243
|
-
return JSON.parse(
|
|
1403
|
+
return JSON.parse(fs8.readFileSync(evidencePath(id, lane), "utf8"));
|
|
1244
1404
|
} catch {
|
|
1245
1405
|
return {};
|
|
1246
1406
|
}
|
|
@@ -1248,13 +1408,13 @@ function readEvidence(id, lane) {
|
|
|
1248
1408
|
function mergeEvidence(id, lane, patch) {
|
|
1249
1409
|
const file = evidencePath(id, lane);
|
|
1250
1410
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
1251
|
-
|
|
1252
|
-
|
|
1411
|
+
fs8.writeFileSync(tmp, JSON.stringify({ ...readEvidence(id, lane), ...patch }, null, 2));
|
|
1412
|
+
fs8.renameSync(tmp, file);
|
|
1253
1413
|
}
|
|
1254
1414
|
|
|
1255
1415
|
// packages/core/dist/pushes.js
|
|
1256
|
-
import * as
|
|
1257
|
-
import * as
|
|
1416
|
+
import * as fs9 from "node:fs";
|
|
1417
|
+
import * as path7 from "node:path";
|
|
1258
1418
|
var MAX_PUSHES = 20;
|
|
1259
1419
|
var PUSH_HEAD = "HEAD";
|
|
1260
1420
|
var VALUE_OPTIONS = /* @__PURE__ */ new Set(["-o", "--push-option", "--repo", "--receive-pack", "--exec"]);
|
|
@@ -1309,18 +1469,18 @@ function gitPushTargets(command) {
|
|
|
1309
1469
|
return found ? [...new Set(targets)] : void 0;
|
|
1310
1470
|
}
|
|
1311
1471
|
function gitDirAt(dir) {
|
|
1312
|
-
let at =
|
|
1472
|
+
let at = path7.resolve(dir);
|
|
1313
1473
|
for (; ; ) {
|
|
1314
|
-
const dotGit =
|
|
1474
|
+
const dotGit = path7.join(at, ".git");
|
|
1315
1475
|
try {
|
|
1316
|
-
const stat =
|
|
1476
|
+
const stat = fs9.statSync(dotGit);
|
|
1317
1477
|
if (stat.isDirectory())
|
|
1318
1478
|
return dotGit;
|
|
1319
|
-
const m = /^gitdir:\s*(.+)$/m.exec(
|
|
1320
|
-
return m ?
|
|
1479
|
+
const m = /^gitdir:\s*(.+)$/m.exec(fs9.readFileSync(dotGit, "utf8"));
|
|
1480
|
+
return m ? path7.resolve(at, m[1].trim()) : void 0;
|
|
1321
1481
|
} catch {
|
|
1322
1482
|
}
|
|
1323
|
-
const up =
|
|
1483
|
+
const up = path7.dirname(at);
|
|
1324
1484
|
if (up === at)
|
|
1325
1485
|
return void 0;
|
|
1326
1486
|
at = up;
|
|
@@ -1331,7 +1491,7 @@ function currentBranchAt(dir) {
|
|
|
1331
1491
|
if (!gitDir)
|
|
1332
1492
|
return void 0;
|
|
1333
1493
|
try {
|
|
1334
|
-
const m = /^ref:\s*refs\/heads\/(.+)$/m.exec(
|
|
1494
|
+
const m = /^ref:\s*refs\/heads\/(.+)$/m.exec(fs9.readFileSync(path7.join(gitDir, "HEAD"), "utf8"));
|
|
1335
1495
|
return m ? m[1].trim() : void 0;
|
|
1336
1496
|
} catch {
|
|
1337
1497
|
return void 0;
|
|
@@ -1350,23 +1510,23 @@ function recordPush(id, lane, branches, at = (/* @__PURE__ */ new Date()).toISOS
|
|
|
1350
1510
|
}
|
|
1351
1511
|
|
|
1352
1512
|
// packages/core/dist/worktrees.js
|
|
1353
|
-
import * as
|
|
1354
|
-
import * as
|
|
1513
|
+
import * as fs10 from "node:fs";
|
|
1514
|
+
import * as path8 from "node:path";
|
|
1355
1515
|
var LANES = ["work", "chore"];
|
|
1356
1516
|
function worktreePath(id) {
|
|
1357
|
-
return
|
|
1517
|
+
return path8.join(lobstahHome(), "worktrees", id);
|
|
1358
1518
|
}
|
|
1359
1519
|
function readRecord(file) {
|
|
1360
1520
|
try {
|
|
1361
|
-
const r = JSON.parse(
|
|
1521
|
+
const r = JSON.parse(fs10.readFileSync(file, "utf8"));
|
|
1362
1522
|
return typeof r.path === "string" ? r : void 0;
|
|
1363
1523
|
} catch {
|
|
1364
1524
|
return void 0;
|
|
1365
1525
|
}
|
|
1366
1526
|
}
|
|
1367
1527
|
function ownerOfPath(p) {
|
|
1368
|
-
const rel =
|
|
1369
|
-
if (!rel || rel.startsWith("..") ||
|
|
1528
|
+
const rel = path8.relative(path8.join(lobstahHome(), "worktrees"), p);
|
|
1529
|
+
if (!rel || rel.startsWith("..") || path8.isAbsolute(rel))
|
|
1370
1530
|
return void 0;
|
|
1371
1531
|
return rel.split(/[\\/]/)[0];
|
|
1372
1532
|
}
|
|
@@ -1379,7 +1539,7 @@ function dispatchWorktree(id, lane) {
|
|
|
1379
1539
|
}
|
|
1380
1540
|
const dirs = laneDirs(l);
|
|
1381
1541
|
for (const dir of [dirs.active, dirs.done]) {
|
|
1382
|
-
const rec = readRecord(
|
|
1542
|
+
const rec = readRecord(path8.join(dir, id, "worktree.json"));
|
|
1383
1543
|
if (rec) {
|
|
1384
1544
|
const owner = rec.of ?? ownerOfPath(rec.path) ?? id;
|
|
1385
1545
|
return { path: rec.path, owner, reused: owner !== id };
|
|
@@ -1391,7 +1551,7 @@ function dispatchWorktree(id, lane) {
|
|
|
1391
1551
|
function laneOf(id) {
|
|
1392
1552
|
for (const l of LANES) {
|
|
1393
1553
|
const d = laneDirs(l);
|
|
1394
|
-
if (
|
|
1554
|
+
if (fs10.existsSync(path8.join(d.queue, `${id}.json`)) || fs10.existsSync(path8.join(d.active, id)) || fs10.existsSync(path8.join(d.done, id))) {
|
|
1395
1555
|
return l;
|
|
1396
1556
|
}
|
|
1397
1557
|
}
|
|
@@ -1411,39 +1571,39 @@ function followUpAncestors(id, lane) {
|
|
|
1411
1571
|
function chainWorktree(followUp) {
|
|
1412
1572
|
for (const id of [followUp, ...followUpAncestors(followUp)]) {
|
|
1413
1573
|
const wt = dispatchWorktree(id, laneOf(id));
|
|
1414
|
-
if (
|
|
1574
|
+
if (fs10.existsSync(wt.path))
|
|
1415
1575
|
return { ...wt, from: id };
|
|
1416
1576
|
}
|
|
1417
1577
|
return void 0;
|
|
1418
1578
|
}
|
|
1419
1579
|
function worktreeGitDir(wt) {
|
|
1420
|
-
const dotGit =
|
|
1580
|
+
const dotGit = path8.join(wt, ".git");
|
|
1421
1581
|
try {
|
|
1422
|
-
const st =
|
|
1582
|
+
const st = fs10.statSync(dotGit);
|
|
1423
1583
|
if (st.isDirectory())
|
|
1424
1584
|
return dotGit;
|
|
1425
|
-
const m = /^gitdir:\s*(.+?)\s*$/m.exec(
|
|
1426
|
-
return m ?
|
|
1585
|
+
const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs10.readFileSync(dotGit, "utf8"));
|
|
1586
|
+
return m ? path8.resolve(wt, m[1]) : void 0;
|
|
1427
1587
|
} catch {
|
|
1428
1588
|
return void 0;
|
|
1429
1589
|
}
|
|
1430
1590
|
}
|
|
1431
1591
|
function worktreeLockFile(wt) {
|
|
1432
1592
|
const gitDir = worktreeGitDir(wt);
|
|
1433
|
-
return gitDir ?
|
|
1593
|
+
return gitDir ? path8.join(gitDir, "lobstah.lock") : void 0;
|
|
1434
1594
|
}
|
|
1435
1595
|
function readWorktreeLock(wt) {
|
|
1436
1596
|
const file = worktreeLockFile(wt);
|
|
1437
1597
|
if (!file)
|
|
1438
1598
|
return void 0;
|
|
1439
1599
|
try {
|
|
1440
|
-
return JSON.parse(
|
|
1600
|
+
return JSON.parse(fs10.readFileSync(file, "utf8"));
|
|
1441
1601
|
} catch {
|
|
1442
1602
|
return void 0;
|
|
1443
1603
|
}
|
|
1444
1604
|
}
|
|
1445
1605
|
function lockIsLive(lock) {
|
|
1446
|
-
return
|
|
1606
|
+
return fs10.existsSync(path8.join(laneDirs(lock.lane).active, lock.id));
|
|
1447
1607
|
}
|
|
1448
1608
|
function worktreeHolder(wt) {
|
|
1449
1609
|
const lock = readWorktreeLock(wt);
|
|
@@ -1456,7 +1616,7 @@ function acquireWorktreeLock(wt, id, lane) {
|
|
|
1456
1616
|
return { id: "(no git dir)", lane, pid: 0, at: mine.at };
|
|
1457
1617
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1458
1618
|
try {
|
|
1459
|
-
|
|
1619
|
+
fs10.writeFileSync(file, JSON.stringify(mine, null, 2), { flag: "wx" });
|
|
1460
1620
|
return void 0;
|
|
1461
1621
|
} catch (err) {
|
|
1462
1622
|
if (err.code !== "EEXIST")
|
|
@@ -1464,7 +1624,7 @@ function acquireWorktreeLock(wt, id, lane) {
|
|
|
1464
1624
|
}
|
|
1465
1625
|
let raw;
|
|
1466
1626
|
try {
|
|
1467
|
-
raw =
|
|
1627
|
+
raw = fs10.readFileSync(file, "utf8");
|
|
1468
1628
|
} catch {
|
|
1469
1629
|
continue;
|
|
1470
1630
|
}
|
|
@@ -1475,14 +1635,14 @@ function acquireWorktreeLock(wt, id, lane) {
|
|
|
1475
1635
|
held2 = void 0;
|
|
1476
1636
|
}
|
|
1477
1637
|
if (held2?.id === id) {
|
|
1478
|
-
|
|
1638
|
+
fs10.writeFileSync(file, JSON.stringify(mine, null, 2));
|
|
1479
1639
|
return void 0;
|
|
1480
1640
|
}
|
|
1481
1641
|
if (held2 && lockIsLive(held2))
|
|
1482
1642
|
return held2;
|
|
1483
1643
|
try {
|
|
1484
|
-
if (
|
|
1485
|
-
|
|
1644
|
+
if (fs10.readFileSync(file, "utf8") === raw)
|
|
1645
|
+
fs10.rmSync(file, { force: true });
|
|
1486
1646
|
} catch {
|
|
1487
1647
|
}
|
|
1488
1648
|
}
|
|
@@ -1494,33 +1654,33 @@ function releaseWorktreeLock(wt, id) {
|
|
|
1494
1654
|
if (!file)
|
|
1495
1655
|
return;
|
|
1496
1656
|
if (readWorktreeLock(wt)?.id === id)
|
|
1497
|
-
|
|
1657
|
+
fs10.rmSync(file, { force: true });
|
|
1498
1658
|
}
|
|
1499
1659
|
|
|
1500
1660
|
// packages/core/dist/soak.js
|
|
1501
1661
|
function regPath(trapId) {
|
|
1502
|
-
return
|
|
1662
|
+
return path9.join(soakingDir(), `${trapId}.json`);
|
|
1503
1663
|
}
|
|
1504
1664
|
function readTrap(trapId) {
|
|
1505
1665
|
try {
|
|
1506
|
-
return JSON.parse(
|
|
1666
|
+
return JSON.parse(fs11.readFileSync(regPath(trapId), "utf8"));
|
|
1507
1667
|
} catch {
|
|
1508
1668
|
return void 0;
|
|
1509
1669
|
}
|
|
1510
1670
|
}
|
|
1511
1671
|
function listTraps() {
|
|
1512
1672
|
try {
|
|
1513
|
-
return
|
|
1673
|
+
return fs11.readdirSync(soakingDir()).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(fs11.readFileSync(path9.join(soakingDir(), f), "utf8"))).filter((r) => typeof r.trapId === "string");
|
|
1514
1674
|
} catch {
|
|
1515
1675
|
return [];
|
|
1516
1676
|
}
|
|
1517
1677
|
}
|
|
1518
1678
|
function claimPath(id, lane) {
|
|
1519
|
-
return
|
|
1679
|
+
return path9.join(laneDirs(lane).active, id, "claim.json");
|
|
1520
1680
|
}
|
|
1521
1681
|
function readSessionClaim(id, lane) {
|
|
1522
1682
|
try {
|
|
1523
|
-
return JSON.parse(
|
|
1683
|
+
return JSON.parse(fs11.readFileSync(claimPath(id, lane), "utf8"));
|
|
1524
1684
|
} catch {
|
|
1525
1685
|
return void 0;
|
|
1526
1686
|
}
|
|
@@ -1529,54 +1689,54 @@ function readSessionClaim(id, lane) {
|
|
|
1529
1689
|
// packages/core/dist/inbox.js
|
|
1530
1690
|
var metaName = (file) => file.replace(/\.msg$/, ".meta.json");
|
|
1531
1691
|
function inboxDir(id, lane) {
|
|
1532
|
-
return
|
|
1692
|
+
return path10.join(laneDirs(lane).inbox, id);
|
|
1533
1693
|
}
|
|
1534
1694
|
function unhandled(id, lane) {
|
|
1535
1695
|
const dir = inboxDir(id, lane);
|
|
1536
|
-
return readDirIfPresent(dir).filter((f) => f.endsWith(".msg")).sort().map((f) => ({ file: f, text:
|
|
1696
|
+
return readDirIfPresent(dir).filter((f) => f.endsWith(".msg")).sort().map((f) => ({ file: f, text: fs12.readFileSync(path10.join(dir, f), "utf8") }));
|
|
1537
1697
|
}
|
|
1538
1698
|
function acknowledge(id, lane, file) {
|
|
1539
1699
|
const dir = inboxDir(id, lane);
|
|
1540
|
-
|
|
1541
|
-
|
|
1700
|
+
fs12.mkdirSync(path10.join(dir, "handled"), { recursive: true });
|
|
1701
|
+
fs12.renameSync(path10.join(dir, file), path10.join(dir, "handled", file));
|
|
1542
1702
|
try {
|
|
1543
|
-
|
|
1703
|
+
fs12.renameSync(path10.join(dir, metaName(file)), path10.join(dir, "handled", metaName(file)));
|
|
1544
1704
|
} catch {
|
|
1545
1705
|
}
|
|
1546
1706
|
}
|
|
1547
1707
|
|
|
1548
1708
|
// packages/core/dist/events.js
|
|
1549
|
-
import * as
|
|
1709
|
+
import * as fs13 from "node:fs";
|
|
1550
1710
|
function appendEvent(id, lane, ev) {
|
|
1551
|
-
|
|
1711
|
+
fs13.appendFileSync(eventsPath(id, lane), `${JSON.stringify(ev)}
|
|
1552
1712
|
`);
|
|
1553
1713
|
}
|
|
1554
1714
|
function touchEvents(id, lane, at = /* @__PURE__ */ new Date()) {
|
|
1555
1715
|
const file = eventsPath(id, lane);
|
|
1556
1716
|
try {
|
|
1557
|
-
|
|
1717
|
+
fs13.utimesSync(file, at, at);
|
|
1558
1718
|
} catch {
|
|
1559
|
-
|
|
1560
|
-
|
|
1719
|
+
fs13.appendFileSync(file, "");
|
|
1720
|
+
fs13.utimesSync(file, at, at);
|
|
1561
1721
|
}
|
|
1562
1722
|
}
|
|
1563
1723
|
|
|
1564
1724
|
// packages/core/dist/harness.js
|
|
1565
|
-
import * as
|
|
1566
|
-
import * as
|
|
1725
|
+
import * as fs14 from "node:fs";
|
|
1726
|
+
import * as path11 from "node:path";
|
|
1567
1727
|
import { createRequire } from "node:module";
|
|
1568
1728
|
function resolveOnPath(bin, pathVar = process.env.PATH) {
|
|
1569
1729
|
if (!pathVar)
|
|
1570
1730
|
return void 0;
|
|
1571
1731
|
const names = process.platform === "win32" ? [bin, `${bin}.exe`, `${bin}.cmd`, `${bin}.bat`] : [bin];
|
|
1572
|
-
for (const dir of pathVar.split(
|
|
1732
|
+
for (const dir of pathVar.split(path11.delimiter)) {
|
|
1573
1733
|
if (!dir)
|
|
1574
1734
|
continue;
|
|
1575
1735
|
for (const name of names) {
|
|
1576
|
-
const candidate =
|
|
1736
|
+
const candidate = path11.join(dir, name);
|
|
1577
1737
|
try {
|
|
1578
|
-
|
|
1579
|
-
if (
|
|
1738
|
+
fs14.accessSync(candidate, fs14.constants.X_OK);
|
|
1739
|
+
if (fs14.statSync(candidate).isFile())
|
|
1580
1740
|
return candidate;
|
|
1581
1741
|
} catch {
|
|
1582
1742
|
}
|
|
@@ -1597,8 +1757,8 @@ function codexInvocation(argv) {
|
|
|
1597
1757
|
} catch {
|
|
1598
1758
|
}
|
|
1599
1759
|
for (const dir of require_.resolve.paths("@openai/codex") ?? []) {
|
|
1600
|
-
const cli =
|
|
1601
|
-
if (
|
|
1760
|
+
const cli = path11.join(dir, "@openai", "codex", "bin", "codex.js");
|
|
1761
|
+
if (fs14.existsSync(cli))
|
|
1602
1762
|
return { file: process.execPath, argv: [cli, ...argv] };
|
|
1603
1763
|
}
|
|
1604
1764
|
return void 0;
|
|
@@ -1677,21 +1837,21 @@ function parseUnresolvedThreads(stdout) {
|
|
|
1677
1837
|
}
|
|
1678
1838
|
|
|
1679
1839
|
// packages/core/dist/prs.js
|
|
1680
|
-
import * as
|
|
1681
|
-
import * as
|
|
1840
|
+
import * as fs15 from "node:fs";
|
|
1841
|
+
import * as path12 from "node:path";
|
|
1682
1842
|
function prsDir() {
|
|
1683
|
-
return
|
|
1843
|
+
return path12.join(lobstahHome(), "prs");
|
|
1684
1844
|
}
|
|
1685
1845
|
function prRecordFile(key) {
|
|
1686
1846
|
const ref = parsePrRef(key);
|
|
1687
1847
|
if (!ref)
|
|
1688
1848
|
throw new Error(`not a PR key: ${key}`);
|
|
1689
1849
|
const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1690
|
-
return
|
|
1850
|
+
return path12.join(prsDir(), `${safe(ref.owner)}__${safe(ref.repo)}__${ref.number}.json`);
|
|
1691
1851
|
}
|
|
1692
1852
|
function readPr(key) {
|
|
1693
1853
|
try {
|
|
1694
|
-
return JSON.parse(
|
|
1854
|
+
return JSON.parse(fs15.readFileSync(prRecordFile(key), "utf8"));
|
|
1695
1855
|
} catch {
|
|
1696
1856
|
return void 0;
|
|
1697
1857
|
}
|
|
@@ -1699,13 +1859,13 @@ function readPr(key) {
|
|
|
1699
1859
|
function readPrs() {
|
|
1700
1860
|
let files;
|
|
1701
1861
|
try {
|
|
1702
|
-
files =
|
|
1862
|
+
files = fs15.readdirSync(prsDir()).filter((f) => f.endsWith(".json"));
|
|
1703
1863
|
} catch {
|
|
1704
1864
|
return [];
|
|
1705
1865
|
}
|
|
1706
1866
|
return files.flatMap((f) => {
|
|
1707
1867
|
try {
|
|
1708
|
-
const r = JSON.parse(
|
|
1868
|
+
const r = JSON.parse(fs15.readFileSync(path12.join(prsDir(), f), "utf8"));
|
|
1709
1869
|
return typeof r.key === "string" && typeof r.url === "string" ? [r] : [];
|
|
1710
1870
|
} catch {
|
|
1711
1871
|
return [];
|
|
@@ -1754,9 +1914,9 @@ function pushRule(branch, id = "<dispatch id>", mode = "rebase", also = []) {
|
|
|
1754
1914
|
}
|
|
1755
1915
|
|
|
1756
1916
|
// packages/core/dist/resume.js
|
|
1757
|
-
import * as
|
|
1917
|
+
import * as fs16 from "node:fs";
|
|
1758
1918
|
import * as os3 from "node:os";
|
|
1759
|
-
import * as
|
|
1919
|
+
import * as path13 from "node:path";
|
|
1760
1920
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1761
1921
|
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;
|
|
1762
1922
|
function harnessFromSessionId(sessionId) {
|
|
@@ -1767,7 +1927,7 @@ function claimOf(id, lane) {
|
|
|
1767
1927
|
const dirs = laneDirs(lane);
|
|
1768
1928
|
for (const dir of [dirs.active, dirs.done]) {
|
|
1769
1929
|
try {
|
|
1770
|
-
return JSON.parse(
|
|
1930
|
+
return JSON.parse(fs16.readFileSync(path13.join(dir, id, "claim.json"), "utf8"));
|
|
1771
1931
|
} catch {
|
|
1772
1932
|
}
|
|
1773
1933
|
}
|
|
@@ -1817,7 +1977,7 @@ function isUnresumable(error) {
|
|
|
1817
1977
|
return error !== void 0 && UNRESUMABLE.test(error);
|
|
1818
1978
|
}
|
|
1819
1979
|
function worktreeProgress(worktree, trunk) {
|
|
1820
|
-
if (!worktree || !
|
|
1980
|
+
if (!worktree || !fs16.existsSync(worktree))
|
|
1821
1981
|
return "No worktree progress recorded.";
|
|
1822
1982
|
const git2 = (...a) => spawnSync2("git", a, { cwd: worktree, encoding: "utf8" }).stdout?.trim() ?? "";
|
|
1823
1983
|
const commits = trunk ? git2("log", "--oneline", `origin/${trunk}..HEAD`) : git2("log", "--oneline", "-15");
|
|
@@ -1850,31 +2010,31 @@ function handoffNote(fromHarness, progress, why) {
|
|
|
1850
2010
|
${progress}`;
|
|
1851
2011
|
}
|
|
1852
2012
|
function codexHome(env = process.env) {
|
|
1853
|
-
return env.CODEX_HOME ??
|
|
2013
|
+
return env.CODEX_HOME ?? path13.join(os3.homedir(), ".codex");
|
|
1854
2014
|
}
|
|
1855
2015
|
function codexRolloutFile(threadId, home = codexHome()) {
|
|
1856
2016
|
const suffix = `-${threadId}.jsonl`;
|
|
1857
2017
|
const walk = (dir, depth) => {
|
|
1858
2018
|
let entries;
|
|
1859
2019
|
try {
|
|
1860
|
-
entries =
|
|
2020
|
+
entries = fs16.readdirSync(dir, { withFileTypes: true });
|
|
1861
2021
|
} catch {
|
|
1862
2022
|
return void 0;
|
|
1863
2023
|
}
|
|
1864
2024
|
for (const e of entries) {
|
|
1865
2025
|
if (e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(suffix))
|
|
1866
|
-
return
|
|
2026
|
+
return path13.join(dir, e.name);
|
|
1867
2027
|
}
|
|
1868
2028
|
if (depth === 0)
|
|
1869
2029
|
return void 0;
|
|
1870
2030
|
for (const e of entries.filter((x) => x.isDirectory()).sort((a, b) => b.name.localeCompare(a.name))) {
|
|
1871
|
-
const hit = walk(
|
|
2031
|
+
const hit = walk(path13.join(dir, e.name), depth - 1);
|
|
1872
2032
|
if (hit)
|
|
1873
2033
|
return hit;
|
|
1874
2034
|
}
|
|
1875
2035
|
return void 0;
|
|
1876
2036
|
};
|
|
1877
|
-
return walk(
|
|
2037
|
+
return walk(path13.join(home, "sessions"), 3) ?? walk(path13.join(home, "archived_sessions"), 0);
|
|
1878
2038
|
}
|
|
1879
2039
|
function codexDesktopThread(threadId, home = codexHome()) {
|
|
1880
2040
|
const file = codexRolloutFile(threadId, home);
|
|
@@ -1882,12 +2042,12 @@ function codexDesktopThread(threadId, home = codexHome()) {
|
|
|
1882
2042
|
return void 0;
|
|
1883
2043
|
let head = "";
|
|
1884
2044
|
try {
|
|
1885
|
-
const fd =
|
|
2045
|
+
const fd = fs16.openSync(file, "r");
|
|
1886
2046
|
try {
|
|
1887
2047
|
const buf = Buffer.alloc(4096);
|
|
1888
|
-
head = buf.subarray(0,
|
|
2048
|
+
head = buf.subarray(0, fs16.readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
|
|
1889
2049
|
} finally {
|
|
1890
|
-
|
|
2050
|
+
fs16.closeSync(fd);
|
|
1891
2051
|
}
|
|
1892
2052
|
} catch {
|
|
1893
2053
|
return void 0;
|
|
@@ -1927,8 +2087,8 @@ function isFinished(id, lane) {
|
|
|
1927
2087
|
}
|
|
1928
2088
|
|
|
1929
2089
|
// packages/runner/src/run.ts
|
|
1930
|
-
import * as
|
|
1931
|
-
import * as
|
|
2090
|
+
import * as fs20 from "node:fs";
|
|
2091
|
+
import * as path17 from "node:path";
|
|
1932
2092
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1933
2093
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1934
2094
|
|
|
@@ -2224,25 +2384,25 @@ function createClaudeAdapter() {
|
|
|
2224
2384
|
}
|
|
2225
2385
|
|
|
2226
2386
|
// packages/adapters/dist/codex.js
|
|
2227
|
-
import * as
|
|
2387
|
+
import * as fs17 from "node:fs";
|
|
2228
2388
|
import * as os4 from "node:os";
|
|
2229
|
-
import * as
|
|
2389
|
+
import * as path14 from "node:path";
|
|
2230
2390
|
import { spawn as spawn2 } from "node:child_process";
|
|
2231
2391
|
function isolatedCodexHome() {
|
|
2232
|
-
const dir =
|
|
2233
|
-
|
|
2234
|
-
const auth =
|
|
2235
|
-
const realAuth =
|
|
2236
|
-
if (!
|
|
2392
|
+
const dir = path14.join(lobstahHome(), "codex-home");
|
|
2393
|
+
fs17.mkdirSync(dir, { recursive: true });
|
|
2394
|
+
const auth = path14.join(dir, "auth.json");
|
|
2395
|
+
const realAuth = path14.join(os4.homedir(), ".codex", "auth.json");
|
|
2396
|
+
if (!fs17.existsSync(auth) && fs17.existsSync(realAuth)) {
|
|
2237
2397
|
try {
|
|
2238
|
-
|
|
2398
|
+
fs17.symlinkSync(realAuth, auth);
|
|
2239
2399
|
} catch {
|
|
2240
|
-
|
|
2400
|
+
fs17.copyFileSync(realAuth, auth);
|
|
2241
2401
|
}
|
|
2242
2402
|
}
|
|
2243
|
-
const cfg =
|
|
2244
|
-
if (!
|
|
2245
|
-
|
|
2403
|
+
const cfg = path14.join(dir, "config.toml");
|
|
2404
|
+
if (!fs17.existsSync(cfg)) {
|
|
2405
|
+
fs17.writeFileSync(cfg, "# lobstah-managed: auth inherited from ~/.codex, interactive config not\n");
|
|
2246
2406
|
}
|
|
2247
2407
|
return dir;
|
|
2248
2408
|
}
|
|
@@ -2445,8 +2605,8 @@ function loadAdapter(name) {
|
|
|
2445
2605
|
}
|
|
2446
2606
|
|
|
2447
2607
|
// packages/worktree/dist/index.js
|
|
2448
|
-
import * as
|
|
2449
|
-
import * as
|
|
2608
|
+
import * as fs18 from "node:fs";
|
|
2609
|
+
import * as path15 from "node:path";
|
|
2450
2610
|
import { createHash } from "node:crypto";
|
|
2451
2611
|
import { exec, execFile, execFileSync } from "node:child_process";
|
|
2452
2612
|
import { promisify } from "node:util";
|
|
@@ -2519,7 +2679,7 @@ function pidAlive(pid) {
|
|
|
2519
2679
|
var BUSY = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
2520
2680
|
function takeRepoLock(file, mine) {
|
|
2521
2681
|
try {
|
|
2522
|
-
|
|
2682
|
+
fs18.writeFileSync(file, mine, { flag: "wx" });
|
|
2523
2683
|
return { state: "held" };
|
|
2524
2684
|
} catch (err) {
|
|
2525
2685
|
const code = err.code ?? "";
|
|
@@ -2529,8 +2689,8 @@ function takeRepoLock(file, mine) {
|
|
|
2529
2689
|
let raw = "";
|
|
2530
2690
|
let age = 0;
|
|
2531
2691
|
try {
|
|
2532
|
-
raw =
|
|
2533
|
-
age = Date.now() -
|
|
2692
|
+
raw = fs18.readFileSync(file, "utf8");
|
|
2693
|
+
age = Date.now() - fs18.statSync(file).mtimeMs;
|
|
2534
2694
|
} catch {
|
|
2535
2695
|
return { state: "retry" };
|
|
2536
2696
|
}
|
|
@@ -2543,8 +2703,8 @@ function takeRepoLock(file, mine) {
|
|
|
2543
2703
|
const stale = age > REPO_LOCK_WAIT_MS || (holder ? !pidAlive(holder.pid) : age > 5e3);
|
|
2544
2704
|
if (stale) {
|
|
2545
2705
|
try {
|
|
2546
|
-
if (
|
|
2547
|
-
|
|
2706
|
+
if (fs18.readFileSync(file, "utf8") === raw)
|
|
2707
|
+
fs18.rmSync(file, { force: true });
|
|
2548
2708
|
} catch {
|
|
2549
2709
|
}
|
|
2550
2710
|
return { state: "retry" };
|
|
@@ -2553,13 +2713,13 @@ function takeRepoLock(file, mine) {
|
|
|
2553
2713
|
}
|
|
2554
2714
|
function releaseRepoLock(file, mine) {
|
|
2555
2715
|
try {
|
|
2556
|
-
if (
|
|
2557
|
-
|
|
2716
|
+
if (fs18.readFileSync(file, "utf8") === mine)
|
|
2717
|
+
fs18.rmSync(file, { force: true, recursive: true, maxRetries: 5 });
|
|
2558
2718
|
} catch {
|
|
2559
2719
|
}
|
|
2560
2720
|
}
|
|
2561
2721
|
async function withRepoLock(common, fn) {
|
|
2562
|
-
const file =
|
|
2722
|
+
const file = path15.join(common, "lobstah-git.lock");
|
|
2563
2723
|
const mine = JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2564
2724
|
const deadline = Date.now() + REPO_LOCK_WAIT_MS;
|
|
2565
2725
|
let held = false;
|
|
@@ -2595,7 +2755,7 @@ async function oneAtATime(cwd, fn) {
|
|
|
2595
2755
|
}
|
|
2596
2756
|
function fetchStamps(common) {
|
|
2597
2757
|
try {
|
|
2598
|
-
return JSON.parse(
|
|
2758
|
+
return JSON.parse(fs18.readFileSync(path15.join(common, "lobstah-fetch.json"), "utf8"));
|
|
2599
2759
|
} catch {
|
|
2600
2760
|
return {};
|
|
2601
2761
|
}
|
|
@@ -2609,15 +2769,15 @@ async function fetchShared(cwd, remote, ref) {
|
|
|
2609
2769
|
const started = Date.now();
|
|
2610
2770
|
await gitShared(cwd, "fetch", remote, ref);
|
|
2611
2771
|
const stamps = { ...fetchStamps(common), [key]: started };
|
|
2612
|
-
|
|
2772
|
+
fs18.writeFileSync(path15.join(common, "lobstah-fetch.json"), JSON.stringify(stamps, null, 2));
|
|
2613
2773
|
});
|
|
2614
2774
|
}
|
|
2615
2775
|
async function allocate(repo, id, fromRemoteBranch = repo.trunk) {
|
|
2616
2776
|
const dir = worktreePath(id);
|
|
2617
|
-
if (
|
|
2777
|
+
if (fs18.existsSync(dir)) {
|
|
2618
2778
|
throw new Error(`worktree for ${id} already exists at ${dir} \u2014 never allocate a second`);
|
|
2619
2779
|
}
|
|
2620
|
-
if (!
|
|
2780
|
+
if (!fs18.existsSync(repo.path)) {
|
|
2621
2781
|
if (!repo.origin)
|
|
2622
2782
|
throw new Error(`repo path ${repo.path} missing and no origin configured`);
|
|
2623
2783
|
await run("git", ["clone", repo.origin, repo.path], { env: process.env });
|
|
@@ -2655,27 +2815,27 @@ function setupHash(repo, dir) {
|
|
|
2655
2815
|
const h = createHash("sha256");
|
|
2656
2816
|
h.update(JSON.stringify(repo.setup ?? []));
|
|
2657
2817
|
for (const name of LOCKFILES) {
|
|
2658
|
-
const file =
|
|
2659
|
-
if (!
|
|
2818
|
+
const file = path15.join(dir, name);
|
|
2819
|
+
if (!fs18.existsSync(file))
|
|
2660
2820
|
continue;
|
|
2661
2821
|
h.update(`\0${name}\0`);
|
|
2662
|
-
h.update(
|
|
2822
|
+
h.update(fs18.readFileSync(file));
|
|
2663
2823
|
}
|
|
2664
2824
|
return h.digest("hex");
|
|
2665
2825
|
}
|
|
2666
2826
|
function setupRecordFile(dir) {
|
|
2667
2827
|
const gitDir = worktreeGitDir(dir);
|
|
2668
|
-
return gitDir ?
|
|
2828
|
+
return gitDir ? path15.join(gitDir, "lobstah-setup.json") : void 0;
|
|
2669
2829
|
}
|
|
2670
2830
|
function recordSetup(repo, dir) {
|
|
2671
2831
|
const file = setupRecordFile(dir);
|
|
2672
2832
|
if (file)
|
|
2673
|
-
|
|
2833
|
+
fs18.writeFileSync(file, JSON.stringify({ hash: setupHash(repo, dir), at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
2674
2834
|
}
|
|
2675
2835
|
function recordedSetupHash(dir) {
|
|
2676
2836
|
const file = setupRecordFile(dir);
|
|
2677
2837
|
try {
|
|
2678
|
-
return file ? JSON.parse(
|
|
2838
|
+
return file ? JSON.parse(fs18.readFileSync(file, "utf8")).hash : void 0;
|
|
2679
2839
|
} catch {
|
|
2680
2840
|
return void 0;
|
|
2681
2841
|
}
|
|
@@ -2684,9 +2844,9 @@ var short = (s) => s.slice(0, 8);
|
|
|
2684
2844
|
function realpath(p) {
|
|
2685
2845
|
let r;
|
|
2686
2846
|
try {
|
|
2687
|
-
r =
|
|
2847
|
+
r = fs18.realpathSync.native(p);
|
|
2688
2848
|
} catch {
|
|
2689
|
-
r =
|
|
2849
|
+
r = path15.resolve(p);
|
|
2690
2850
|
}
|
|
2691
2851
|
return process.platform === "win32" ? r.toLowerCase() : r;
|
|
2692
2852
|
}
|
|
@@ -2728,8 +2888,8 @@ async function chooseWorktree(input) {
|
|
|
2728
2888
|
for (const t of listTraps()) {
|
|
2729
2889
|
if (typeof t.worktree !== "string")
|
|
2730
2890
|
continue;
|
|
2731
|
-
const rel =
|
|
2732
|
-
if (!rel.startsWith("..") && !
|
|
2891
|
+
const rel = path15.relative(here, realpath(t.worktree));
|
|
2892
|
+
if (!rel.startsWith("..") && !path15.isAbsolute(rel))
|
|
2733
2893
|
return { reuse: false, reason: "origin worktree is a trap\u2019s" };
|
|
2734
2894
|
}
|
|
2735
2895
|
for (const l of ["work", "chore"]) {
|
|
@@ -3137,8 +3297,8 @@ function startWallClock(opts) {
|
|
|
3137
3297
|
import { execFile as execFile2 } from "node:child_process";
|
|
3138
3298
|
import { promisify as promisify2 } from "node:util";
|
|
3139
3299
|
import { fileURLToPath } from "node:url";
|
|
3140
|
-
import * as
|
|
3141
|
-
import * as
|
|
3300
|
+
import * as fs19 from "node:fs";
|
|
3301
|
+
import * as path16 from "node:path";
|
|
3142
3302
|
var exec2 = promisify2(execFile2);
|
|
3143
3303
|
function firstLine(err) {
|
|
3144
3304
|
const stderr = err && typeof err === "object" && "stderr" in err ? String(err.stderr).trim() : "";
|
|
@@ -3168,10 +3328,10 @@ function keepRemote(opts) {
|
|
|
3168
3328
|
const note = (message) => mergeEvidence(id, lane, { note: message.slice(0, 300) });
|
|
3169
3329
|
if (existingPrUrl) mergeEvidence(id, lane, { prUrl: existingPrUrl });
|
|
3170
3330
|
const registerWatch = async (url) => {
|
|
3171
|
-
const root =
|
|
3172
|
-
const localCli =
|
|
3331
|
+
const root = path16.resolve(path16.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
3332
|
+
const localCli = path16.join(root, "apps", "cli", "dist", "main.js");
|
|
3173
3333
|
const args = ["watch", "add", url, "--for", id];
|
|
3174
|
-
if (
|
|
3334
|
+
if (fs19.existsSync(localCli)) await command(process.execPath, [localCli, ...args], 15e3);
|
|
3175
3335
|
else await command("lobstah", args, 15e3);
|
|
3176
3336
|
};
|
|
3177
3337
|
let lastSeenHead;
|
|
@@ -3350,16 +3510,16 @@ function coldNote(cold, cwd, trunk) {
|
|
|
3350
3510
|
}
|
|
3351
3511
|
async function main(activeDir, lane, seams = {}) {
|
|
3352
3512
|
const deps = { ...defaultDeps, ...seams };
|
|
3353
|
-
const id =
|
|
3513
|
+
const id = path17.basename(activeDir);
|
|
3354
3514
|
if (isTrapCatch(id, lane)) throw new Error(`refusing headless runner for trap catch ${id}`);
|
|
3355
3515
|
const status = (verb, note) => appendStatus(id, lane, verb, note);
|
|
3356
3516
|
const cfg = loadConfig();
|
|
3357
|
-
const descriptor = JSON.parse(
|
|
3517
|
+
const descriptor = JSON.parse(fs20.readFileSync(path17.join(activeDir, "descriptor.json"), "utf8"));
|
|
3358
3518
|
const resolved = resolveDispatch(descriptor, cfg);
|
|
3359
3519
|
const repo = cfg.repos[descriptor.repo];
|
|
3360
|
-
const briefFile =
|
|
3361
|
-
if (!
|
|
3362
|
-
const brief =
|
|
3520
|
+
const briefFile = path17.join(activeDir, "brief.md");
|
|
3521
|
+
if (!fs20.existsSync(briefFile)) fs20.writeFileSync(briefFile, descriptor.brief);
|
|
3522
|
+
const brief = fs20.readFileSync(briefFile, "utf8");
|
|
3363
3523
|
const attempts = Number(process.env.LOBSTAH_ATTEMPTS ?? "1");
|
|
3364
3524
|
const wake = process.env.LOBSTAH_WAKE;
|
|
3365
3525
|
const runnerInfo = {
|
|
@@ -3367,7 +3527,7 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
3367
3527
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3368
3528
|
attempts
|
|
3369
3529
|
};
|
|
3370
|
-
|
|
3530
|
+
fs20.writeFileSync(path17.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
|
|
3371
3531
|
if (descriptor.systemRepair && descriptor.pr?.url) {
|
|
3372
3532
|
const state = deps.prState(descriptor.pr.url) ?? readPr(parsePrRef(descriptor.pr.url)?.key ?? "")?.state;
|
|
3373
3533
|
if (state === "MERGED" || state === "CLOSED") {
|
|
@@ -3384,13 +3544,13 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
3384
3544
|
resolvedHarness: resolved.harness,
|
|
3385
3545
|
envResume: process.env.LOBSTAH_RESUME
|
|
3386
3546
|
});
|
|
3387
|
-
const wtFile =
|
|
3547
|
+
const wtFile = path17.join(activeDir, "worktree.json");
|
|
3388
3548
|
let recorded;
|
|
3389
3549
|
let choice;
|
|
3390
|
-
if (
|
|
3391
|
-
recorded = JSON.parse(
|
|
3392
|
-
if (!
|
|
3393
|
-
} else if (
|
|
3550
|
+
if (fs20.existsSync(wtFile)) {
|
|
3551
|
+
recorded = JSON.parse(fs20.readFileSync(wtFile, "utf8")).path;
|
|
3552
|
+
if (!fs20.existsSync(recorded)) throw new Error(`recorded worktree ${recorded} is gone`);
|
|
3553
|
+
} else if (fs20.existsSync(worktreePath(id))) {
|
|
3394
3554
|
throw new Error(`unrecorded worktree already exists for ${id} \u2014 refusing to proceed`);
|
|
3395
3555
|
} else if (descriptor.followUp && cfg.limits.reuseWorktree !== false) {
|
|
3396
3556
|
choice = await deps.chooseWorktree({ id, lane, repoKey: descriptor.repo, repo, followUp: descriptor.followUp });
|
|
@@ -3424,13 +3584,13 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
3424
3584
|
type: "runner",
|
|
3425
3585
|
data: { worktree: cwd, worktreeOf: choice.owner, setupRan }
|
|
3426
3586
|
});
|
|
3427
|
-
|
|
3587
|
+
fs20.writeFileSync(wtFile, JSON.stringify({ path: cwd, of: choice.owner }, null, 2));
|
|
3428
3588
|
} else {
|
|
3429
3589
|
cwd = await deps.allocate(repo, id, descriptor.systemRepair ? descriptor.pr?.headRefName : void 0);
|
|
3430
3590
|
acquireWorktreeLock(cwd, id, lane);
|
|
3431
|
-
|
|
3591
|
+
fs20.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
|
|
3432
3592
|
}
|
|
3433
|
-
const worktreeOf = JSON.parse(
|
|
3593
|
+
const worktreeOf = JSON.parse(fs20.readFileSync(wtFile, "utf8")).of;
|
|
3434
3594
|
mergeEvidence(id, lane, { worktree: cwd, worktreeOf });
|
|
3435
3595
|
const existingPr = boundPr(descriptor, lane);
|
|
3436
3596
|
if (existingPr) mergeEvidence(id, lane, { prUrl: existingPr.url });
|
|
@@ -3461,14 +3621,14 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
3461
3621
|
const envNudge = [process.env.LOBSTAH_NUDGE, wakeNote()].filter(Boolean).join("\n\n") || void 0;
|
|
3462
3622
|
const planNudge = plan.cold && !envNudge ? coldNote(plan.cold, cwd, repo.trunk) : void 0;
|
|
3463
3623
|
const promptWith = (nudge) => buildPrompt(brief, { id, nudge, attachments: descriptor.attachments, existingPr });
|
|
3464
|
-
const binDir =
|
|
3465
|
-
const workerPath =
|
|
3624
|
+
const binDir = path17.resolve(path17.dirname(fileURLToPath2(import.meta.url)), "..", "..", "..", "bin");
|
|
3625
|
+
const workerPath = fs20.existsSync(path17.join(binDir, "lobstah")) ? `${binDir}${path17.delimiter}${process.env.PATH ?? ""}` : void 0;
|
|
3466
3626
|
let wallClockHit = false;
|
|
3467
3627
|
let current;
|
|
3468
|
-
const wallFile =
|
|
3628
|
+
const wallFile = path17.join(activeDir, "wallclock.json");
|
|
3469
3629
|
const wallState = (() => {
|
|
3470
3630
|
try {
|
|
3471
|
-
return JSON.parse(
|
|
3631
|
+
return JSON.parse(fs20.readFileSync(wallFile, "utf8"));
|
|
3472
3632
|
} catch {
|
|
3473
3633
|
return { elapsedMs: 0, windowMs: resolved.limits.wallClockSecs * 1e3 };
|
|
3474
3634
|
}
|
|
@@ -3487,8 +3647,8 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
3487
3647
|
) * 1e3;
|
|
3488
3648
|
const persistWall = (elapsedMs, windowMs) => {
|
|
3489
3649
|
const tmp = `${wallFile}.tmp-${process.pid}`;
|
|
3490
|
-
|
|
3491
|
-
|
|
3650
|
+
fs20.writeFileSync(tmp, JSON.stringify({ ...wallState, elapsedMs, windowMs }));
|
|
3651
|
+
fs20.renameSync(tmp, wallFile);
|
|
3492
3652
|
};
|
|
3493
3653
|
const madeProgress = () => {
|
|
3494
3654
|
const activity = readActivity(id, lane);
|
|
@@ -3738,13 +3898,13 @@ async function reapStarted(root = process.pid, graceMs) {
|
|
|
3738
3898
|
var EXIT_LINGER_MS = 5e3;
|
|
3739
3899
|
function runRunner(activeDirArg2, laneArg2) {
|
|
3740
3900
|
const lane = laneArg2 === "chore" ? "chore" : "work";
|
|
3741
|
-
const id =
|
|
3901
|
+
const id = path18.basename(activeDirArg2);
|
|
3742
3902
|
main(activeDirArg2, lane, { reap: () => reapStarted(process.pid) }).catch((err) => {
|
|
3743
3903
|
try {
|
|
3744
3904
|
if (!isFinished(id, lane)) {
|
|
3745
3905
|
appendStatus(id, lane, "failed", String(err instanceof Error ? err.message : err).slice(0, 500));
|
|
3746
3906
|
}
|
|
3747
|
-
|
|
3907
|
+
fs21.renameSync(activeDirArg2, path18.join(laneDirs(lane).done, id));
|
|
3748
3908
|
} catch {
|
|
3749
3909
|
}
|
|
3750
3910
|
process.exitCode = 1;
|