lobstah 0.5.11 → 0.5.13
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 +2 -1
- package/dist/main.js +4681 -1752
- package/dist/runner.js +1163 -152
- package/docs/configuration.md +29 -7
- package/docs/design.md +1 -1
- package/docs/harness/claude-code.md +7 -3
- package/docs/harness/codex.md +43 -5
- package/docs/man.md +196 -29
- package/docs/pickup.md +10 -0
- package/docs/vocabulary.md +126 -36
- package/package.json +1 -1
package/dist/runner.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// packages/runner/src/index.ts
|
|
2
|
-
import * as
|
|
3
|
-
import * as
|
|
2
|
+
import * as fs18 from "node:fs";
|
|
3
|
+
import * as path16 from "node:path";
|
|
4
4
|
|
|
5
5
|
// packages/core/dist/types.js
|
|
6
6
|
var VERBS = ["working", "needs-decision", "blocked", "paused", "done", "failed"];
|
|
7
7
|
var TERMINAL_VERBS = ["done", "failed"];
|
|
8
|
+
var WAITING_ON = ["review", "pr", "deploy", "person", "external"];
|
|
9
|
+
var WAITING_ON_VERBS = ["paused", "needs-decision", "blocked"];
|
|
8
10
|
|
|
9
11
|
// packages/core/dist/paths.js
|
|
10
12
|
import * as os from "node:os";
|
|
@@ -34,6 +36,9 @@ function statusPath(id, lane) {
|
|
|
34
36
|
function eventsPath(id, lane) {
|
|
35
37
|
return path.join(laneDirs(lane).state, `${id}.events`);
|
|
36
38
|
}
|
|
39
|
+
function activityPath(id, lane) {
|
|
40
|
+
return path.join(laneDirs(lane).state, `${id}.activity`);
|
|
41
|
+
}
|
|
37
42
|
function evidencePath(id, lane) {
|
|
38
43
|
return path.join(laneDirs(lane).state, `${id}.evidence`);
|
|
39
44
|
}
|
|
@@ -50,28 +55,281 @@ function attachmentBlock(attachments) {
|
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
// packages/core/dist/queue.js
|
|
58
|
+
import * as fs3 from "node:fs";
|
|
59
|
+
import * as path3 from "node:path";
|
|
60
|
+
|
|
61
|
+
// packages/core/dist/status.js
|
|
53
62
|
import * as fs2 from "node:fs";
|
|
63
|
+
|
|
64
|
+
// packages/core/dist/activity.js
|
|
65
|
+
import * as fs from "node:fs";
|
|
54
66
|
import * as path2 from "node:path";
|
|
67
|
+
var ACTIVITY_KINDS = ["tool", "message", "thinking", "waiting"];
|
|
68
|
+
var ACTIVITY_SUMMARY_MAX = 80;
|
|
69
|
+
var SECRET_PATTERNS = [
|
|
70
|
+
/\b(?:sk|pk|rk)[-_][A-Za-z0-9_-]{8,}/i,
|
|
71
|
+
/\b(?:gh[posur]_|github_pat_|glpat-|xox[abprs]-|npm_|hf_)[A-Za-z0-9_-]{16,}/,
|
|
72
|
+
/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
|
|
73
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/,
|
|
74
|
+
/\bbearer\b/i,
|
|
75
|
+
/(?:token|secret|passw(?:or)?d|pwd|api[-_]?key|auth|credential|private[-_]?key)[^\s]*[=:]/i
|
|
76
|
+
];
|
|
77
|
+
function looksSecret(word) {
|
|
78
|
+
if (SECRET_PATTERNS.some((re) => re.test(word)))
|
|
79
|
+
return true;
|
|
80
|
+
for (const run2 of word.match(/[A-Za-z0-9+/_=-]{24,}/g) ?? []) {
|
|
81
|
+
if (/[0-9]/.test(run2) && /[A-Za-z]/.test(run2))
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
function redactSummary(s, max = ACTIVITY_SUMMARY_MAX) {
|
|
87
|
+
const clean = s.replace(/[\r\n\t]+/g, " ").split(" ").filter(Boolean).map((w) => looksSecret(w) ? "[redacted]" : w).join(" ");
|
|
88
|
+
return clean.length > max ? `${clean.slice(0, max - 1)}\u2026` : clean;
|
|
89
|
+
}
|
|
90
|
+
var SHELLS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "fish", "pwsh", "powershell", "cmd"]);
|
|
91
|
+
function commandWord(command) {
|
|
92
|
+
let words = command.trim().split(/\s+/).filter(Boolean);
|
|
93
|
+
while (words.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]))
|
|
94
|
+
words = words.slice(1);
|
|
95
|
+
if (words[0] === "env") {
|
|
96
|
+
words = words.slice(1);
|
|
97
|
+
while (words.length > 0 && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]) || words[0].startsWith("-")))
|
|
98
|
+
words = words.slice(1);
|
|
99
|
+
}
|
|
100
|
+
const first = words[0];
|
|
101
|
+
if (!first)
|
|
102
|
+
return void 0;
|
|
103
|
+
const name = baseName(first.replace(/^["']|["']$/g, "")).replace(/\.(exe|cmd|bat)$/i, "");
|
|
104
|
+
if (SHELLS.has(name.toLowerCase()) && words[1] && /^(-\w*c|\/c|-command)$/i.test(words[1])) {
|
|
105
|
+
return commandWord(words.slice(2).join(" ").replace(/^["']/, "")) ?? name;
|
|
106
|
+
}
|
|
107
|
+
return name || void 0;
|
|
108
|
+
}
|
|
109
|
+
function baseName(p) {
|
|
110
|
+
const parts = p.split(/[\\/]/).filter(Boolean);
|
|
111
|
+
return parts.at(-1) ?? p;
|
|
112
|
+
}
|
|
113
|
+
function isWindowsPath(p) {
|
|
114
|
+
return /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\");
|
|
115
|
+
}
|
|
116
|
+
function relativeTarget(p, root) {
|
|
117
|
+
if (!root)
|
|
118
|
+
return baseName(p);
|
|
119
|
+
const win = isWindowsPath(p) || isWindowsPath(root);
|
|
120
|
+
const lib = win ? path2.win32 : path2.posix;
|
|
121
|
+
if (!lib.isAbsolute(p))
|
|
122
|
+
return p.replace(/\\/g, "/");
|
|
123
|
+
const rel = lib.relative(win ? root : root.replace(/\\/g, "/"), p);
|
|
124
|
+
if (!rel || rel.startsWith("..") || lib.isAbsolute(rel))
|
|
125
|
+
return `\u2026/${baseName(p)}`;
|
|
126
|
+
return rel.replace(/\\/g, "/");
|
|
127
|
+
}
|
|
128
|
+
function urlHost(u) {
|
|
129
|
+
try {
|
|
130
|
+
return new URL(u).host || void 0;
|
|
131
|
+
} catch {
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function toolTarget(input) {
|
|
136
|
+
if (!input || typeof input !== "object")
|
|
137
|
+
return void 0;
|
|
138
|
+
const i = input;
|
|
139
|
+
const str = (k) => typeof i[k] === "string" && i[k].length > 0 ? i[k] : void 0;
|
|
140
|
+
const file = str("file_path") ?? str("notebook_path") ?? str("path");
|
|
141
|
+
if (file)
|
|
142
|
+
return file;
|
|
143
|
+
const command = str("command") ?? (Array.isArray(i.command) ? i.command.map(String).join(" ") : void 0);
|
|
144
|
+
if (command)
|
|
145
|
+
return commandWord(command);
|
|
146
|
+
const url = str("url");
|
|
147
|
+
if (url)
|
|
148
|
+
return urlHost(url);
|
|
149
|
+
const changes = i.changes;
|
|
150
|
+
if (Array.isArray(changes) && changes.length > 0) {
|
|
151
|
+
const first = changes[0];
|
|
152
|
+
if (typeof first?.path === "string")
|
|
153
|
+
return first.path;
|
|
154
|
+
}
|
|
155
|
+
return str("subagent_type") ?? str("skill");
|
|
156
|
+
}
|
|
157
|
+
function toolSummary(name, target, root) {
|
|
158
|
+
const looksPath = target !== void 0 && (/[\\/]/.test(target) || isWindowsPath(target));
|
|
159
|
+
const shown = target === void 0 ? "" : looksPath ? relativeTarget(target, root) : target;
|
|
160
|
+
return redactSummary(shown ? `${name} ${shown}` : name);
|
|
161
|
+
}
|
|
162
|
+
function activityFromEvent(ev, root) {
|
|
163
|
+
const data = ev.data ?? {};
|
|
164
|
+
switch (ev.type) {
|
|
165
|
+
case "tool-start": {
|
|
166
|
+
const name = typeof data.name === "string" && data.name ? data.name : "tool";
|
|
167
|
+
const target = typeof data.target === "string" ? data.target : void 0;
|
|
168
|
+
return { kind: "tool", summary: toolSummary(name, target, root) };
|
|
169
|
+
}
|
|
170
|
+
case "text":
|
|
171
|
+
return { kind: "message", summary: "writing a message" };
|
|
172
|
+
case "thinking":
|
|
173
|
+
return { kind: "thinking", summary: "thinking" };
|
|
174
|
+
case "runner": {
|
|
175
|
+
if (typeof data.on === "string")
|
|
176
|
+
return { kind: "waiting", summary: redactSummary(`waiting on ${data.on}`) };
|
|
177
|
+
if (typeof data.waiting === "string")
|
|
178
|
+
return { kind: "waiting", summary: redactSummary(`waiting for an answer (${data.waiting})`) };
|
|
179
|
+
if (data.holding === "background")
|
|
180
|
+
return { kind: "waiting", summary: "waiting on background work" };
|
|
181
|
+
return void 0;
|
|
182
|
+
}
|
|
183
|
+
default:
|
|
184
|
+
return void 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function atomicWrite(file, content) {
|
|
188
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
189
|
+
fs.writeFileSync(tmp, content);
|
|
190
|
+
fs.renameSync(tmp, file);
|
|
191
|
+
}
|
|
192
|
+
function writeActivity(id, lane, a) {
|
|
193
|
+
const safe = { at: a.at, kind: a.kind, summary: redactSummary(a.summary) };
|
|
194
|
+
atomicWrite(activityPath(id, lane), JSON.stringify(safe));
|
|
195
|
+
}
|
|
196
|
+
function readActivity(id, lane) {
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(fs.readFileSync(activityPath(id, lane), "utf8"));
|
|
199
|
+
if (typeof parsed.at !== "string" || typeof parsed.summary !== "string")
|
|
200
|
+
return void 0;
|
|
201
|
+
if (!ACTIVITY_KINDS.includes(parsed.kind))
|
|
202
|
+
return void 0;
|
|
203
|
+
return { ...parsed, summary: redactSummary(parsed.summary) };
|
|
204
|
+
} catch {
|
|
205
|
+
return void 0;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
var ActivityTracker = class {
|
|
209
|
+
write;
|
|
210
|
+
opts;
|
|
211
|
+
lastWriteAt = -Infinity;
|
|
212
|
+
lastKind;
|
|
213
|
+
pending;
|
|
214
|
+
timer;
|
|
215
|
+
throttleMs;
|
|
216
|
+
now;
|
|
217
|
+
constructor(write, opts = {}) {
|
|
218
|
+
this.write = write;
|
|
219
|
+
this.opts = opts;
|
|
220
|
+
this.throttleMs = opts.throttleMs ?? 1e4;
|
|
221
|
+
this.now = opts.now ?? Date.now;
|
|
222
|
+
}
|
|
223
|
+
observe(ev) {
|
|
224
|
+
const derived = activityFromEvent(ev, this.opts.root);
|
|
225
|
+
if (derived)
|
|
226
|
+
this.offer({ at: new Date(this.now()).toISOString(), ...derived });
|
|
227
|
+
}
|
|
228
|
+
offer(a) {
|
|
229
|
+
const t = this.now();
|
|
230
|
+
if (a.kind !== this.lastKind || t - this.lastWriteAt >= this.throttleMs) {
|
|
231
|
+
this.emit(a, t);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
this.pending = a;
|
|
235
|
+
if (!this.timer) {
|
|
236
|
+
this.timer = setTimeout(() => {
|
|
237
|
+
this.timer = void 0;
|
|
238
|
+
this.flush();
|
|
239
|
+
}, Math.max(0, this.throttleMs - (t - this.lastWriteAt)));
|
|
240
|
+
this.timer.unref?.();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** Write a held record now. */
|
|
244
|
+
flush() {
|
|
245
|
+
if (!this.pending)
|
|
246
|
+
return;
|
|
247
|
+
const a = this.pending;
|
|
248
|
+
this.emit(a, this.now());
|
|
249
|
+
}
|
|
250
|
+
stop() {
|
|
251
|
+
if (this.timer)
|
|
252
|
+
clearTimeout(this.timer);
|
|
253
|
+
this.timer = void 0;
|
|
254
|
+
this.pending = void 0;
|
|
255
|
+
}
|
|
256
|
+
emit(a, t) {
|
|
257
|
+
this.pending = void 0;
|
|
258
|
+
if (this.timer) {
|
|
259
|
+
clearTimeout(this.timer);
|
|
260
|
+
this.timer = void 0;
|
|
261
|
+
}
|
|
262
|
+
this.lastWriteAt = t;
|
|
263
|
+
this.lastKind = a.kind;
|
|
264
|
+
try {
|
|
265
|
+
this.write(a);
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
55
270
|
|
|
56
271
|
// packages/core/dist/status.js
|
|
57
|
-
import * as fs from "node:fs";
|
|
58
272
|
function isVerb(v) {
|
|
59
273
|
return VERBS.includes(v);
|
|
60
274
|
}
|
|
61
|
-
function
|
|
275
|
+
function isWaitingOn(v) {
|
|
276
|
+
return WAITING_ON.includes(v);
|
|
277
|
+
}
|
|
278
|
+
var DURATION = /^(\d+(?:\.\d+)?)\s*(s|m|h|d)$/i;
|
|
279
|
+
var UNIT_MS = { s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
|
|
280
|
+
function parseUntil(v, now2 = Date.now()) {
|
|
281
|
+
const d = DURATION.exec(v.trim());
|
|
282
|
+
if (d)
|
|
283
|
+
return new Date(now2 + Number(d[1]) * UNIT_MS[d[2].toLowerCase()]).toISOString();
|
|
284
|
+
const t = Date.parse(v);
|
|
285
|
+
if (Number.isNaN(t))
|
|
286
|
+
throw new Error(`invalid --until "${v}" \u2014 an ISO time (2026-10-01T09:00Z) or a duration (30m, 4h, 2d)`);
|
|
287
|
+
return new Date(t).toISOString();
|
|
288
|
+
}
|
|
289
|
+
function waitingFields(verb, w, now2 = Date.now()) {
|
|
290
|
+
const out = {};
|
|
291
|
+
if ((w.waitingOn !== void 0 || w.link !== void 0) && !WAITING_ON_VERBS.includes(verb)) {
|
|
292
|
+
throw new Error(`--waiting-on and --link are valid only with ${WAITING_ON_VERBS.join(", ")} \u2014 not ${verb}`);
|
|
293
|
+
}
|
|
294
|
+
if (w.until !== void 0 && verb !== "paused")
|
|
295
|
+
throw new Error(`--until is valid only with paused \u2014 not ${verb}`);
|
|
296
|
+
if (w.waitingOn !== void 0) {
|
|
297
|
+
if (!isWaitingOn(w.waitingOn)) {
|
|
298
|
+
throw new Error(`invalid --waiting-on "${w.waitingOn}" \u2014 must be one of: ${WAITING_ON.join(", ")}`);
|
|
299
|
+
}
|
|
300
|
+
out.waitingOn = w.waitingOn;
|
|
301
|
+
}
|
|
302
|
+
if (w.link !== void 0) {
|
|
303
|
+
let url;
|
|
304
|
+
try {
|
|
305
|
+
url = new URL(w.link);
|
|
306
|
+
} catch {
|
|
307
|
+
url = void 0;
|
|
308
|
+
}
|
|
309
|
+
if (!url || url.protocol !== "http:" && url.protocol !== "https:") {
|
|
310
|
+
throw new Error(`invalid --link "${w.link}" \u2014 must be an http or https URL`);
|
|
311
|
+
}
|
|
312
|
+
out.link = url.href;
|
|
313
|
+
}
|
|
314
|
+
if (w.until !== void 0)
|
|
315
|
+
out.until = parseUntil(w.until, now2);
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
function appendStatus(id, lane, verb, note, at, waiting) {
|
|
62
319
|
if (!isVerb(verb)) {
|
|
63
320
|
throw new Error(`invalid status verb "${verb}" \u2014 must be one of: ${VERBS.join(", ")}`);
|
|
64
321
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
322
|
+
const extra = waiting ? waitingFields(verb, waiting) : {};
|
|
323
|
+
const entry = { at: at ?? (/* @__PURE__ */ new Date()).toISOString(), verb, ...note ? { note } : {}, ...extra };
|
|
324
|
+
fs2.appendFileSync(statusPath(id, lane), `${JSON.stringify(entry)}
|
|
67
325
|
`);
|
|
68
326
|
return entry;
|
|
69
327
|
}
|
|
70
328
|
function readStatusLog(id, lane) {
|
|
71
329
|
const file = statusPath(id, lane);
|
|
72
|
-
if (!
|
|
330
|
+
if (!fs2.existsSync(file))
|
|
73
331
|
return [];
|
|
74
|
-
return
|
|
332
|
+
return fs2.readFileSync(file, "utf8").split("\n").filter(Boolean).flatMap((line) => {
|
|
75
333
|
try {
|
|
76
334
|
const parsed = JSON.parse(line);
|
|
77
335
|
return isVerb(parsed.verb) ? [parsed] : [];
|
|
@@ -80,51 +338,57 @@ function readStatusLog(id, lane) {
|
|
|
80
338
|
}
|
|
81
339
|
});
|
|
82
340
|
}
|
|
341
|
+
function pausedWaiting(entry) {
|
|
342
|
+
return entry?.verb === "paused" && entry.waitingOn !== void 0;
|
|
343
|
+
}
|
|
83
344
|
|
|
84
345
|
// packages/core/dist/queue.js
|
|
85
346
|
function storedDescriptor(id, lane) {
|
|
86
347
|
const dirs = laneDirs(lane);
|
|
87
348
|
for (const file of [
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
349
|
+
path3.join(dirs.queue, `${id}.json`),
|
|
350
|
+
path3.join(dirs.active, id, "descriptor.json"),
|
|
351
|
+
path3.join(dirs.done, id, "descriptor.json")
|
|
91
352
|
]) {
|
|
92
353
|
try {
|
|
93
|
-
return JSON.parse(
|
|
354
|
+
return JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
94
355
|
} catch {
|
|
95
356
|
}
|
|
96
357
|
}
|
|
97
358
|
return void 0;
|
|
98
359
|
}
|
|
360
|
+
function activeIds(lane) {
|
|
361
|
+
return fs3.readdirSync(laneDirs(lane).active).filter((f) => !f.startsWith("."));
|
|
362
|
+
}
|
|
99
363
|
function complete(id, lane) {
|
|
100
364
|
const dirs = laneDirs(lane);
|
|
101
|
-
|
|
365
|
+
fs3.renameSync(path3.join(dirs.active, id), path3.join(dirs.done, id));
|
|
102
366
|
}
|
|
103
367
|
function cancelRequested(id, lane) {
|
|
104
|
-
return
|
|
368
|
+
return fs3.existsSync(path3.join(laneDirs(lane).active, id, "cancel"));
|
|
105
369
|
}
|
|
106
370
|
|
|
107
371
|
// packages/core/dist/events.js
|
|
108
|
-
import * as
|
|
372
|
+
import * as fs4 from "node:fs";
|
|
109
373
|
function appendEvent(id, lane, ev) {
|
|
110
|
-
|
|
374
|
+
fs4.appendFileSync(eventsPath(id, lane), `${JSON.stringify(ev)}
|
|
111
375
|
`);
|
|
112
376
|
}
|
|
113
377
|
function touchEvents(id, lane, at = /* @__PURE__ */ new Date()) {
|
|
114
378
|
const file = eventsPath(id, lane);
|
|
115
379
|
try {
|
|
116
|
-
|
|
380
|
+
fs4.utimesSync(file, at, at);
|
|
117
381
|
} catch {
|
|
118
|
-
|
|
119
|
-
|
|
382
|
+
fs4.appendFileSync(file, "");
|
|
383
|
+
fs4.utimesSync(file, at, at);
|
|
120
384
|
}
|
|
121
385
|
}
|
|
122
386
|
|
|
123
387
|
// packages/core/dist/evidence.js
|
|
124
|
-
import * as
|
|
388
|
+
import * as fs5 from "node:fs";
|
|
125
389
|
function readEvidence(id, lane) {
|
|
126
390
|
try {
|
|
127
|
-
return JSON.parse(
|
|
391
|
+
return JSON.parse(fs5.readFileSync(evidencePath(id, lane), "utf8"));
|
|
128
392
|
} catch {
|
|
129
393
|
return {};
|
|
130
394
|
}
|
|
@@ -132,37 +396,70 @@ function readEvidence(id, lane) {
|
|
|
132
396
|
function mergeEvidence(id, lane, patch) {
|
|
133
397
|
const file = evidencePath(id, lane);
|
|
134
398
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
135
|
-
|
|
136
|
-
|
|
399
|
+
fs5.writeFileSync(tmp, JSON.stringify({ ...readEvidence(id, lane), ...patch }, null, 2));
|
|
400
|
+
fs5.renameSync(tmp, file);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// packages/core/dist/inbox.js
|
|
404
|
+
import * as fs7 from "node:fs";
|
|
405
|
+
import * as path5 from "node:path";
|
|
406
|
+
|
|
407
|
+
// packages/core/dist/soak.js
|
|
408
|
+
import * as fs6 from "node:fs";
|
|
409
|
+
import * as path4 from "node:path";
|
|
410
|
+
function regPath(trapId) {
|
|
411
|
+
return path4.join(soakingDir(), `${trapId}.json`);
|
|
412
|
+
}
|
|
413
|
+
function readTrap(trapId) {
|
|
414
|
+
try {
|
|
415
|
+
return JSON.parse(fs6.readFileSync(regPath(trapId), "utf8"));
|
|
416
|
+
} catch {
|
|
417
|
+
return void 0;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function listTraps() {
|
|
421
|
+
try {
|
|
422
|
+
return fs6.readdirSync(soakingDir()).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(fs6.readFileSync(path4.join(soakingDir(), f), "utf8"))).filter((r) => typeof r.trapId === "string");
|
|
423
|
+
} catch {
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function claimPath(id, lane) {
|
|
428
|
+
return path4.join(laneDirs(lane).active, id, "claim.json");
|
|
429
|
+
}
|
|
430
|
+
function readSessionClaim(id, lane) {
|
|
431
|
+
try {
|
|
432
|
+
return JSON.parse(fs6.readFileSync(claimPath(id, lane), "utf8"));
|
|
433
|
+
} catch {
|
|
434
|
+
return void 0;
|
|
435
|
+
}
|
|
137
436
|
}
|
|
138
437
|
|
|
139
438
|
// packages/core/dist/inbox.js
|
|
140
|
-
import * as fs5 from "node:fs";
|
|
141
|
-
import * as path3 from "node:path";
|
|
142
439
|
var metaName = (file) => file.replace(/\.msg$/, ".meta.json");
|
|
143
440
|
function inboxDir(id, lane) {
|
|
144
|
-
return
|
|
441
|
+
return path5.join(laneDirs(lane).inbox, id);
|
|
145
442
|
}
|
|
146
443
|
function unhandled(id, lane) {
|
|
147
444
|
const dir = inboxDir(id, lane);
|
|
148
|
-
if (!
|
|
445
|
+
if (!fs7.existsSync(dir))
|
|
149
446
|
return [];
|
|
150
|
-
return
|
|
447
|
+
return fs7.readdirSync(dir).filter((f) => f.endsWith(".msg")).sort().map((f) => ({ file: f, text: fs7.readFileSync(path5.join(dir, f), "utf8") }));
|
|
151
448
|
}
|
|
152
449
|
function acknowledge(id, lane, file) {
|
|
153
450
|
const dir = inboxDir(id, lane);
|
|
154
|
-
|
|
155
|
-
|
|
451
|
+
fs7.mkdirSync(path5.join(dir, "handled"), { recursive: true });
|
|
452
|
+
fs7.renameSync(path5.join(dir, file), path5.join(dir, "handled", file));
|
|
156
453
|
try {
|
|
157
|
-
|
|
454
|
+
fs7.renameSync(path5.join(dir, metaName(file)), path5.join(dir, "handled", metaName(file)));
|
|
158
455
|
} catch {
|
|
159
456
|
}
|
|
160
457
|
}
|
|
161
458
|
|
|
162
459
|
// packages/core/dist/config.js
|
|
163
|
-
import * as
|
|
460
|
+
import * as fs8 from "node:fs";
|
|
164
461
|
import * as os2 from "node:os";
|
|
165
|
-
import * as
|
|
462
|
+
import * as path6 from "node:path";
|
|
166
463
|
|
|
167
464
|
// node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/date.js
|
|
168
465
|
var DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[T ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|[-+]\d{2}:\d{2})?$/i;
|
|
@@ -811,7 +1108,7 @@ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
|
|
|
811
1108
|
|
|
812
1109
|
// packages/core/dist/config.js
|
|
813
1110
|
var ATTENTION_KINDS = ["question", "landed", "pr:draft", "pr:review", "pr:checks", "pr:conflict", "pr:ready"];
|
|
814
|
-
var DEFAULT_ATTENTION_KINDS = ["question", "pr:
|
|
1111
|
+
var DEFAULT_ATTENTION_KINDS = ["question", "pr:ready", "pr:review", "pr:conflict", "pr:checks"];
|
|
815
1112
|
function parseAttentionKinds(raw) {
|
|
816
1113
|
if (raw === void 0)
|
|
817
1114
|
return [...DEFAULT_ATTENTION_KINDS];
|
|
@@ -827,7 +1124,9 @@ function parseAttentionKinds(raw) {
|
|
|
827
1124
|
}
|
|
828
1125
|
var DEFAULT_SOAK = {
|
|
829
1126
|
deferSecs: 90,
|
|
830
|
-
ttlSecs: 1800
|
|
1127
|
+
ttlSecs: 1800,
|
|
1128
|
+
beat: true,
|
|
1129
|
+
pausedTtlSecs: 86400
|
|
831
1130
|
};
|
|
832
1131
|
var DEFAULT_HELM = {
|
|
833
1132
|
ttlSecs: 1800,
|
|
@@ -835,28 +1134,33 @@ var DEFAULT_HELM = {
|
|
|
835
1134
|
armGraceSecs: 5
|
|
836
1135
|
};
|
|
837
1136
|
var DEFAULT_GLASS = { port: 4949 };
|
|
838
|
-
var DEFAULT_WATCH = { maxForksPerCycle: 3 };
|
|
1137
|
+
var DEFAULT_WATCH = { maxForksPerCycle: 3, autoRepair: true, conflicts: true, checks: true, maxRepairsPerPr: 2 };
|
|
839
1138
|
var DEFAULT_LIMITS = {
|
|
840
1139
|
maxConcurrent: 2,
|
|
841
1140
|
choreConcurrent: 1,
|
|
842
1141
|
wedgeThresholdSecs: 600,
|
|
843
1142
|
maxRestartAttempts: 2,
|
|
844
1143
|
wallClockSecs: 3600,
|
|
1144
|
+
pushEarly: true,
|
|
1145
|
+
draftPr: true,
|
|
1146
|
+
checkpointOnStop: true,
|
|
845
1147
|
backgroundWaitSecs: 1800,
|
|
846
1148
|
choreRetentionDays: 7,
|
|
847
1149
|
attachmentMaxBytes: 25 * 1024 * 1024,
|
|
848
1150
|
retentionDays: 0,
|
|
849
|
-
minFreeGB: 0
|
|
1151
|
+
minFreeGB: 0,
|
|
1152
|
+
reuseWorktree: true,
|
|
1153
|
+
releaseOnMerge: false
|
|
850
1154
|
};
|
|
851
1155
|
function configPath() {
|
|
852
|
-
return
|
|
1156
|
+
return path6.join(lobstahHome(), "config.toml");
|
|
853
1157
|
}
|
|
854
1158
|
function expandHome(p) {
|
|
855
|
-
return p.startsWith("~/") ?
|
|
1159
|
+
return p.startsWith("~/") ? path6.join(os2.homedir(), p.slice(2)) : p;
|
|
856
1160
|
}
|
|
857
1161
|
function loadConfig() {
|
|
858
1162
|
const file = configPath();
|
|
859
|
-
const raw =
|
|
1163
|
+
const raw = fs8.existsSync(file) ? parse(fs8.readFileSync(file, "utf8")) : {};
|
|
860
1164
|
const reposRaw = raw.repos ?? {};
|
|
861
1165
|
const repos = {};
|
|
862
1166
|
for (const [key, r] of Object.entries(reposRaw)) {
|
|
@@ -867,7 +1171,11 @@ function loadConfig() {
|
|
|
867
1171
|
setup: Array.isArray(r.setup) ? r.setup.map(String) : void 0,
|
|
868
1172
|
env: r.env ?? void 0,
|
|
869
1173
|
harness: r.harness ?? void 0,
|
|
870
|
-
pickup: r.pickup === void 0 ? void 0 : Boolean(r.pickup)
|
|
1174
|
+
pickup: r.pickup === void 0 ? void 0 : Boolean(r.pickup),
|
|
1175
|
+
scratch: Array.isArray(r.scratch) ? r.scratch.map(String) : void 0,
|
|
1176
|
+
pushEarly: r.pushEarly === void 0 ? void 0 : Boolean(r.pushEarly),
|
|
1177
|
+
draftPr: r.draftPr === void 0 ? void 0 : Boolean(r.draftPr),
|
|
1178
|
+
checkpointOnStop: r.checkpointOnStop === void 0 ? void 0 : Boolean(r.checkpointOnStop)
|
|
871
1179
|
};
|
|
872
1180
|
}
|
|
873
1181
|
const groundsRaw = raw.grounds ?? {};
|
|
@@ -910,21 +1218,21 @@ function resolveDispatch(d, cfg) {
|
|
|
910
1218
|
}
|
|
911
1219
|
|
|
912
1220
|
// packages/core/dist/harness.js
|
|
913
|
-
import * as
|
|
914
|
-
import * as
|
|
1221
|
+
import * as fs9 from "node:fs";
|
|
1222
|
+
import * as path7 from "node:path";
|
|
915
1223
|
import { createRequire } from "node:module";
|
|
916
1224
|
function resolveOnPath(bin, pathVar = process.env.PATH) {
|
|
917
1225
|
if (!pathVar)
|
|
918
1226
|
return void 0;
|
|
919
1227
|
const names = process.platform === "win32" ? [bin, `${bin}.exe`, `${bin}.cmd`, `${bin}.bat`] : [bin];
|
|
920
|
-
for (const dir of pathVar.split(
|
|
1228
|
+
for (const dir of pathVar.split(path7.delimiter)) {
|
|
921
1229
|
if (!dir)
|
|
922
1230
|
continue;
|
|
923
1231
|
for (const name of names) {
|
|
924
|
-
const candidate =
|
|
1232
|
+
const candidate = path7.join(dir, name);
|
|
925
1233
|
try {
|
|
926
|
-
|
|
927
|
-
if (
|
|
1234
|
+
fs9.accessSync(candidate, fs9.constants.X_OK);
|
|
1235
|
+
if (fs9.statSync(candidate).isFile())
|
|
928
1236
|
return candidate;
|
|
929
1237
|
} catch {
|
|
930
1238
|
}
|
|
@@ -945,22 +1253,28 @@ function codexInvocation(argv) {
|
|
|
945
1253
|
} catch {
|
|
946
1254
|
}
|
|
947
1255
|
for (const dir of require_.resolve.paths("@openai/codex") ?? []) {
|
|
948
|
-
const cli =
|
|
949
|
-
if (
|
|
1256
|
+
const cli = path7.join(dir, "@openai", "codex", "bin", "codex.js");
|
|
1257
|
+
if (fs9.existsSync(cli))
|
|
950
1258
|
return { file: process.execPath, argv: [cli, ...argv] };
|
|
951
1259
|
}
|
|
952
1260
|
return void 0;
|
|
953
1261
|
}
|
|
954
1262
|
|
|
955
|
-
// packages/core/dist/
|
|
956
|
-
import * as
|
|
957
|
-
import * as
|
|
958
|
-
function
|
|
959
|
-
return
|
|
1263
|
+
// packages/core/dist/watch.js
|
|
1264
|
+
import * as fs10 from "node:fs";
|
|
1265
|
+
import * as path8 from "node:path";
|
|
1266
|
+
function watchesDir() {
|
|
1267
|
+
return path8.join(lobstahHome(), "watches");
|
|
960
1268
|
}
|
|
961
|
-
function
|
|
1269
|
+
function slug(key) {
|
|
1270
|
+
return key.replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
1271
|
+
}
|
|
1272
|
+
function watchPath(key) {
|
|
1273
|
+
return path8.join(watchesDir(), `${slug(key)}.json`);
|
|
1274
|
+
}
|
|
1275
|
+
function readWatch(key) {
|
|
962
1276
|
try {
|
|
963
|
-
return JSON.parse(
|
|
1277
|
+
return JSON.parse(fs10.readFileSync(watchPath(key), "utf8"));
|
|
964
1278
|
} catch {
|
|
965
1279
|
return void 0;
|
|
966
1280
|
}
|
|
@@ -976,13 +1290,70 @@ var COMPILED_BINARY = (() => {
|
|
|
976
1290
|
})();
|
|
977
1291
|
|
|
978
1292
|
// packages/core/dist/pr.js
|
|
1293
|
+
function parsePrRef(s) {
|
|
1294
|
+
const m = /^(?:pr:)?([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)#(\d+)$/.exec(s.trim()) ?? /^https?:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/(\d+)(?:[/?#].*)?$/.exec(s.trim());
|
|
1295
|
+
if (!m)
|
|
1296
|
+
return void 0;
|
|
1297
|
+
const [, owner, repo, n] = m;
|
|
1298
|
+
const number = Number(n);
|
|
1299
|
+
return { owner, repo, number, key: `pr:${owner}/${repo}#${number}`, url: `https://github.com/${owner}/${repo}/pull/${number}` };
|
|
1300
|
+
}
|
|
979
1301
|
var PR_VIEW_FIELDS = "state,isDraft,headRefOid,baseRefName,headRefName,mergeStateStatus,reviewDecision,statusCheckRollup,mergedAt,closedAt,updatedAt,reviews";
|
|
980
1302
|
var PR_VIEW_FIELDS_NO_CHECKS = PR_VIEW_FIELDS.split(",").filter((f) => f !== "statusCheckRollup").join(",");
|
|
981
1303
|
|
|
1304
|
+
// packages/core/dist/prs.js
|
|
1305
|
+
import * as fs11 from "node:fs";
|
|
1306
|
+
import * as path9 from "node:path";
|
|
1307
|
+
function prsDir() {
|
|
1308
|
+
return path9.join(lobstahHome(), "prs");
|
|
1309
|
+
}
|
|
1310
|
+
function readPrs() {
|
|
1311
|
+
let files;
|
|
1312
|
+
try {
|
|
1313
|
+
files = fs11.readdirSync(prsDir()).filter((f) => f.endsWith(".json"));
|
|
1314
|
+
} catch {
|
|
1315
|
+
return [];
|
|
1316
|
+
}
|
|
1317
|
+
return files.flatMap((f) => {
|
|
1318
|
+
try {
|
|
1319
|
+
const r = JSON.parse(fs11.readFileSync(path9.join(prsDir(), f), "utf8"));
|
|
1320
|
+
return typeof r.key === "string" && typeof r.url === "string" ? [r] : [];
|
|
1321
|
+
} catch {
|
|
1322
|
+
return [];
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// packages/core/dist/chain-pr.js
|
|
1328
|
+
function chainPr(id, lane) {
|
|
1329
|
+
const ancestry = [];
|
|
1330
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1331
|
+
let at = id;
|
|
1332
|
+
while (at && !seen.has(at)) {
|
|
1333
|
+
ancestry.push(at);
|
|
1334
|
+
seen.add(at);
|
|
1335
|
+
at = storedDescriptor(at, lane)?.followUp;
|
|
1336
|
+
}
|
|
1337
|
+
const records = readPrs();
|
|
1338
|
+
for (const member of ancestry.reverse()) {
|
|
1339
|
+
const evidence = readEvidence(member, lane);
|
|
1340
|
+
const url = evidence.prUrl ?? evidence.pr?.url;
|
|
1341
|
+
const ref = url && parsePrRef(url);
|
|
1342
|
+
if (ref) {
|
|
1343
|
+
const record2 = records.find((pr) => pr.key === ref.key);
|
|
1344
|
+
return { url: ref.url, headRefName: record2?.headRefName ?? evidence.pr?.headRefName ?? evidence.branch };
|
|
1345
|
+
}
|
|
1346
|
+
const record = records.find((pr) => pr.dispatches.includes(member));
|
|
1347
|
+
if (record)
|
|
1348
|
+
return { url: record.url, headRefName: record.headRefName };
|
|
1349
|
+
}
|
|
1350
|
+
return void 0;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
982
1353
|
// packages/core/dist/resume.js
|
|
983
|
-
import * as
|
|
1354
|
+
import * as fs12 from "node:fs";
|
|
984
1355
|
import * as os3 from "node:os";
|
|
985
|
-
import * as
|
|
1356
|
+
import * as path10 from "node:path";
|
|
986
1357
|
import { spawnSync } from "node:child_process";
|
|
987
1358
|
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;
|
|
988
1359
|
function harnessFromSessionId(sessionId) {
|
|
@@ -993,7 +1364,7 @@ function claimOf(id, lane) {
|
|
|
993
1364
|
const dirs = laneDirs(lane);
|
|
994
1365
|
for (const dir of [dirs.active, dirs.done]) {
|
|
995
1366
|
try {
|
|
996
|
-
return JSON.parse(
|
|
1367
|
+
return JSON.parse(fs12.readFileSync(path10.join(dir, id, "claim.json"), "utf8"));
|
|
997
1368
|
} catch {
|
|
998
1369
|
}
|
|
999
1370
|
}
|
|
@@ -1043,7 +1414,7 @@ function isUnresumable(error) {
|
|
|
1043
1414
|
return error !== void 0 && UNRESUMABLE.test(error);
|
|
1044
1415
|
}
|
|
1045
1416
|
function worktreeProgress(worktree, trunk) {
|
|
1046
|
-
if (!worktree || !
|
|
1417
|
+
if (!worktree || !fs12.existsSync(worktree))
|
|
1047
1418
|
return "No worktree progress recorded.";
|
|
1048
1419
|
const git2 = (...a) => spawnSync("git", a, { cwd: worktree, encoding: "utf8" }).stdout?.trim() ?? "";
|
|
1049
1420
|
const commits = trunk ? git2("log", "--oneline", `origin/${trunk}..HEAD`) : git2("log", "--oneline", "-15");
|
|
@@ -1076,31 +1447,31 @@ function handoffNote(fromHarness, progress, why) {
|
|
|
1076
1447
|
${progress}`;
|
|
1077
1448
|
}
|
|
1078
1449
|
function codexHome(env = process.env) {
|
|
1079
|
-
return env.CODEX_HOME ??
|
|
1450
|
+
return env.CODEX_HOME ?? path10.join(os3.homedir(), ".codex");
|
|
1080
1451
|
}
|
|
1081
1452
|
function codexRolloutFile(threadId, home = codexHome()) {
|
|
1082
1453
|
const suffix = `-${threadId}.jsonl`;
|
|
1083
1454
|
const walk = (dir, depth) => {
|
|
1084
1455
|
let entries;
|
|
1085
1456
|
try {
|
|
1086
|
-
entries =
|
|
1457
|
+
entries = fs12.readdirSync(dir, { withFileTypes: true });
|
|
1087
1458
|
} catch {
|
|
1088
1459
|
return void 0;
|
|
1089
1460
|
}
|
|
1090
1461
|
for (const e of entries) {
|
|
1091
1462
|
if (e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(suffix))
|
|
1092
|
-
return
|
|
1463
|
+
return path10.join(dir, e.name);
|
|
1093
1464
|
}
|
|
1094
1465
|
if (depth === 0)
|
|
1095
1466
|
return void 0;
|
|
1096
1467
|
for (const e of entries.filter((x) => x.isDirectory()).sort((a, b) => b.name.localeCompare(a.name))) {
|
|
1097
|
-
const hit = walk(
|
|
1468
|
+
const hit = walk(path10.join(dir, e.name), depth - 1);
|
|
1098
1469
|
if (hit)
|
|
1099
1470
|
return hit;
|
|
1100
1471
|
}
|
|
1101
1472
|
return void 0;
|
|
1102
1473
|
};
|
|
1103
|
-
return walk(
|
|
1474
|
+
return walk(path10.join(home, "sessions"), 3) ?? walk(path10.join(home, "archived_sessions"), 0);
|
|
1104
1475
|
}
|
|
1105
1476
|
function codexDesktopThread(threadId, home = codexHome()) {
|
|
1106
1477
|
const file = codexRolloutFile(threadId, home);
|
|
@@ -1108,12 +1479,12 @@ function codexDesktopThread(threadId, home = codexHome()) {
|
|
|
1108
1479
|
return void 0;
|
|
1109
1480
|
let head = "";
|
|
1110
1481
|
try {
|
|
1111
|
-
const fd =
|
|
1482
|
+
const fd = fs12.openSync(file, "r");
|
|
1112
1483
|
try {
|
|
1113
1484
|
const buf = Buffer.alloc(4096);
|
|
1114
|
-
head = buf.subarray(0,
|
|
1485
|
+
head = buf.subarray(0, fs12.readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
|
|
1115
1486
|
} finally {
|
|
1116
|
-
|
|
1487
|
+
fs12.closeSync(fd);
|
|
1117
1488
|
}
|
|
1118
1489
|
} catch {
|
|
1119
1490
|
return void 0;
|
|
@@ -1143,10 +1514,164 @@ function droppedModelNote(harness, dropped) {
|
|
|
1143
1514
|
return `model ${dropped.model} is a ${dropped.owner} model \u2014 dropped, using ${harness}'s default`;
|
|
1144
1515
|
}
|
|
1145
1516
|
|
|
1517
|
+
// packages/core/dist/worktrees.js
|
|
1518
|
+
import * as fs13 from "node:fs";
|
|
1519
|
+
import * as path11 from "node:path";
|
|
1520
|
+
var LANES = ["work", "chore"];
|
|
1521
|
+
function worktreePath(id) {
|
|
1522
|
+
return path11.join(lobstahHome(), "worktrees", id);
|
|
1523
|
+
}
|
|
1524
|
+
function readRecord(file) {
|
|
1525
|
+
try {
|
|
1526
|
+
const r = JSON.parse(fs13.readFileSync(file, "utf8"));
|
|
1527
|
+
return typeof r.path === "string" ? r : void 0;
|
|
1528
|
+
} catch {
|
|
1529
|
+
return void 0;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
function ownerOfPath(p) {
|
|
1533
|
+
const rel = path11.relative(path11.join(lobstahHome(), "worktrees"), p);
|
|
1534
|
+
if (!rel || rel.startsWith("..") || path11.isAbsolute(rel))
|
|
1535
|
+
return void 0;
|
|
1536
|
+
return rel.split(/[\\/]/)[0];
|
|
1537
|
+
}
|
|
1538
|
+
function dispatchWorktree(id, lane) {
|
|
1539
|
+
for (const l of lane ? [lane] : LANES) {
|
|
1540
|
+
const ev = readEvidence(id, l);
|
|
1541
|
+
if (ev.worktree) {
|
|
1542
|
+
const owner = ev.worktreeOf ?? ownerOfPath(ev.worktree) ?? id;
|
|
1543
|
+
return { path: ev.worktree, owner, reused: owner !== id };
|
|
1544
|
+
}
|
|
1545
|
+
const dirs = laneDirs(l);
|
|
1546
|
+
for (const dir of [dirs.active, dirs.done]) {
|
|
1547
|
+
const rec = readRecord(path11.join(dir, id, "worktree.json"));
|
|
1548
|
+
if (rec) {
|
|
1549
|
+
const owner = rec.of ?? ownerOfPath(rec.path) ?? id;
|
|
1550
|
+
return { path: rec.path, owner, reused: owner !== id };
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return { path: worktreePath(id), owner: id, reused: false };
|
|
1555
|
+
}
|
|
1556
|
+
function laneOf2(id) {
|
|
1557
|
+
for (const l of LANES) {
|
|
1558
|
+
const d = laneDirs(l);
|
|
1559
|
+
if (fs13.existsSync(path11.join(d.queue, `${id}.json`)) || fs13.existsSync(path11.join(d.active, id)) || fs13.existsSync(path11.join(d.done, id))) {
|
|
1560
|
+
return l;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return void 0;
|
|
1564
|
+
}
|
|
1565
|
+
function followUpAncestors(id, lane) {
|
|
1566
|
+
const out = [];
|
|
1567
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
1568
|
+
let cur = storedDescriptor(id, lane ?? laneOf2(id) ?? "work")?.followUp;
|
|
1569
|
+
while (cur && !seen.has(cur)) {
|
|
1570
|
+
out.push(cur);
|
|
1571
|
+
seen.add(cur);
|
|
1572
|
+
cur = storedDescriptor(cur, laneOf2(cur) ?? lane ?? "work")?.followUp;
|
|
1573
|
+
}
|
|
1574
|
+
return out;
|
|
1575
|
+
}
|
|
1576
|
+
function chainWorktree(followUp) {
|
|
1577
|
+
for (const id of [followUp, ...followUpAncestors(followUp)]) {
|
|
1578
|
+
const wt = dispatchWorktree(id, laneOf2(id));
|
|
1579
|
+
if (fs13.existsSync(wt.path))
|
|
1580
|
+
return { ...wt, from: id };
|
|
1581
|
+
}
|
|
1582
|
+
return void 0;
|
|
1583
|
+
}
|
|
1584
|
+
function worktreeGitDir(wt) {
|
|
1585
|
+
const dotGit = path11.join(wt, ".git");
|
|
1586
|
+
try {
|
|
1587
|
+
const st = fs13.statSync(dotGit);
|
|
1588
|
+
if (st.isDirectory())
|
|
1589
|
+
return dotGit;
|
|
1590
|
+
const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs13.readFileSync(dotGit, "utf8"));
|
|
1591
|
+
return m ? path11.resolve(wt, m[1]) : void 0;
|
|
1592
|
+
} catch {
|
|
1593
|
+
return void 0;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
function worktreeLockFile(wt) {
|
|
1597
|
+
const gitDir = worktreeGitDir(wt);
|
|
1598
|
+
return gitDir ? path11.join(gitDir, "lobstah.lock") : void 0;
|
|
1599
|
+
}
|
|
1600
|
+
function readWorktreeLock(wt) {
|
|
1601
|
+
const file = worktreeLockFile(wt);
|
|
1602
|
+
if (!file)
|
|
1603
|
+
return void 0;
|
|
1604
|
+
try {
|
|
1605
|
+
return JSON.parse(fs13.readFileSync(file, "utf8"));
|
|
1606
|
+
} catch {
|
|
1607
|
+
return void 0;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
function lockIsLive(lock) {
|
|
1611
|
+
return fs13.existsSync(path11.join(laneDirs(lock.lane).active, lock.id));
|
|
1612
|
+
}
|
|
1613
|
+
function worktreeHolder(wt) {
|
|
1614
|
+
const lock = readWorktreeLock(wt);
|
|
1615
|
+
return lock && lockIsLive(lock) ? lock : void 0;
|
|
1616
|
+
}
|
|
1617
|
+
function acquireWorktreeLock(wt, id, lane) {
|
|
1618
|
+
const file = worktreeLockFile(wt);
|
|
1619
|
+
const mine = { id, lane, pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1620
|
+
if (!file)
|
|
1621
|
+
return { id: "(no git dir)", lane, pid: 0, at: mine.at };
|
|
1622
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1623
|
+
try {
|
|
1624
|
+
fs13.writeFileSync(file, JSON.stringify(mine, null, 2), { flag: "wx" });
|
|
1625
|
+
return void 0;
|
|
1626
|
+
} catch (err) {
|
|
1627
|
+
if (err.code !== "EEXIST")
|
|
1628
|
+
throw err;
|
|
1629
|
+
}
|
|
1630
|
+
let raw;
|
|
1631
|
+
try {
|
|
1632
|
+
raw = fs13.readFileSync(file, "utf8");
|
|
1633
|
+
} catch {
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
let held2;
|
|
1637
|
+
try {
|
|
1638
|
+
held2 = JSON.parse(raw);
|
|
1639
|
+
} catch {
|
|
1640
|
+
held2 = void 0;
|
|
1641
|
+
}
|
|
1642
|
+
if (held2?.id === id) {
|
|
1643
|
+
fs13.writeFileSync(file, JSON.stringify(mine, null, 2));
|
|
1644
|
+
return void 0;
|
|
1645
|
+
}
|
|
1646
|
+
if (held2 && lockIsLive(held2))
|
|
1647
|
+
return held2;
|
|
1648
|
+
try {
|
|
1649
|
+
if (fs13.readFileSync(file, "utf8") === raw)
|
|
1650
|
+
fs13.rmSync(file, { force: true });
|
|
1651
|
+
} catch {
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
const held = readWorktreeLock(wt);
|
|
1655
|
+
return held ?? { id: "(contended)", lane, pid: 0, at: mine.at };
|
|
1656
|
+
}
|
|
1657
|
+
function releaseWorktreeLock(wt, id) {
|
|
1658
|
+
const file = worktreeLockFile(wt);
|
|
1659
|
+
if (!file)
|
|
1660
|
+
return;
|
|
1661
|
+
if (readWorktreeLock(wt)?.id === id)
|
|
1662
|
+
fs13.rmSync(file, { force: true });
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// packages/core/dist/slots.js
|
|
1666
|
+
function isTrapCatch(id, lane) {
|
|
1667
|
+
return readSessionClaim(id, lane)?.by.startsWith("wt:") ?? false;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1146
1670
|
// packages/runner/src/run.ts
|
|
1147
|
-
import * as
|
|
1148
|
-
import * as
|
|
1149
|
-
import {
|
|
1671
|
+
import * as fs17 from "node:fs";
|
|
1672
|
+
import * as path15 from "node:path";
|
|
1673
|
+
import { execFileSync } from "node:child_process";
|
|
1674
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1150
1675
|
|
|
1151
1676
|
// packages/adapters/dist/claude.js
|
|
1152
1677
|
import { spawn } from "node:child_process";
|
|
@@ -1178,7 +1703,7 @@ var AsyncQueue = class {
|
|
|
1178
1703
|
return Promise.resolve({ value: item, done: false });
|
|
1179
1704
|
if (this.closed)
|
|
1180
1705
|
return Promise.resolve({ value: void 0, done: true });
|
|
1181
|
-
return new Promise((
|
|
1706
|
+
return new Promise((resolve6) => this.waiters.push(resolve6));
|
|
1182
1707
|
}
|
|
1183
1708
|
};
|
|
1184
1709
|
}
|
|
@@ -1203,7 +1728,7 @@ var InputGate = class {
|
|
|
1203
1728
|
return msg;
|
|
1204
1729
|
if (this.ended)
|
|
1205
1730
|
return void 0;
|
|
1206
|
-
await new Promise((
|
|
1731
|
+
await new Promise((resolve6) => this.waiter = resolve6);
|
|
1207
1732
|
this.waiter = null;
|
|
1208
1733
|
}
|
|
1209
1734
|
}
|
|
@@ -1241,7 +1766,10 @@ function pumpClaudeMessage(msg, push, onSession) {
|
|
|
1241
1766
|
} else if (msg.type === "assistant") {
|
|
1242
1767
|
for (const block of msg.message?.content ?? []) {
|
|
1243
1768
|
if (block.type === "tool_use") {
|
|
1244
|
-
|
|
1769
|
+
const target = toolTarget(block.input);
|
|
1770
|
+
push({ at, type: "tool-start", data: { name: block.name, ...target ? { target } : {} } });
|
|
1771
|
+
} else if (block.type === "thinking" || block.type === "redacted_thinking") {
|
|
1772
|
+
push({ at, type: "thinking", data: {} });
|
|
1245
1773
|
} else if (block.type === "text" && block.text) {
|
|
1246
1774
|
push({ at, type: "text", data: { text: String(block.text).slice(0, 2e3) } });
|
|
1247
1775
|
}
|
|
@@ -1343,20 +1871,20 @@ function startClaudeCli(invocation, opts) {
|
|
|
1343
1871
|
}
|
|
1344
1872
|
}
|
|
1345
1873
|
});
|
|
1346
|
-
const done = new Promise((
|
|
1874
|
+
const done = new Promise((resolve6) => {
|
|
1347
1875
|
child.on("error", (err) => {
|
|
1348
1876
|
events.push({ at: now(), type: "error", data: { message: err.message } });
|
|
1349
1877
|
events.close();
|
|
1350
|
-
|
|
1878
|
+
resolve6({ sessionId, error: err.message });
|
|
1351
1879
|
});
|
|
1352
1880
|
child.on("exit", (code) => {
|
|
1353
1881
|
events.close();
|
|
1354
1882
|
if (code === 0)
|
|
1355
|
-
|
|
1883
|
+
resolve6({ sessionId });
|
|
1356
1884
|
else {
|
|
1357
1885
|
const detail = stderrTail.trim().split("\n").at(-1);
|
|
1358
1886
|
const error = `claude exited ${code}${detail ? ` \u2014 ${detail}` : ""}`;
|
|
1359
|
-
|
|
1887
|
+
resolve6({ sessionId, error });
|
|
1360
1888
|
}
|
|
1361
1889
|
});
|
|
1362
1890
|
});
|
|
@@ -1435,25 +1963,25 @@ function createClaudeAdapter() {
|
|
|
1435
1963
|
}
|
|
1436
1964
|
|
|
1437
1965
|
// packages/adapters/dist/codex.js
|
|
1438
|
-
import * as
|
|
1966
|
+
import * as fs14 from "node:fs";
|
|
1439
1967
|
import * as os4 from "node:os";
|
|
1440
|
-
import * as
|
|
1968
|
+
import * as path12 from "node:path";
|
|
1441
1969
|
import { spawn as spawn2 } from "node:child_process";
|
|
1442
1970
|
function isolatedCodexHome() {
|
|
1443
|
-
const dir =
|
|
1444
|
-
|
|
1445
|
-
const auth =
|
|
1446
|
-
const realAuth =
|
|
1447
|
-
if (!
|
|
1971
|
+
const dir = path12.join(lobstahHome(), "codex-home");
|
|
1972
|
+
fs14.mkdirSync(dir, { recursive: true });
|
|
1973
|
+
const auth = path12.join(dir, "auth.json");
|
|
1974
|
+
const realAuth = path12.join(os4.homedir(), ".codex", "auth.json");
|
|
1975
|
+
if (!fs14.existsSync(auth) && fs14.existsSync(realAuth)) {
|
|
1448
1976
|
try {
|
|
1449
|
-
|
|
1977
|
+
fs14.symlinkSync(realAuth, auth);
|
|
1450
1978
|
} catch {
|
|
1451
|
-
|
|
1979
|
+
fs14.copyFileSync(realAuth, auth);
|
|
1452
1980
|
}
|
|
1453
1981
|
}
|
|
1454
|
-
const cfg =
|
|
1455
|
-
if (!
|
|
1456
|
-
|
|
1982
|
+
const cfg = path12.join(dir, "config.toml");
|
|
1983
|
+
if (!fs14.existsSync(cfg)) {
|
|
1984
|
+
fs14.writeFileSync(cfg, "# lobstah-managed: auth inherited from ~/.codex, interactive config not\n");
|
|
1457
1985
|
}
|
|
1458
1986
|
return dir;
|
|
1459
1987
|
}
|
|
@@ -1465,8 +1993,12 @@ function pumpCodexEvent(ev, push, onSession) {
|
|
|
1465
1993
|
push({ at, type: "session", data: { sessionId: ev.thread_id } });
|
|
1466
1994
|
} else if (ev.type === "turn.started") {
|
|
1467
1995
|
push({ at, type: "turn-start", data: {} });
|
|
1996
|
+
} else if (ev.type === "item.started" && ev.item?.type === "reasoning") {
|
|
1997
|
+
push({ at, type: "thinking", data: {} });
|
|
1468
1998
|
} else if (ev.type === "item.started") {
|
|
1469
|
-
|
|
1999
|
+
const item = ev.item ?? {};
|
|
2000
|
+
const target = toolTarget(item) ?? (typeof item.tool === "string" ? typeof item.server === "string" ? `${item.server}.${item.tool}` : item.tool : void 0);
|
|
2001
|
+
push({ at, type: "tool-start", data: { name: item.type, ...target ? { target } : {} } });
|
|
1470
2002
|
} else if (ev.type === "item.completed") {
|
|
1471
2003
|
if (ev.item?.type === "agent_message" && ev.item?.text) {
|
|
1472
2004
|
push({ at, type: "text", data: { text: String(ev.item.text).slice(0, 2e3) } });
|
|
@@ -1499,7 +2031,7 @@ function startCodexCli(invoke, opts) {
|
|
|
1499
2031
|
let current;
|
|
1500
2032
|
let killed = false;
|
|
1501
2033
|
function runTurn(prompt, resumeThread) {
|
|
1502
|
-
return new Promise((
|
|
2034
|
+
return new Promise((resolve6) => {
|
|
1503
2035
|
const inv = invoke(codexExecArgs(opts, prompt, resumeThread));
|
|
1504
2036
|
const child = spawn2(inv.file, inv.argv, {
|
|
1505
2037
|
cwd: opts.cwd,
|
|
@@ -1530,14 +2062,14 @@ function startCodexCli(invoke, opts) {
|
|
|
1530
2062
|
}
|
|
1531
2063
|
}
|
|
1532
2064
|
});
|
|
1533
|
-
child.on("error", (err) =>
|
|
2065
|
+
child.on("error", (err) => resolve6(err.message));
|
|
1534
2066
|
child.on("exit", (code) => {
|
|
1535
2067
|
if (turnError)
|
|
1536
|
-
return
|
|
2068
|
+
return resolve6(turnError);
|
|
1537
2069
|
if (code === 0 || killed)
|
|
1538
|
-
return
|
|
2070
|
+
return resolve6(void 0);
|
|
1539
2071
|
const detail = stderrTail.trim().split("\n").at(-1);
|
|
1540
|
-
|
|
2072
|
+
resolve6(`codex exited ${code}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
1541
2073
|
});
|
|
1542
2074
|
});
|
|
1543
2075
|
}
|
|
@@ -1650,15 +2182,13 @@ function loadAdapter(name) {
|
|
|
1650
2182
|
}
|
|
1651
2183
|
|
|
1652
2184
|
// packages/worktree/dist/index.js
|
|
1653
|
-
import * as
|
|
1654
|
-
import * as
|
|
2185
|
+
import * as fs15 from "node:fs";
|
|
2186
|
+
import * as path13 from "node:path";
|
|
2187
|
+
import { createHash } from "node:crypto";
|
|
1655
2188
|
import { exec, execFile } from "node:child_process";
|
|
1656
2189
|
import { promisify } from "node:util";
|
|
1657
2190
|
var run = promisify(execFile);
|
|
1658
2191
|
var shell = promisify(exec);
|
|
1659
|
-
function worktreePath(id) {
|
|
1660
|
-
return path9.join(lobstahHome(), "worktrees", id);
|
|
1661
|
-
}
|
|
1662
2192
|
async function git(cwd, ...args) {
|
|
1663
2193
|
const { stdout } = await run("git", args, { cwd, env: process.env });
|
|
1664
2194
|
return stdout.trim();
|
|
@@ -1710,23 +2240,158 @@ ${gitStderr(err)}`, { cause: err });
|
|
|
1710
2240
|
}
|
|
1711
2241
|
async function allocate(repo, id) {
|
|
1712
2242
|
const dir = worktreePath(id);
|
|
1713
|
-
if (
|
|
2243
|
+
if (fs15.existsSync(dir)) {
|
|
1714
2244
|
throw new Error(`worktree for ${id} already exists at ${dir} \u2014 never allocate a second`);
|
|
1715
2245
|
}
|
|
1716
|
-
if (!
|
|
2246
|
+
if (!fs15.existsSync(repo.path)) {
|
|
1717
2247
|
if (!repo.origin)
|
|
1718
2248
|
throw new Error(`repo path ${repo.path} missing and no origin configured`);
|
|
1719
2249
|
await run("git", ["clone", repo.origin, repo.path], { env: process.env });
|
|
1720
2250
|
}
|
|
1721
2251
|
await gitShared(repo.path, "fetch", "origin", repo.trunk);
|
|
1722
2252
|
await gitShared(repo.path, "worktree", "add", "--no-track", dir, "-b", `lobstah/${id}`, `origin/${repo.trunk}`);
|
|
2253
|
+
await runSetup(repo, dir);
|
|
2254
|
+
return dir;
|
|
2255
|
+
}
|
|
2256
|
+
async function runSetup(repo, dir) {
|
|
1723
2257
|
for (const cmd of repo.setup ?? []) {
|
|
1724
2258
|
await shell(cmd, { cwd: dir, env: { ...process.env, ...repo.env ?? {} } });
|
|
1725
2259
|
}
|
|
1726
|
-
|
|
2260
|
+
recordSetup(repo, dir);
|
|
2261
|
+
}
|
|
2262
|
+
var LOCKFILES = [
|
|
2263
|
+
"pnpm-lock.yaml",
|
|
2264
|
+
"package-lock.json",
|
|
2265
|
+
"npm-shrinkwrap.json",
|
|
2266
|
+
"yarn.lock",
|
|
2267
|
+
"bun.lock",
|
|
2268
|
+
"bun.lockb",
|
|
2269
|
+
"Cargo.lock",
|
|
2270
|
+
"go.sum",
|
|
2271
|
+
"poetry.lock",
|
|
2272
|
+
"uv.lock",
|
|
2273
|
+
"Pipfile.lock",
|
|
2274
|
+
"Gemfile.lock",
|
|
2275
|
+
"composer.lock",
|
|
2276
|
+
"Package.resolved"
|
|
2277
|
+
];
|
|
2278
|
+
function setupHash(repo, dir) {
|
|
2279
|
+
const h = createHash("sha256");
|
|
2280
|
+
h.update(JSON.stringify(repo.setup ?? []));
|
|
2281
|
+
for (const name of LOCKFILES) {
|
|
2282
|
+
const file = path13.join(dir, name);
|
|
2283
|
+
if (!fs15.existsSync(file))
|
|
2284
|
+
continue;
|
|
2285
|
+
h.update(`\0${name}\0`);
|
|
2286
|
+
h.update(fs15.readFileSync(file));
|
|
2287
|
+
}
|
|
2288
|
+
return h.digest("hex");
|
|
1727
2289
|
}
|
|
1728
|
-
|
|
1729
|
-
const
|
|
2290
|
+
function setupRecordFile(dir) {
|
|
2291
|
+
const gitDir = worktreeGitDir(dir);
|
|
2292
|
+
return gitDir ? path13.join(gitDir, "lobstah-setup.json") : void 0;
|
|
2293
|
+
}
|
|
2294
|
+
function recordSetup(repo, dir) {
|
|
2295
|
+
const file = setupRecordFile(dir);
|
|
2296
|
+
if (file)
|
|
2297
|
+
fs15.writeFileSync(file, JSON.stringify({ hash: setupHash(repo, dir), at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
2298
|
+
}
|
|
2299
|
+
function recordedSetupHash(dir) {
|
|
2300
|
+
const file = setupRecordFile(dir);
|
|
2301
|
+
try {
|
|
2302
|
+
return file ? JSON.parse(fs15.readFileSync(file, "utf8")).hash : void 0;
|
|
2303
|
+
} catch {
|
|
2304
|
+
return void 0;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
var short = (s) => s.slice(0, 8);
|
|
2308
|
+
function realpath(p) {
|
|
2309
|
+
let r;
|
|
2310
|
+
try {
|
|
2311
|
+
r = fs15.realpathSync.native(p);
|
|
2312
|
+
} catch {
|
|
2313
|
+
r = path13.resolve(p);
|
|
2314
|
+
}
|
|
2315
|
+
return process.platform === "win32" ? r.toLowerCase() : r;
|
|
2316
|
+
}
|
|
2317
|
+
function norm(p) {
|
|
2318
|
+
return p.replace(/^"(.*)"$/, "$1").replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
2319
|
+
}
|
|
2320
|
+
function onlyScratch(porcelain, scratch) {
|
|
2321
|
+
const lines = porcelain.split("\n").filter((l) => l.trim() !== "");
|
|
2322
|
+
if (lines.length === 0)
|
|
2323
|
+
return true;
|
|
2324
|
+
const roots = (scratch ?? []).map(norm).filter(Boolean);
|
|
2325
|
+
if (roots.length === 0)
|
|
2326
|
+
return false;
|
|
2327
|
+
return lines.every((line) => {
|
|
2328
|
+
if (!line.startsWith("?? "))
|
|
2329
|
+
return false;
|
|
2330
|
+
const p = norm(line.slice(3));
|
|
2331
|
+
return roots.some((r) => p === r || p.startsWith(`${r}/`));
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
async function chooseWorktree(input) {
|
|
2335
|
+
const { id, lane, repoKey, repo, followUp } = input;
|
|
2336
|
+
const found = chainWorktree(followUp);
|
|
2337
|
+
if (!found)
|
|
2338
|
+
return { reuse: false, reason: "origin worktree is gone" };
|
|
2339
|
+
const ownerRepo = storedDescriptor(found.owner, laneOf2(found.owner) ?? lane)?.repo ?? storedDescriptor(found.from, laneOf2(found.from) ?? lane)?.repo;
|
|
2340
|
+
if (ownerRepo !== void 0 && ownerRepo !== repoKey) {
|
|
2341
|
+
return { reuse: false, reason: `origin worktree belongs to repo ${ownerRepo}` };
|
|
2342
|
+
}
|
|
2343
|
+
try {
|
|
2344
|
+
const common = await git(found.path, "rev-parse", "--path-format=absolute", "--git-common-dir");
|
|
2345
|
+
const mine = await git(repo.path, "rev-parse", "--path-format=absolute", "--git-common-dir");
|
|
2346
|
+
if (realpath(common) !== realpath(mine))
|
|
2347
|
+
return { reuse: false, reason: "origin worktree belongs to another repo" };
|
|
2348
|
+
} catch {
|
|
2349
|
+
return { reuse: false, reason: "origin worktree is not a readable git checkout" };
|
|
2350
|
+
}
|
|
2351
|
+
const here = realpath(found.path);
|
|
2352
|
+
for (const t of listTraps()) {
|
|
2353
|
+
if (typeof t.worktree !== "string")
|
|
2354
|
+
continue;
|
|
2355
|
+
const rel = path13.relative(here, realpath(t.worktree));
|
|
2356
|
+
if (!rel.startsWith("..") && !path13.isAbsolute(rel))
|
|
2357
|
+
return { reuse: false, reason: "origin worktree is a trap\u2019s" };
|
|
2358
|
+
}
|
|
2359
|
+
for (const l of ["work", "chore"]) {
|
|
2360
|
+
for (const other of activeIds(l)) {
|
|
2361
|
+
if (other === id)
|
|
2362
|
+
continue;
|
|
2363
|
+
if (realpath(dispatchWorktree(other, l).path) === here) {
|
|
2364
|
+
return { reuse: false, reason: `origin worktree is in use by ${short(other)}` };
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
const holder = worktreeHolder(found.path);
|
|
2369
|
+
if (holder && holder.id !== id)
|
|
2370
|
+
return { reuse: false, reason: `origin worktree is in use by ${short(holder.id)}` };
|
|
2371
|
+
let porcelain;
|
|
2372
|
+
try {
|
|
2373
|
+
porcelain = await git(found.path, "status", "--porcelain");
|
|
2374
|
+
} catch {
|
|
2375
|
+
return { reuse: false, reason: "origin worktree is not a readable git checkout" };
|
|
2376
|
+
}
|
|
2377
|
+
if (!onlyScratch(porcelain, repo.scratch)) {
|
|
2378
|
+
return { reuse: false, reason: "origin worktree has uncommitted changes; allocated a fresh one" };
|
|
2379
|
+
}
|
|
2380
|
+
const held = acquireWorktreeLock(found.path, id, lane);
|
|
2381
|
+
if (held)
|
|
2382
|
+
return { reuse: false, reason: `origin worktree is in use by ${short(held.id)}` };
|
|
2383
|
+
return { reuse: true, path: found.path, owner: found.owner, from: found.from };
|
|
2384
|
+
}
|
|
2385
|
+
async function prepareReuse(repo, dir) {
|
|
2386
|
+
await gitShared(dir, "fetch", "origin", repo.trunk);
|
|
2387
|
+
if (!repo.setup?.length)
|
|
2388
|
+
return { setupRan: false };
|
|
2389
|
+
if (recordedSetupHash(dir) === setupHash(repo, dir))
|
|
2390
|
+
return { setupRan: false };
|
|
2391
|
+
await runSetup(repo, dir);
|
|
2392
|
+
return { setupRan: true };
|
|
2393
|
+
}
|
|
2394
|
+
async function collectEvidence(repo, dir) {
|
|
1730
2395
|
const branch = await git(dir, "rev-parse", "--abbrev-ref", "HEAD");
|
|
1731
2396
|
const log = await git(dir, "log", "--oneline", `origin/${repo.trunk}..HEAD`);
|
|
1732
2397
|
return { branch, commits: log ? log.split("\n") : [] };
|
|
@@ -1740,9 +2405,12 @@ function buildPrompt(brief, opts) {
|
|
|
1740
2405
|
`Work only inside the current directory \u2014 it is an isolated git worktree allocated for this dispatch.`,
|
|
1741
2406
|
reporting,
|
|
1742
2407
|
`Use \`needs-decision\` when you are blocked on a question only a human can answer, then stop. New operator messages may arrive between your turns as user messages; treat them as instructions from the dispatcher.`,
|
|
1743
|
-
`
|
|
2408
|
+
`Before you wait on something outside lobstah (a human review, a PR review, a deploy), report \`lobstah report ${opts.id} paused "<note>" --waiting-on ${WAITING_ON.join("|")} --link <url>\`. Report \`working\` when you resume.`,
|
|
2409
|
+
opts.existingPr ? `Commit your work with clear messages. This chain already has PR ${opts.existingPr.url}. The runner does not push or open another PR. Push your changes to its existing head branch${opts.existingPr.headRefName ? ` ${opts.existingPr.headRefName}` : " (inspect the PR to find it)"}; do not create a duplicate PR. Report done with the same PR URL. Do not merge anything.` : `Commit your work with clear messages. The runner pushes committed HEAD early and opens or adopts one draft PR for the branch when available; do not create a duplicate PR. When finished, mark the draft ready for review if appropriate, then report done with its URL. Do not merge anything.`,
|
|
1744
2410
|
`--- BRIEF ---`,
|
|
1745
|
-
brief
|
|
2411
|
+
opts.existingPr ? `${brief}
|
|
2412
|
+
|
|
2413
|
+
Existing chain PR: ${opts.existingPr.url}. Head branch: ${opts.existingPr.headRefName ?? "inspect the PR"}. The runner will not push this follow-up.` : brief
|
|
1746
2414
|
];
|
|
1747
2415
|
if (opts.attachments?.length) parts.push(attachmentBlock(opts.attachments));
|
|
1748
2416
|
if (opts.nudge) parts.push(`--- SUPERVISOR NOTE ---`, opts.nudge);
|
|
@@ -1754,7 +2422,7 @@ var WAITING_VERBS = ["needs-decision", "blocked", "paused"];
|
|
|
1754
2422
|
function unreportedNudge(id) {
|
|
1755
2423
|
return `Your turn ended without a status report, so this dispatch is not finished. If the work is complete, run \`lobstah report ${id} done "<what you did>"\`, adding \`--pr <url>\` if you opened a PR. If no change was needed, report \`done\` with a note saying why. If a human has to answer a question, or you are blocked, report \`needs-decision\` or \`blocked\` with it. If you are waiting on something, run it in the background: you are woken when it finishes.`;
|
|
1756
2424
|
}
|
|
1757
|
-
var sleep = (ms) => new Promise((
|
|
2425
|
+
var sleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
1758
2426
|
function deliver(run2, id, lane) {
|
|
1759
2427
|
const msgs = unhandled(id, lane);
|
|
1760
2428
|
for (const m of msgs) {
|
|
@@ -1772,6 +2440,14 @@ async function drive(run2, opts) {
|
|
|
1772
2440
|
backgroundWaitMs = 30 * 6e4,
|
|
1773
2441
|
settleGraceMs = 12e4
|
|
1774
2442
|
} = opts;
|
|
2443
|
+
const tracker = new ActivityTracker((a) => writeActivity(id, lane, a), {
|
|
2444
|
+
root: opts.cwd,
|
|
2445
|
+
throttleMs: opts.activityThrottleMs
|
|
2446
|
+
});
|
|
2447
|
+
const record = (ev) => {
|
|
2448
|
+
appendEvent(id, lane, ev);
|
|
2449
|
+
tracker.observe(ev);
|
|
2450
|
+
};
|
|
1775
2451
|
let cancelled = false;
|
|
1776
2452
|
let activity = 0;
|
|
1777
2453
|
let finished = false;
|
|
@@ -1785,7 +2461,8 @@ async function drive(run2, opts) {
|
|
|
1785
2461
|
appendEvent(id, lane, { at: (/* @__PURE__ */ new Date()).toISOString(), type: "runner", data: { resumed: verb } });
|
|
1786
2462
|
};
|
|
1787
2463
|
const awaitAnswer = async (verb) => {
|
|
1788
|
-
|
|
2464
|
+
const on = readStatusLog(id, lane).at(-1)?.waitingOn;
|
|
2465
|
+
record({ at: (/* @__PURE__ */ new Date()).toISOString(), type: "runner", data: { waiting: verb, ...on ? { on } : {} } });
|
|
1789
2466
|
while (true) {
|
|
1790
2467
|
if (stopped() || finished) return;
|
|
1791
2468
|
if (cancelRequested(id, lane)) return cancel();
|
|
@@ -1797,7 +2474,7 @@ async function drive(run2, opts) {
|
|
|
1797
2474
|
let liveBackground = 0;
|
|
1798
2475
|
let nudged = false;
|
|
1799
2476
|
let hold;
|
|
1800
|
-
const runnerEvent = (data) =>
|
|
2477
|
+
const runnerEvent = (data) => record({ at: (/* @__PURE__ */ new Date()).toISOString(), type: "runner", data });
|
|
1801
2478
|
const releaseHold = () => {
|
|
1802
2479
|
if (!hold) return;
|
|
1803
2480
|
clearInterval(hold.heartbeat);
|
|
@@ -1832,7 +2509,7 @@ async function drive(run2, opts) {
|
|
|
1832
2509
|
hold = { heartbeat, expiry };
|
|
1833
2510
|
};
|
|
1834
2511
|
for await (const ev of run2.events) {
|
|
1835
|
-
|
|
2512
|
+
record(ev);
|
|
1836
2513
|
if (ev.type === "background") {
|
|
1837
2514
|
liveBackground = Number(ev.data?.live ?? 0);
|
|
1838
2515
|
if (hold && liveBackground === 0) holdOpen(settleGraceMs);
|
|
@@ -1875,18 +2552,20 @@ async function drive(run2, opts) {
|
|
|
1875
2552
|
unreported();
|
|
1876
2553
|
}
|
|
1877
2554
|
releaseHold();
|
|
2555
|
+
tracker.flush();
|
|
2556
|
+
tracker.stop();
|
|
1878
2557
|
return { cancelled, activity };
|
|
1879
2558
|
}
|
|
1880
2559
|
function settle(id, lane, r) {
|
|
1881
2560
|
const lastVerb = readStatusLog(id, lane).at(-1)?.verb;
|
|
1882
2561
|
if (r.cancelled) appendStatus(id, lane, "failed", "cancelled by operator");
|
|
1883
|
-
else if (r.wallClockHit) appendStatus(id, lane, "failed", "
|
|
2562
|
+
else if (r.wallClockHit) appendStatus(id, lane, "failed", `budget: out of time${r.budgetNote ? `; ${r.budgetNote}` : ""}; send continue to resume`);
|
|
1884
2563
|
else if (r.error) appendStatus(id, lane, "failed", r.error.slice(0, 500));
|
|
1885
2564
|
else if (!lastVerb || !TERMINAL_VERBS.includes(lastVerb)) appendStatus(id, lane, "failed", "stopped without reporting a result");
|
|
1886
2565
|
}
|
|
1887
2566
|
|
|
1888
2567
|
// packages/runner/src/plan.ts
|
|
1889
|
-
var
|
|
2568
|
+
var short2 = (s) => s.slice(0, 8);
|
|
1890
2569
|
function askedHarness(id, lane, cfg) {
|
|
1891
2570
|
const d = storedDescriptor(id, lane);
|
|
1892
2571
|
return d?.harness ?? (d ? cfg.repos[d.repo]?.harness?.default : void 0) ?? cfg.harness.default ?? "claude";
|
|
@@ -1900,13 +2579,13 @@ function planStart(input) {
|
|
|
1900
2579
|
return {
|
|
1901
2580
|
harness: resolvedHarness,
|
|
1902
2581
|
cold: { why: CODEX_DESKTOP_THREAD, fromHarness: harness },
|
|
1903
|
-
note: `restart: ${CODEX_DESKTOP_THREAD} (${
|
|
2582
|
+
note: `restart: ${CODEX_DESKTOP_THREAD} (${short2(envResume)}), starting cold on ${resolvedHarness}`
|
|
1904
2583
|
};
|
|
1905
2584
|
}
|
|
1906
2585
|
return {
|
|
1907
2586
|
harness,
|
|
1908
2587
|
resume: { sessionId: envResume, own: true },
|
|
1909
|
-
note: harness !== resolvedHarness ? `resuming own ${harness} session ${
|
|
2588
|
+
note: harness !== resolvedHarness ? `resuming own ${harness} session ${short2(envResume)}` : void 0
|
|
1910
2589
|
};
|
|
1911
2590
|
}
|
|
1912
2591
|
const prior = readEvidence(id, lane).sessionId;
|
|
@@ -1914,7 +2593,7 @@ function planStart(input) {
|
|
|
1914
2593
|
const from = resolveSessionHarness(id, cfg, lane).harness;
|
|
1915
2594
|
return {
|
|
1916
2595
|
harness: resolvedHarness,
|
|
1917
|
-
cold: { why: `session ${
|
|
2596
|
+
cold: { why: `session ${short2(prior)} is not resumed on a respawn`, fromHarness: from },
|
|
1918
2597
|
note: `respawn: starting cold on ${resolvedHarness}${from ? ` (previous session: ${from})` : ""}`
|
|
1919
2598
|
};
|
|
1920
2599
|
}
|
|
@@ -1926,15 +2605,15 @@ function planStart(input) {
|
|
|
1926
2605
|
if (!origin.sessionId) {
|
|
1927
2606
|
return {
|
|
1928
2607
|
harness: resolvedHarness,
|
|
1929
|
-
cold: { why: `follow-up origin ${
|
|
1930
|
-
note: `follow-up of ${
|
|
2608
|
+
cold: { why: `follow-up origin ${short2(originId)} has no recorded session`, origin: originRef },
|
|
2609
|
+
note: `follow-up of ${short2(originId)}: no session recorded \u2014 starting cold on ${resolvedHarness} with a progress note`
|
|
1931
2610
|
};
|
|
1932
2611
|
}
|
|
1933
2612
|
if (!origin.harness) {
|
|
1934
2613
|
return {
|
|
1935
2614
|
harness: resolvedHarness,
|
|
1936
2615
|
resume: { sessionId: origin.sessionId, own: false, origin: originRef },
|
|
1937
|
-
note: `follow-up of ${
|
|
2616
|
+
note: `follow-up of ${short2(originId)}: resuming session ${short2(origin.sessionId)} on ${resolvedHarness} (origin harness unknown)`
|
|
1938
2617
|
};
|
|
1939
2618
|
}
|
|
1940
2619
|
const requested = descriptor.harness;
|
|
@@ -1943,48 +2622,273 @@ function planStart(input) {
|
|
|
1943
2622
|
return {
|
|
1944
2623
|
harness: requested,
|
|
1945
2624
|
cold: { why: `swap from ${origin.harness} to ${requested}`, fromHarness: origin.harness, origin: originRef },
|
|
1946
|
-
note: `follow-up of ${
|
|
2625
|
+
note: `follow-up of ${short2(originId)}: swap \u2014 origin session is ${origin.harness}, ${requested} requested; starting cold on ${requested} with a progress note`
|
|
1947
2626
|
};
|
|
1948
2627
|
}
|
|
1949
2628
|
if (origin.harness === "codex" && codexDesktopThread(origin.sessionId)) {
|
|
1950
2629
|
return {
|
|
1951
2630
|
harness: resolvedHarness,
|
|
1952
2631
|
cold: { why: CODEX_DESKTOP_THREAD, fromHarness: origin.harness, origin: originRef },
|
|
1953
|
-
note: `follow-up of ${
|
|
2632
|
+
note: `follow-up of ${short2(originId)}: ${CODEX_DESKTOP_THREAD} (${short2(origin.sessionId)}), starting cold on ${resolvedHarness} with a progress note`
|
|
1954
2633
|
};
|
|
1955
2634
|
}
|
|
1956
2635
|
const ignored = requested !== void 0 && requested !== origin.harness ? ` (--harness ${requested} ignored for the resume)` : "";
|
|
1957
2636
|
return {
|
|
1958
2637
|
harness: origin.harness,
|
|
1959
2638
|
resume: { sessionId: origin.sessionId, own: false, origin: originRef },
|
|
1960
|
-
note: `follow-up of ${
|
|
2639
|
+
note: `follow-up of ${short2(originId)}: resuming ${origin.harness} session ${short2(origin.sessionId)} (harness from ${origin.source})${ignored}`
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// packages/runner/src/wallclock.ts
|
|
2644
|
+
function startWallClock(opts) {
|
|
2645
|
+
const now2 = opts.now ?? Date.now;
|
|
2646
|
+
const tickMs = Math.max(1, Math.min(opts.tickMs ?? 5e3, opts.limitMs));
|
|
2647
|
+
let elapsed = opts.initialElapsedMs ?? 0;
|
|
2648
|
+
const maxMs = Math.max(opts.limitMs, opts.maxMs ?? opts.limitMs);
|
|
2649
|
+
let windowMs = Math.min(maxMs, Math.max(opts.limitMs, opts.initialWindowMs ?? opts.limitMs));
|
|
2650
|
+
let last = now2();
|
|
2651
|
+
let done = false;
|
|
2652
|
+
const check = () => {
|
|
2653
|
+
const t = now2();
|
|
2654
|
+
let paused = false;
|
|
2655
|
+
try {
|
|
2656
|
+
paused = opts.paused();
|
|
2657
|
+
} catch {
|
|
2658
|
+
paused = false;
|
|
2659
|
+
}
|
|
2660
|
+
if (!paused) elapsed += t - last;
|
|
2661
|
+
last = t;
|
|
2662
|
+
if (!done && elapsed >= windowMs && elapsed < maxMs) {
|
|
2663
|
+
let advanced = false;
|
|
2664
|
+
try {
|
|
2665
|
+
advanced = opts.progress?.() ?? false;
|
|
2666
|
+
} catch {
|
|
2667
|
+
}
|
|
2668
|
+
if (advanced) windowMs = Math.min(maxMs, windowMs + opts.limitMs);
|
|
2669
|
+
}
|
|
2670
|
+
opts.onTick?.(elapsed, windowMs);
|
|
2671
|
+
if (!done && elapsed >= windowMs) {
|
|
2672
|
+
done = true;
|
|
2673
|
+
clearInterval(timer);
|
|
2674
|
+
opts.onExpire();
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2677
|
+
const timer = setInterval(check, tickMs);
|
|
2678
|
+
timer.unref?.();
|
|
2679
|
+
return {
|
|
2680
|
+
stop: () => {
|
|
2681
|
+
done = true;
|
|
2682
|
+
clearInterval(timer);
|
|
2683
|
+
},
|
|
2684
|
+
elapsed: () => elapsed,
|
|
2685
|
+
window: () => windowMs
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// packages/runner/src/remote.ts
|
|
2690
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
2691
|
+
import { promisify as promisify2 } from "node:util";
|
|
2692
|
+
import { fileURLToPath } from "node:url";
|
|
2693
|
+
import * as fs16 from "node:fs";
|
|
2694
|
+
import * as path14 from "node:path";
|
|
2695
|
+
var exec2 = promisify2(execFile2);
|
|
2696
|
+
function firstLine(err) {
|
|
2697
|
+
const stderr = err && typeof err === "object" && "stderr" in err ? String(err.stderr).trim() : "";
|
|
2698
|
+
return (stderr || (err instanceof Error ? err.message : String(err))).replace(/\s+/g, " ").slice(0, 240);
|
|
2699
|
+
}
|
|
2700
|
+
function checkpointAllowed(file) {
|
|
2701
|
+
const parts = file.toLowerCase().split("/");
|
|
2702
|
+
const base = parts.at(-1) ?? "";
|
|
2703
|
+
if (parts.some((p) => [".git", "node_modules", "dist", "build", "coverage", ".next", ".turbo"].includes(p))) return false;
|
|
2704
|
+
if (base === ".env" || base.startsWith(".env.") || base.endsWith(".pem") || base.endsWith(".key") || base.endsWith(".p12")) return false;
|
|
2705
|
+
if (/^(credentials?|secrets?|id_rsa|id_ed25519)(\.|$)/.test(base)) return false;
|
|
2706
|
+
return true;
|
|
2707
|
+
}
|
|
2708
|
+
function keepRemote(opts) {
|
|
2709
|
+
const { id, lane, cwd, trunk, policy, existingPrUrl } = opts;
|
|
2710
|
+
const command = async (bin, args, timeout = 12e4) => {
|
|
2711
|
+
const file = bin === "gh" || bin === "lobstah" ? resolveOnPath(bin) ?? bin : bin;
|
|
2712
|
+
return (await exec2(file, args, {
|
|
2713
|
+
cwd,
|
|
2714
|
+
encoding: "utf8",
|
|
2715
|
+
timeout,
|
|
2716
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
2717
|
+
shell: process.platform === "win32" && /\.(?:cmd|bat)$/i.test(file)
|
|
2718
|
+
})).stdout.trim();
|
|
2719
|
+
};
|
|
2720
|
+
const git2 = (...args) => command("git", args);
|
|
2721
|
+
const note = (message) => mergeEvidence(id, lane, { note: message.slice(0, 300) });
|
|
2722
|
+
if (existingPrUrl) mergeEvidence(id, lane, { prUrl: existingPrUrl });
|
|
2723
|
+
const registerWatch = async (url) => {
|
|
2724
|
+
const root = path14.resolve(path14.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
2725
|
+
const localCli = path14.join(root, "apps", "cli", "dist", "main.js");
|
|
2726
|
+
const args = ["watch", "add", url, "--for", id];
|
|
2727
|
+
if (fs16.existsSync(localCli)) await command(process.execPath, [localCli, ...args], 15e3);
|
|
2728
|
+
else await command("lobstah", args, 15e3);
|
|
2729
|
+
};
|
|
2730
|
+
let lastSeenHead;
|
|
2731
|
+
let pushedHead;
|
|
2732
|
+
let busy;
|
|
2733
|
+
let stopped = false;
|
|
2734
|
+
let opened = false;
|
|
2735
|
+
let opening;
|
|
2736
|
+
let pushProblem;
|
|
2737
|
+
let draftProblem;
|
|
2738
|
+
const trapClaimed = () => isTrapCatch(id, lane);
|
|
2739
|
+
const existingWatch = existingPrUrl && parsePrRef(existingPrUrl);
|
|
2740
|
+
const watching = existingWatch && !readWatch(existingWatch.key) ? (async () => {
|
|
2741
|
+
await registerWatch(existingWatch.url).catch((err) => note(`PR watch unavailable: ${firstLine(err)}`));
|
|
2742
|
+
})() : void 0;
|
|
2743
|
+
const branchName = async () => {
|
|
2744
|
+
const branch = await git2("symbolic-ref", "--quiet", "--short", "HEAD").catch(() => "");
|
|
2745
|
+
if (!branch || branch === trunk || branch === `origin/${trunk}`) return void 0;
|
|
2746
|
+
return branch;
|
|
2747
|
+
};
|
|
2748
|
+
const draft = async (branch) => {
|
|
2749
|
+
if (!policy.draftPr || opened || readEvidence(id, lane).prUrl) return;
|
|
2750
|
+
let url = "";
|
|
2751
|
+
try {
|
|
2752
|
+
url = await command("gh", ["pr", "view", branch, "--json", "url", "--jq", ".url"]);
|
|
2753
|
+
} catch {
|
|
2754
|
+
}
|
|
2755
|
+
if (!url) {
|
|
2756
|
+
try {
|
|
2757
|
+
url = await command("gh", [
|
|
2758
|
+
"pr",
|
|
2759
|
+
"create",
|
|
2760
|
+
"--draft",
|
|
2761
|
+
"--base",
|
|
2762
|
+
trunk,
|
|
2763
|
+
"--head",
|
|
2764
|
+
branch,
|
|
2765
|
+
"--title",
|
|
2766
|
+
opts.title.slice(0, 120),
|
|
2767
|
+
"--body",
|
|
2768
|
+
`Work in progress for lobstah dispatch ${id}.`
|
|
2769
|
+
]);
|
|
2770
|
+
} catch (err) {
|
|
2771
|
+
draftProblem = `draft PR unavailable: ${firstLine(err)}`;
|
|
2772
|
+
note(draftProblem);
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
const match = url.match(/https:\/\/github\.com\/[^\s]+\/pull\/\d+/);
|
|
2777
|
+
if (!match) return;
|
|
2778
|
+
opened = true;
|
|
2779
|
+
draftProblem = void 0;
|
|
2780
|
+
mergeEvidence(id, lane, { prUrl: match[0] });
|
|
2781
|
+
await registerWatch(match[0]).catch(() => {
|
|
2782
|
+
});
|
|
2783
|
+
};
|
|
2784
|
+
const check = async () => {
|
|
2785
|
+
if (!policy.pushEarly || trapClaimed() || existingPrUrl) return;
|
|
2786
|
+
const branch = await branchName();
|
|
2787
|
+
if (!branch) return;
|
|
2788
|
+
const head = await git2("rev-parse", "HEAD");
|
|
2789
|
+
const ahead = await git2("rev-list", "--count", `origin/${trunk}..HEAD`).catch(() => "1");
|
|
2790
|
+
if (ahead === "0") return;
|
|
2791
|
+
if (head === lastSeenHead) return;
|
|
2792
|
+
lastSeenHead = head;
|
|
2793
|
+
if (head === pushedHead) return;
|
|
2794
|
+
try {
|
|
2795
|
+
await git2("push", "-u", "origin", branch);
|
|
2796
|
+
pushedHead = head;
|
|
2797
|
+
pushProblem = void 0;
|
|
2798
|
+
mergeEvidence(id, lane, { branch });
|
|
2799
|
+
note(`remote saved: ${branch}@${head.slice(0, 12)}`);
|
|
2800
|
+
if (!opening) opening = draft(branch).finally(() => {
|
|
2801
|
+
opening = void 0;
|
|
2802
|
+
});
|
|
2803
|
+
} catch (err) {
|
|
2804
|
+
pushProblem = `push rejected for ${branch}@${head.slice(0, 12)}: ${firstLine(err)}; retry after HEAD moves`;
|
|
2805
|
+
note(pushProblem);
|
|
2806
|
+
}
|
|
2807
|
+
};
|
|
2808
|
+
const poll = () => {
|
|
2809
|
+
if (busy || stopped) return;
|
|
2810
|
+
busy = check().catch((err) => note(`remote check unavailable: ${firstLine(err)}`)).finally(() => {
|
|
2811
|
+
busy = void 0;
|
|
2812
|
+
});
|
|
2813
|
+
};
|
|
2814
|
+
if (policy.pushEarly) poll();
|
|
2815
|
+
const timer = setInterval(poll, Math.min(1e4, Math.max(1e3, opts.intervalMs ?? 1e4)));
|
|
2816
|
+
timer.unref?.();
|
|
2817
|
+
return {
|
|
2818
|
+
stop: async () => {
|
|
2819
|
+
stopped = true;
|
|
2820
|
+
clearInterval(timer);
|
|
2821
|
+
await busy;
|
|
2822
|
+
await check().catch((err) => note(`final push unavailable: ${firstLine(err)}`));
|
|
2823
|
+
await opening;
|
|
2824
|
+
await watching;
|
|
2825
|
+
},
|
|
2826
|
+
saveBeforeStop: async () => {
|
|
2827
|
+
stopped = true;
|
|
2828
|
+
clearInterval(timer);
|
|
2829
|
+
await busy;
|
|
2830
|
+
if (trapClaimed()) return "checkpoint skipped: trap-claimed dispatch";
|
|
2831
|
+
const branch = await branchName();
|
|
2832
|
+
if (!branch) {
|
|
2833
|
+
const message2 = "checkpoint skipped: detached or trunk; files left in place";
|
|
2834
|
+
note(message2);
|
|
2835
|
+
return message2;
|
|
2836
|
+
}
|
|
2837
|
+
let saved = "no working-tree changes";
|
|
2838
|
+
if (policy.checkpointOnStop) {
|
|
2839
|
+
try {
|
|
2840
|
+
const paths = (await git2("ls-files", "-m", "-o", "-d", "--exclude-standard", "-z")).split("\0").filter(Boolean).filter(checkpointAllowed);
|
|
2841
|
+
if (paths.length) {
|
|
2842
|
+
await git2("add", "-A", "--", ...paths);
|
|
2843
|
+
const staged = await git2("diff", "--cached", "--name-only", "--", ...paths);
|
|
2844
|
+
if (staged) {
|
|
2845
|
+
await git2("commit", "--only", "-m", "Checkpoint before runner stop", "--", ...paths);
|
|
2846
|
+
saved = `checkpoint committed (${staged.split("\n").length} files)`;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
} catch (err) {
|
|
2850
|
+
saved = `checkpoint unavailable: ${firstLine(err)}`;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
await check().catch((err) => note(`final push unavailable: ${firstLine(err)}`));
|
|
2854
|
+
await opening;
|
|
2855
|
+
await watching;
|
|
2856
|
+
const head = await git2("rev-parse", "--short", "HEAD").catch(() => "unknown");
|
|
2857
|
+
const pr = readEvidence(id, lane).prUrl;
|
|
2858
|
+
const message = `${saved}; ${branch}@${head}${pr ? `; ${existingPrUrl ? "existing" : "draft"} PR ${pr}` : ""}${pushProblem ? `; ${pushProblem}` : ""}${draftProblem ? `; ${draftProblem}` : ""}`;
|
|
2859
|
+
note(message);
|
|
2860
|
+
return message;
|
|
2861
|
+
}
|
|
1961
2862
|
};
|
|
1962
2863
|
}
|
|
1963
2864
|
|
|
1964
2865
|
// packages/runner/src/run.ts
|
|
1965
|
-
var defaultDeps = { loadAdapter, allocate, collectEvidence };
|
|
2866
|
+
var defaultDeps = { loadAdapter, allocate, chooseWorktree, prepareReuse, collectEvidence };
|
|
2867
|
+
var short3 = (s) => s.slice(0, 8);
|
|
1966
2868
|
function coldNote(cold, cwd, trunk) {
|
|
1967
2869
|
const parts = [worktreeProgress(cwd, trunk)];
|
|
1968
2870
|
if (cold.origin) parts.push(originProgress(cold.origin.id, cold.origin.lane));
|
|
1969
2871
|
return handoffNote(cold.fromHarness ?? "unknown", parts.join("\n\n"), cold.why);
|
|
1970
2872
|
}
|
|
1971
|
-
async function main(activeDir, lane,
|
|
1972
|
-
const
|
|
2873
|
+
async function main(activeDir, lane, seams = {}) {
|
|
2874
|
+
const deps = { ...defaultDeps, ...seams };
|
|
2875
|
+
const id = path15.basename(activeDir);
|
|
2876
|
+
if (isTrapCatch(id, lane)) throw new Error(`refusing headless runner for trap catch ${id}`);
|
|
1973
2877
|
const status = (verb, note) => appendStatus(id, lane, verb, note);
|
|
1974
2878
|
const cfg = loadConfig();
|
|
1975
|
-
const descriptor = JSON.parse(
|
|
2879
|
+
const descriptor = JSON.parse(fs17.readFileSync(path15.join(activeDir, "descriptor.json"), "utf8"));
|
|
1976
2880
|
const resolved = resolveDispatch(descriptor, cfg);
|
|
1977
2881
|
const repo = cfg.repos[descriptor.repo];
|
|
1978
|
-
const briefFile =
|
|
1979
|
-
if (!
|
|
1980
|
-
const brief =
|
|
2882
|
+
const briefFile = path15.join(activeDir, "brief.md");
|
|
2883
|
+
if (!fs17.existsSync(briefFile)) fs17.writeFileSync(briefFile, descriptor.brief);
|
|
2884
|
+
const brief = fs17.readFileSync(briefFile, "utf8");
|
|
1981
2885
|
const attempts = Number(process.env.LOBSTAH_ATTEMPTS ?? "1");
|
|
1982
2886
|
const runnerInfo = {
|
|
1983
2887
|
pid: process.pid,
|
|
1984
2888
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1985
2889
|
attempts
|
|
1986
2890
|
};
|
|
1987
|
-
|
|
2891
|
+
fs17.writeFileSync(path15.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
|
|
1988
2892
|
const plan = planStart({
|
|
1989
2893
|
id,
|
|
1990
2894
|
lane,
|
|
@@ -1993,11 +2897,24 @@ async function main(activeDir, lane, deps = defaultDeps) {
|
|
|
1993
2897
|
resolvedHarness: resolved.harness,
|
|
1994
2898
|
envResume: process.env.LOBSTAH_RESUME
|
|
1995
2899
|
});
|
|
2900
|
+
const wtFile = path15.join(activeDir, "worktree.json");
|
|
2901
|
+
let recorded;
|
|
2902
|
+
let choice;
|
|
2903
|
+
if (fs17.existsSync(wtFile)) {
|
|
2904
|
+
recorded = JSON.parse(fs17.readFileSync(wtFile, "utf8")).path;
|
|
2905
|
+
if (!fs17.existsSync(recorded)) throw new Error(`recorded worktree ${recorded} is gone`);
|
|
2906
|
+
} else if (fs17.existsSync(worktreePath(id))) {
|
|
2907
|
+
throw new Error(`unrecorded worktree already exists for ${id} \u2014 refusing to proceed`);
|
|
2908
|
+
} else if (descriptor.followUp && cfg.limits.reuseWorktree !== false) {
|
|
2909
|
+
choice = await deps.chooseWorktree({ id, lane, repoKey: descriptor.repo, repo, followUp: descriptor.followUp });
|
|
2910
|
+
}
|
|
2911
|
+
const worktreeNote = choice ? choice.reuse ? `reusing worktree of ${short3(choice.from)}` : `fresh worktree (${choice.reason})` : void 0;
|
|
1996
2912
|
const firstModel = modelForHarness(plan.harness, resolved.model);
|
|
1997
2913
|
const firstNote = [
|
|
1998
2914
|
attempts > 1 ? `attempt ${attempts}` : void 0,
|
|
1999
2915
|
plan.note,
|
|
2000
|
-
firstModel.dropped ? droppedModelNote(plan.harness, firstModel.dropped) : void 0
|
|
2916
|
+
firstModel.dropped ? droppedModelNote(plan.harness, firstModel.dropped) : void 0,
|
|
2917
|
+
worktreeNote
|
|
2001
2918
|
].filter(Boolean).join("; ");
|
|
2002
2919
|
status("working", firstNote || void 0);
|
|
2003
2920
|
appendEvent(id, lane, {
|
|
@@ -2007,28 +2924,112 @@ async function main(activeDir, lane, deps = defaultDeps) {
|
|
|
2007
2924
|
});
|
|
2008
2925
|
const own = readEvidence(id, lane).sessionId;
|
|
2009
2926
|
mergeEvidence(id, lane, { harness: plan.harness, sessionId: plan.resume?.own ? own : void 0 });
|
|
2010
|
-
const wtFile = path10.join(activeDir, "worktree.json");
|
|
2011
2927
|
let cwd;
|
|
2012
|
-
if (
|
|
2013
|
-
cwd =
|
|
2014
|
-
|
|
2015
|
-
} else if (
|
|
2016
|
-
|
|
2928
|
+
if (recorded) {
|
|
2929
|
+
cwd = recorded;
|
|
2930
|
+
acquireWorktreeLock(cwd, id, lane);
|
|
2931
|
+
} else if (choice?.reuse) {
|
|
2932
|
+
cwd = choice.path;
|
|
2933
|
+
const { setupRan } = await deps.prepareReuse(repo, cwd);
|
|
2934
|
+
if (setupRan) console.log(`[runner] lockfile changed since setup last ran in ${cwd}: setup re-run`);
|
|
2935
|
+
appendEvent(id, lane, {
|
|
2936
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2937
|
+
type: "runner",
|
|
2938
|
+
data: { worktree: cwd, worktreeOf: choice.owner, setupRan }
|
|
2939
|
+
});
|
|
2940
|
+
fs17.writeFileSync(wtFile, JSON.stringify({ path: cwd, of: choice.owner }, null, 2));
|
|
2017
2941
|
} else {
|
|
2018
2942
|
cwd = await deps.allocate(repo, id);
|
|
2019
|
-
|
|
2020
|
-
|
|
2943
|
+
acquireWorktreeLock(cwd, id, lane);
|
|
2944
|
+
fs17.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
|
|
2945
|
+
}
|
|
2946
|
+
const worktreeOf = JSON.parse(fs17.readFileSync(wtFile, "utf8")).of;
|
|
2947
|
+
mergeEvidence(id, lane, { worktree: cwd, worktreeOf });
|
|
2948
|
+
const existingPr = descriptor.followUp ? chainPr(descriptor.followUp, lane) : void 0;
|
|
2949
|
+
if (existingPr) mergeEvidence(id, lane, { prUrl: existingPr.url });
|
|
2950
|
+
const remotePolicy = {
|
|
2951
|
+
pushEarly: repo.pushEarly ?? cfg.limits.pushEarly,
|
|
2952
|
+
draftPr: repo.draftPr ?? cfg.limits.draftPr,
|
|
2953
|
+
checkpointOnStop: repo.checkpointOnStop ?? cfg.limits.checkpointOnStop
|
|
2954
|
+
};
|
|
2955
|
+
const remoteEnabled = Object.values(remotePolicy).some(Boolean);
|
|
2956
|
+
const remote = keepRemote({
|
|
2957
|
+
id,
|
|
2958
|
+
lane,
|
|
2959
|
+
cwd,
|
|
2960
|
+
trunk: repo.trunk,
|
|
2961
|
+
title: descriptor.brief.split("\n")[0]?.trim() || `Lobstah dispatch ${short3(id)}`,
|
|
2962
|
+
policy: remotePolicy,
|
|
2963
|
+
existingPrUrl: existingPr?.url
|
|
2964
|
+
});
|
|
2021
2965
|
const envNudge = process.env.LOBSTAH_NUDGE;
|
|
2022
2966
|
const planNudge = plan.cold && !envNudge ? coldNote(plan.cold, cwd, repo.trunk) : void 0;
|
|
2023
|
-
const promptWith = (nudge) => buildPrompt(brief, { id, nudge, attachments: descriptor.attachments });
|
|
2024
|
-
const binDir =
|
|
2025
|
-
const workerPath =
|
|
2967
|
+
const promptWith = (nudge) => buildPrompt(brief, { id, nudge, attachments: descriptor.attachments, existingPr });
|
|
2968
|
+
const binDir = path15.resolve(path15.dirname(fileURLToPath2(import.meta.url)), "..", "..", "..", "bin");
|
|
2969
|
+
const workerPath = fs17.existsSync(path15.join(binDir, "lobstah")) ? `${binDir}${path15.delimiter}${process.env.PATH ?? ""}` : void 0;
|
|
2026
2970
|
let wallClockHit = false;
|
|
2027
2971
|
let current;
|
|
2028
|
-
const
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2972
|
+
const wallFile = path15.join(activeDir, "wallclock.json");
|
|
2973
|
+
const wallState = (() => {
|
|
2974
|
+
try {
|
|
2975
|
+
return JSON.parse(fs17.readFileSync(wallFile, "utf8"));
|
|
2976
|
+
} catch {
|
|
2977
|
+
return { elapsedMs: 0, windowMs: resolved.limits.wallClockSecs * 1e3 };
|
|
2978
|
+
}
|
|
2979
|
+
})();
|
|
2980
|
+
const head = () => {
|
|
2981
|
+
try {
|
|
2982
|
+
return execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
2983
|
+
} catch {
|
|
2984
|
+
return void 0;
|
|
2985
|
+
}
|
|
2986
|
+
};
|
|
2987
|
+
wallState.lastHead ??= head();
|
|
2988
|
+
const maxWallMs = Math.max(
|
|
2989
|
+
resolved.limits.wallClockSecs ?? 0,
|
|
2990
|
+
cfg.limits.maxWallClockSecs ?? (resolved.limits.wallClockSecs ?? 0) * 4
|
|
2991
|
+
) * 1e3;
|
|
2992
|
+
const persistWall = (elapsedMs, windowMs) => {
|
|
2993
|
+
const tmp = `${wallFile}.tmp-${process.pid}`;
|
|
2994
|
+
fs17.writeFileSync(tmp, JSON.stringify({ ...wallState, elapsedMs, windowMs }));
|
|
2995
|
+
fs17.renameSync(tmp, wallFile);
|
|
2996
|
+
};
|
|
2997
|
+
const madeProgress = () => {
|
|
2998
|
+
const activity = readActivity(id, lane);
|
|
2999
|
+
const recent = activity && Date.now() - Date.parse(activity.at) <= cfg.limits.wedgeThresholdSecs * 1e3;
|
|
3000
|
+
if (recent && activity.at !== wallState.lastProgressAt) {
|
|
3001
|
+
wallState.lastProgressAt = activity.at;
|
|
3002
|
+
wallState.lastHead = head();
|
|
3003
|
+
return true;
|
|
3004
|
+
}
|
|
3005
|
+
const currentHead = head();
|
|
3006
|
+
if (currentHead && currentHead !== wallState.lastHead) {
|
|
3007
|
+
wallState.lastHead = currentHead;
|
|
3008
|
+
return true;
|
|
3009
|
+
}
|
|
3010
|
+
return false;
|
|
3011
|
+
};
|
|
3012
|
+
const wallTimer = resolved.limits.wallClockSecs ? startWallClock({
|
|
3013
|
+
limitMs: resolved.limits.wallClockSecs * 1e3,
|
|
3014
|
+
maxMs: maxWallMs,
|
|
3015
|
+
initialElapsedMs: wallState.elapsedMs,
|
|
3016
|
+
initialWindowMs: wallState.windowMs,
|
|
3017
|
+
paused: () => pausedWaiting(readStatusLog(id, lane).at(-1)),
|
|
3018
|
+
progress: madeProgress,
|
|
3019
|
+
onTick: persistWall,
|
|
3020
|
+
onExpire: () => {
|
|
3021
|
+
wallClockHit = true;
|
|
3022
|
+
current?.kill();
|
|
3023
|
+
}
|
|
3024
|
+
}) : void 0;
|
|
3025
|
+
if (wallTimer && wallState.elapsedMs >= maxWallMs) {
|
|
3026
|
+
wallTimer.stop();
|
|
3027
|
+
const saved = remoteEnabled ? await remote.saveBeforeStop() : void 0;
|
|
3028
|
+
settle(id, lane, { cancelled: false, wallClockHit: true, budgetNote: saved });
|
|
3029
|
+
releaseWorktreeLock(cwd, id);
|
|
3030
|
+
complete(id, lane);
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
2032
3033
|
const runOnce = async (harness, prompt, resumeSession) => {
|
|
2033
3034
|
const run2 = await deps.loadAdapter(harness).start({
|
|
2034
3035
|
id,
|
|
@@ -2049,6 +3050,7 @@ async function main(activeDir, lane, deps = defaultDeps) {
|
|
|
2049
3050
|
const { cancelled: cancelled2, activity } = await drive(run2, {
|
|
2050
3051
|
id,
|
|
2051
3052
|
lane,
|
|
3053
|
+
cwd,
|
|
2052
3054
|
stopped: () => wallClockHit,
|
|
2053
3055
|
backgroundWaitMs: cfg.limits.backgroundWaitSecs * 1e3
|
|
2054
3056
|
});
|
|
@@ -2081,10 +3083,18 @@ async function main(activeDir, lane, deps = defaultDeps) {
|
|
|
2081
3083
|
);
|
|
2082
3084
|
outcome = await runOnce(coldHarness, promptWith([envNudge, note].filter(Boolean).join("\n\n")), void 0);
|
|
2083
3085
|
}
|
|
2084
|
-
|
|
3086
|
+
wallTimer?.stop();
|
|
2085
3087
|
const { cancelled, result } = outcome;
|
|
3088
|
+
const terminal = ["done", "failed"].includes(readStatusLog(id, lane).at(-1)?.verb ?? "");
|
|
3089
|
+
let savedNote;
|
|
3090
|
+
if (remoteEnabled && (wallClockHit || cancelled || !!result.error || !terminal)) {
|
|
3091
|
+
savedNote = await remote.saveBeforeStop();
|
|
3092
|
+
status("working", `work saved before stop: ${savedNote}`);
|
|
3093
|
+
} else {
|
|
3094
|
+
await remote.stop();
|
|
3095
|
+
}
|
|
2086
3096
|
try {
|
|
2087
|
-
const gitEvidence = await deps.collectEvidence(repo,
|
|
3097
|
+
const gitEvidence = await deps.collectEvidence(repo, cwd);
|
|
2088
3098
|
console.log(`[runner] git evidence: ${JSON.stringify(gitEvidence)}`);
|
|
2089
3099
|
mergeEvidence(id, lane, { ...gitEvidence, sessionId: result.sessionId ?? readEvidence(id, lane).sessionId });
|
|
2090
3100
|
console.log(`[runner] evidence after merge: ${JSON.stringify(readEvidence(id, lane))}`);
|
|
@@ -2093,7 +3103,8 @@ async function main(activeDir, lane, deps = defaultDeps) {
|
|
|
2093
3103
|
note: `evidence collection failed: ${err instanceof Error ? err.message : String(err)}`.slice(0, 300)
|
|
2094
3104
|
});
|
|
2095
3105
|
}
|
|
2096
|
-
settle(id, lane, { cancelled, wallClockHit, error: result.error });
|
|
3106
|
+
settle(id, lane, { cancelled, wallClockHit, error: result.error, budgetNote: savedNote });
|
|
3107
|
+
releaseWorktreeLock(cwd, id);
|
|
2097
3108
|
complete(id, lane);
|
|
2098
3109
|
}
|
|
2099
3110
|
|
|
@@ -2101,10 +3112,10 @@ async function main(activeDir, lane, deps = defaultDeps) {
|
|
|
2101
3112
|
function runRunner(activeDirArg2, laneArg2) {
|
|
2102
3113
|
const lane = laneArg2 === "chore" ? "chore" : "work";
|
|
2103
3114
|
main(activeDirArg2, lane).catch((err) => {
|
|
2104
|
-
const id =
|
|
3115
|
+
const id = path16.basename(activeDirArg2);
|
|
2105
3116
|
try {
|
|
2106
3117
|
appendStatus(id, lane, "failed", String(err instanceof Error ? err.message : err).slice(0, 500));
|
|
2107
|
-
|
|
3118
|
+
fs18.renameSync(activeDirArg2, path16.join(laneDirs(lane).done, id));
|
|
2108
3119
|
} catch {
|
|
2109
3120
|
}
|
|
2110
3121
|
process.exitCode = 1;
|