lobstah 0.5.13 → 0.6.1
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 +7052 -3121
- package/dist/runner.js +1513 -1113
- package/docs/configuration.md +15 -9
- package/docs/harness/claude-code.md +14 -0
- package/docs/harness/codex.md +12 -0
- package/docs/man.md +472 -39
- package/docs/pickup.md +11 -3
- package/docs/vocabulary.md +120 -22
- 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 fs20 from "node:fs";
|
|
3
|
+
import * as path17 from "node:path";
|
|
4
4
|
|
|
5
5
|
// packages/core/dist/types.js
|
|
6
6
|
var VERBS = ["working", "needs-decision", "blocked", "paused", "done", "failed"];
|
|
@@ -11,6 +11,17 @@ var WAITING_ON_VERBS = ["paused", "needs-decision", "blocked"];
|
|
|
11
11
|
// packages/core/dist/paths.js
|
|
12
12
|
import * as os from "node:os";
|
|
13
13
|
import * as path from "node:path";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
function readDirIfPresent(dir) {
|
|
16
|
+
try {
|
|
17
|
+
return fs.readdirSync(dir);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
const code = e.code;
|
|
20
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
21
|
+
return [];
|
|
22
|
+
throw e;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
14
25
|
function lobstahHome() {
|
|
15
26
|
return process.env.LOBSTAH_HOME ?? path.join(os.homedir(), ".lobstah");
|
|
16
27
|
}
|
|
@@ -54,615 +65,236 @@ function attachmentBlock(attachments) {
|
|
|
54
65
|
].join("\n");
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
// packages/core/dist/
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
// packages/core/dist/decisions.js
|
|
69
|
+
var DECISION_DETAIL_MAX = 64 * 1024;
|
|
70
|
+
var ANSWER_TYPES = {
|
|
71
|
+
".png": "image/png",
|
|
72
|
+
".jpg": "image/jpeg",
|
|
73
|
+
".jpeg": "image/jpeg",
|
|
74
|
+
".gif": "image/gif",
|
|
75
|
+
".webp": "image/webp",
|
|
76
|
+
".pdf": "application/pdf",
|
|
77
|
+
".txt": "text/plain",
|
|
78
|
+
".md": "text/markdown",
|
|
79
|
+
".csv": "text/csv",
|
|
80
|
+
".json": "application/json",
|
|
81
|
+
".log": "text/plain",
|
|
82
|
+
".diff": "text/plain",
|
|
83
|
+
".patch": "text/plain",
|
|
84
|
+
".yaml": "text/plain",
|
|
85
|
+
".yml": "text/plain",
|
|
86
|
+
".toml": "text/plain",
|
|
87
|
+
".zip": "application/zip"
|
|
88
|
+
};
|
|
89
|
+
var ANSWER_EXTENSIONS = Object.keys(ANSWER_TYPES);
|
|
60
90
|
|
|
61
|
-
// packages/core/dist/
|
|
91
|
+
// packages/core/dist/config.js
|
|
62
92
|
import * as fs2 from "node:fs";
|
|
63
|
-
|
|
64
|
-
// packages/core/dist/activity.js
|
|
65
|
-
import * as fs from "node:fs";
|
|
93
|
+
import * as os2 from "node:os";
|
|
66
94
|
import * as path2 from "node:path";
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
var
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return commandWord(words.slice(2).join(" ").replace(/^["']/, "")) ?? name;
|
|
95
|
+
|
|
96
|
+
// node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/date.js
|
|
97
|
+
var DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[T ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|[-+]\d{2}:\d{2})?$/i;
|
|
98
|
+
var TomlDate = class _TomlDate extends Date {
|
|
99
|
+
#hasDate = false;
|
|
100
|
+
#hasTime = false;
|
|
101
|
+
#offset = null;
|
|
102
|
+
constructor(date) {
|
|
103
|
+
let hasDate = true;
|
|
104
|
+
let hasTime = true;
|
|
105
|
+
let offset = "Z";
|
|
106
|
+
if (typeof date === "string") {
|
|
107
|
+
let match = date.match(DATE_TIME_RE);
|
|
108
|
+
if (match) {
|
|
109
|
+
if (!match[1]) {
|
|
110
|
+
hasDate = false;
|
|
111
|
+
date = `0000-01-01T${date}`;
|
|
112
|
+
}
|
|
113
|
+
hasTime = !!match[2];
|
|
114
|
+
hasTime && date[10] === " " && (date = date.replace(" ", "T"));
|
|
115
|
+
if (match[2] && +match[2] > 23) {
|
|
116
|
+
date = "";
|
|
117
|
+
} else {
|
|
118
|
+
offset = match[3] || null;
|
|
119
|
+
date = date.toUpperCase();
|
|
120
|
+
if (!offset && hasTime)
|
|
121
|
+
date += "Z";
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
date = "";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
super(date);
|
|
128
|
+
if (!isNaN(this.getTime())) {
|
|
129
|
+
this.#hasDate = hasDate;
|
|
130
|
+
this.#hasTime = hasTime;
|
|
131
|
+
this.#offset = offset;
|
|
132
|
+
}
|
|
106
133
|
}
|
|
107
|
-
|
|
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;
|
|
134
|
+
isDateTime() {
|
|
135
|
+
return this.#hasDate && this.#hasTime;
|
|
133
136
|
}
|
|
134
|
-
|
|
135
|
-
|
|
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;
|
|
137
|
+
isLocal() {
|
|
138
|
+
return !this.#hasDate || !this.#hasTime || !this.#offset;
|
|
154
139
|
}
|
|
155
|
-
|
|
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;
|
|
140
|
+
isDate() {
|
|
141
|
+
return this.#hasDate && !this.#hasTime;
|
|
185
142
|
}
|
|
186
|
-
|
|
187
|
-
|
|
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;
|
|
143
|
+
isTime() {
|
|
144
|
+
return this.#hasTime && !this.#hasDate;
|
|
206
145
|
}
|
|
207
|
-
|
|
208
|
-
|
|
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;
|
|
146
|
+
isValid() {
|
|
147
|
+
return this.#hasDate || this.#hasTime;
|
|
222
148
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
149
|
+
toISOString() {
|
|
150
|
+
let iso = super.toISOString();
|
|
151
|
+
if (this.isDate())
|
|
152
|
+
return iso.slice(0, 10);
|
|
153
|
+
if (this.isTime())
|
|
154
|
+
return iso.slice(11, 23);
|
|
155
|
+
if (this.#offset === null)
|
|
156
|
+
return iso.slice(0, -1);
|
|
157
|
+
if (this.#offset === "Z")
|
|
158
|
+
return iso;
|
|
159
|
+
let offset = +this.#offset.slice(1, 3) * 60 + +this.#offset.slice(4, 6);
|
|
160
|
+
offset = this.#offset[0] === "-" ? offset : -offset;
|
|
161
|
+
let offsetDate = new Date(this.getTime() - offset * 6e4);
|
|
162
|
+
return offsetDate.toISOString().slice(0, -1) + this.#offset;
|
|
227
163
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
}
|
|
164
|
+
static wrapAsOffsetDateTime(jsDate, offset = "Z") {
|
|
165
|
+
let date = new _TomlDate(jsDate);
|
|
166
|
+
date.#offset = offset;
|
|
167
|
+
return date;
|
|
242
168
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const a = this.pending;
|
|
248
|
-
this.emit(a, this.now());
|
|
169
|
+
static wrapAsLocalDateTime(jsDate) {
|
|
170
|
+
let date = new _TomlDate(jsDate);
|
|
171
|
+
date.#offset = null;
|
|
172
|
+
return date;
|
|
249
173
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
174
|
+
static wrapAsLocalDate(jsDate) {
|
|
175
|
+
let date = new _TomlDate(jsDate);
|
|
176
|
+
date.#hasTime = false;
|
|
177
|
+
date.#offset = null;
|
|
178
|
+
return date;
|
|
255
179
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
this.lastWriteAt = t;
|
|
263
|
-
this.lastKind = a.kind;
|
|
264
|
-
try {
|
|
265
|
-
this.write(a);
|
|
266
|
-
} catch {
|
|
267
|
-
}
|
|
180
|
+
static wrapAsLocalTime(jsDate) {
|
|
181
|
+
let date = new _TomlDate(jsDate);
|
|
182
|
+
date.#hasDate = false;
|
|
183
|
+
date.#offset = null;
|
|
184
|
+
return date;
|
|
268
185
|
}
|
|
269
186
|
};
|
|
270
187
|
|
|
271
|
-
//
|
|
272
|
-
function
|
|
273
|
-
|
|
274
|
-
|
|
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();
|
|
188
|
+
// node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/error.js
|
|
189
|
+
function getLineColFromPtr(string, ptr) {
|
|
190
|
+
let lines = string.slice(0, ptr).split(/\r\n|\n|\r/g);
|
|
191
|
+
return [lines.length, lines.pop().length + 1];
|
|
288
192
|
}
|
|
289
|
-
function
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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`);
|
|
193
|
+
function makeCodeBlock(string, line, column) {
|
|
194
|
+
let lines = string.split(/\r\n|\n|\r/g);
|
|
195
|
+
let codeblock = "";
|
|
196
|
+
let numberLen = (Math.log10(line + 1) | 0) + 1;
|
|
197
|
+
for (let i = line - 1; i <= line + 1; i++) {
|
|
198
|
+
let l = lines[i - 1];
|
|
199
|
+
if (!l)
|
|
200
|
+
continue;
|
|
201
|
+
codeblock += i.toString().padEnd(numberLen, " ");
|
|
202
|
+
codeblock += ": ";
|
|
203
|
+
codeblock += l;
|
|
204
|
+
codeblock += "\n";
|
|
205
|
+
if (i === line) {
|
|
206
|
+
codeblock += " ".repeat(numberLen + column + 2);
|
|
207
|
+
codeblock += "^\n";
|
|
311
208
|
}
|
|
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) {
|
|
319
|
-
if (!isVerb(verb)) {
|
|
320
|
-
throw new Error(`invalid status verb "${verb}" \u2014 must be one of: ${VERBS.join(", ")}`);
|
|
321
209
|
}
|
|
322
|
-
|
|
323
|
-
const entry = { at: at ?? (/* @__PURE__ */ new Date()).toISOString(), verb, ...note ? { note } : {}, ...extra };
|
|
324
|
-
fs2.appendFileSync(statusPath(id, lane), `${JSON.stringify(entry)}
|
|
325
|
-
`);
|
|
326
|
-
return entry;
|
|
327
|
-
}
|
|
328
|
-
function readStatusLog(id, lane) {
|
|
329
|
-
const file = statusPath(id, lane);
|
|
330
|
-
if (!fs2.existsSync(file))
|
|
331
|
-
return [];
|
|
332
|
-
return fs2.readFileSync(file, "utf8").split("\n").filter(Boolean).flatMap((line) => {
|
|
333
|
-
try {
|
|
334
|
-
const parsed = JSON.parse(line);
|
|
335
|
-
return isVerb(parsed.verb) ? [parsed] : [];
|
|
336
|
-
} catch {
|
|
337
|
-
return [];
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
function pausedWaiting(entry) {
|
|
342
|
-
return entry?.verb === "paused" && entry.waitingOn !== void 0;
|
|
210
|
+
return codeblock;
|
|
343
211
|
}
|
|
212
|
+
var TomlError = class extends Error {
|
|
213
|
+
line;
|
|
214
|
+
column;
|
|
215
|
+
codeblock;
|
|
216
|
+
constructor(message, options) {
|
|
217
|
+
const [line, column] = getLineColFromPtr(options.toml, options.ptr);
|
|
218
|
+
const codeblock = makeCodeBlock(options.toml, line, column);
|
|
219
|
+
super(`Invalid TOML document: ${message}
|
|
344
220
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
path3.join(dirs.queue, `${id}.json`),
|
|
350
|
-
path3.join(dirs.active, id, "descriptor.json"),
|
|
351
|
-
path3.join(dirs.done, id, "descriptor.json")
|
|
352
|
-
]) {
|
|
353
|
-
try {
|
|
354
|
-
return JSON.parse(fs3.readFileSync(file, "utf8"));
|
|
355
|
-
} catch {
|
|
356
|
-
}
|
|
221
|
+
${codeblock}`, options);
|
|
222
|
+
this.line = line;
|
|
223
|
+
this.column = column;
|
|
224
|
+
this.codeblock = codeblock;
|
|
357
225
|
}
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
function activeIds(lane) {
|
|
361
|
-
return fs3.readdirSync(laneDirs(lane).active).filter((f) => !f.startsWith("."));
|
|
362
|
-
}
|
|
363
|
-
function complete(id, lane) {
|
|
364
|
-
const dirs = laneDirs(lane);
|
|
365
|
-
fs3.renameSync(path3.join(dirs.active, id), path3.join(dirs.done, id));
|
|
366
|
-
}
|
|
367
|
-
function cancelRequested(id, lane) {
|
|
368
|
-
return fs3.existsSync(path3.join(laneDirs(lane).active, id, "cancel"));
|
|
369
|
-
}
|
|
226
|
+
};
|
|
370
227
|
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
function touchEvents(id, lane, at = /* @__PURE__ */ new Date()) {
|
|
378
|
-
const file = eventsPath(id, lane);
|
|
379
|
-
try {
|
|
380
|
-
fs4.utimesSync(file, at, at);
|
|
381
|
-
} catch {
|
|
382
|
-
fs4.appendFileSync(file, "");
|
|
383
|
-
fs4.utimesSync(file, at, at);
|
|
384
|
-
}
|
|
228
|
+
// node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/util.js
|
|
229
|
+
function indexOfNewline(str, start = 0) {
|
|
230
|
+
let idx = str.indexOf("\n", start);
|
|
231
|
+
if (str.charCodeAt(idx - 1) === 13)
|
|
232
|
+
idx--;
|
|
233
|
+
return idx;
|
|
385
234
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
235
|
+
function skipComment(ctx) {
|
|
236
|
+
for (; ctx.p < ctx.s.length; ctx.p++) {
|
|
237
|
+
let c = ctx.s.charCodeAt(ctx.p);
|
|
238
|
+
if (c === 10)
|
|
239
|
+
break;
|
|
240
|
+
if (c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10) {
|
|
241
|
+
ctx.p++;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
if (c < 32 && c !== 9 || c === 127) {
|
|
245
|
+
throw new TomlError("control characters are not allowed in comments", {
|
|
246
|
+
toml: ctx.s,
|
|
247
|
+
ptr: ctx.p
|
|
248
|
+
});
|
|
249
|
+
}
|
|
394
250
|
}
|
|
395
251
|
}
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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;
|
|
252
|
+
function skipVoid(ctx, banNewLines, banComments) {
|
|
253
|
+
let c;
|
|
254
|
+
while (1) {
|
|
255
|
+
while ((c = ctx.s.charCodeAt(ctx.p)) === 32 || c === 9 || !banNewLines && (c === 10 || c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10))
|
|
256
|
+
ctx.p++;
|
|
257
|
+
if (banComments || c !== 35)
|
|
258
|
+
break;
|
|
259
|
+
skipComment(ctx);
|
|
418
260
|
}
|
|
419
261
|
}
|
|
420
|
-
function
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
262
|
+
function skipUntil(ctx, sep, end) {
|
|
263
|
+
let ptr = ctx.p;
|
|
264
|
+
if (!end) {
|
|
265
|
+
ptr = indexOfNewline(ctx.s, ptr);
|
|
266
|
+
ctx.p = ptr < 0 ? ctx.s.length : ptr;
|
|
267
|
+
return;
|
|
425
268
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
} catch {
|
|
434
|
-
return void 0;
|
|
269
|
+
for (; ctx.p < ctx.s.length; ctx.p++) {
|
|
270
|
+
let c = ctx.s.charCodeAt(ctx.p);
|
|
271
|
+
if (c === 35) {
|
|
272
|
+
skipComment(ctx);
|
|
273
|
+
} else if (c === end || c === sep) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
435
276
|
}
|
|
277
|
+
throw new TomlError("cannot find end of structure", {
|
|
278
|
+
toml: ctx.s,
|
|
279
|
+
ptr
|
|
280
|
+
});
|
|
436
281
|
}
|
|
437
282
|
|
|
438
|
-
//
|
|
439
|
-
var
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
try {
|
|
454
|
-
fs7.renameSync(path5.join(dir, metaName(file)), path5.join(dir, "handled", metaName(file)));
|
|
455
|
-
} catch {
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// packages/core/dist/config.js
|
|
460
|
-
import * as fs8 from "node:fs";
|
|
461
|
-
import * as os2 from "node:os";
|
|
462
|
-
import * as path6 from "node:path";
|
|
463
|
-
|
|
464
|
-
// node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/date.js
|
|
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;
|
|
466
|
-
var TomlDate = class _TomlDate extends Date {
|
|
467
|
-
#hasDate = false;
|
|
468
|
-
#hasTime = false;
|
|
469
|
-
#offset = null;
|
|
470
|
-
constructor(date) {
|
|
471
|
-
let hasDate = true;
|
|
472
|
-
let hasTime = true;
|
|
473
|
-
let offset = "Z";
|
|
474
|
-
if (typeof date === "string") {
|
|
475
|
-
let match = date.match(DATE_TIME_RE);
|
|
476
|
-
if (match) {
|
|
477
|
-
if (!match[1]) {
|
|
478
|
-
hasDate = false;
|
|
479
|
-
date = `0000-01-01T${date}`;
|
|
480
|
-
}
|
|
481
|
-
hasTime = !!match[2];
|
|
482
|
-
hasTime && date[10] === " " && (date = date.replace(" ", "T"));
|
|
483
|
-
if (match[2] && +match[2] > 23) {
|
|
484
|
-
date = "";
|
|
485
|
-
} else {
|
|
486
|
-
offset = match[3] || null;
|
|
487
|
-
date = date.toUpperCase();
|
|
488
|
-
if (!offset && hasTime)
|
|
489
|
-
date += "Z";
|
|
490
|
-
}
|
|
491
|
-
} else {
|
|
492
|
-
date = "";
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
super(date);
|
|
496
|
-
if (!isNaN(this.getTime())) {
|
|
497
|
-
this.#hasDate = hasDate;
|
|
498
|
-
this.#hasTime = hasTime;
|
|
499
|
-
this.#offset = offset;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
isDateTime() {
|
|
503
|
-
return this.#hasDate && this.#hasTime;
|
|
504
|
-
}
|
|
505
|
-
isLocal() {
|
|
506
|
-
return !this.#hasDate || !this.#hasTime || !this.#offset;
|
|
507
|
-
}
|
|
508
|
-
isDate() {
|
|
509
|
-
return this.#hasDate && !this.#hasTime;
|
|
510
|
-
}
|
|
511
|
-
isTime() {
|
|
512
|
-
return this.#hasTime && !this.#hasDate;
|
|
513
|
-
}
|
|
514
|
-
isValid() {
|
|
515
|
-
return this.#hasDate || this.#hasTime;
|
|
516
|
-
}
|
|
517
|
-
toISOString() {
|
|
518
|
-
let iso = super.toISOString();
|
|
519
|
-
if (this.isDate())
|
|
520
|
-
return iso.slice(0, 10);
|
|
521
|
-
if (this.isTime())
|
|
522
|
-
return iso.slice(11, 23);
|
|
523
|
-
if (this.#offset === null)
|
|
524
|
-
return iso.slice(0, -1);
|
|
525
|
-
if (this.#offset === "Z")
|
|
526
|
-
return iso;
|
|
527
|
-
let offset = +this.#offset.slice(1, 3) * 60 + +this.#offset.slice(4, 6);
|
|
528
|
-
offset = this.#offset[0] === "-" ? offset : -offset;
|
|
529
|
-
let offsetDate = new Date(this.getTime() - offset * 6e4);
|
|
530
|
-
return offsetDate.toISOString().slice(0, -1) + this.#offset;
|
|
531
|
-
}
|
|
532
|
-
static wrapAsOffsetDateTime(jsDate, offset = "Z") {
|
|
533
|
-
let date = new _TomlDate(jsDate);
|
|
534
|
-
date.#offset = offset;
|
|
535
|
-
return date;
|
|
536
|
-
}
|
|
537
|
-
static wrapAsLocalDateTime(jsDate) {
|
|
538
|
-
let date = new _TomlDate(jsDate);
|
|
539
|
-
date.#offset = null;
|
|
540
|
-
return date;
|
|
541
|
-
}
|
|
542
|
-
static wrapAsLocalDate(jsDate) {
|
|
543
|
-
let date = new _TomlDate(jsDate);
|
|
544
|
-
date.#hasTime = false;
|
|
545
|
-
date.#offset = null;
|
|
546
|
-
return date;
|
|
547
|
-
}
|
|
548
|
-
static wrapAsLocalTime(jsDate) {
|
|
549
|
-
let date = new _TomlDate(jsDate);
|
|
550
|
-
date.#hasDate = false;
|
|
551
|
-
date.#offset = null;
|
|
552
|
-
return date;
|
|
553
|
-
}
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
// node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/error.js
|
|
557
|
-
function getLineColFromPtr(string, ptr) {
|
|
558
|
-
let lines = string.slice(0, ptr).split(/\r\n|\n|\r/g);
|
|
559
|
-
return [lines.length, lines.pop().length + 1];
|
|
560
|
-
}
|
|
561
|
-
function makeCodeBlock(string, line, column) {
|
|
562
|
-
let lines = string.split(/\r\n|\n|\r/g);
|
|
563
|
-
let codeblock = "";
|
|
564
|
-
let numberLen = (Math.log10(line + 1) | 0) + 1;
|
|
565
|
-
for (let i = line - 1; i <= line + 1; i++) {
|
|
566
|
-
let l = lines[i - 1];
|
|
567
|
-
if (!l)
|
|
568
|
-
continue;
|
|
569
|
-
codeblock += i.toString().padEnd(numberLen, " ");
|
|
570
|
-
codeblock += ": ";
|
|
571
|
-
codeblock += l;
|
|
572
|
-
codeblock += "\n";
|
|
573
|
-
if (i === line) {
|
|
574
|
-
codeblock += " ".repeat(numberLen + column + 2);
|
|
575
|
-
codeblock += "^\n";
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
return codeblock;
|
|
579
|
-
}
|
|
580
|
-
var TomlError = class extends Error {
|
|
581
|
-
line;
|
|
582
|
-
column;
|
|
583
|
-
codeblock;
|
|
584
|
-
constructor(message, options) {
|
|
585
|
-
const [line, column] = getLineColFromPtr(options.toml, options.ptr);
|
|
586
|
-
const codeblock = makeCodeBlock(options.toml, line, column);
|
|
587
|
-
super(`Invalid TOML document: ${message}
|
|
588
|
-
|
|
589
|
-
${codeblock}`, options);
|
|
590
|
-
this.line = line;
|
|
591
|
-
this.column = column;
|
|
592
|
-
this.codeblock = codeblock;
|
|
593
|
-
}
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
// node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/util.js
|
|
597
|
-
function indexOfNewline(str, start = 0) {
|
|
598
|
-
let idx = str.indexOf("\n", start);
|
|
599
|
-
if (str.charCodeAt(idx - 1) === 13)
|
|
600
|
-
idx--;
|
|
601
|
-
return idx;
|
|
602
|
-
}
|
|
603
|
-
function skipComment(ctx) {
|
|
604
|
-
for (; ctx.p < ctx.s.length; ctx.p++) {
|
|
605
|
-
let c = ctx.s.charCodeAt(ctx.p);
|
|
606
|
-
if (c === 10)
|
|
607
|
-
break;
|
|
608
|
-
if (c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10) {
|
|
609
|
-
ctx.p++;
|
|
610
|
-
break;
|
|
611
|
-
}
|
|
612
|
-
if (c < 32 && c !== 9 || c === 127) {
|
|
613
|
-
throw new TomlError("control characters are not allowed in comments", {
|
|
614
|
-
toml: ctx.s,
|
|
615
|
-
ptr: ctx.p
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
function skipVoid(ctx, banNewLines, banComments) {
|
|
621
|
-
let c;
|
|
622
|
-
while (1) {
|
|
623
|
-
while ((c = ctx.s.charCodeAt(ctx.p)) === 32 || c === 9 || !banNewLines && (c === 10 || c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10))
|
|
624
|
-
ctx.p++;
|
|
625
|
-
if (banComments || c !== 35)
|
|
626
|
-
break;
|
|
627
|
-
skipComment(ctx);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
function skipUntil(ctx, sep, end) {
|
|
631
|
-
let ptr = ctx.p;
|
|
632
|
-
if (!end) {
|
|
633
|
-
ptr = indexOfNewline(ctx.s, ptr);
|
|
634
|
-
ctx.p = ptr < 0 ? ctx.s.length : ptr;
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
for (; ctx.p < ctx.s.length; ctx.p++) {
|
|
638
|
-
let c = ctx.s.charCodeAt(ctx.p);
|
|
639
|
-
if (c === 35) {
|
|
640
|
-
skipComment(ctx);
|
|
641
|
-
} else if (c === end || c === sep) {
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
throw new TomlError("cannot find end of structure", {
|
|
646
|
-
toml: ctx.s,
|
|
647
|
-
ptr
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/primitive.js
|
|
652
|
-
var INT_REGEX = /^((0x[0-9a-fA-F](_?[0-9a-fA-F])*)|(([+-]|0[ob])?\d(_?\d)*))$/;
|
|
653
|
-
var FLOAT_REGEX = /^[+-]?\d(_?\d)*(\.\d(_?\d)*)?([eE][+-]?\d(_?\d)*)?$/;
|
|
654
|
-
var LEADING_ZERO = /^[+-]?0[0-9_]/;
|
|
655
|
-
function parseString(ctx) {
|
|
656
|
-
let start = ctx.p;
|
|
657
|
-
let c = ctx.s.charCodeAt(ctx.p++);
|
|
658
|
-
let first = c;
|
|
659
|
-
let isLiteral = c === 39;
|
|
660
|
-
let isMultiline = c === ctx.s.charCodeAt(ctx.p) && c === ctx.s.charCodeAt(ctx.p + 1);
|
|
661
|
-
if (isMultiline) {
|
|
662
|
-
if ((c = ctx.s.charCodeAt(ctx.p += 2)) === 10)
|
|
663
|
-
ctx.p++;
|
|
664
|
-
else if (c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10)
|
|
665
|
-
ctx.p += 2;
|
|
283
|
+
// node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/primitive.js
|
|
284
|
+
var INT_REGEX = /^((0x[0-9a-fA-F](_?[0-9a-fA-F])*)|(([+-]|0[ob])?\d(_?\d)*))$/;
|
|
285
|
+
var FLOAT_REGEX = /^[+-]?\d(_?\d)*(\.\d(_?\d)*)?([eE][+-]?\d(_?\d)*)?$/;
|
|
286
|
+
var LEADING_ZERO = /^[+-]?0[0-9_]/;
|
|
287
|
+
function parseString(ctx) {
|
|
288
|
+
let start = ctx.p;
|
|
289
|
+
let c = ctx.s.charCodeAt(ctx.p++);
|
|
290
|
+
let first = c;
|
|
291
|
+
let isLiteral = c === 39;
|
|
292
|
+
let isMultiline = c === ctx.s.charCodeAt(ctx.p) && c === ctx.s.charCodeAt(ctx.p + 1);
|
|
293
|
+
if (isMultiline) {
|
|
294
|
+
if ((c = ctx.s.charCodeAt(ctx.p += 2)) === 10)
|
|
295
|
+
ctx.p++;
|
|
296
|
+
else if (c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10)
|
|
297
|
+
ctx.p += 2;
|
|
666
298
|
}
|
|
667
299
|
let parsed = "";
|
|
668
300
|
let sliceStart = ctx.p;
|
|
@@ -1025,214 +657,892 @@ function peekTable(key, table, meta, type) {
|
|
|
1025
657
|
state.d = true;
|
|
1026
658
|
t[k] = [];
|
|
1027
659
|
}
|
|
1028
|
-
t[k].push(t = {});
|
|
1029
|
-
state.c[state.i++] = state = { t: 1, d: false, i: 0, c: {} };
|
|
1030
|
-
}
|
|
1031
|
-
if (state.d) {
|
|
1032
|
-
return null;
|
|
1033
|
-
}
|
|
1034
|
-
state.d = true;
|
|
1035
|
-
if (type === 1) {
|
|
1036
|
-
t = hasOwn ? t[k] : t[k] = {};
|
|
1037
|
-
} else if (type === 0 && hasOwn) {
|
|
1038
|
-
return null;
|
|
1039
|
-
}
|
|
1040
|
-
return [k, t, state.c];
|
|
1041
|
-
}
|
|
1042
|
-
function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
|
|
1043
|
-
let ctx = { s: toml, p: 0, d: maxDepth };
|
|
1044
|
-
let res = {};
|
|
1045
|
-
let meta = {};
|
|
1046
|
-
let tmp;
|
|
1047
|
-
let tbl = res;
|
|
1048
|
-
let m = meta;
|
|
1049
|
-
skipVoid(ctx);
|
|
1050
|
-
while (ctx.p < toml.length) {
|
|
1051
|
-
if (toml.charCodeAt(ctx.p) === 91) {
|
|
1052
|
-
let isTableArray = toml.charCodeAt(++ctx.p) === 91;
|
|
1053
|
-
tmp = ctx.p += +isTableArray;
|
|
1054
|
-
let k = parseKey(ctx, "]");
|
|
1055
|
-
if (isTableArray) {
|
|
1056
|
-
if (toml.charCodeAt(ctx.p - 1) !== 93) {
|
|
1057
|
-
throw new TomlError("expected end of table declaration", {
|
|
1058
|
-
toml,
|
|
1059
|
-
ptr: ctx.p - 1
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
ctx.p++;
|
|
1063
|
-
}
|
|
1064
|
-
let p = peekTable(
|
|
1065
|
-
k,
|
|
1066
|
-
res,
|
|
1067
|
-
meta,
|
|
1068
|
-
isTableArray ? 2 : 1
|
|
1069
|
-
/* Type.EXPLICIT */
|
|
1070
|
-
);
|
|
1071
|
-
if (!p) {
|
|
1072
|
-
throw new TomlError("trying to redefine an already defined table or value", {
|
|
1073
|
-
toml,
|
|
1074
|
-
ptr: tmp
|
|
1075
|
-
});
|
|
1076
|
-
}
|
|
1077
|
-
m = p[2];
|
|
1078
|
-
tbl = p[1];
|
|
1079
|
-
} else {
|
|
1080
|
-
tmp = ctx.p;
|
|
1081
|
-
let k = parseKey(ctx);
|
|
1082
|
-
let p = peekTable(
|
|
1083
|
-
k,
|
|
1084
|
-
tbl,
|
|
1085
|
-
m,
|
|
1086
|
-
0
|
|
1087
|
-
/* Type.DOTTED */
|
|
1088
|
-
);
|
|
1089
|
-
if (!p) {
|
|
1090
|
-
throw new TomlError("trying to redefine an already defined table or value", {
|
|
1091
|
-
toml,
|
|
1092
|
-
ptr: tmp
|
|
1093
|
-
});
|
|
1094
|
-
}
|
|
1095
|
-
p[1][p[0]] = extractValue(ctx, void 0, integersAsBigInt);
|
|
660
|
+
t[k].push(t = {});
|
|
661
|
+
state.c[state.i++] = state = { t: 1, d: false, i: 0, c: {} };
|
|
662
|
+
}
|
|
663
|
+
if (state.d) {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
state.d = true;
|
|
667
|
+
if (type === 1) {
|
|
668
|
+
t = hasOwn ? t[k] : t[k] = {};
|
|
669
|
+
} else if (type === 0 && hasOwn) {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
return [k, t, state.c];
|
|
673
|
+
}
|
|
674
|
+
function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
|
|
675
|
+
let ctx = { s: toml, p: 0, d: maxDepth };
|
|
676
|
+
let res = {};
|
|
677
|
+
let meta = {};
|
|
678
|
+
let tmp;
|
|
679
|
+
let tbl = res;
|
|
680
|
+
let m = meta;
|
|
681
|
+
skipVoid(ctx);
|
|
682
|
+
while (ctx.p < toml.length) {
|
|
683
|
+
if (toml.charCodeAt(ctx.p) === 91) {
|
|
684
|
+
let isTableArray = toml.charCodeAt(++ctx.p) === 91;
|
|
685
|
+
tmp = ctx.p += +isTableArray;
|
|
686
|
+
let k = parseKey(ctx, "]");
|
|
687
|
+
if (isTableArray) {
|
|
688
|
+
if (toml.charCodeAt(ctx.p - 1) !== 93) {
|
|
689
|
+
throw new TomlError("expected end of table declaration", {
|
|
690
|
+
toml,
|
|
691
|
+
ptr: ctx.p - 1
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
ctx.p++;
|
|
695
|
+
}
|
|
696
|
+
let p = peekTable(
|
|
697
|
+
k,
|
|
698
|
+
res,
|
|
699
|
+
meta,
|
|
700
|
+
isTableArray ? 2 : 1
|
|
701
|
+
/* Type.EXPLICIT */
|
|
702
|
+
);
|
|
703
|
+
if (!p) {
|
|
704
|
+
throw new TomlError("trying to redefine an already defined table or value", {
|
|
705
|
+
toml,
|
|
706
|
+
ptr: tmp
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
m = p[2];
|
|
710
|
+
tbl = p[1];
|
|
711
|
+
} else {
|
|
712
|
+
tmp = ctx.p;
|
|
713
|
+
let k = parseKey(ctx);
|
|
714
|
+
let p = peekTable(
|
|
715
|
+
k,
|
|
716
|
+
tbl,
|
|
717
|
+
m,
|
|
718
|
+
0
|
|
719
|
+
/* Type.DOTTED */
|
|
720
|
+
);
|
|
721
|
+
if (!p) {
|
|
722
|
+
throw new TomlError("trying to redefine an already defined table or value", {
|
|
723
|
+
toml,
|
|
724
|
+
ptr: tmp
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
p[1][p[0]] = extractValue(ctx, void 0, integersAsBigInt);
|
|
728
|
+
}
|
|
729
|
+
skipVoid(ctx, true);
|
|
730
|
+
if (ctx.p < toml.length && (tmp = toml.charCodeAt(ctx.p)) !== 10 && tmp !== 13) {
|
|
731
|
+
throw new TomlError("each key-value declaration must be followed by an end-of-line", {
|
|
732
|
+
toml,
|
|
733
|
+
ptr: ctx.p
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
skipVoid(ctx);
|
|
737
|
+
}
|
|
738
|
+
return res;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// packages/core/dist/config.js
|
|
742
|
+
var ATTENTION_KINDS = ["question", "decision", "landed", "pr:draft", "pr:review", "pr:checks", "pr:conflict", "pr:ready", "report"];
|
|
743
|
+
var DEFAULT_ATTENTION_KINDS = ["question", "decision", "pr:ready", "pr:review", "pr:conflict", "pr:checks"];
|
|
744
|
+
function parseAttentionKinds(raw) {
|
|
745
|
+
if (raw === void 0)
|
|
746
|
+
return [...DEFAULT_ATTENTION_KINDS];
|
|
747
|
+
if (!Array.isArray(raw)) {
|
|
748
|
+
throw new Error(`attentionKinds must be an array of kinds (${ATTENTION_KINDS.join(", ")}) in ${configPath()}`);
|
|
749
|
+
}
|
|
750
|
+
for (const k of raw) {
|
|
751
|
+
if (!ATTENTION_KINDS.includes(k)) {
|
|
752
|
+
throw new Error(`attentionKinds: unknown kind "${String(k)}" in ${configPath()} \u2014 valid kinds: ${ATTENTION_KINDS.join(", ")}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return [...new Set(raw)];
|
|
756
|
+
}
|
|
757
|
+
var DEFAULT_SOAK = {
|
|
758
|
+
deferSecs: 90,
|
|
759
|
+
ttlSecs: 1800,
|
|
760
|
+
beat: true,
|
|
761
|
+
pausedTtlSecs: 86400
|
|
762
|
+
};
|
|
763
|
+
var DEFAULT_HELM = {
|
|
764
|
+
ttlSecs: 1800,
|
|
765
|
+
reportSecs: 900,
|
|
766
|
+
armGraceSecs: 5
|
|
767
|
+
};
|
|
768
|
+
var DEFAULT_GLASS = { port: 4949 };
|
|
769
|
+
var DEFAULT_WATCH = { maxForksPerCycle: 3, autoRepair: true, conflicts: true, checks: true, maxRepairsPerPr: 2, repairSettleSecs: 600, repairTrapWaitSecs: 600 };
|
|
770
|
+
var DEFAULT_LIMITS = {
|
|
771
|
+
maxConcurrent: 2,
|
|
772
|
+
choreConcurrent: 1,
|
|
773
|
+
wedgeThresholdSecs: 600,
|
|
774
|
+
maxRestartAttempts: 2,
|
|
775
|
+
wallClockSecs: 3600,
|
|
776
|
+
pushEarly: true,
|
|
777
|
+
draftPr: true,
|
|
778
|
+
checkpointOnStop: true,
|
|
779
|
+
backgroundWaitSecs: 1800,
|
|
780
|
+
exitGraceSecs: 30,
|
|
781
|
+
choreRetentionDays: 7,
|
|
782
|
+
attachmentMaxBytes: 25 * 1024 * 1024,
|
|
783
|
+
retentionDays: 0,
|
|
784
|
+
minFreeGB: 0,
|
|
785
|
+
reuseWorktree: true,
|
|
786
|
+
releaseOnMerge: false
|
|
787
|
+
};
|
|
788
|
+
function configPath() {
|
|
789
|
+
return path2.join(lobstahHome(), "config.toml");
|
|
790
|
+
}
|
|
791
|
+
function expandHome(p) {
|
|
792
|
+
return p.startsWith("~/") ? path2.join(os2.homedir(), p.slice(2)) : p;
|
|
793
|
+
}
|
|
794
|
+
function loadConfig() {
|
|
795
|
+
const file = configPath();
|
|
796
|
+
const raw = fs2.existsSync(file) ? parse(fs2.readFileSync(file, "utf8")) : {};
|
|
797
|
+
const reposRaw = raw.repos ?? {};
|
|
798
|
+
const repos = {};
|
|
799
|
+
for (const [key, r] of Object.entries(reposRaw)) {
|
|
800
|
+
repos[key] = {
|
|
801
|
+
path: expandHome(String(r.path ?? "")),
|
|
802
|
+
origin: r.origin ? String(r.origin) : void 0,
|
|
803
|
+
trunk: String(r.trunk ?? "main"),
|
|
804
|
+
setup: Array.isArray(r.setup) ? r.setup.map(String) : void 0,
|
|
805
|
+
env: r.env ?? void 0,
|
|
806
|
+
harness: r.harness ?? void 0,
|
|
807
|
+
pickup: r.pickup === void 0 ? void 0 : Boolean(r.pickup),
|
|
808
|
+
scratch: Array.isArray(r.scratch) ? r.scratch.map(String) : void 0,
|
|
809
|
+
pushEarly: r.pushEarly === void 0 ? void 0 : Boolean(r.pushEarly),
|
|
810
|
+
draftPr: r.draftPr === void 0 ? void 0 : Boolean(r.draftPr),
|
|
811
|
+
checkpointOnStop: r.checkpointOnStop === void 0 ? void 0 : Boolean(r.checkpointOnStop),
|
|
812
|
+
humanGateChecks: Array.isArray(r.humanGateChecks) ? r.humanGateChecks.map(String).filter(Boolean) : void 0
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
const groundsRaw = raw.grounds ?? {};
|
|
816
|
+
const grounds = {};
|
|
817
|
+
for (const [key, g] of Object.entries(groundsRaw)) {
|
|
818
|
+
grounds[key] = { repos: Array.isArray(g.repos) ? g.repos.map(String) : [] };
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
repos,
|
|
822
|
+
harness: raw.harness ?? {},
|
|
823
|
+
limits: { ...DEFAULT_LIMITS, ...raw.limits ?? {} },
|
|
824
|
+
soak: { ...DEFAULT_SOAK, ...raw.soak ?? {} },
|
|
825
|
+
helm: { ...DEFAULT_HELM, ...raw.helm ?? {} },
|
|
826
|
+
glass: { ...DEFAULT_GLASS, ...raw.glass ?? {} },
|
|
827
|
+
watch: { ...DEFAULT_WATCH, ...raw.watch ?? {} },
|
|
828
|
+
grounds,
|
|
829
|
+
notifyCommand: raw.notifyCommand ? String(raw.notifyCommand) : void 0,
|
|
830
|
+
notifyVerbs: Array.isArray(raw.notifyVerbs) ? raw.notifyVerbs.map(String) : void 0,
|
|
831
|
+
remindSecs: raw.remindSecs !== void 0 ? Number(raw.remindSecs) : void 0,
|
|
832
|
+
attentionKinds: parseAttentionKinds(raw.attentionKinds)
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function resolveDispatch(d, cfg) {
|
|
836
|
+
const repo = cfg.repos[d.repo];
|
|
837
|
+
if (!repo)
|
|
838
|
+
throw new Error(`unknown repo key "${d.repo}" \u2014 add it to ${configPath()}`);
|
|
839
|
+
const rh = repo.harness ?? {};
|
|
840
|
+
const gh = cfg.harness;
|
|
841
|
+
return {
|
|
842
|
+
harness: d.harness ?? rh.default ?? gh.default ?? "claude",
|
|
843
|
+
model: d.model ?? rh.model ?? gh.model,
|
|
844
|
+
effort: d.effort ?? rh.effort ?? gh.effort,
|
|
845
|
+
limits: {
|
|
846
|
+
wallClockSecs: cfg.limits.wallClockSecs,
|
|
847
|
+
...d.limits ?? {}
|
|
848
|
+
},
|
|
849
|
+
env: { ...repo.env ?? {}, ...d.env ?? {} },
|
|
850
|
+
flags: d.flags ?? []
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// packages/core/dist/disk.js
|
|
855
|
+
var GB = 1024 ** 3;
|
|
856
|
+
|
|
857
|
+
// packages/core/dist/watch.js
|
|
858
|
+
import * as fs3 from "node:fs";
|
|
859
|
+
import * as path3 from "node:path";
|
|
860
|
+
function watchesDir() {
|
|
861
|
+
return path3.join(lobstahHome(), "watches");
|
|
862
|
+
}
|
|
863
|
+
function slug(key) {
|
|
864
|
+
return key.replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
865
|
+
}
|
|
866
|
+
function watchPath(key) {
|
|
867
|
+
return path3.join(watchesDir(), `${slug(key)}.json`);
|
|
868
|
+
}
|
|
869
|
+
function readWatch(key) {
|
|
870
|
+
try {
|
|
871
|
+
return JSON.parse(fs3.readFileSync(watchPath(key), "utf8"));
|
|
872
|
+
} catch {
|
|
873
|
+
return void 0;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// packages/core/dist/inbox.js
|
|
878
|
+
import * as fs11 from "node:fs";
|
|
879
|
+
import * as path9 from "node:path";
|
|
880
|
+
|
|
881
|
+
// packages/core/dist/soak.js
|
|
882
|
+
import * as fs10 from "node:fs";
|
|
883
|
+
import * as path8 from "node:path";
|
|
884
|
+
|
|
885
|
+
// packages/core/dist/queue.js
|
|
886
|
+
import * as fs6 from "node:fs";
|
|
887
|
+
import * as path5 from "node:path";
|
|
888
|
+
|
|
889
|
+
// packages/core/dist/status.js
|
|
890
|
+
import * as fs5 from "node:fs";
|
|
891
|
+
|
|
892
|
+
// packages/core/dist/activity.js
|
|
893
|
+
import * as fs4 from "node:fs";
|
|
894
|
+
import * as path4 from "node:path";
|
|
895
|
+
var ACTIVITY_KINDS = ["tool", "message", "thinking", "waiting"];
|
|
896
|
+
var ACTIVITY_SUMMARY_MAX = 80;
|
|
897
|
+
var SECRET_PATTERNS = [
|
|
898
|
+
/\b(?:sk|pk|rk)[-_][A-Za-z0-9_-]{8,}/i,
|
|
899
|
+
/\b(?:gh[posur]_|github_pat_|glpat-|xox[abprs]-|npm_|hf_)[A-Za-z0-9_-]{16,}/,
|
|
900
|
+
/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
|
|
901
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/,
|
|
902
|
+
/\bbearer\b/i,
|
|
903
|
+
/(?:token|secret|passw(?:or)?d|pwd|api[-_]?key|auth|credential|private[-_]?key)[^\s]*[=:]/i
|
|
904
|
+
];
|
|
905
|
+
function looksSecret(word) {
|
|
906
|
+
if (SECRET_PATTERNS.some((re) => re.test(word)))
|
|
907
|
+
return true;
|
|
908
|
+
for (const run2 of word.match(/[A-Za-z0-9+/_=-]{24,}/g) ?? []) {
|
|
909
|
+
if (/[0-9]/.test(run2) && /[A-Za-z]/.test(run2))
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
return false;
|
|
913
|
+
}
|
|
914
|
+
function redactSummary(s, max = ACTIVITY_SUMMARY_MAX) {
|
|
915
|
+
const clean = s.replace(/[\r\n\t]+/g, " ").split(" ").filter(Boolean).map((w) => looksSecret(w) ? "[redacted]" : w).join(" ");
|
|
916
|
+
return clean.length > max ? `${clean.slice(0, max - 1)}\u2026` : clean;
|
|
917
|
+
}
|
|
918
|
+
var SHELLS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "fish", "pwsh", "powershell", "cmd"]);
|
|
919
|
+
function commandWord(command) {
|
|
920
|
+
let words2 = command.trim().split(/\s+/).filter(Boolean);
|
|
921
|
+
while (words2.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words2[0]))
|
|
922
|
+
words2 = words2.slice(1);
|
|
923
|
+
if (words2[0] === "env") {
|
|
924
|
+
words2 = words2.slice(1);
|
|
925
|
+
while (words2.length > 0 && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words2[0]) || words2[0].startsWith("-")))
|
|
926
|
+
words2 = words2.slice(1);
|
|
927
|
+
}
|
|
928
|
+
const first = words2[0];
|
|
929
|
+
if (!first)
|
|
930
|
+
return void 0;
|
|
931
|
+
const name = baseName(first.replace(/^["']|["']$/g, "")).replace(/\.(exe|cmd|bat)$/i, "");
|
|
932
|
+
if (SHELLS.has(name.toLowerCase()) && words2[1] && /^(-\w*c|\/c|-command)$/i.test(words2[1])) {
|
|
933
|
+
return commandWord(words2.slice(2).join(" ").replace(/^["']/, "")) ?? name;
|
|
934
|
+
}
|
|
935
|
+
return name || void 0;
|
|
936
|
+
}
|
|
937
|
+
function baseName(p) {
|
|
938
|
+
const parts = p.split(/[\\/]/).filter(Boolean);
|
|
939
|
+
return parts.at(-1) ?? p;
|
|
940
|
+
}
|
|
941
|
+
function isWindowsPath(p) {
|
|
942
|
+
return /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\");
|
|
943
|
+
}
|
|
944
|
+
function relativeTarget(p, root) {
|
|
945
|
+
if (!root)
|
|
946
|
+
return baseName(p);
|
|
947
|
+
const win = isWindowsPath(p) || isWindowsPath(root);
|
|
948
|
+
const lib = win ? path4.win32 : path4.posix;
|
|
949
|
+
if (!lib.isAbsolute(p))
|
|
950
|
+
return p.replace(/\\/g, "/");
|
|
951
|
+
const rel = lib.relative(win ? root : root.replace(/\\/g, "/"), p);
|
|
952
|
+
if (!rel || rel.startsWith("..") || lib.isAbsolute(rel))
|
|
953
|
+
return `\u2026/${baseName(p)}`;
|
|
954
|
+
return rel.replace(/\\/g, "/");
|
|
955
|
+
}
|
|
956
|
+
function urlHost(u) {
|
|
957
|
+
try {
|
|
958
|
+
return new URL(u).host || void 0;
|
|
959
|
+
} catch {
|
|
960
|
+
return void 0;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
function toolTarget(input) {
|
|
964
|
+
if (!input || typeof input !== "object")
|
|
965
|
+
return void 0;
|
|
966
|
+
const i = input;
|
|
967
|
+
const str = (k) => typeof i[k] === "string" && i[k].length > 0 ? i[k] : void 0;
|
|
968
|
+
const file = str("file_path") ?? str("notebook_path") ?? str("path");
|
|
969
|
+
if (file)
|
|
970
|
+
return file;
|
|
971
|
+
const command = str("command") ?? (Array.isArray(i.command) ? i.command.map(String).join(" ") : void 0);
|
|
972
|
+
if (command)
|
|
973
|
+
return commandWord(command);
|
|
974
|
+
const url = str("url");
|
|
975
|
+
if (url)
|
|
976
|
+
return urlHost(url);
|
|
977
|
+
const changes = i.changes;
|
|
978
|
+
if (Array.isArray(changes) && changes.length > 0) {
|
|
979
|
+
const first = changes[0];
|
|
980
|
+
if (typeof first?.path === "string")
|
|
981
|
+
return first.path;
|
|
982
|
+
}
|
|
983
|
+
return str("subagent_type") ?? str("skill");
|
|
984
|
+
}
|
|
985
|
+
function toolSummary(name, target, root) {
|
|
986
|
+
const looksPath = target !== void 0 && (/[\\/]/.test(target) || isWindowsPath(target));
|
|
987
|
+
const shown = target === void 0 ? "" : looksPath ? relativeTarget(target, root) : target;
|
|
988
|
+
return redactSummary(shown ? `${name} ${shown}` : name);
|
|
989
|
+
}
|
|
990
|
+
function activityFromEvent(ev, root) {
|
|
991
|
+
const data = ev.data ?? {};
|
|
992
|
+
switch (ev.type) {
|
|
993
|
+
case "tool-start": {
|
|
994
|
+
const name = typeof data.name === "string" && data.name ? data.name : "tool";
|
|
995
|
+
const target = typeof data.target === "string" ? data.target : void 0;
|
|
996
|
+
return { kind: "tool", summary: toolSummary(name, target, root) };
|
|
997
|
+
}
|
|
998
|
+
case "text":
|
|
999
|
+
return { kind: "message", summary: "writing a message" };
|
|
1000
|
+
case "thinking":
|
|
1001
|
+
return { kind: "thinking", summary: "thinking" };
|
|
1002
|
+
case "runner": {
|
|
1003
|
+
if (typeof data.on === "string")
|
|
1004
|
+
return { kind: "waiting", summary: redactSummary(`waiting on ${data.on}`) };
|
|
1005
|
+
if (typeof data.waiting === "string")
|
|
1006
|
+
return { kind: "waiting", summary: redactSummary(`waiting for an answer (${data.waiting})`) };
|
|
1007
|
+
if (data.holding === "background")
|
|
1008
|
+
return { kind: "waiting", summary: "waiting on background work" };
|
|
1009
|
+
return void 0;
|
|
1010
|
+
}
|
|
1011
|
+
default:
|
|
1012
|
+
return void 0;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
function atomicWrite(file, content) {
|
|
1016
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
1017
|
+
fs4.writeFileSync(tmp, content);
|
|
1018
|
+
fs4.renameSync(tmp, file);
|
|
1019
|
+
}
|
|
1020
|
+
function writeActivity(id, lane, a) {
|
|
1021
|
+
const safe = { at: a.at, kind: a.kind, summary: redactSummary(a.summary) };
|
|
1022
|
+
atomicWrite(activityPath(id, lane), JSON.stringify(safe));
|
|
1023
|
+
}
|
|
1024
|
+
function readActivity(id, lane) {
|
|
1025
|
+
try {
|
|
1026
|
+
const parsed = JSON.parse(fs4.readFileSync(activityPath(id, lane), "utf8"));
|
|
1027
|
+
if (typeof parsed.at !== "string" || typeof parsed.summary !== "string")
|
|
1028
|
+
return void 0;
|
|
1029
|
+
if (!ACTIVITY_KINDS.includes(parsed.kind))
|
|
1030
|
+
return void 0;
|
|
1031
|
+
return { ...parsed, summary: redactSummary(parsed.summary) };
|
|
1032
|
+
} catch {
|
|
1033
|
+
return void 0;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
var ActivityTracker = class {
|
|
1037
|
+
write;
|
|
1038
|
+
opts;
|
|
1039
|
+
lastWriteAt = -Infinity;
|
|
1040
|
+
lastKind;
|
|
1041
|
+
pending;
|
|
1042
|
+
timer;
|
|
1043
|
+
throttleMs;
|
|
1044
|
+
now;
|
|
1045
|
+
constructor(write, opts = {}) {
|
|
1046
|
+
this.write = write;
|
|
1047
|
+
this.opts = opts;
|
|
1048
|
+
this.throttleMs = opts.throttleMs ?? 1e4;
|
|
1049
|
+
this.now = opts.now ?? Date.now;
|
|
1050
|
+
}
|
|
1051
|
+
observe(ev) {
|
|
1052
|
+
const derived = activityFromEvent(ev, this.opts.root);
|
|
1053
|
+
if (derived)
|
|
1054
|
+
this.offer({ at: new Date(this.now()).toISOString(), ...derived });
|
|
1055
|
+
}
|
|
1056
|
+
offer(a) {
|
|
1057
|
+
const t = this.now();
|
|
1058
|
+
if (a.kind !== this.lastKind || t - this.lastWriteAt >= this.throttleMs) {
|
|
1059
|
+
this.emit(a, t);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
this.pending = a;
|
|
1063
|
+
if (!this.timer) {
|
|
1064
|
+
this.timer = setTimeout(() => {
|
|
1065
|
+
this.timer = void 0;
|
|
1066
|
+
this.flush();
|
|
1067
|
+
}, Math.max(0, this.throttleMs - (t - this.lastWriteAt)));
|
|
1068
|
+
this.timer.unref?.();
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
/** Write a held record now. */
|
|
1072
|
+
flush() {
|
|
1073
|
+
if (!this.pending)
|
|
1074
|
+
return;
|
|
1075
|
+
const a = this.pending;
|
|
1076
|
+
this.emit(a, this.now());
|
|
1077
|
+
}
|
|
1078
|
+
stop() {
|
|
1079
|
+
if (this.timer)
|
|
1080
|
+
clearTimeout(this.timer);
|
|
1081
|
+
this.timer = void 0;
|
|
1082
|
+
this.pending = void 0;
|
|
1083
|
+
}
|
|
1084
|
+
emit(a, t) {
|
|
1085
|
+
this.pending = void 0;
|
|
1086
|
+
if (this.timer) {
|
|
1087
|
+
clearTimeout(this.timer);
|
|
1088
|
+
this.timer = void 0;
|
|
1089
|
+
}
|
|
1090
|
+
this.lastWriteAt = t;
|
|
1091
|
+
this.lastKind = a.kind;
|
|
1092
|
+
try {
|
|
1093
|
+
this.write(a);
|
|
1094
|
+
} catch {
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
// packages/core/dist/status.js
|
|
1100
|
+
function isVerb(v) {
|
|
1101
|
+
return VERBS.includes(v);
|
|
1102
|
+
}
|
|
1103
|
+
function isWaitingOn(v) {
|
|
1104
|
+
return WAITING_ON.includes(v);
|
|
1105
|
+
}
|
|
1106
|
+
var DURATION = /^(\d+(?:\.\d+)?)\s*(s|m|h|d)$/i;
|
|
1107
|
+
var UNIT_MS = { s: 1e3, m: 6e4, h: 36e5, d: 864e5 };
|
|
1108
|
+
function parseUntil(v, now2 = Date.now()) {
|
|
1109
|
+
const d = DURATION.exec(v.trim());
|
|
1110
|
+
if (d)
|
|
1111
|
+
return new Date(now2 + Number(d[1]) * UNIT_MS[d[2].toLowerCase()]).toISOString();
|
|
1112
|
+
const t = Date.parse(v);
|
|
1113
|
+
if (Number.isNaN(t))
|
|
1114
|
+
throw new Error(`invalid --until "${v}" \u2014 an ISO time (2026-10-01T09:00Z) or a duration (30m, 4h, 2d)`);
|
|
1115
|
+
return new Date(t).toISOString();
|
|
1116
|
+
}
|
|
1117
|
+
function waitingFields(verb, w, now2 = Date.now()) {
|
|
1118
|
+
const out = {};
|
|
1119
|
+
if ((w.waitingOn !== void 0 || w.link !== void 0) && !WAITING_ON_VERBS.includes(verb)) {
|
|
1120
|
+
throw new Error(`--waiting-on and --link are valid only with ${WAITING_ON_VERBS.join(", ")} \u2014 not ${verb}`);
|
|
1121
|
+
}
|
|
1122
|
+
if (w.until !== void 0 && verb !== "paused")
|
|
1123
|
+
throw new Error(`--until is valid only with paused \u2014 not ${verb}`);
|
|
1124
|
+
if (w.waitingOn !== void 0) {
|
|
1125
|
+
if (!isWaitingOn(w.waitingOn)) {
|
|
1126
|
+
throw new Error(`invalid --waiting-on "${w.waitingOn}" \u2014 must be one of: ${WAITING_ON.join(", ")}`);
|
|
1127
|
+
}
|
|
1128
|
+
out.waitingOn = w.waitingOn;
|
|
1129
|
+
}
|
|
1130
|
+
if (w.link !== void 0) {
|
|
1131
|
+
let url;
|
|
1132
|
+
try {
|
|
1133
|
+
url = new URL(w.link);
|
|
1134
|
+
} catch {
|
|
1135
|
+
url = void 0;
|
|
1136
|
+
}
|
|
1137
|
+
if (!url || url.protocol !== "http:" && url.protocol !== "https:") {
|
|
1138
|
+
throw new Error(`invalid --link "${w.link}" \u2014 must be an http or https URL`);
|
|
1139
|
+
}
|
|
1140
|
+
out.link = url.href;
|
|
1141
|
+
}
|
|
1142
|
+
if (w.until !== void 0)
|
|
1143
|
+
out.until = parseUntil(w.until, now2);
|
|
1144
|
+
return out;
|
|
1145
|
+
}
|
|
1146
|
+
function appendStatus(id, lane, verb, note, at, waiting, reported = false) {
|
|
1147
|
+
if (!isVerb(verb)) {
|
|
1148
|
+
throw new Error(`invalid status verb "${verb}" \u2014 must be one of: ${VERBS.join(", ")}`);
|
|
1149
|
+
}
|
|
1150
|
+
const extra = waiting ? waitingFields(verb, waiting) : {};
|
|
1151
|
+
const entry = {
|
|
1152
|
+
at: at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1153
|
+
verb,
|
|
1154
|
+
...note ? { note } : {},
|
|
1155
|
+
...extra,
|
|
1156
|
+
...reported ? { reported: true } : {}
|
|
1157
|
+
};
|
|
1158
|
+
fs5.appendFileSync(statusPath(id, lane), `${JSON.stringify(entry)}
|
|
1159
|
+
`);
|
|
1160
|
+
return entry;
|
|
1161
|
+
}
|
|
1162
|
+
function readStatusLog(id, lane) {
|
|
1163
|
+
const file = statusPath(id, lane);
|
|
1164
|
+
if (!fs5.existsSync(file))
|
|
1165
|
+
return [];
|
|
1166
|
+
return fs5.readFileSync(file, "utf8").split("\n").filter(Boolean).flatMap((line) => {
|
|
1167
|
+
try {
|
|
1168
|
+
const parsed = JSON.parse(line);
|
|
1169
|
+
return isVerb(parsed.verb) ? [parsed] : [];
|
|
1170
|
+
} catch {
|
|
1171
|
+
return [];
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
function pausedWaiting(entry) {
|
|
1176
|
+
return entry?.verb === "paused" && entry.waitingOn !== void 0;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// packages/core/dist/queue.js
|
|
1180
|
+
function storedDescriptor(id, lane) {
|
|
1181
|
+
const dirs = laneDirs(lane);
|
|
1182
|
+
for (const file of [
|
|
1183
|
+
path5.join(dirs.queue, `${id}.json`),
|
|
1184
|
+
path5.join(dirs.active, id, "descriptor.json"),
|
|
1185
|
+
path5.join(dirs.done, id, "descriptor.json")
|
|
1186
|
+
]) {
|
|
1187
|
+
try {
|
|
1188
|
+
return JSON.parse(fs6.readFileSync(file, "utf8"));
|
|
1189
|
+
} catch {
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return void 0;
|
|
1193
|
+
}
|
|
1194
|
+
function activeIds(lane) {
|
|
1195
|
+
return readDirIfPresent(laneDirs(lane).active).filter((f) => !f.startsWith("."));
|
|
1196
|
+
}
|
|
1197
|
+
function complete(id, lane) {
|
|
1198
|
+
const dirs = laneDirs(lane);
|
|
1199
|
+
fs6.renameSync(path5.join(dirs.active, id), path5.join(dirs.done, id));
|
|
1200
|
+
}
|
|
1201
|
+
function cancelRequested(id, lane) {
|
|
1202
|
+
return fs6.existsSync(path5.join(laneDirs(lane).active, id, "cancel"));
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// packages/core/dist/evidence.js
|
|
1206
|
+
import * as fs7 from "node:fs";
|
|
1207
|
+
function readEvidence(id, lane) {
|
|
1208
|
+
try {
|
|
1209
|
+
return JSON.parse(fs7.readFileSync(evidencePath(id, lane), "utf8"));
|
|
1210
|
+
} catch {
|
|
1211
|
+
return {};
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
function mergeEvidence(id, lane, patch) {
|
|
1215
|
+
const file = evidencePath(id, lane);
|
|
1216
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
1217
|
+
fs7.writeFileSync(tmp, JSON.stringify({ ...readEvidence(id, lane), ...patch }, null, 2));
|
|
1218
|
+
fs7.renameSync(tmp, file);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// packages/core/dist/pushes.js
|
|
1222
|
+
import * as fs8 from "node:fs";
|
|
1223
|
+
import * as path6 from "node:path";
|
|
1224
|
+
var MAX_PUSHES = 20;
|
|
1225
|
+
var PUSH_HEAD = "HEAD";
|
|
1226
|
+
var VALUE_OPTIONS = /* @__PURE__ */ new Set(["-o", "--push-option", "--repo", "--receive-pack", "--exec"]);
|
|
1227
|
+
function words(segment) {
|
|
1228
|
+
const out = [];
|
|
1229
|
+
const re = /"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g;
|
|
1230
|
+
for (let m = re.exec(segment); m; m = re.exec(segment))
|
|
1231
|
+
out.push(m[1] ?? m[2] ?? m[3] ?? "");
|
|
1232
|
+
return out;
|
|
1233
|
+
}
|
|
1234
|
+
function branchOf(ref) {
|
|
1235
|
+
const name = ref.replace(/^\+/, "").replace(/^refs\/heads\//, "");
|
|
1236
|
+
return name === "@" || name === "" ? PUSH_HEAD : name;
|
|
1237
|
+
}
|
|
1238
|
+
function gitPushTargets(command) {
|
|
1239
|
+
if (command === void 0)
|
|
1240
|
+
return void 0;
|
|
1241
|
+
const text = typeof command === "string" ? command : command.join(" ");
|
|
1242
|
+
if (!/\bgit\b[^\n]*\bpush\b/.test(text))
|
|
1243
|
+
return void 0;
|
|
1244
|
+
const targets = [];
|
|
1245
|
+
let found = false;
|
|
1246
|
+
for (const segment of text.split(/&&|\|\||[;|\n]/)) {
|
|
1247
|
+
const w = words(segment);
|
|
1248
|
+
let i = w.findIndex((x) => x === "git" || /[\\/]git(?:\.exe)?$/.test(x));
|
|
1249
|
+
if (i < 0)
|
|
1250
|
+
continue;
|
|
1251
|
+
for (i++; i < w.length && w[i].startsWith("-"); i++)
|
|
1252
|
+
if (w[i] === "-C" || w[i] === "-c")
|
|
1253
|
+
i++;
|
|
1254
|
+
if (w[i] !== "push")
|
|
1255
|
+
continue;
|
|
1256
|
+
found = true;
|
|
1257
|
+
const positional = [];
|
|
1258
|
+
for (let j = i + 1; j < w.length; j++) {
|
|
1259
|
+
const arg = w[j];
|
|
1260
|
+
if (arg === "--")
|
|
1261
|
+
continue;
|
|
1262
|
+
if (arg.startsWith("-")) {
|
|
1263
|
+
if (VALUE_OPTIONS.has(arg))
|
|
1264
|
+
j++;
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
positional.push(arg);
|
|
1268
|
+
}
|
|
1269
|
+
const refspecs = positional.slice(1);
|
|
1270
|
+
if (refspecs.length === 0)
|
|
1271
|
+
targets.push(PUSH_HEAD);
|
|
1272
|
+
for (const spec of refspecs)
|
|
1273
|
+
targets.push(branchOf(spec.includes(":") ? spec.slice(spec.indexOf(":") + 1) : spec));
|
|
1274
|
+
}
|
|
1275
|
+
return found ? [...new Set(targets)] : void 0;
|
|
1276
|
+
}
|
|
1277
|
+
function gitDirAt(dir) {
|
|
1278
|
+
let at = path6.resolve(dir);
|
|
1279
|
+
for (; ; ) {
|
|
1280
|
+
const dotGit = path6.join(at, ".git");
|
|
1281
|
+
try {
|
|
1282
|
+
const stat = fs8.statSync(dotGit);
|
|
1283
|
+
if (stat.isDirectory())
|
|
1284
|
+
return dotGit;
|
|
1285
|
+
const m = /^gitdir:\s*(.+)$/m.exec(fs8.readFileSync(dotGit, "utf8"));
|
|
1286
|
+
return m ? path6.resolve(at, m[1].trim()) : void 0;
|
|
1287
|
+
} catch {
|
|
1288
|
+
}
|
|
1289
|
+
const up = path6.dirname(at);
|
|
1290
|
+
if (up === at)
|
|
1291
|
+
return void 0;
|
|
1292
|
+
at = up;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
function currentBranchAt(dir) {
|
|
1296
|
+
const gitDir = gitDirAt(dir);
|
|
1297
|
+
if (!gitDir)
|
|
1298
|
+
return void 0;
|
|
1299
|
+
try {
|
|
1300
|
+
const m = /^ref:\s*refs\/heads\/(.+)$/m.exec(fs8.readFileSync(path6.join(gitDir, "HEAD"), "utf8"));
|
|
1301
|
+
return m ? m[1].trim() : void 0;
|
|
1302
|
+
} catch {
|
|
1303
|
+
return void 0;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
function resolvePushTargets(targets, cwd) {
|
|
1307
|
+
const head = cwd ? currentBranchAt(cwd) : void 0;
|
|
1308
|
+
return [...new Set(targets.flatMap((t) => t === PUSH_HEAD ? head ? [head] : [] : [t]))];
|
|
1309
|
+
}
|
|
1310
|
+
function recordPush(id, lane, branches, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
1311
|
+
if (branches.length === 0)
|
|
1312
|
+
return;
|
|
1313
|
+
const kept = (readEvidence(id, lane).pushes ?? []).filter((p) => !branches.includes(p.branch));
|
|
1314
|
+
const pushes = [...kept, ...branches.map((branch) => ({ branch, at }))].slice(-MAX_PUSHES);
|
|
1315
|
+
mergeEvidence(id, lane, { pushes });
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// packages/core/dist/worktrees.js
|
|
1319
|
+
import * as fs9 from "node:fs";
|
|
1320
|
+
import * as path7 from "node:path";
|
|
1321
|
+
var LANES = ["work", "chore"];
|
|
1322
|
+
function worktreePath(id) {
|
|
1323
|
+
return path7.join(lobstahHome(), "worktrees", id);
|
|
1324
|
+
}
|
|
1325
|
+
function readRecord(file) {
|
|
1326
|
+
try {
|
|
1327
|
+
const r = JSON.parse(fs9.readFileSync(file, "utf8"));
|
|
1328
|
+
return typeof r.path === "string" ? r : void 0;
|
|
1329
|
+
} catch {
|
|
1330
|
+
return void 0;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function ownerOfPath(p) {
|
|
1334
|
+
const rel = path7.relative(path7.join(lobstahHome(), "worktrees"), p);
|
|
1335
|
+
if (!rel || rel.startsWith("..") || path7.isAbsolute(rel))
|
|
1336
|
+
return void 0;
|
|
1337
|
+
return rel.split(/[\\/]/)[0];
|
|
1338
|
+
}
|
|
1339
|
+
function dispatchWorktree(id, lane) {
|
|
1340
|
+
for (const l of lane ? [lane] : LANES) {
|
|
1341
|
+
const ev = readEvidence(id, l);
|
|
1342
|
+
if (ev.worktree) {
|
|
1343
|
+
const owner = ev.worktreeOf ?? ownerOfPath(ev.worktree) ?? id;
|
|
1344
|
+
return { path: ev.worktree, owner, reused: owner !== id };
|
|
1345
|
+
}
|
|
1346
|
+
const dirs = laneDirs(l);
|
|
1347
|
+
for (const dir of [dirs.active, dirs.done]) {
|
|
1348
|
+
const rec = readRecord(path7.join(dir, id, "worktree.json"));
|
|
1349
|
+
if (rec) {
|
|
1350
|
+
const owner = rec.of ?? ownerOfPath(rec.path) ?? id;
|
|
1351
|
+
return { path: rec.path, owner, reused: owner !== id };
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return { path: worktreePath(id), owner: id, reused: false };
|
|
1356
|
+
}
|
|
1357
|
+
function laneOf(id) {
|
|
1358
|
+
for (const l of LANES) {
|
|
1359
|
+
const d = laneDirs(l);
|
|
1360
|
+
if (fs9.existsSync(path7.join(d.queue, `${id}.json`)) || fs9.existsSync(path7.join(d.active, id)) || fs9.existsSync(path7.join(d.done, id))) {
|
|
1361
|
+
return l;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return void 0;
|
|
1365
|
+
}
|
|
1366
|
+
function followUpAncestors(id, lane) {
|
|
1367
|
+
const out = [];
|
|
1368
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
1369
|
+
let cur = storedDescriptor(id, lane ?? laneOf(id) ?? "work")?.followUp;
|
|
1370
|
+
while (cur && !seen.has(cur)) {
|
|
1371
|
+
out.push(cur);
|
|
1372
|
+
seen.add(cur);
|
|
1373
|
+
cur = storedDescriptor(cur, laneOf(cur) ?? lane ?? "work")?.followUp;
|
|
1374
|
+
}
|
|
1375
|
+
return out;
|
|
1376
|
+
}
|
|
1377
|
+
function chainWorktree(followUp) {
|
|
1378
|
+
for (const id of [followUp, ...followUpAncestors(followUp)]) {
|
|
1379
|
+
const wt = dispatchWorktree(id, laneOf(id));
|
|
1380
|
+
if (fs9.existsSync(wt.path))
|
|
1381
|
+
return { ...wt, from: id };
|
|
1382
|
+
}
|
|
1383
|
+
return void 0;
|
|
1384
|
+
}
|
|
1385
|
+
function worktreeGitDir(wt) {
|
|
1386
|
+
const dotGit = path7.join(wt, ".git");
|
|
1387
|
+
try {
|
|
1388
|
+
const st = fs9.statSync(dotGit);
|
|
1389
|
+
if (st.isDirectory())
|
|
1390
|
+
return dotGit;
|
|
1391
|
+
const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs9.readFileSync(dotGit, "utf8"));
|
|
1392
|
+
return m ? path7.resolve(wt, m[1]) : void 0;
|
|
1393
|
+
} catch {
|
|
1394
|
+
return void 0;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function worktreeLockFile(wt) {
|
|
1398
|
+
const gitDir = worktreeGitDir(wt);
|
|
1399
|
+
return gitDir ? path7.join(gitDir, "lobstah.lock") : void 0;
|
|
1400
|
+
}
|
|
1401
|
+
function readWorktreeLock(wt) {
|
|
1402
|
+
const file = worktreeLockFile(wt);
|
|
1403
|
+
if (!file)
|
|
1404
|
+
return void 0;
|
|
1405
|
+
try {
|
|
1406
|
+
return JSON.parse(fs9.readFileSync(file, "utf8"));
|
|
1407
|
+
} catch {
|
|
1408
|
+
return void 0;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
function lockIsLive(lock) {
|
|
1412
|
+
return fs9.existsSync(path7.join(laneDirs(lock.lane).active, lock.id));
|
|
1413
|
+
}
|
|
1414
|
+
function worktreeHolder(wt) {
|
|
1415
|
+
const lock = readWorktreeLock(wt);
|
|
1416
|
+
return lock && lockIsLive(lock) ? lock : void 0;
|
|
1417
|
+
}
|
|
1418
|
+
function acquireWorktreeLock(wt, id, lane) {
|
|
1419
|
+
const file = worktreeLockFile(wt);
|
|
1420
|
+
const mine = { id, lane, pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1421
|
+
if (!file)
|
|
1422
|
+
return { id: "(no git dir)", lane, pid: 0, at: mine.at };
|
|
1423
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1424
|
+
try {
|
|
1425
|
+
fs9.writeFileSync(file, JSON.stringify(mine, null, 2), { flag: "wx" });
|
|
1426
|
+
return void 0;
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
if (err.code !== "EEXIST")
|
|
1429
|
+
throw err;
|
|
1430
|
+
}
|
|
1431
|
+
let raw;
|
|
1432
|
+
try {
|
|
1433
|
+
raw = fs9.readFileSync(file, "utf8");
|
|
1434
|
+
} catch {
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
let held2;
|
|
1438
|
+
try {
|
|
1439
|
+
held2 = JSON.parse(raw);
|
|
1440
|
+
} catch {
|
|
1441
|
+
held2 = void 0;
|
|
1442
|
+
}
|
|
1443
|
+
if (held2?.id === id) {
|
|
1444
|
+
fs9.writeFileSync(file, JSON.stringify(mine, null, 2));
|
|
1445
|
+
return void 0;
|
|
1096
1446
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1447
|
+
if (held2 && lockIsLive(held2))
|
|
1448
|
+
return held2;
|
|
1449
|
+
try {
|
|
1450
|
+
if (fs9.readFileSync(file, "utf8") === raw)
|
|
1451
|
+
fs9.rmSync(file, { force: true });
|
|
1452
|
+
} catch {
|
|
1103
1453
|
}
|
|
1104
|
-
skipVoid(ctx);
|
|
1105
1454
|
}
|
|
1106
|
-
|
|
1455
|
+
const held = readWorktreeLock(wt);
|
|
1456
|
+
return held ?? { id: "(contended)", lane, pid: 0, at: mine.at };
|
|
1457
|
+
}
|
|
1458
|
+
function releaseWorktreeLock(wt, id) {
|
|
1459
|
+
const file = worktreeLockFile(wt);
|
|
1460
|
+
if (!file)
|
|
1461
|
+
return;
|
|
1462
|
+
if (readWorktreeLock(wt)?.id === id)
|
|
1463
|
+
fs9.rmSync(file, { force: true });
|
|
1107
1464
|
}
|
|
1108
1465
|
|
|
1109
|
-
// packages/core/dist/
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
for (const k of raw) {
|
|
1119
|
-
if (!ATTENTION_KINDS.includes(k)) {
|
|
1120
|
-
throw new Error(`attentionKinds: unknown kind "${String(k)}" in ${configPath()} \u2014 valid kinds: ${ATTENTION_KINDS.join(", ")}`);
|
|
1121
|
-
}
|
|
1466
|
+
// packages/core/dist/soak.js
|
|
1467
|
+
function regPath(trapId) {
|
|
1468
|
+
return path8.join(soakingDir(), `${trapId}.json`);
|
|
1469
|
+
}
|
|
1470
|
+
function readTrap(trapId) {
|
|
1471
|
+
try {
|
|
1472
|
+
return JSON.parse(fs10.readFileSync(regPath(trapId), "utf8"));
|
|
1473
|
+
} catch {
|
|
1474
|
+
return void 0;
|
|
1122
1475
|
}
|
|
1123
|
-
return [...new Set(raw)];
|
|
1124
1476
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
}
|
|
1131
|
-
var DEFAULT_HELM = {
|
|
1132
|
-
ttlSecs: 1800,
|
|
1133
|
-
reportSecs: 900,
|
|
1134
|
-
armGraceSecs: 5
|
|
1135
|
-
};
|
|
1136
|
-
var DEFAULT_GLASS = { port: 4949 };
|
|
1137
|
-
var DEFAULT_WATCH = { maxForksPerCycle: 3, autoRepair: true, conflicts: true, checks: true, maxRepairsPerPr: 2 };
|
|
1138
|
-
var DEFAULT_LIMITS = {
|
|
1139
|
-
maxConcurrent: 2,
|
|
1140
|
-
choreConcurrent: 1,
|
|
1141
|
-
wedgeThresholdSecs: 600,
|
|
1142
|
-
maxRestartAttempts: 2,
|
|
1143
|
-
wallClockSecs: 3600,
|
|
1144
|
-
pushEarly: true,
|
|
1145
|
-
draftPr: true,
|
|
1146
|
-
checkpointOnStop: true,
|
|
1147
|
-
backgroundWaitSecs: 1800,
|
|
1148
|
-
choreRetentionDays: 7,
|
|
1149
|
-
attachmentMaxBytes: 25 * 1024 * 1024,
|
|
1150
|
-
retentionDays: 0,
|
|
1151
|
-
minFreeGB: 0,
|
|
1152
|
-
reuseWorktree: true,
|
|
1153
|
-
releaseOnMerge: false
|
|
1154
|
-
};
|
|
1155
|
-
function configPath() {
|
|
1156
|
-
return path6.join(lobstahHome(), "config.toml");
|
|
1477
|
+
function listTraps() {
|
|
1478
|
+
try {
|
|
1479
|
+
return fs10.readdirSync(soakingDir()).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(fs10.readFileSync(path8.join(soakingDir(), f), "utf8"))).filter((r) => typeof r.trapId === "string");
|
|
1480
|
+
} catch {
|
|
1481
|
+
return [];
|
|
1482
|
+
}
|
|
1157
1483
|
}
|
|
1158
|
-
function
|
|
1159
|
-
return
|
|
1484
|
+
function claimPath(id, lane) {
|
|
1485
|
+
return path8.join(laneDirs(lane).active, id, "claim.json");
|
|
1160
1486
|
}
|
|
1161
|
-
function
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
for (const [key, r] of Object.entries(reposRaw)) {
|
|
1167
|
-
repos[key] = {
|
|
1168
|
-
path: expandHome(String(r.path ?? "")),
|
|
1169
|
-
origin: r.origin ? String(r.origin) : void 0,
|
|
1170
|
-
trunk: String(r.trunk ?? "main"),
|
|
1171
|
-
setup: Array.isArray(r.setup) ? r.setup.map(String) : void 0,
|
|
1172
|
-
env: r.env ?? void 0,
|
|
1173
|
-
harness: r.harness ?? void 0,
|
|
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)
|
|
1179
|
-
};
|
|
1487
|
+
function readSessionClaim(id, lane) {
|
|
1488
|
+
try {
|
|
1489
|
+
return JSON.parse(fs10.readFileSync(claimPath(id, lane), "utf8"));
|
|
1490
|
+
} catch {
|
|
1491
|
+
return void 0;
|
|
1180
1492
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// packages/core/dist/inbox.js
|
|
1496
|
+
var metaName = (file) => file.replace(/\.msg$/, ".meta.json");
|
|
1497
|
+
function inboxDir(id, lane) {
|
|
1498
|
+
return path9.join(laneDirs(lane).inbox, id);
|
|
1499
|
+
}
|
|
1500
|
+
function unhandled(id, lane) {
|
|
1501
|
+
const dir = inboxDir(id, lane);
|
|
1502
|
+
return readDirIfPresent(dir).filter((f) => f.endsWith(".msg")).sort().map((f) => ({ file: f, text: fs11.readFileSync(path9.join(dir, f), "utf8") }));
|
|
1503
|
+
}
|
|
1504
|
+
function acknowledge(id, lane, file) {
|
|
1505
|
+
const dir = inboxDir(id, lane);
|
|
1506
|
+
fs11.mkdirSync(path9.join(dir, "handled"), { recursive: true });
|
|
1507
|
+
fs11.renameSync(path9.join(dir, file), path9.join(dir, "handled", file));
|
|
1508
|
+
try {
|
|
1509
|
+
fs11.renameSync(path9.join(dir, metaName(file)), path9.join(dir, "handled", metaName(file)));
|
|
1510
|
+
} catch {
|
|
1185
1511
|
}
|
|
1186
|
-
return {
|
|
1187
|
-
repos,
|
|
1188
|
-
harness: raw.harness ?? {},
|
|
1189
|
-
limits: { ...DEFAULT_LIMITS, ...raw.limits ?? {} },
|
|
1190
|
-
soak: { ...DEFAULT_SOAK, ...raw.soak ?? {} },
|
|
1191
|
-
helm: { ...DEFAULT_HELM, ...raw.helm ?? {} },
|
|
1192
|
-
glass: { ...DEFAULT_GLASS, ...raw.glass ?? {} },
|
|
1193
|
-
watch: { ...DEFAULT_WATCH, ...raw.watch ?? {} },
|
|
1194
|
-
grounds,
|
|
1195
|
-
notifyCommand: raw.notifyCommand ? String(raw.notifyCommand) : void 0,
|
|
1196
|
-
notifyVerbs: Array.isArray(raw.notifyVerbs) ? raw.notifyVerbs.map(String) : void 0,
|
|
1197
|
-
remindSecs: raw.remindSecs !== void 0 ? Number(raw.remindSecs) : void 0,
|
|
1198
|
-
attentionKinds: parseAttentionKinds(raw.attentionKinds)
|
|
1199
|
-
};
|
|
1200
1512
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
flags: d.flags ?? []
|
|
1217
|
-
};
|
|
1513
|
+
|
|
1514
|
+
// packages/core/dist/events.js
|
|
1515
|
+
import * as fs12 from "node:fs";
|
|
1516
|
+
function appendEvent(id, lane, ev) {
|
|
1517
|
+
fs12.appendFileSync(eventsPath(id, lane), `${JSON.stringify(ev)}
|
|
1518
|
+
`);
|
|
1519
|
+
}
|
|
1520
|
+
function touchEvents(id, lane, at = /* @__PURE__ */ new Date()) {
|
|
1521
|
+
const file = eventsPath(id, lane);
|
|
1522
|
+
try {
|
|
1523
|
+
fs12.utimesSync(file, at, at);
|
|
1524
|
+
} catch {
|
|
1525
|
+
fs12.appendFileSync(file, "");
|
|
1526
|
+
fs12.utimesSync(file, at, at);
|
|
1527
|
+
}
|
|
1218
1528
|
}
|
|
1219
1529
|
|
|
1220
1530
|
// packages/core/dist/harness.js
|
|
1221
|
-
import * as
|
|
1222
|
-
import * as
|
|
1531
|
+
import * as fs13 from "node:fs";
|
|
1532
|
+
import * as path10 from "node:path";
|
|
1223
1533
|
import { createRequire } from "node:module";
|
|
1224
1534
|
function resolveOnPath(bin, pathVar = process.env.PATH) {
|
|
1225
1535
|
if (!pathVar)
|
|
1226
1536
|
return void 0;
|
|
1227
1537
|
const names = process.platform === "win32" ? [bin, `${bin}.exe`, `${bin}.cmd`, `${bin}.bat`] : [bin];
|
|
1228
|
-
for (const dir of pathVar.split(
|
|
1538
|
+
for (const dir of pathVar.split(path10.delimiter)) {
|
|
1229
1539
|
if (!dir)
|
|
1230
1540
|
continue;
|
|
1231
1541
|
for (const name of names) {
|
|
1232
|
-
const candidate =
|
|
1542
|
+
const candidate = path10.join(dir, name);
|
|
1233
1543
|
try {
|
|
1234
|
-
|
|
1235
|
-
if (
|
|
1544
|
+
fs13.accessSync(candidate, fs13.constants.X_OK);
|
|
1545
|
+
if (fs13.statSync(candidate).isFile())
|
|
1236
1546
|
return candidate;
|
|
1237
1547
|
} catch {
|
|
1238
1548
|
}
|
|
@@ -1241,48 +1551,25 @@ function resolveOnPath(bin, pathVar = process.env.PATH) {
|
|
|
1241
1551
|
return void 0;
|
|
1242
1552
|
}
|
|
1243
1553
|
function onPath(bin, pathVar = process.env.PATH) {
|
|
1244
|
-
return resolveOnPath(bin, pathVar) !== void 0;
|
|
1245
|
-
}
|
|
1246
|
-
function codexInvocation(argv) {
|
|
1247
|
-
if (onPath("codex"))
|
|
1248
|
-
return { file: "codex", argv };
|
|
1249
|
-
const require_ = createRequire(import.meta.url);
|
|
1250
|
-
try {
|
|
1251
|
-
const cli = require_.resolve("@openai/codex/bin/codex.js");
|
|
1252
|
-
return { file: process.execPath, argv: [cli, ...argv] };
|
|
1253
|
-
} catch {
|
|
1254
|
-
}
|
|
1255
|
-
for (const dir of require_.resolve.paths("@openai/codex") ?? []) {
|
|
1256
|
-
const cli = path7.join(dir, "@openai", "codex", "bin", "codex.js");
|
|
1257
|
-
if (fs9.existsSync(cli))
|
|
1258
|
-
return { file: process.execPath, argv: [cli, ...argv] };
|
|
1259
|
-
}
|
|
1260
|
-
return void 0;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
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");
|
|
1268
|
-
}
|
|
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`);
|
|
1554
|
+
return resolveOnPath(bin, pathVar) !== void 0;
|
|
1274
1555
|
}
|
|
1275
|
-
function
|
|
1556
|
+
function codexInvocation(argv) {
|
|
1557
|
+
if (onPath("codex"))
|
|
1558
|
+
return { file: "codex", argv };
|
|
1559
|
+
const require_ = createRequire(import.meta.url);
|
|
1276
1560
|
try {
|
|
1277
|
-
|
|
1561
|
+
const cli = require_.resolve("@openai/codex/bin/codex.js");
|
|
1562
|
+
return { file: process.execPath, argv: [cli, ...argv] };
|
|
1278
1563
|
} catch {
|
|
1279
|
-
return void 0;
|
|
1280
1564
|
}
|
|
1565
|
+
for (const dir of require_.resolve.paths("@openai/codex") ?? []) {
|
|
1566
|
+
const cli = path10.join(dir, "@openai", "codex", "bin", "codex.js");
|
|
1567
|
+
if (fs13.existsSync(cli))
|
|
1568
|
+
return { file: process.execPath, argv: [cli, ...argv] };
|
|
1569
|
+
}
|
|
1570
|
+
return void 0;
|
|
1281
1571
|
}
|
|
1282
1572
|
|
|
1283
|
-
// packages/core/dist/disk.js
|
|
1284
|
-
var GB = 1024 ** 3;
|
|
1285
|
-
|
|
1286
1573
|
// packages/core/dist/compiled.js
|
|
1287
1574
|
var COMPILED_BINARY = (() => {
|
|
1288
1575
|
const bun = globalThis.Bun;
|
|
@@ -1298,25 +1585,25 @@ function parsePrRef(s) {
|
|
|
1298
1585
|
const number = Number(n);
|
|
1299
1586
|
return { owner, repo, number, key: `pr:${owner}/${repo}#${number}`, url: `https://github.com/${owner}/${repo}/pull/${number}` };
|
|
1300
1587
|
}
|
|
1301
|
-
var PR_VIEW_FIELDS = "state,isDraft,headRefOid,baseRefName,headRefName,mergeStateStatus,reviewDecision,statusCheckRollup,mergedAt,closedAt,updatedAt,reviews";
|
|
1588
|
+
var PR_VIEW_FIELDS = "title,state,isDraft,headRefOid,baseRefName,baseRefOid,headRefName,mergeStateStatus,reviewDecision,statusCheckRollup,mergedAt,closedAt,updatedAt,reviews";
|
|
1302
1589
|
var PR_VIEW_FIELDS_NO_CHECKS = PR_VIEW_FIELDS.split(",").filter((f) => f !== "statusCheckRollup").join(",");
|
|
1303
1590
|
|
|
1304
1591
|
// packages/core/dist/prs.js
|
|
1305
|
-
import * as
|
|
1306
|
-
import * as
|
|
1592
|
+
import * as fs14 from "node:fs";
|
|
1593
|
+
import * as path11 from "node:path";
|
|
1307
1594
|
function prsDir() {
|
|
1308
|
-
return
|
|
1595
|
+
return path11.join(lobstahHome(), "prs");
|
|
1309
1596
|
}
|
|
1310
1597
|
function readPrs() {
|
|
1311
1598
|
let files;
|
|
1312
1599
|
try {
|
|
1313
|
-
files =
|
|
1600
|
+
files = fs14.readdirSync(prsDir()).filter((f) => f.endsWith(".json"));
|
|
1314
1601
|
} catch {
|
|
1315
1602
|
return [];
|
|
1316
1603
|
}
|
|
1317
1604
|
return files.flatMap((f) => {
|
|
1318
1605
|
try {
|
|
1319
|
-
const r = JSON.parse(
|
|
1606
|
+
const r = JSON.parse(fs14.readFileSync(path11.join(prsDir(), f), "utf8"));
|
|
1320
1607
|
return typeof r.key === "string" && typeof r.url === "string" ? [r] : [];
|
|
1321
1608
|
} catch {
|
|
1322
1609
|
return [];
|
|
@@ -1332,11 +1619,11 @@ function chainPr(id, lane) {
|
|
|
1332
1619
|
while (at && !seen.has(at)) {
|
|
1333
1620
|
ancestry.push(at);
|
|
1334
1621
|
seen.add(at);
|
|
1335
|
-
at = storedDescriptor(at, lane)?.followUp;
|
|
1622
|
+
at = storedDescriptor(at, laneOf(at) ?? lane)?.followUp;
|
|
1336
1623
|
}
|
|
1337
1624
|
const records = readPrs();
|
|
1338
1625
|
for (const member of ancestry.reverse()) {
|
|
1339
|
-
const evidence = readEvidence(member, lane);
|
|
1626
|
+
const evidence = readEvidence(member, laneOf(member) ?? lane);
|
|
1340
1627
|
const url = evidence.prUrl ?? evidence.pr?.url;
|
|
1341
1628
|
const ref = url && parsePrRef(url);
|
|
1342
1629
|
if (ref) {
|
|
@@ -1350,10 +1637,24 @@ function chainPr(id, lane) {
|
|
|
1350
1637
|
return void 0;
|
|
1351
1638
|
}
|
|
1352
1639
|
|
|
1640
|
+
// packages/core/dist/pr-repair.js
|
|
1641
|
+
var PUSH_RETRIES = 3;
|
|
1642
|
+
var PUSH_REJECTED = "push rejected:";
|
|
1643
|
+
function pushRule(branch, id = "<dispatch id>") {
|
|
1644
|
+
const b = branch ?? "the PR's head branch";
|
|
1645
|
+
return [
|
|
1646
|
+
`Push only to the existing branch ${b}.`,
|
|
1647
|
+
`If the push is rejected as non-fast-forward because ${b} moved, fetch ${b}, rebase your commits onto the moved head again, and push with \`--force-with-lease=${b}:<the head you just fetched>\`. Retry at most ${PUSH_RETRIES} times.`,
|
|
1648
|
+
"If a push hook fails with a real test or type error, do not retry the push: fix the error, commit, and push again.",
|
|
1649
|
+
`If you still cannot push, report \`lobstah report ${id} failed "${PUSH_REJECTED} <rejection text>; moved head <full sha of the head you fetched>"\` and leave the PR as it was.`,
|
|
1650
|
+
"Never push to another branch. Never open a new PR."
|
|
1651
|
+
].join(" ");
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1353
1654
|
// packages/core/dist/resume.js
|
|
1354
|
-
import * as
|
|
1655
|
+
import * as fs15 from "node:fs";
|
|
1355
1656
|
import * as os3 from "node:os";
|
|
1356
|
-
import * as
|
|
1657
|
+
import * as path12 from "node:path";
|
|
1357
1658
|
import { spawnSync } from "node:child_process";
|
|
1358
1659
|
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;
|
|
1359
1660
|
function harnessFromSessionId(sessionId) {
|
|
@@ -1364,13 +1665,13 @@ function claimOf(id, lane) {
|
|
|
1364
1665
|
const dirs = laneDirs(lane);
|
|
1365
1666
|
for (const dir of [dirs.active, dirs.done]) {
|
|
1366
1667
|
try {
|
|
1367
|
-
return JSON.parse(
|
|
1668
|
+
return JSON.parse(fs15.readFileSync(path12.join(dir, id, "claim.json"), "utf8"));
|
|
1368
1669
|
} catch {
|
|
1369
1670
|
}
|
|
1370
1671
|
}
|
|
1371
1672
|
return void 0;
|
|
1372
1673
|
}
|
|
1373
|
-
function
|
|
1674
|
+
function laneOf2(id) {
|
|
1374
1675
|
for (const lane of ["work", "chore"]) {
|
|
1375
1676
|
if (storedDescriptor(id, lane) || readEvidence(id, lane).sessionId)
|
|
1376
1677
|
return lane;
|
|
@@ -1378,7 +1679,7 @@ function laneOf(id) {
|
|
|
1378
1679
|
return void 0;
|
|
1379
1680
|
}
|
|
1380
1681
|
function resolveSessionHarness(id, cfg, laneHint) {
|
|
1381
|
-
const lane = laneHint ??
|
|
1682
|
+
const lane = laneHint ?? laneOf2(id);
|
|
1382
1683
|
if (!lane)
|
|
1383
1684
|
return {};
|
|
1384
1685
|
const ev = readEvidence(id, lane);
|
|
@@ -1414,7 +1715,7 @@ function isUnresumable(error) {
|
|
|
1414
1715
|
return error !== void 0 && UNRESUMABLE.test(error);
|
|
1415
1716
|
}
|
|
1416
1717
|
function worktreeProgress(worktree, trunk) {
|
|
1417
|
-
if (!worktree || !
|
|
1718
|
+
if (!worktree || !fs15.existsSync(worktree))
|
|
1418
1719
|
return "No worktree progress recorded.";
|
|
1419
1720
|
const git2 = (...a) => spawnSync("git", a, { cwd: worktree, encoding: "utf8" }).stdout?.trim() ?? "";
|
|
1420
1721
|
const commits = trunk ? git2("log", "--oneline", `origin/${trunk}..HEAD`) : git2("log", "--oneline", "-15");
|
|
@@ -1447,31 +1748,31 @@ function handoffNote(fromHarness, progress, why) {
|
|
|
1447
1748
|
${progress}`;
|
|
1448
1749
|
}
|
|
1449
1750
|
function codexHome(env = process.env) {
|
|
1450
|
-
return env.CODEX_HOME ??
|
|
1751
|
+
return env.CODEX_HOME ?? path12.join(os3.homedir(), ".codex");
|
|
1451
1752
|
}
|
|
1452
1753
|
function codexRolloutFile(threadId, home = codexHome()) {
|
|
1453
1754
|
const suffix = `-${threadId}.jsonl`;
|
|
1454
1755
|
const walk = (dir, depth) => {
|
|
1455
1756
|
let entries;
|
|
1456
1757
|
try {
|
|
1457
|
-
entries =
|
|
1758
|
+
entries = fs15.readdirSync(dir, { withFileTypes: true });
|
|
1458
1759
|
} catch {
|
|
1459
1760
|
return void 0;
|
|
1460
1761
|
}
|
|
1461
1762
|
for (const e of entries) {
|
|
1462
1763
|
if (e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(suffix))
|
|
1463
|
-
return
|
|
1764
|
+
return path12.join(dir, e.name);
|
|
1464
1765
|
}
|
|
1465
1766
|
if (depth === 0)
|
|
1466
1767
|
return void 0;
|
|
1467
1768
|
for (const e of entries.filter((x) => x.isDirectory()).sort((a, b) => b.name.localeCompare(a.name))) {
|
|
1468
|
-
const hit = walk(
|
|
1769
|
+
const hit = walk(path12.join(dir, e.name), depth - 1);
|
|
1469
1770
|
if (hit)
|
|
1470
1771
|
return hit;
|
|
1471
1772
|
}
|
|
1472
1773
|
return void 0;
|
|
1473
1774
|
};
|
|
1474
|
-
return walk(
|
|
1775
|
+
return walk(path12.join(home, "sessions"), 3) ?? walk(path12.join(home, "archived_sessions"), 0);
|
|
1475
1776
|
}
|
|
1476
1777
|
function codexDesktopThread(threadId, home = codexHome()) {
|
|
1477
1778
|
const file = codexRolloutFile(threadId, home);
|
|
@@ -1479,12 +1780,12 @@ function codexDesktopThread(threadId, home = codexHome()) {
|
|
|
1479
1780
|
return void 0;
|
|
1480
1781
|
let head = "";
|
|
1481
1782
|
try {
|
|
1482
|
-
const fd =
|
|
1783
|
+
const fd = fs15.openSync(file, "r");
|
|
1483
1784
|
try {
|
|
1484
1785
|
const buf = Buffer.alloc(4096);
|
|
1485
|
-
head = buf.subarray(0,
|
|
1786
|
+
head = buf.subarray(0, fs15.readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
|
|
1486
1787
|
} finally {
|
|
1487
|
-
|
|
1788
|
+
fs15.closeSync(fd);
|
|
1488
1789
|
}
|
|
1489
1790
|
} catch {
|
|
1490
1791
|
return void 0;
|
|
@@ -1514,162 +1815,18 @@ function droppedModelNote(harness, dropped) {
|
|
|
1514
1815
|
return `model ${dropped.model} is a ${dropped.owner} model \u2014 dropped, using ${harness}'s default`;
|
|
1515
1816
|
}
|
|
1516
1817
|
|
|
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
1818
|
// packages/core/dist/slots.js
|
|
1666
1819
|
function isTrapCatch(id, lane) {
|
|
1667
1820
|
return readSessionClaim(id, lane)?.by.startsWith("wt:") ?? false;
|
|
1668
1821
|
}
|
|
1822
|
+
function isFinished(id, lane) {
|
|
1823
|
+
const verb = readStatusLog(id, lane).at(-1)?.verb;
|
|
1824
|
+
return verb !== void 0 && TERMINAL_VERBS.includes(verb);
|
|
1825
|
+
}
|
|
1669
1826
|
|
|
1670
1827
|
// packages/runner/src/run.ts
|
|
1671
|
-
import * as
|
|
1672
|
-
import * as
|
|
1828
|
+
import * as fs19 from "node:fs";
|
|
1829
|
+
import * as path16 from "node:path";
|
|
1673
1830
|
import { execFileSync } from "node:child_process";
|
|
1674
1831
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1675
1832
|
|
|
@@ -1703,7 +1860,7 @@ var AsyncQueue = class {
|
|
|
1703
1860
|
return Promise.resolve({ value: item, done: false });
|
|
1704
1861
|
if (this.closed)
|
|
1705
1862
|
return Promise.resolve({ value: void 0, done: true });
|
|
1706
|
-
return new Promise((
|
|
1863
|
+
return new Promise((resolve7) => this.waiters.push(resolve7));
|
|
1707
1864
|
}
|
|
1708
1865
|
};
|
|
1709
1866
|
}
|
|
@@ -1728,7 +1885,7 @@ var InputGate = class {
|
|
|
1728
1885
|
return msg;
|
|
1729
1886
|
if (this.ended)
|
|
1730
1887
|
return void 0;
|
|
1731
|
-
await new Promise((
|
|
1888
|
+
await new Promise((resolve7) => this.waiter = resolve7);
|
|
1732
1889
|
this.waiter = null;
|
|
1733
1890
|
}
|
|
1734
1891
|
}
|
|
@@ -1767,7 +1924,9 @@ function pumpClaudeMessage(msg, push, onSession) {
|
|
|
1767
1924
|
for (const block of msg.message?.content ?? []) {
|
|
1768
1925
|
if (block.type === "tool_use") {
|
|
1769
1926
|
const target = toolTarget(block.input);
|
|
1770
|
-
|
|
1927
|
+
const command = block.input?.command;
|
|
1928
|
+
const pushes = typeof command === "string" ? gitPushTargets(command) : void 0;
|
|
1929
|
+
push({ at, type: "tool-start", data: { name: block.name, ...target ? { target } : {}, ...pushes ? { pushes } : {} } });
|
|
1771
1930
|
} else if (block.type === "thinking" || block.type === "redacted_thinking") {
|
|
1772
1931
|
push({ at, type: "thinking", data: {} });
|
|
1773
1932
|
} else if (block.type === "text" && block.text) {
|
|
@@ -1871,20 +2030,20 @@ function startClaudeCli(invocation, opts) {
|
|
|
1871
2030
|
}
|
|
1872
2031
|
}
|
|
1873
2032
|
});
|
|
1874
|
-
const done = new Promise((
|
|
2033
|
+
const done = new Promise((resolve7) => {
|
|
1875
2034
|
child.on("error", (err) => {
|
|
1876
2035
|
events.push({ at: now(), type: "error", data: { message: err.message } });
|
|
1877
2036
|
events.close();
|
|
1878
|
-
|
|
2037
|
+
resolve7({ sessionId, error: err.message });
|
|
1879
2038
|
});
|
|
1880
2039
|
child.on("exit", (code) => {
|
|
1881
2040
|
events.close();
|
|
1882
2041
|
if (code === 0)
|
|
1883
|
-
|
|
2042
|
+
resolve7({ sessionId });
|
|
1884
2043
|
else {
|
|
1885
2044
|
const detail = stderrTail.trim().split("\n").at(-1);
|
|
1886
2045
|
const error = `claude exited ${code}${detail ? ` \u2014 ${detail}` : ""}`;
|
|
1887
|
-
|
|
2046
|
+
resolve7({ sessionId, error });
|
|
1888
2047
|
}
|
|
1889
2048
|
});
|
|
1890
2049
|
});
|
|
@@ -1963,25 +2122,25 @@ function createClaudeAdapter() {
|
|
|
1963
2122
|
}
|
|
1964
2123
|
|
|
1965
2124
|
// packages/adapters/dist/codex.js
|
|
1966
|
-
import * as
|
|
2125
|
+
import * as fs16 from "node:fs";
|
|
1967
2126
|
import * as os4 from "node:os";
|
|
1968
|
-
import * as
|
|
2127
|
+
import * as path13 from "node:path";
|
|
1969
2128
|
import { spawn as spawn2 } from "node:child_process";
|
|
1970
2129
|
function isolatedCodexHome() {
|
|
1971
|
-
const dir =
|
|
1972
|
-
|
|
1973
|
-
const auth =
|
|
1974
|
-
const realAuth =
|
|
1975
|
-
if (!
|
|
2130
|
+
const dir = path13.join(lobstahHome(), "codex-home");
|
|
2131
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
2132
|
+
const auth = path13.join(dir, "auth.json");
|
|
2133
|
+
const realAuth = path13.join(os4.homedir(), ".codex", "auth.json");
|
|
2134
|
+
if (!fs16.existsSync(auth) && fs16.existsSync(realAuth)) {
|
|
1976
2135
|
try {
|
|
1977
|
-
|
|
2136
|
+
fs16.symlinkSync(realAuth, auth);
|
|
1978
2137
|
} catch {
|
|
1979
|
-
|
|
2138
|
+
fs16.copyFileSync(realAuth, auth);
|
|
1980
2139
|
}
|
|
1981
2140
|
}
|
|
1982
|
-
const cfg =
|
|
1983
|
-
if (!
|
|
1984
|
-
|
|
2141
|
+
const cfg = path13.join(dir, "config.toml");
|
|
2142
|
+
if (!fs16.existsSync(cfg)) {
|
|
2143
|
+
fs16.writeFileSync(cfg, "# lobstah-managed: auth inherited from ~/.codex, interactive config not\n");
|
|
1985
2144
|
}
|
|
1986
2145
|
return dir;
|
|
1987
2146
|
}
|
|
@@ -1998,7 +2157,9 @@ function pumpCodexEvent(ev, push, onSession) {
|
|
|
1998
2157
|
} else if (ev.type === "item.started") {
|
|
1999
2158
|
const item = ev.item ?? {};
|
|
2000
2159
|
const target = toolTarget(item) ?? (typeof item.tool === "string" ? typeof item.server === "string" ? `${item.server}.${item.tool}` : item.tool : void 0);
|
|
2001
|
-
|
|
2160
|
+
const command = item.command;
|
|
2161
|
+
const pushes = typeof command === "string" || Array.isArray(command) && command.every((c) => typeof c === "string") ? gitPushTargets(command) : void 0;
|
|
2162
|
+
push({ at, type: "tool-start", data: { name: item.type, ...target ? { target } : {}, ...pushes ? { pushes } : {} } });
|
|
2002
2163
|
} else if (ev.type === "item.completed") {
|
|
2003
2164
|
if (ev.item?.type === "agent_message" && ev.item?.text) {
|
|
2004
2165
|
push({ at, type: "text", data: { text: String(ev.item.text).slice(0, 2e3) } });
|
|
@@ -2031,7 +2192,7 @@ function startCodexCli(invoke, opts) {
|
|
|
2031
2192
|
let current;
|
|
2032
2193
|
let killed = false;
|
|
2033
2194
|
function runTurn(prompt, resumeThread) {
|
|
2034
|
-
return new Promise((
|
|
2195
|
+
return new Promise((resolve7) => {
|
|
2035
2196
|
const inv = invoke(codexExecArgs(opts, prompt, resumeThread));
|
|
2036
2197
|
const child = spawn2(inv.file, inv.argv, {
|
|
2037
2198
|
cwd: opts.cwd,
|
|
@@ -2062,14 +2223,14 @@ function startCodexCli(invoke, opts) {
|
|
|
2062
2223
|
}
|
|
2063
2224
|
}
|
|
2064
2225
|
});
|
|
2065
|
-
child.on("error", (err) =>
|
|
2226
|
+
child.on("error", (err) => resolve7(err.message));
|
|
2066
2227
|
child.on("exit", (code) => {
|
|
2067
2228
|
if (turnError)
|
|
2068
|
-
return
|
|
2229
|
+
return resolve7(turnError);
|
|
2069
2230
|
if (code === 0 || killed)
|
|
2070
|
-
return
|
|
2231
|
+
return resolve7(void 0);
|
|
2071
2232
|
const detail = stderrTail.trim().split("\n").at(-1);
|
|
2072
|
-
|
|
2233
|
+
resolve7(`codex exited ${code}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
2073
2234
|
});
|
|
2074
2235
|
});
|
|
2075
2236
|
}
|
|
@@ -2182,8 +2343,8 @@ function loadAdapter(name) {
|
|
|
2182
2343
|
}
|
|
2183
2344
|
|
|
2184
2345
|
// packages/worktree/dist/index.js
|
|
2185
|
-
import * as
|
|
2186
|
-
import * as
|
|
2346
|
+
import * as fs17 from "node:fs";
|
|
2347
|
+
import * as path14 from "node:path";
|
|
2187
2348
|
import { createHash } from "node:crypto";
|
|
2188
2349
|
import { exec, execFile } from "node:child_process";
|
|
2189
2350
|
import { promisify } from "node:util";
|
|
@@ -2238,18 +2399,18 @@ ${gitStderr(err)}`, { cause: err });
|
|
|
2238
2399
|
}
|
|
2239
2400
|
}
|
|
2240
2401
|
}
|
|
2241
|
-
async function allocate(repo, id) {
|
|
2402
|
+
async function allocate(repo, id, fromRemoteBranch = repo.trunk) {
|
|
2242
2403
|
const dir = worktreePath(id);
|
|
2243
|
-
if (
|
|
2404
|
+
if (fs17.existsSync(dir)) {
|
|
2244
2405
|
throw new Error(`worktree for ${id} already exists at ${dir} \u2014 never allocate a second`);
|
|
2245
2406
|
}
|
|
2246
|
-
if (!
|
|
2407
|
+
if (!fs17.existsSync(repo.path)) {
|
|
2247
2408
|
if (!repo.origin)
|
|
2248
2409
|
throw new Error(`repo path ${repo.path} missing and no origin configured`);
|
|
2249
2410
|
await run("git", ["clone", repo.origin, repo.path], { env: process.env });
|
|
2250
2411
|
}
|
|
2251
|
-
await gitShared(repo.path, "fetch", "origin",
|
|
2252
|
-
await gitShared(repo.path, "worktree", "add", "--no-track", dir, "-b", `lobstah/${id}`, `origin/${
|
|
2412
|
+
await gitShared(repo.path, "fetch", "origin", fromRemoteBranch);
|
|
2413
|
+
await gitShared(repo.path, "worktree", "add", "--no-track", dir, "-b", `lobstah/${id}`, `origin/${fromRemoteBranch}`);
|
|
2253
2414
|
await runSetup(repo, dir);
|
|
2254
2415
|
return dir;
|
|
2255
2416
|
}
|
|
@@ -2279,27 +2440,27 @@ function setupHash(repo, dir) {
|
|
|
2279
2440
|
const h = createHash("sha256");
|
|
2280
2441
|
h.update(JSON.stringify(repo.setup ?? []));
|
|
2281
2442
|
for (const name of LOCKFILES) {
|
|
2282
|
-
const file =
|
|
2283
|
-
if (!
|
|
2443
|
+
const file = path14.join(dir, name);
|
|
2444
|
+
if (!fs17.existsSync(file))
|
|
2284
2445
|
continue;
|
|
2285
2446
|
h.update(`\0${name}\0`);
|
|
2286
|
-
h.update(
|
|
2447
|
+
h.update(fs17.readFileSync(file));
|
|
2287
2448
|
}
|
|
2288
2449
|
return h.digest("hex");
|
|
2289
2450
|
}
|
|
2290
2451
|
function setupRecordFile(dir) {
|
|
2291
2452
|
const gitDir = worktreeGitDir(dir);
|
|
2292
|
-
return gitDir ?
|
|
2453
|
+
return gitDir ? path14.join(gitDir, "lobstah-setup.json") : void 0;
|
|
2293
2454
|
}
|
|
2294
2455
|
function recordSetup(repo, dir) {
|
|
2295
2456
|
const file = setupRecordFile(dir);
|
|
2296
2457
|
if (file)
|
|
2297
|
-
|
|
2458
|
+
fs17.writeFileSync(file, JSON.stringify({ hash: setupHash(repo, dir), at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
2298
2459
|
}
|
|
2299
2460
|
function recordedSetupHash(dir) {
|
|
2300
2461
|
const file = setupRecordFile(dir);
|
|
2301
2462
|
try {
|
|
2302
|
-
return file ? JSON.parse(
|
|
2463
|
+
return file ? JSON.parse(fs17.readFileSync(file, "utf8")).hash : void 0;
|
|
2303
2464
|
} catch {
|
|
2304
2465
|
return void 0;
|
|
2305
2466
|
}
|
|
@@ -2308,9 +2469,9 @@ var short = (s) => s.slice(0, 8);
|
|
|
2308
2469
|
function realpath(p) {
|
|
2309
2470
|
let r;
|
|
2310
2471
|
try {
|
|
2311
|
-
r =
|
|
2472
|
+
r = fs17.realpathSync.native(p);
|
|
2312
2473
|
} catch {
|
|
2313
|
-
r =
|
|
2474
|
+
r = path14.resolve(p);
|
|
2314
2475
|
}
|
|
2315
2476
|
return process.platform === "win32" ? r.toLowerCase() : r;
|
|
2316
2477
|
}
|
|
@@ -2336,7 +2497,7 @@ async function chooseWorktree(input) {
|
|
|
2336
2497
|
const found = chainWorktree(followUp);
|
|
2337
2498
|
if (!found)
|
|
2338
2499
|
return { reuse: false, reason: "origin worktree is gone" };
|
|
2339
|
-
const ownerRepo = storedDescriptor(found.owner,
|
|
2500
|
+
const ownerRepo = storedDescriptor(found.owner, laneOf(found.owner) ?? lane)?.repo ?? storedDescriptor(found.from, laneOf(found.from) ?? lane)?.repo;
|
|
2340
2501
|
if (ownerRepo !== void 0 && ownerRepo !== repoKey) {
|
|
2341
2502
|
return { reuse: false, reason: `origin worktree belongs to repo ${ownerRepo}` };
|
|
2342
2503
|
}
|
|
@@ -2352,8 +2513,8 @@ async function chooseWorktree(input) {
|
|
|
2352
2513
|
for (const t of listTraps()) {
|
|
2353
2514
|
if (typeof t.worktree !== "string")
|
|
2354
2515
|
continue;
|
|
2355
|
-
const rel =
|
|
2356
|
-
if (!rel.startsWith("..") && !
|
|
2516
|
+
const rel = path14.relative(here, realpath(t.worktree));
|
|
2517
|
+
if (!rel.startsWith("..") && !path14.isAbsolute(rel))
|
|
2357
2518
|
return { reuse: false, reason: "origin worktree is a trap\u2019s" };
|
|
2358
2519
|
}
|
|
2359
2520
|
for (const l of ["work", "chore"]) {
|
|
@@ -2399,14 +2560,14 @@ async function collectEvidence(repo, dir) {
|
|
|
2399
2560
|
|
|
2400
2561
|
// packages/runner/src/contract.ts
|
|
2401
2562
|
function buildPrompt(brief, opts) {
|
|
2402
|
-
const reporting = `Report status by running \`lobstah report ${opts.id} <verb> [note]\` (verbs: ${VERBS.join(", ")}). Attach a PR URL to your final report with \`--pr <url>\`. Only your report finishes this dispatch: ending a turn without \`done\` or \`failed\` does not. If you start background work and end your turn to wait for it, you are woken when it finishes. At natural checkpoints, check for operator messages with \`lobstah inbox ${opts.id}\` \u2014 messages also arrive automatically between your turns.`;
|
|
2563
|
+
const reporting = `Report status by running \`lobstah report ${opts.id} <verb> [note]\` (verbs: ${VERBS.join(", ")}). Attach a PR URL to your final report with \`--pr <url>\`. If your output is findings rather than code, end with \`done "<one-line note>" --report <file.md>\` and add its images with \`--attach <file>\`. Only your report finishes this dispatch: ending a turn without \`done\` or \`failed\` does not. If you start background work and end your turn to wait for it, you are woken when it finishes. At natural checkpoints, check for operator messages with \`lobstah inbox ${opts.id}\` \u2014 messages also arrive automatically between your turns.`;
|
|
2403
2564
|
const parts = [
|
|
2404
2565
|
`You are a dispatched coding agent supervised by lobstah. Dispatch id: ${opts.id}.`,
|
|
2405
2566
|
`Work only inside the current directory \u2014 it is an isolated git worktree allocated for this dispatch.`,
|
|
2406
2567
|
reporting,
|
|
2407
2568
|
`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.`,
|
|
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.
|
|
2569
|
+
`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. A turn that ends on \`paused\` ends your session and stops its background work. The same session resumes when an operator message arrives, the \`--until\` time passes, or the PR you wait on ends; a merged PR finishes the dispatch as done.`,
|
|
2570
|
+
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. ${pushRule(opts.existingPr.headRefName, opts.id)} 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.`,
|
|
2410
2571
|
`--- BRIEF ---`,
|
|
2411
2572
|
opts.existingPr ? `${brief}
|
|
2412
2573
|
|
|
@@ -2422,7 +2583,20 @@ var WAITING_VERBS = ["needs-decision", "blocked", "paused"];
|
|
|
2422
2583
|
function unreportedNudge(id) {
|
|
2423
2584
|
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.`;
|
|
2424
2585
|
}
|
|
2425
|
-
|
|
2586
|
+
async function* until(source, stop) {
|
|
2587
|
+
const it = source[Symbol.asyncIterator]();
|
|
2588
|
+
const stopped = stop.then(() => ({ done: true, value: void 0 }));
|
|
2589
|
+
while (true) {
|
|
2590
|
+
const next = await Promise.race([it.next(), stopped]);
|
|
2591
|
+
if (next.done) {
|
|
2592
|
+
void it.return?.()?.catch?.(() => {
|
|
2593
|
+
});
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
yield next.value;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
2426
2600
|
function deliver(run2, id, lane) {
|
|
2427
2601
|
const msgs = unhandled(id, lane);
|
|
2428
2602
|
for (const m of msgs) {
|
|
@@ -2438,7 +2612,8 @@ async function drive(run2, opts) {
|
|
|
2438
2612
|
pollMs = 3e3,
|
|
2439
2613
|
stopped = () => false,
|
|
2440
2614
|
backgroundWaitMs = 30 * 6e4,
|
|
2441
|
-
settleGraceMs = 12e4
|
|
2615
|
+
settleGraceMs = 12e4,
|
|
2616
|
+
exitGraceMs = 3e4
|
|
2442
2617
|
} = opts;
|
|
2443
2618
|
const tracker = new ActivityTracker((a) => writeActivity(id, lane, a), {
|
|
2444
2619
|
root: opts.cwd,
|
|
@@ -2452,9 +2627,55 @@ async function drive(run2, opts) {
|
|
|
2452
2627
|
let activity = 0;
|
|
2453
2628
|
let finished = false;
|
|
2454
2629
|
void run2.done.then(() => finished = true);
|
|
2630
|
+
let killed = false;
|
|
2631
|
+
const kill = () => {
|
|
2632
|
+
if (killed) return;
|
|
2633
|
+
killed = true;
|
|
2634
|
+
run2.kill();
|
|
2635
|
+
};
|
|
2455
2636
|
const cancel = () => {
|
|
2456
2637
|
cancelled = true;
|
|
2457
|
-
|
|
2638
|
+
kill();
|
|
2639
|
+
};
|
|
2640
|
+
let final = false;
|
|
2641
|
+
let stoppedHarness;
|
|
2642
|
+
let finalAt = 0;
|
|
2643
|
+
let exit;
|
|
2644
|
+
let abandon;
|
|
2645
|
+
const abandoned = new Promise((resolve7) => abandon = resolve7);
|
|
2646
|
+
const clearExit = () => {
|
|
2647
|
+
if (!exit) return;
|
|
2648
|
+
clearInterval(exit.poll);
|
|
2649
|
+
clearTimeout(exit.expiry);
|
|
2650
|
+
exit = void 0;
|
|
2651
|
+
};
|
|
2652
|
+
const stopHarness = (reason) => {
|
|
2653
|
+
clearExit();
|
|
2654
|
+
const afterMs = Date.now() - finalAt;
|
|
2655
|
+
stoppedHarness = { reason, afterMs };
|
|
2656
|
+
record({ at: (/* @__PURE__ */ new Date()).toISOString(), type: "runner", data: { stopped: reason, afterSecs: Math.round(afterMs / 1e3) } });
|
|
2657
|
+
kill();
|
|
2658
|
+
abandon();
|
|
2659
|
+
};
|
|
2660
|
+
let parked = false;
|
|
2661
|
+
const finish = (park = false) => {
|
|
2662
|
+
if (final || parked) return;
|
|
2663
|
+
if (park) {
|
|
2664
|
+
parked = true;
|
|
2665
|
+
const on = readStatusLog(id, lane).at(-1)?.waitingOn;
|
|
2666
|
+
record({ at: (/* @__PURE__ */ new Date()).toISOString(), type: "runner", data: { parked: "paused", ...on ? { on } : {} } });
|
|
2667
|
+
opts.onPark?.();
|
|
2668
|
+
} else {
|
|
2669
|
+
final = true;
|
|
2670
|
+
opts.onFinal?.();
|
|
2671
|
+
}
|
|
2672
|
+
finalAt = Date.now();
|
|
2673
|
+
run2.end();
|
|
2674
|
+
const poll = setInterval(() => {
|
|
2675
|
+
if (cancelRequested(id, lane)) stopHarness("cancel");
|
|
2676
|
+
}, Math.min(pollMs, exitGraceMs));
|
|
2677
|
+
const expiry = setTimeout(() => stopHarness("exit-grace"), exitGraceMs);
|
|
2678
|
+
exit = { poll, expiry };
|
|
2458
2679
|
};
|
|
2459
2680
|
const answered = (verb) => {
|
|
2460
2681
|
appendStatus(id, lane, "working", "operator message delivered");
|
|
@@ -2508,7 +2729,7 @@ async function drive(run2, opts) {
|
|
|
2508
2729
|
}, ms);
|
|
2509
2730
|
hold = { heartbeat, expiry };
|
|
2510
2731
|
};
|
|
2511
|
-
for await (const ev of run2.events) {
|
|
2732
|
+
for await (const ev of until(run2.events, abandoned)) {
|
|
2512
2733
|
record(ev);
|
|
2513
2734
|
if (ev.type === "background") {
|
|
2514
2735
|
liveBackground = Number(ev.data?.live ?? 0);
|
|
@@ -2517,26 +2738,33 @@ async function drive(run2, opts) {
|
|
|
2517
2738
|
}
|
|
2518
2739
|
releaseHold();
|
|
2519
2740
|
if (ev.type === "tool-start" || ev.type === "text") activity++;
|
|
2741
|
+
if (ev.type === "tool-start" && Array.isArray(ev.data?.pushes)) {
|
|
2742
|
+
recordPush(id, lane, resolvePushTargets(ev.data.pushes.map(String), opts.cwd), ev.at);
|
|
2743
|
+
}
|
|
2520
2744
|
if (ev.type === "session" && ev.data?.sessionId) {
|
|
2521
2745
|
mergeEvidence(id, lane, { sessionId: String(ev.data.sessionId) });
|
|
2522
2746
|
}
|
|
2523
|
-
if (ev.type !== "turn-end" || cancelled || stopped()) continue;
|
|
2747
|
+
if (ev.type !== "turn-end" || cancelled || stopped() || final || parked) continue;
|
|
2748
|
+
const lastVerb = readStatusLog(id, lane).at(-1)?.verb;
|
|
2749
|
+
if (lastVerb !== void 0 && TERMINAL_VERBS.includes(lastVerb)) {
|
|
2750
|
+
finish();
|
|
2751
|
+
continue;
|
|
2752
|
+
}
|
|
2524
2753
|
if (cancelRequested(id, lane)) {
|
|
2525
2754
|
cancel();
|
|
2526
2755
|
continue;
|
|
2527
2756
|
}
|
|
2528
|
-
const lastVerb = readStatusLog(id, lane).at(-1)?.verb;
|
|
2529
2757
|
const waiting = lastVerb !== void 0 && WAITING_VERBS.includes(lastVerb);
|
|
2530
2758
|
if (deliver(run2, id, lane) > 0) {
|
|
2531
2759
|
if (waiting) answered(lastVerb);
|
|
2532
2760
|
continue;
|
|
2533
2761
|
}
|
|
2534
|
-
if (
|
|
2535
|
-
|
|
2762
|
+
if (lastVerb === "paused") {
|
|
2763
|
+
finish(true);
|
|
2536
2764
|
continue;
|
|
2537
2765
|
}
|
|
2538
|
-
if (
|
|
2539
|
-
|
|
2766
|
+
if (waiting) {
|
|
2767
|
+
await awaitAnswer(lastVerb);
|
|
2540
2768
|
continue;
|
|
2541
2769
|
}
|
|
2542
2770
|
const subtype = ev.data?.subtype;
|
|
@@ -2552,16 +2780,18 @@ async function drive(run2, opts) {
|
|
|
2552
2780
|
unreported();
|
|
2553
2781
|
}
|
|
2554
2782
|
releaseHold();
|
|
2783
|
+
clearExit();
|
|
2555
2784
|
tracker.flush();
|
|
2556
2785
|
tracker.stop();
|
|
2557
|
-
return { cancelled, activity };
|
|
2786
|
+
return { cancelled, activity, final, ...parked ? { parked } : {}, ...stoppedHarness ? { stopped: stoppedHarness } : {} };
|
|
2558
2787
|
}
|
|
2559
2788
|
function settle(id, lane, r) {
|
|
2560
2789
|
const lastVerb = readStatusLog(id, lane).at(-1)?.verb;
|
|
2790
|
+
if (lastVerb !== void 0 && TERMINAL_VERBS.includes(lastVerb)) return;
|
|
2561
2791
|
if (r.cancelled) appendStatus(id, lane, "failed", "cancelled by operator");
|
|
2562
2792
|
else if (r.wallClockHit) appendStatus(id, lane, "failed", `budget: out of time${r.budgetNote ? `; ${r.budgetNote}` : ""}; send continue to resume`);
|
|
2563
2793
|
else if (r.error) appendStatus(id, lane, "failed", r.error.slice(0, 500));
|
|
2564
|
-
else
|
|
2794
|
+
else appendStatus(id, lane, "failed", "stopped without reporting a result");
|
|
2565
2795
|
}
|
|
2566
2796
|
|
|
2567
2797
|
// packages/runner/src/plan.ts
|
|
@@ -2690,8 +2920,8 @@ function startWallClock(opts) {
|
|
|
2690
2920
|
import { execFile as execFile2 } from "node:child_process";
|
|
2691
2921
|
import { promisify as promisify2 } from "node:util";
|
|
2692
2922
|
import { fileURLToPath } from "node:url";
|
|
2693
|
-
import * as
|
|
2694
|
-
import * as
|
|
2923
|
+
import * as fs18 from "node:fs";
|
|
2924
|
+
import * as path15 from "node:path";
|
|
2695
2925
|
var exec2 = promisify2(execFile2);
|
|
2696
2926
|
function firstLine(err) {
|
|
2697
2927
|
const stderr = err && typeof err === "object" && "stderr" in err ? String(err.stderr).trim() : "";
|
|
@@ -2721,10 +2951,10 @@ function keepRemote(opts) {
|
|
|
2721
2951
|
const note = (message) => mergeEvidence(id, lane, { note: message.slice(0, 300) });
|
|
2722
2952
|
if (existingPrUrl) mergeEvidence(id, lane, { prUrl: existingPrUrl });
|
|
2723
2953
|
const registerWatch = async (url) => {
|
|
2724
|
-
const root =
|
|
2725
|
-
const localCli =
|
|
2954
|
+
const root = path15.resolve(path15.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
2955
|
+
const localCli = path15.join(root, "apps", "cli", "dist", "main.js");
|
|
2726
2956
|
const args = ["watch", "add", url, "--for", id];
|
|
2727
|
-
if (
|
|
2957
|
+
if (fs18.existsSync(localCli)) await command(process.execPath, [localCli, ...args], 15e3);
|
|
2728
2958
|
else await command("lobstah", args, 15e3);
|
|
2729
2959
|
};
|
|
2730
2960
|
let lastSeenHead;
|
|
@@ -2796,6 +3026,7 @@ function keepRemote(opts) {
|
|
|
2796
3026
|
pushedHead = head;
|
|
2797
3027
|
pushProblem = void 0;
|
|
2798
3028
|
mergeEvidence(id, lane, { branch });
|
|
3029
|
+
recordPush(id, lane, [branch]);
|
|
2799
3030
|
note(`remote saved: ${branch}@${head.slice(0, 12)}`);
|
|
2800
3031
|
if (!opening) opening = draft(branch).finally(() => {
|
|
2801
3032
|
opening = void 0;
|
|
@@ -2863,8 +3094,28 @@ function keepRemote(opts) {
|
|
|
2863
3094
|
}
|
|
2864
3095
|
|
|
2865
3096
|
// packages/runner/src/run.ts
|
|
2866
|
-
var defaultDeps = {
|
|
3097
|
+
var defaultDeps = {
|
|
3098
|
+
loadAdapter,
|
|
3099
|
+
allocate,
|
|
3100
|
+
chooseWorktree,
|
|
3101
|
+
prepareReuse,
|
|
3102
|
+
collectEvidence,
|
|
3103
|
+
reap: async () => 0
|
|
3104
|
+
};
|
|
3105
|
+
var DONE_WAIT_MS = 2e3;
|
|
3106
|
+
function within(p, ms, fallback) {
|
|
3107
|
+
let timer;
|
|
3108
|
+
const late = new Promise((resolve7) => timer = setTimeout(() => resolve7(fallback), ms));
|
|
3109
|
+
return Promise.race([p, late]).finally(() => clearTimeout(timer));
|
|
3110
|
+
}
|
|
2867
3111
|
var short3 = (s) => s.slice(0, 8);
|
|
3112
|
+
function boundPr(descriptor, lane) {
|
|
3113
|
+
if (descriptor.pr?.url) {
|
|
3114
|
+
const chain = descriptor.followUp ? chainPr(descriptor.followUp, lane) : void 0;
|
|
3115
|
+
return { url: descriptor.pr.url, headRefName: descriptor.pr.headRefName ?? (chain?.url === descriptor.pr.url ? chain.headRefName : void 0) };
|
|
3116
|
+
}
|
|
3117
|
+
return descriptor.followUp ? chainPr(descriptor.followUp, lane) : void 0;
|
|
3118
|
+
}
|
|
2868
3119
|
function coldNote(cold, cwd, trunk) {
|
|
2869
3120
|
const parts = [worktreeProgress(cwd, trunk)];
|
|
2870
3121
|
if (cold.origin) parts.push(originProgress(cold.origin.id, cold.origin.lane));
|
|
@@ -2872,23 +3123,24 @@ function coldNote(cold, cwd, trunk) {
|
|
|
2872
3123
|
}
|
|
2873
3124
|
async function main(activeDir, lane, seams = {}) {
|
|
2874
3125
|
const deps = { ...defaultDeps, ...seams };
|
|
2875
|
-
const id =
|
|
3126
|
+
const id = path16.basename(activeDir);
|
|
2876
3127
|
if (isTrapCatch(id, lane)) throw new Error(`refusing headless runner for trap catch ${id}`);
|
|
2877
3128
|
const status = (verb, note) => appendStatus(id, lane, verb, note);
|
|
2878
3129
|
const cfg = loadConfig();
|
|
2879
|
-
const descriptor = JSON.parse(
|
|
3130
|
+
const descriptor = JSON.parse(fs19.readFileSync(path16.join(activeDir, "descriptor.json"), "utf8"));
|
|
2880
3131
|
const resolved = resolveDispatch(descriptor, cfg);
|
|
2881
3132
|
const repo = cfg.repos[descriptor.repo];
|
|
2882
|
-
const briefFile =
|
|
2883
|
-
if (!
|
|
2884
|
-
const brief =
|
|
3133
|
+
const briefFile = path16.join(activeDir, "brief.md");
|
|
3134
|
+
if (!fs19.existsSync(briefFile)) fs19.writeFileSync(briefFile, descriptor.brief);
|
|
3135
|
+
const brief = fs19.readFileSync(briefFile, "utf8");
|
|
2885
3136
|
const attempts = Number(process.env.LOBSTAH_ATTEMPTS ?? "1");
|
|
3137
|
+
const wake = process.env.LOBSTAH_WAKE;
|
|
2886
3138
|
const runnerInfo = {
|
|
2887
3139
|
pid: process.pid,
|
|
2888
3140
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2889
3141
|
attempts
|
|
2890
3142
|
};
|
|
2891
|
-
|
|
3143
|
+
fs19.writeFileSync(path16.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
|
|
2892
3144
|
const plan = planStart({
|
|
2893
3145
|
id,
|
|
2894
3146
|
lane,
|
|
@@ -2897,13 +3149,13 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
2897
3149
|
resolvedHarness: resolved.harness,
|
|
2898
3150
|
envResume: process.env.LOBSTAH_RESUME
|
|
2899
3151
|
});
|
|
2900
|
-
const wtFile =
|
|
3152
|
+
const wtFile = path16.join(activeDir, "worktree.json");
|
|
2901
3153
|
let recorded;
|
|
2902
3154
|
let choice;
|
|
2903
|
-
if (
|
|
2904
|
-
recorded = JSON.parse(
|
|
2905
|
-
if (!
|
|
2906
|
-
} else if (
|
|
3155
|
+
if (fs19.existsSync(wtFile)) {
|
|
3156
|
+
recorded = JSON.parse(fs19.readFileSync(wtFile, "utf8")).path;
|
|
3157
|
+
if (!fs19.existsSync(recorded)) throw new Error(`recorded worktree ${recorded} is gone`);
|
|
3158
|
+
} else if (fs19.existsSync(worktreePath(id))) {
|
|
2907
3159
|
throw new Error(`unrecorded worktree already exists for ${id} \u2014 refusing to proceed`);
|
|
2908
3160
|
} else if (descriptor.followUp && cfg.limits.reuseWorktree !== false) {
|
|
2909
3161
|
choice = await deps.chooseWorktree({ id, lane, repoKey: descriptor.repo, repo, followUp: descriptor.followUp });
|
|
@@ -2911,7 +3163,7 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
2911
3163
|
const worktreeNote = choice ? choice.reuse ? `reusing worktree of ${short3(choice.from)}` : `fresh worktree (${choice.reason})` : void 0;
|
|
2912
3164
|
const firstModel = modelForHarness(plan.harness, resolved.model);
|
|
2913
3165
|
const firstNote = [
|
|
2914
|
-
attempts > 1 ? `attempt ${attempts}` : void 0,
|
|
3166
|
+
wake ? `woke from pause: ${wake}` : attempts > 1 ? `attempt ${attempts}` : void 0,
|
|
2915
3167
|
plan.note,
|
|
2916
3168
|
firstModel.dropped ? droppedModelNote(plan.harness, firstModel.dropped) : void 0,
|
|
2917
3169
|
worktreeNote
|
|
@@ -2937,15 +3189,15 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
2937
3189
|
type: "runner",
|
|
2938
3190
|
data: { worktree: cwd, worktreeOf: choice.owner, setupRan }
|
|
2939
3191
|
});
|
|
2940
|
-
|
|
3192
|
+
fs19.writeFileSync(wtFile, JSON.stringify({ path: cwd, of: choice.owner }, null, 2));
|
|
2941
3193
|
} else {
|
|
2942
|
-
cwd = await deps.allocate(repo, id);
|
|
3194
|
+
cwd = await deps.allocate(repo, id, descriptor.systemRepair ? descriptor.pr?.headRefName : void 0);
|
|
2943
3195
|
acquireWorktreeLock(cwd, id, lane);
|
|
2944
|
-
|
|
3196
|
+
fs19.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
|
|
2945
3197
|
}
|
|
2946
|
-
const worktreeOf = JSON.parse(
|
|
3198
|
+
const worktreeOf = JSON.parse(fs19.readFileSync(wtFile, "utf8")).of;
|
|
2947
3199
|
mergeEvidence(id, lane, { worktree: cwd, worktreeOf });
|
|
2948
|
-
const existingPr =
|
|
3200
|
+
const existingPr = boundPr(descriptor, lane);
|
|
2949
3201
|
if (existingPr) mergeEvidence(id, lane, { prUrl: existingPr.url });
|
|
2950
3202
|
const remotePolicy = {
|
|
2951
3203
|
pushEarly: repo.pushEarly ?? cfg.limits.pushEarly,
|
|
@@ -2962,17 +3214,26 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
2962
3214
|
policy: remotePolicy,
|
|
2963
3215
|
existingPrUrl: existingPr?.url
|
|
2964
3216
|
});
|
|
2965
|
-
const
|
|
3217
|
+
const wakeNote = () => {
|
|
3218
|
+
if (!wake) return void 0;
|
|
3219
|
+
const msgs = unhandled(id, lane);
|
|
3220
|
+
for (const m of msgs) acknowledge(id, lane, m.file);
|
|
3221
|
+
return [
|
|
3222
|
+
`You were paused and this dispatch was parked. The wait ended: ${wake}. Continue the work, and report as before.`,
|
|
3223
|
+
...msgs.map((m) => m.text)
|
|
3224
|
+
].join("\n\n");
|
|
3225
|
+
};
|
|
3226
|
+
const envNudge = [process.env.LOBSTAH_NUDGE, wakeNote()].filter(Boolean).join("\n\n") || void 0;
|
|
2966
3227
|
const planNudge = plan.cold && !envNudge ? coldNote(plan.cold, cwd, repo.trunk) : void 0;
|
|
2967
3228
|
const promptWith = (nudge) => buildPrompt(brief, { id, nudge, attachments: descriptor.attachments, existingPr });
|
|
2968
|
-
const binDir =
|
|
2969
|
-
const workerPath =
|
|
3229
|
+
const binDir = path16.resolve(path16.dirname(fileURLToPath2(import.meta.url)), "..", "..", "..", "bin");
|
|
3230
|
+
const workerPath = fs19.existsSync(path16.join(binDir, "lobstah")) ? `${binDir}${path16.delimiter}${process.env.PATH ?? ""}` : void 0;
|
|
2970
3231
|
let wallClockHit = false;
|
|
2971
3232
|
let current;
|
|
2972
|
-
const wallFile =
|
|
3233
|
+
const wallFile = path16.join(activeDir, "wallclock.json");
|
|
2973
3234
|
const wallState = (() => {
|
|
2974
3235
|
try {
|
|
2975
|
-
return JSON.parse(
|
|
3236
|
+
return JSON.parse(fs19.readFileSync(wallFile, "utf8"));
|
|
2976
3237
|
} catch {
|
|
2977
3238
|
return { elapsedMs: 0, windowMs: resolved.limits.wallClockSecs * 1e3 };
|
|
2978
3239
|
}
|
|
@@ -2991,8 +3252,8 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
2991
3252
|
) * 1e3;
|
|
2992
3253
|
const persistWall = (elapsedMs, windowMs) => {
|
|
2993
3254
|
const tmp = `${wallFile}.tmp-${process.pid}`;
|
|
2994
|
-
|
|
2995
|
-
|
|
3255
|
+
fs19.writeFileSync(tmp, JSON.stringify({ ...wallState, elapsedMs, windowMs }));
|
|
3256
|
+
fs19.renameSync(tmp, wallFile);
|
|
2996
3257
|
};
|
|
2997
3258
|
const madeProgress = () => {
|
|
2998
3259
|
const activity = readActivity(id, lane);
|
|
@@ -3014,10 +3275,16 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
3014
3275
|
maxMs: maxWallMs,
|
|
3015
3276
|
initialElapsedMs: wallState.elapsedMs,
|
|
3016
3277
|
initialWindowMs: wallState.windowMs,
|
|
3017
|
-
|
|
3278
|
+
// A finished dispatch spends no time: the worker's final report
|
|
3279
|
+
// stops the clock, even before the turn ends.
|
|
3280
|
+
paused: () => {
|
|
3281
|
+
const last = readStatusLog(id, lane).at(-1);
|
|
3282
|
+
return pausedWaiting(last) || isFinished(id, lane);
|
|
3283
|
+
},
|
|
3018
3284
|
progress: madeProgress,
|
|
3019
3285
|
onTick: persistWall,
|
|
3020
3286
|
onExpire: () => {
|
|
3287
|
+
if (isFinished(id, lane)) return;
|
|
3021
3288
|
wallClockHit = true;
|
|
3022
3289
|
current?.kill();
|
|
3023
3290
|
}
|
|
@@ -3047,86 +3314,219 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
3047
3314
|
resumeSession
|
|
3048
3315
|
});
|
|
3049
3316
|
current = run2;
|
|
3050
|
-
const
|
|
3317
|
+
const driven = await drive(run2, {
|
|
3051
3318
|
id,
|
|
3052
3319
|
lane,
|
|
3053
3320
|
cwd,
|
|
3054
3321
|
stopped: () => wallClockHit,
|
|
3055
|
-
backgroundWaitMs: cfg.limits.backgroundWaitSecs * 1e3
|
|
3322
|
+
backgroundWaitMs: cfg.limits.backgroundWaitSecs * 1e3,
|
|
3323
|
+
exitGraceMs: (cfg.limits.exitGraceSecs ?? 30) * 1e3,
|
|
3324
|
+
onFinal: () => wallTimer?.stop(),
|
|
3325
|
+
onPark: () => wallTimer?.stop()
|
|
3056
3326
|
});
|
|
3057
|
-
const
|
|
3058
|
-
|
|
3327
|
+
const { cancelled, activity, parked } = driven;
|
|
3328
|
+
if (driven.stopped) {
|
|
3329
|
+
const reaped = await deps.reap();
|
|
3330
|
+
console.log(`[runner] harness stopped (${driven.stopped.reason}) ${Math.round(driven.stopped.afterMs / 1e3)}s after the final report; ${reaped} process(es) stopped`);
|
|
3331
|
+
mergeEvidence(id, lane, {
|
|
3332
|
+
harnessStopped: {
|
|
3333
|
+
reason: driven.stopped.reason,
|
|
3334
|
+
afterSecs: Math.round(driven.stopped.afterMs / 1e3),
|
|
3335
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3336
|
+
}
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
const result = driven.final || parked ? await within(run2.done, DONE_WAIT_MS, {}) : await run2.done;
|
|
3340
|
+
return { cancelled, activity, parked: parked === true, result };
|
|
3059
3341
|
};
|
|
3060
|
-
let
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3342
|
+
let finalReport = false;
|
|
3343
|
+
let parkedExit = false;
|
|
3344
|
+
try {
|
|
3345
|
+
let outcome = await runOnce(plan.harness, promptWith(envNudge ?? planNudge), plan.resume?.sessionId);
|
|
3346
|
+
if (plan.resume && !outcome.cancelled && !wallClockHit && outcome.activity === 0 && isUnresumable(outcome.result.error)) {
|
|
3347
|
+
const reason = outcome.result.error.replace(/\s+/g, " ").slice(0, 200).trim();
|
|
3348
|
+
const coldHarness = resolved.harness;
|
|
3349
|
+
const coldModel = modelForHarness(coldHarness, resolved.model);
|
|
3350
|
+
status(
|
|
3351
|
+
"working",
|
|
3352
|
+
`resume-fallback: ${reason} \u2014 starting cold on ${coldHarness}` + (coldModel.dropped ? `; ${droppedModelNote(coldHarness, coldModel.dropped)}` : "")
|
|
3353
|
+
);
|
|
3354
|
+
appendEvent(id, lane, {
|
|
3355
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3356
|
+
type: "runner",
|
|
3357
|
+
data: { resumeFallback: reason, harness: coldHarness }
|
|
3358
|
+
});
|
|
3359
|
+
mergeEvidence(id, lane, { resumeFallback: reason, sessionId: void 0, harness: coldHarness });
|
|
3360
|
+
const note = coldNote(
|
|
3361
|
+
{
|
|
3362
|
+
why: `resume of session ${plan.resume.sessionId} failed`,
|
|
3363
|
+
fromHarness: plan.harness,
|
|
3364
|
+
...plan.resume.origin ? { origin: plan.resume.origin } : {}
|
|
3365
|
+
},
|
|
3366
|
+
cwd,
|
|
3367
|
+
repo.trunk
|
|
3368
|
+
);
|
|
3369
|
+
outcome = await runOnce(coldHarness, promptWith([envNudge, note].filter(Boolean).join("\n\n")), void 0);
|
|
3370
|
+
}
|
|
3371
|
+
wallTimer?.stop();
|
|
3372
|
+
const { cancelled, result } = outcome;
|
|
3373
|
+
if (outcome.parked && !cancelled && !wallClockHit && readStatusLog(id, lane).at(-1)?.verb === "paused") {
|
|
3374
|
+
parkedExit = true;
|
|
3375
|
+
await remote.stop();
|
|
3376
|
+
const gitEvidence = await deps.collectEvidence(repo, cwd).catch(() => ({}));
|
|
3377
|
+
mergeEvidence(id, lane, { ...gitEvidence, sessionId: result.sessionId ?? readEvidence(id, lane).sessionId });
|
|
3378
|
+
console.log("[runner] worker paused: session ended, dispatch parked");
|
|
3379
|
+
return;
|
|
3380
|
+
}
|
|
3381
|
+
const terminal = isFinished(id, lane);
|
|
3382
|
+
finalReport = terminal;
|
|
3383
|
+
let savedNote;
|
|
3384
|
+
if (remoteEnabled && !terminal) {
|
|
3385
|
+
savedNote = await remote.saveBeforeStop();
|
|
3386
|
+
status("working", `work saved before stop: ${savedNote}`);
|
|
3387
|
+
} else {
|
|
3388
|
+
await remote.stop();
|
|
3389
|
+
}
|
|
3390
|
+
try {
|
|
3391
|
+
const gitEvidence = await deps.collectEvidence(repo, cwd);
|
|
3392
|
+
console.log(`[runner] git evidence: ${JSON.stringify(gitEvidence)}`);
|
|
3393
|
+
mergeEvidence(id, lane, { ...gitEvidence, sessionId: result.sessionId ?? readEvidence(id, lane).sessionId });
|
|
3394
|
+
console.log(`[runner] evidence after merge: ${JSON.stringify(readEvidence(id, lane))}`);
|
|
3395
|
+
} catch (err) {
|
|
3396
|
+
mergeEvidence(id, lane, {
|
|
3397
|
+
note: `evidence collection failed: ${err instanceof Error ? err.message : String(err)}`.slice(0, 300)
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
settle(id, lane, { cancelled, wallClockHit, error: result.error, budgetNote: savedNote });
|
|
3401
|
+
} finally {
|
|
3402
|
+
wallTimer?.stop();
|
|
3403
|
+
if (finalReport || parkedExit) {
|
|
3404
|
+
const reaped = await deps.reap().catch(() => 0);
|
|
3405
|
+
if (reaped > 0) console.log(`[runner] ${reaped} leftover process(es) stopped`);
|
|
3406
|
+
}
|
|
3407
|
+
if (!parkedExit) {
|
|
3408
|
+
releaseWorktreeLock(cwd, id);
|
|
3409
|
+
complete(id, lane);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
// packages/runner/src/reap.ts
|
|
3415
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3416
|
+
function listProcesses() {
|
|
3417
|
+
if (process.platform === "win32") {
|
|
3418
|
+
const out2 = execFileSync2(
|
|
3419
|
+
"powershell.exe",
|
|
3420
|
+
[
|
|
3421
|
+
"-NoProfile",
|
|
3422
|
+
"-NonInteractive",
|
|
3423
|
+
"-Command",
|
|
3424
|
+
'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId)" }'
|
|
3425
|
+
],
|
|
3426
|
+
{ encoding: "utf8", windowsHide: true, timeout: 3e4, stdio: ["ignore", "pipe", "ignore"] }
|
|
3083
3427
|
);
|
|
3084
|
-
|
|
3085
|
-
}
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3428
|
+
return out2.split(/\r?\n/).map((l) => l.trim().split(/\s+/).map(Number)).filter(([pid, ppid]) => Number.isInteger(pid) && Number.isInteger(ppid)).map(([pid, ppid]) => ({ pid, ppid }));
|
|
3429
|
+
}
|
|
3430
|
+
const out = execFileSync2("ps", ["-A", "-o", "pid=,ppid=,pgid="], {
|
|
3431
|
+
encoding: "utf8",
|
|
3432
|
+
timeout: 3e4,
|
|
3433
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3434
|
+
});
|
|
3435
|
+
return out.split("\n").map((l) => l.trim().split(/\s+/).map(Number)).filter(([pid, ppid, pgid]) => Number.isInteger(pid) && Number.isInteger(ppid) && Number.isInteger(pgid)).map(([pid, ppid, pgid]) => ({ pid, ppid, pgid }));
|
|
3436
|
+
}
|
|
3437
|
+
function startedBy(root, rows) {
|
|
3438
|
+
const children = /* @__PURE__ */ new Map();
|
|
3439
|
+
for (const r of rows) {
|
|
3440
|
+
if (r.pid === r.ppid) continue;
|
|
3441
|
+
const list = children.get(r.ppid) ?? [];
|
|
3442
|
+
list.push(r.pid);
|
|
3443
|
+
children.set(r.ppid, list);
|
|
3444
|
+
}
|
|
3445
|
+
const found = /* @__PURE__ */ new Set();
|
|
3446
|
+
const stack = [root];
|
|
3447
|
+
while (stack.length > 0) {
|
|
3448
|
+
for (const child of children.get(stack.pop()) ?? []) {
|
|
3449
|
+
if (child === root || found.has(child)) continue;
|
|
3450
|
+
found.add(child);
|
|
3451
|
+
stack.push(child);
|
|
3452
|
+
}
|
|
3095
3453
|
}
|
|
3454
|
+
const leads = rows.find((r) => r.pid === root)?.pgid === root;
|
|
3455
|
+
if (leads) {
|
|
3456
|
+
for (const r of rows) if (r.pgid === root && r.pid !== root) found.add(r.pid);
|
|
3457
|
+
}
|
|
3458
|
+
return [...found];
|
|
3459
|
+
}
|
|
3460
|
+
var alive = (pid) => {
|
|
3096
3461
|
try {
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
mergeEvidence(id, lane, { ...gitEvidence, sessionId: result.sessionId ?? readEvidence(id, lane).sessionId });
|
|
3100
|
-
console.log(`[runner] evidence after merge: ${JSON.stringify(readEvidence(id, lane))}`);
|
|
3462
|
+
process.kill(pid, 0);
|
|
3463
|
+
return true;
|
|
3101
3464
|
} catch (err) {
|
|
3102
|
-
|
|
3103
|
-
note: `evidence collection failed: ${err instanceof Error ? err.message : String(err)}`.slice(0, 300)
|
|
3104
|
-
});
|
|
3465
|
+
return err.code === "EPERM";
|
|
3105
3466
|
}
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3467
|
+
};
|
|
3468
|
+
async function stopPids(pids, graceMs = 2e3) {
|
|
3469
|
+
if (pids.length === 0) return;
|
|
3470
|
+
if (process.platform === "win32") {
|
|
3471
|
+
for (const pid of pids) {
|
|
3472
|
+
try {
|
|
3473
|
+
execFileSync2("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true, timeout: 3e4 });
|
|
3474
|
+
} catch {
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
return;
|
|
3478
|
+
}
|
|
3479
|
+
const signal = (pid, sig) => {
|
|
3480
|
+
try {
|
|
3481
|
+
process.kill(pid, sig);
|
|
3482
|
+
} catch {
|
|
3483
|
+
}
|
|
3484
|
+
};
|
|
3485
|
+
for (const pid of pids) signal(pid, "SIGTERM");
|
|
3486
|
+
const deadline = Date.now() + graceMs;
|
|
3487
|
+
while (pids.some(alive) && Date.now() < deadline) await new Promise((r) => setTimeout(r, 100));
|
|
3488
|
+
for (const pid of pids.filter(alive)) signal(pid, "SIGKILL");
|
|
3489
|
+
}
|
|
3490
|
+
async function reapStarted(root = process.pid, graceMs) {
|
|
3491
|
+
let rows;
|
|
3492
|
+
try {
|
|
3493
|
+
rows = listProcesses();
|
|
3494
|
+
} catch {
|
|
3495
|
+
return 0;
|
|
3496
|
+
}
|
|
3497
|
+
const pids = startedBy(root, rows);
|
|
3498
|
+
await stopPids(pids, graceMs);
|
|
3499
|
+
return pids.length;
|
|
3109
3500
|
}
|
|
3110
3501
|
|
|
3111
3502
|
// packages/runner/src/index.ts
|
|
3503
|
+
var EXIT_LINGER_MS = 5e3;
|
|
3112
3504
|
function runRunner(activeDirArg2, laneArg2) {
|
|
3113
3505
|
const lane = laneArg2 === "chore" ? "chore" : "work";
|
|
3114
|
-
|
|
3115
|
-
|
|
3506
|
+
const id = path17.basename(activeDirArg2);
|
|
3507
|
+
main(activeDirArg2, lane, { reap: () => reapStarted(process.pid) }).catch((err) => {
|
|
3116
3508
|
try {
|
|
3117
|
-
|
|
3118
|
-
|
|
3509
|
+
if (!isFinished(id, lane)) {
|
|
3510
|
+
appendStatus(id, lane, "failed", String(err instanceof Error ? err.message : err).slice(0, 500));
|
|
3511
|
+
}
|
|
3512
|
+
fs20.renameSync(activeDirArg2, path17.join(laneDirs(lane).done, id));
|
|
3119
3513
|
} catch {
|
|
3120
3514
|
}
|
|
3121
3515
|
process.exitCode = 1;
|
|
3516
|
+
}).finally(() => {
|
|
3517
|
+
setTimeout(() => process.exit(), EXIT_LINGER_MS).unref();
|
|
3122
3518
|
});
|
|
3123
3519
|
}
|
|
3124
3520
|
var [activeDirArg, laneArg] = process.argv.slice(2);
|
|
3125
3521
|
if (activeDirArg && activeDirArg !== "__runner") runRunner(activeDirArg, laneArg);
|
|
3126
3522
|
export {
|
|
3523
|
+
listProcesses,
|
|
3127
3524
|
main,
|
|
3128
3525
|
planStart,
|
|
3129
|
-
|
|
3526
|
+
reapStarted,
|
|
3527
|
+
runRunner,
|
|
3528
|
+
startedBy,
|
|
3529
|
+
stopPids
|
|
3130
3530
|
};
|
|
3131
3531
|
/*! Bundled license information:
|
|
3132
3532
|
|