lobstah 0.5.13 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runner.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // packages/runner/src/index.ts
2
- import * as fs18 from "node:fs";
3
- import * as path16 from "node:path";
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/queue.js
58
- import * as fs3 from "node:fs";
59
- import * as path3 from "node:path";
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/status.js
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
- var ACTIVITY_KINDS = ["tool", "message", "thinking", "waiting"];
68
- var ACTIVITY_SUMMARY_MAX = 80;
69
- var SECRET_PATTERNS = [
70
- /\b(?:sk|pk|rk)[-_][A-Za-z0-9_-]{8,}/i,
71
- /\b(?:gh[posur]_|github_pat_|glpat-|xox[abprs]-|npm_|hf_)[A-Za-z0-9_-]{16,}/,
72
- /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
73
- /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}/,
74
- /\bbearer\b/i,
75
- /(?:token|secret|passw(?:or)?d|pwd|api[-_]?key|auth|credential|private[-_]?key)[^\s]*[=:]/i
76
- ];
77
- function looksSecret(word) {
78
- if (SECRET_PATTERNS.some((re) => re.test(word)))
79
- return true;
80
- for (const run2 of word.match(/[A-Za-z0-9+/_=-]{24,}/g) ?? []) {
81
- if (/[0-9]/.test(run2) && /[A-Za-z]/.test(run2))
82
- return true;
83
- }
84
- return false;
85
- }
86
- function redactSummary(s, max = ACTIVITY_SUMMARY_MAX) {
87
- const clean = s.replace(/[\r\n\t]+/g, " ").split(" ").filter(Boolean).map((w) => looksSecret(w) ? "[redacted]" : w).join(" ");
88
- return clean.length > max ? `${clean.slice(0, max - 1)}\u2026` : clean;
89
- }
90
- var SHELLS = /* @__PURE__ */ new Set(["bash", "sh", "zsh", "dash", "fish", "pwsh", "powershell", "cmd"]);
91
- function commandWord(command) {
92
- let words = command.trim().split(/\s+/).filter(Boolean);
93
- while (words.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]))
94
- words = words.slice(1);
95
- if (words[0] === "env") {
96
- words = words.slice(1);
97
- while (words.length > 0 && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0]) || words[0].startsWith("-")))
98
- words = words.slice(1);
99
- }
100
- const first = words[0];
101
- if (!first)
102
- return void 0;
103
- const name = baseName(first.replace(/^["']|["']$/g, "")).replace(/\.(exe|cmd|bat)$/i, "");
104
- if (SHELLS.has(name.toLowerCase()) && words[1] && /^(-\w*c|\/c|-command)$/i.test(words[1])) {
105
- return commandWord(words.slice(2).join(" ").replace(/^["']/, "")) ?? name;
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
- return name || void 0;
108
- }
109
- function baseName(p) {
110
- const parts = p.split(/[\\/]/).filter(Boolean);
111
- return parts.at(-1) ?? p;
112
- }
113
- function isWindowsPath(p) {
114
- return /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\");
115
- }
116
- function relativeTarget(p, root) {
117
- if (!root)
118
- return baseName(p);
119
- const win = isWindowsPath(p) || isWindowsPath(root);
120
- const lib = win ? path2.win32 : path2.posix;
121
- if (!lib.isAbsolute(p))
122
- return p.replace(/\\/g, "/");
123
- const rel = lib.relative(win ? root : root.replace(/\\/g, "/"), p);
124
- if (!rel || rel.startsWith("..") || lib.isAbsolute(rel))
125
- return `\u2026/${baseName(p)}`;
126
- return rel.replace(/\\/g, "/");
127
- }
128
- function urlHost(u) {
129
- try {
130
- return new URL(u).host || void 0;
131
- } catch {
132
- return void 0;
134
+ isDateTime() {
135
+ return this.#hasDate && this.#hasTime;
133
136
  }
134
- }
135
- function toolTarget(input) {
136
- if (!input || typeof input !== "object")
137
- return void 0;
138
- const i = input;
139
- const str = (k) => typeof i[k] === "string" && i[k].length > 0 ? i[k] : void 0;
140
- const file = str("file_path") ?? str("notebook_path") ?? str("path");
141
- if (file)
142
- return file;
143
- const command = str("command") ?? (Array.isArray(i.command) ? i.command.map(String).join(" ") : void 0);
144
- if (command)
145
- return commandWord(command);
146
- const url = str("url");
147
- if (url)
148
- return urlHost(url);
149
- const changes = i.changes;
150
- if (Array.isArray(changes) && changes.length > 0) {
151
- const first = changes[0];
152
- if (typeof first?.path === "string")
153
- return first.path;
137
+ isLocal() {
138
+ return !this.#hasDate || !this.#hasTime || !this.#offset;
154
139
  }
155
- return str("subagent_type") ?? str("skill");
156
- }
157
- function toolSummary(name, target, root) {
158
- const looksPath = target !== void 0 && (/[\\/]/.test(target) || isWindowsPath(target));
159
- const shown = target === void 0 ? "" : looksPath ? relativeTarget(target, root) : target;
160
- return redactSummary(shown ? `${name} ${shown}` : name);
161
- }
162
- function activityFromEvent(ev, root) {
163
- const data = ev.data ?? {};
164
- switch (ev.type) {
165
- case "tool-start": {
166
- const name = typeof data.name === "string" && data.name ? data.name : "tool";
167
- const target = typeof data.target === "string" ? data.target : void 0;
168
- return { kind: "tool", summary: toolSummary(name, target, root) };
169
- }
170
- case "text":
171
- return { kind: "message", summary: "writing a message" };
172
- case "thinking":
173
- return { kind: "thinking", summary: "thinking" };
174
- case "runner": {
175
- if (typeof data.on === "string")
176
- return { kind: "waiting", summary: redactSummary(`waiting on ${data.on}`) };
177
- if (typeof data.waiting === "string")
178
- return { kind: "waiting", summary: redactSummary(`waiting for an answer (${data.waiting})`) };
179
- if (data.holding === "background")
180
- return { kind: "waiting", summary: "waiting on background work" };
181
- return void 0;
182
- }
183
- default:
184
- return void 0;
140
+ isDate() {
141
+ return this.#hasDate && !this.#hasTime;
185
142
  }
186
- }
187
- function atomicWrite(file, content) {
188
- const tmp = `${file}.tmp-${process.pid}`;
189
- fs.writeFileSync(tmp, content);
190
- fs.renameSync(tmp, file);
191
- }
192
- function writeActivity(id, lane, a) {
193
- const safe = { at: a.at, kind: a.kind, summary: redactSummary(a.summary) };
194
- atomicWrite(activityPath(id, lane), JSON.stringify(safe));
195
- }
196
- function readActivity(id, lane) {
197
- try {
198
- const parsed = JSON.parse(fs.readFileSync(activityPath(id, lane), "utf8"));
199
- if (typeof parsed.at !== "string" || typeof parsed.summary !== "string")
200
- return void 0;
201
- if (!ACTIVITY_KINDS.includes(parsed.kind))
202
- return void 0;
203
- return { ...parsed, summary: redactSummary(parsed.summary) };
204
- } catch {
205
- return void 0;
143
+ isTime() {
144
+ return this.#hasTime && !this.#hasDate;
206
145
  }
207
- }
208
- var ActivityTracker = class {
209
- write;
210
- opts;
211
- lastWriteAt = -Infinity;
212
- lastKind;
213
- pending;
214
- timer;
215
- throttleMs;
216
- now;
217
- constructor(write, opts = {}) {
218
- this.write = write;
219
- this.opts = opts;
220
- this.throttleMs = opts.throttleMs ?? 1e4;
221
- this.now = opts.now ?? Date.now;
146
+ isValid() {
147
+ return this.#hasDate || this.#hasTime;
222
148
  }
223
- observe(ev) {
224
- const derived = activityFromEvent(ev, this.opts.root);
225
- if (derived)
226
- this.offer({ at: new Date(this.now()).toISOString(), ...derived });
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
- offer(a) {
229
- const t = this.now();
230
- if (a.kind !== this.lastKind || t - this.lastWriteAt >= this.throttleMs) {
231
- this.emit(a, t);
232
- return;
233
- }
234
- this.pending = a;
235
- if (!this.timer) {
236
- this.timer = setTimeout(() => {
237
- this.timer = void 0;
238
- this.flush();
239
- }, Math.max(0, this.throttleMs - (t - this.lastWriteAt)));
240
- this.timer.unref?.();
241
- }
164
+ static wrapAsOffsetDateTime(jsDate, offset = "Z") {
165
+ let date = new _TomlDate(jsDate);
166
+ date.#offset = offset;
167
+ return date;
242
168
  }
243
- /** Write a held record now. */
244
- flush() {
245
- if (!this.pending)
246
- return;
247
- const a = this.pending;
248
- this.emit(a, this.now());
169
+ static wrapAsLocalDateTime(jsDate) {
170
+ let date = new _TomlDate(jsDate);
171
+ date.#offset = null;
172
+ return date;
249
173
  }
250
- stop() {
251
- if (this.timer)
252
- clearTimeout(this.timer);
253
- this.timer = void 0;
254
- this.pending = void 0;
174
+ static wrapAsLocalDate(jsDate) {
175
+ let date = new _TomlDate(jsDate);
176
+ date.#hasTime = false;
177
+ date.#offset = null;
178
+ return date;
255
179
  }
256
- emit(a, t) {
257
- this.pending = void 0;
258
- if (this.timer) {
259
- clearTimeout(this.timer);
260
- this.timer = void 0;
261
- }
262
- this.lastWriteAt = t;
263
- this.lastKind = a.kind;
264
- try {
265
- this.write(a);
266
- } catch {
267
- }
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
- // packages/core/dist/status.js
272
- function isVerb(v) {
273
- return VERBS.includes(v);
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 waitingFields(verb, w, now2 = Date.now()) {
290
- const out = {};
291
- if ((w.waitingOn !== void 0 || w.link !== void 0) && !WAITING_ON_VERBS.includes(verb)) {
292
- throw new Error(`--waiting-on and --link are valid only with ${WAITING_ON_VERBS.join(", ")} \u2014 not ${verb}`);
293
- }
294
- if (w.until !== void 0 && verb !== "paused")
295
- throw new Error(`--until is valid only with paused \u2014 not ${verb}`);
296
- if (w.waitingOn !== void 0) {
297
- if (!isWaitingOn(w.waitingOn)) {
298
- throw new Error(`invalid --waiting-on "${w.waitingOn}" \u2014 must be one of: ${WAITING_ON.join(", ")}`);
299
- }
300
- out.waitingOn = w.waitingOn;
301
- }
302
- if (w.link !== void 0) {
303
- let url;
304
- try {
305
- url = new URL(w.link);
306
- } catch {
307
- url = void 0;
308
- }
309
- if (!url || url.protocol !== "http:" && url.protocol !== "https:") {
310
- throw new Error(`invalid --link "${w.link}" \u2014 must be an http or https URL`);
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
- const extra = waiting ? waitingFields(verb, waiting) : {};
323
- const entry = { at: at ?? (/* @__PURE__ */ new Date()).toISOString(), verb, ...note ? { note } : {}, ...extra };
324
- fs2.appendFileSync(statusPath(id, lane), `${JSON.stringify(entry)}
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
- // packages/core/dist/queue.js
346
- function storedDescriptor(id, lane) {
347
- const dirs = laneDirs(lane);
348
- for (const file of [
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
- return void 0;
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
- // packages/core/dist/events.js
372
- import * as fs4 from "node:fs";
373
- function appendEvent(id, lane, ev) {
374
- fs4.appendFileSync(eventsPath(id, lane), `${JSON.stringify(ev)}
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
- // packages/core/dist/evidence.js
388
- import * as fs5 from "node:fs";
389
- function readEvidence(id, lane) {
390
- try {
391
- return JSON.parse(fs5.readFileSync(evidencePath(id, lane), "utf8"));
392
- } catch {
393
- return {};
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 mergeEvidence(id, lane, patch) {
397
- const file = evidencePath(id, lane);
398
- const tmp = `${file}.tmp-${process.pid}`;
399
- fs5.writeFileSync(tmp, JSON.stringify({ ...readEvidence(id, lane), ...patch }, null, 2));
400
- fs5.renameSync(tmp, file);
401
- }
402
-
403
- // packages/core/dist/inbox.js
404
- import * as fs7 from "node:fs";
405
- import * as path5 from "node:path";
406
-
407
- // packages/core/dist/soak.js
408
- import * as fs6 from "node:fs";
409
- import * as path4 from "node:path";
410
- function regPath(trapId) {
411
- return path4.join(soakingDir(), `${trapId}.json`);
412
- }
413
- function readTrap(trapId) {
414
- try {
415
- return JSON.parse(fs6.readFileSync(regPath(trapId), "utf8"));
416
- } catch {
417
- return void 0;
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 listTraps() {
421
- try {
422
- return fs6.readdirSync(soakingDir()).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(fs6.readFileSync(path4.join(soakingDir(), f), "utf8"))).filter((r) => typeof r.trapId === "string");
423
- } catch {
424
- return [];
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
- function claimPath(id, lane) {
428
- return path4.join(laneDirs(lane).active, id, "claim.json");
429
- }
430
- function readSessionClaim(id, lane) {
431
- try {
432
- return JSON.parse(fs6.readFileSync(claimPath(id, lane), "utf8"));
433
- } catch {
434
- return void 0;
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
- // packages/core/dist/inbox.js
439
- var metaName = (file) => file.replace(/\.msg$/, ".meta.json");
440
- function inboxDir(id, lane) {
441
- return path5.join(laneDirs(lane).inbox, id);
442
- }
443
- function unhandled(id, lane) {
444
- const dir = inboxDir(id, lane);
445
- if (!fs7.existsSync(dir))
446
- return [];
447
- return fs7.readdirSync(dir).filter((f) => f.endsWith(".msg")).sort().map((f) => ({ file: f, text: fs7.readFileSync(path5.join(dir, f), "utf8") }));
448
- }
449
- function acknowledge(id, lane, file) {
450
- const dir = inboxDir(id, lane);
451
- fs7.mkdirSync(path5.join(dir, "handled"), { recursive: true });
452
- fs7.renameSync(path5.join(dir, file), path5.join(dir, "handled", file));
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
- skipVoid(ctx, true);
1098
- if (ctx.p < toml.length && (tmp = toml.charCodeAt(ctx.p)) !== 10 && tmp !== 13) {
1099
- throw new TomlError("each key-value declaration must be followed by an end-of-line", {
1100
- toml,
1101
- ptr: ctx.p
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
- return res;
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/config.js
1110
- var ATTENTION_KINDS = ["question", "landed", "pr:draft", "pr:review", "pr:checks", "pr:conflict", "pr:ready"];
1111
- var DEFAULT_ATTENTION_KINDS = ["question", "pr:ready", "pr:review", "pr:conflict", "pr:checks"];
1112
- function parseAttentionKinds(raw) {
1113
- if (raw === void 0)
1114
- return [...DEFAULT_ATTENTION_KINDS];
1115
- if (!Array.isArray(raw)) {
1116
- throw new Error(`attentionKinds must be an array of kinds (${ATTENTION_KINDS.join(", ")}) in ${configPath()}`);
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
- var DEFAULT_SOAK = {
1126
- deferSecs: 90,
1127
- ttlSecs: 1800,
1128
- beat: true,
1129
- pausedTtlSecs: 86400
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 expandHome(p) {
1159
- return p.startsWith("~/") ? path6.join(os2.homedir(), p.slice(2)) : p;
1484
+ function claimPath(id, lane) {
1485
+ return path8.join(laneDirs(lane).active, id, "claim.json");
1160
1486
  }
1161
- function loadConfig() {
1162
- const file = configPath();
1163
- const raw = fs8.existsSync(file) ? parse(fs8.readFileSync(file, "utf8")) : {};
1164
- const reposRaw = raw.repos ?? {};
1165
- const repos = {};
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
- const groundsRaw = raw.grounds ?? {};
1182
- const grounds = {};
1183
- for (const [key, g] of Object.entries(groundsRaw)) {
1184
- grounds[key] = { repos: Array.isArray(g.repos) ? g.repos.map(String) : [] };
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
- function resolveDispatch(d, cfg) {
1202
- const repo = cfg.repos[d.repo];
1203
- if (!repo)
1204
- throw new Error(`unknown repo key "${d.repo}" \u2014 add it to ${configPath()}`);
1205
- const rh = repo.harness ?? {};
1206
- const gh = cfg.harness;
1207
- return {
1208
- harness: d.harness ?? rh.default ?? gh.default ?? "claude",
1209
- model: d.model ?? rh.model ?? gh.model,
1210
- effort: d.effort ?? rh.effort ?? gh.effort,
1211
- limits: {
1212
- wallClockSecs: cfg.limits.wallClockSecs,
1213
- ...d.limits ?? {}
1214
- },
1215
- env: { ...repo.env ?? {}, ...d.env ?? {} },
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 fs9 from "node:fs";
1222
- import * as path7 from "node:path";
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(path7.delimiter)) {
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 = path7.join(dir, name);
1542
+ const candidate = path10.join(dir, name);
1233
1543
  try {
1234
- fs9.accessSync(candidate, fs9.constants.X_OK);
1235
- if (fs9.statSync(candidate).isFile())
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 readWatch(key) {
1556
+ function codexInvocation(argv) {
1557
+ if (onPath("codex"))
1558
+ return { file: "codex", argv };
1559
+ const require_ = createRequire(import.meta.url);
1276
1560
  try {
1277
- return JSON.parse(fs10.readFileSync(watchPath(key), "utf8"));
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 fs11 from "node:fs";
1306
- import * as path9 from "node:path";
1592
+ import * as fs14 from "node:fs";
1593
+ import * as path11 from "node:path";
1307
1594
  function prsDir() {
1308
- return path9.join(lobstahHome(), "prs");
1595
+ return path11.join(lobstahHome(), "prs");
1309
1596
  }
1310
1597
  function readPrs() {
1311
1598
  let files;
1312
1599
  try {
1313
- files = fs11.readdirSync(prsDir()).filter((f) => f.endsWith(".json"));
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(fs11.readFileSync(path9.join(prsDir(), f), "utf8"));
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 fs12 from "node:fs";
1655
+ import * as fs15 from "node:fs";
1355
1656
  import * as os3 from "node:os";
1356
- import * as path10 from "node:path";
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(fs12.readFileSync(path10.join(dir, id, "claim.json"), "utf8"));
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 laneOf(id) {
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 ?? laneOf(id);
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 || !fs12.existsSync(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 ?? path10.join(os3.homedir(), ".codex");
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 = fs12.readdirSync(dir, { withFileTypes: true });
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 path10.join(dir, e.name);
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(path10.join(dir, e.name), depth - 1);
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(path10.join(home, "sessions"), 3) ?? walk(path10.join(home, "archived_sessions"), 0);
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 = fs12.openSync(file, "r");
1783
+ const fd = fs15.openSync(file, "r");
1483
1784
  try {
1484
1785
  const buf = Buffer.alloc(4096);
1485
- head = buf.subarray(0, fs12.readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
1786
+ head = buf.subarray(0, fs15.readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
1486
1787
  } finally {
1487
- fs12.closeSync(fd);
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 fs17 from "node:fs";
1672
- import * as path15 from "node:path";
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((resolve6) => this.waiters.push(resolve6));
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((resolve6) => this.waiter = resolve6);
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
- push({ at, type: "tool-start", data: { name: block.name, ...target ? { target } : {} } });
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((resolve6) => {
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
- resolve6({ sessionId, error: err.message });
2037
+ resolve7({ sessionId, error: err.message });
1879
2038
  });
1880
2039
  child.on("exit", (code) => {
1881
2040
  events.close();
1882
2041
  if (code === 0)
1883
- resolve6({ sessionId });
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
- resolve6({ sessionId, error });
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 fs14 from "node:fs";
2125
+ import * as fs16 from "node:fs";
1967
2126
  import * as os4 from "node:os";
1968
- import * as path12 from "node:path";
2127
+ import * as path13 from "node:path";
1969
2128
  import { spawn as spawn2 } from "node:child_process";
1970
2129
  function isolatedCodexHome() {
1971
- const dir = path12.join(lobstahHome(), "codex-home");
1972
- fs14.mkdirSync(dir, { recursive: true });
1973
- const auth = path12.join(dir, "auth.json");
1974
- const realAuth = path12.join(os4.homedir(), ".codex", "auth.json");
1975
- if (!fs14.existsSync(auth) && fs14.existsSync(realAuth)) {
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
- fs14.symlinkSync(realAuth, auth);
2136
+ fs16.symlinkSync(realAuth, auth);
1978
2137
  } catch {
1979
- fs14.copyFileSync(realAuth, auth);
2138
+ fs16.copyFileSync(realAuth, auth);
1980
2139
  }
1981
2140
  }
1982
- const cfg = path12.join(dir, "config.toml");
1983
- if (!fs14.existsSync(cfg)) {
1984
- fs14.writeFileSync(cfg, "# lobstah-managed: auth inherited from ~/.codex, interactive config not\n");
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
- push({ at, type: "tool-start", data: { name: item.type, ...target ? { target } : {} } });
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((resolve6) => {
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) => resolve6(err.message));
2226
+ child.on("error", (err) => resolve7(err.message));
2066
2227
  child.on("exit", (code) => {
2067
2228
  if (turnError)
2068
- return resolve6(turnError);
2229
+ return resolve7(turnError);
2069
2230
  if (code === 0 || killed)
2070
- return resolve6(void 0);
2231
+ return resolve7(void 0);
2071
2232
  const detail = stderrTail.trim().split("\n").at(-1);
2072
- resolve6(`codex exited ${code}${detail ? ` \u2014 ${detail}` : ""}`);
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 fs15 from "node:fs";
2186
- import * as path13 from "node:path";
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 (fs15.existsSync(dir)) {
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 (!fs15.existsSync(repo.path)) {
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", repo.trunk);
2252
- await gitShared(repo.path, "worktree", "add", "--no-track", dir, "-b", `lobstah/${id}`, `origin/${repo.trunk}`);
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 = path13.join(dir, name);
2283
- if (!fs15.existsSync(file))
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(fs15.readFileSync(file));
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 ? path13.join(gitDir, "lobstah-setup.json") : void 0;
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
- fs15.writeFileSync(file, JSON.stringify({ hash: setupHash(repo, dir), at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
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(fs15.readFileSync(file, "utf8")).hash : void 0;
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 = fs15.realpathSync.native(p);
2472
+ r = fs17.realpathSync.native(p);
2312
2473
  } catch {
2313
- r = path13.resolve(p);
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, laneOf2(found.owner) ?? lane)?.repo ?? storedDescriptor(found.from, laneOf2(found.from) ?? lane)?.repo;
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 = path13.relative(here, realpath(t.worktree));
2356
- if (!rel.startsWith("..") && !path13.isAbsolute(rel))
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. Push your changes to its existing head branch${opts.existingPr.headRefName ? ` ${opts.existingPr.headRefName}` : " (inspect the PR to find it)"}; do not create a duplicate PR. Report done with the same PR URL. Do not merge anything.` : `Commit your work with clear messages. The runner pushes committed HEAD early and opens or adopts one draft PR for the branch when available; do not create a duplicate PR. When finished, mark the draft ready for review if appropriate, then report done with its URL. Do not merge anything.`,
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
- var sleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
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
- run2.kill();
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 (waiting) {
2535
- await awaitAnswer(lastVerb);
2762
+ if (lastVerb === "paused") {
2763
+ finish(true);
2536
2764
  continue;
2537
2765
  }
2538
- if (lastVerb !== void 0 && TERMINAL_VERBS.includes(lastVerb)) {
2539
- run2.end();
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 if (!lastVerb || !TERMINAL_VERBS.includes(lastVerb)) appendStatus(id, lane, "failed", "stopped without reporting a result");
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 fs16 from "node:fs";
2694
- import * as path14 from "node:path";
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 = path14.resolve(path14.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
2725
- const localCli = path14.join(root, "apps", "cli", "dist", "main.js");
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 (fs16.existsSync(localCli)) await command(process.execPath, [localCli, ...args], 15e3);
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 = { loadAdapter, allocate, chooseWorktree, prepareReuse, collectEvidence };
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 = path15.basename(activeDir);
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(fs17.readFileSync(path15.join(activeDir, "descriptor.json"), "utf8"));
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 = path15.join(activeDir, "brief.md");
2883
- if (!fs17.existsSync(briefFile)) fs17.writeFileSync(briefFile, descriptor.brief);
2884
- const brief = fs17.readFileSync(briefFile, "utf8");
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
- fs17.writeFileSync(path15.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
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 = path15.join(activeDir, "worktree.json");
3152
+ const wtFile = path16.join(activeDir, "worktree.json");
2901
3153
  let recorded;
2902
3154
  let choice;
2903
- if (fs17.existsSync(wtFile)) {
2904
- recorded = JSON.parse(fs17.readFileSync(wtFile, "utf8")).path;
2905
- if (!fs17.existsSync(recorded)) throw new Error(`recorded worktree ${recorded} is gone`);
2906
- } else if (fs17.existsSync(worktreePath(id))) {
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
- fs17.writeFileSync(wtFile, JSON.stringify({ path: cwd, of: choice.owner }, null, 2));
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
- fs17.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
3196
+ fs19.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
2945
3197
  }
2946
- const worktreeOf = JSON.parse(fs17.readFileSync(wtFile, "utf8")).of;
3198
+ const worktreeOf = JSON.parse(fs19.readFileSync(wtFile, "utf8")).of;
2947
3199
  mergeEvidence(id, lane, { worktree: cwd, worktreeOf });
2948
- const existingPr = descriptor.followUp ? chainPr(descriptor.followUp, lane) : void 0;
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 envNudge = process.env.LOBSTAH_NUDGE;
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 = path15.resolve(path15.dirname(fileURLToPath2(import.meta.url)), "..", "..", "..", "bin");
2969
- const workerPath = fs17.existsSync(path15.join(binDir, "lobstah")) ? `${binDir}${path15.delimiter}${process.env.PATH ?? ""}` : void 0;
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 = path15.join(activeDir, "wallclock.json");
3233
+ const wallFile = path16.join(activeDir, "wallclock.json");
2973
3234
  const wallState = (() => {
2974
3235
  try {
2975
- return JSON.parse(fs17.readFileSync(wallFile, "utf8"));
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
- fs17.writeFileSync(tmp, JSON.stringify({ ...wallState, elapsedMs, windowMs }));
2995
- fs17.renameSync(tmp, wallFile);
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
- paused: () => pausedWaiting(readStatusLog(id, lane).at(-1)),
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 { cancelled: cancelled2, activity } = await drive(run2, {
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 result2 = await run2.done;
3058
- return { cancelled: cancelled2, activity, result: result2 };
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 outcome = await runOnce(plan.harness, promptWith(envNudge ?? planNudge), plan.resume?.sessionId);
3061
- if (plan.resume && !outcome.cancelled && !wallClockHit && outcome.activity === 0 && isUnresumable(outcome.result.error)) {
3062
- const reason = outcome.result.error.replace(/\s+/g, " ").slice(0, 200).trim();
3063
- const coldHarness = resolved.harness;
3064
- const coldModel = modelForHarness(coldHarness, resolved.model);
3065
- status(
3066
- "working",
3067
- `resume-fallback: ${reason} \u2014 starting cold on ${coldHarness}` + (coldModel.dropped ? `; ${droppedModelNote(coldHarness, coldModel.dropped)}` : "")
3068
- );
3069
- appendEvent(id, lane, {
3070
- at: (/* @__PURE__ */ new Date()).toISOString(),
3071
- type: "runner",
3072
- data: { resumeFallback: reason, harness: coldHarness }
3073
- });
3074
- mergeEvidence(id, lane, { resumeFallback: reason, sessionId: void 0, harness: coldHarness });
3075
- const note = coldNote(
3076
- {
3077
- why: `resume of session ${plan.resume.sessionId} failed`,
3078
- fromHarness: plan.harness,
3079
- ...plan.resume.origin ? { origin: plan.resume.origin } : {}
3080
- },
3081
- cwd,
3082
- repo.trunk
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
- outcome = await runOnce(coldHarness, promptWith([envNudge, note].filter(Boolean).join("\n\n")), void 0);
3085
- }
3086
- wallTimer?.stop();
3087
- const { cancelled, result } = outcome;
3088
- const terminal = ["done", "failed"].includes(readStatusLog(id, lane).at(-1)?.verb ?? "");
3089
- let savedNote;
3090
- if (remoteEnabled && (wallClockHit || cancelled || !!result.error || !terminal)) {
3091
- savedNote = await remote.saveBeforeStop();
3092
- status("working", `work saved before stop: ${savedNote}`);
3093
- } else {
3094
- await remote.stop();
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
- const gitEvidence = await deps.collectEvidence(repo, cwd);
3098
- console.log(`[runner] git evidence: ${JSON.stringify(gitEvidence)}`);
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
- mergeEvidence(id, lane, {
3103
- note: `evidence collection failed: ${err instanceof Error ? err.message : String(err)}`.slice(0, 300)
3104
- });
3465
+ return err.code === "EPERM";
3105
3466
  }
3106
- settle(id, lane, { cancelled, wallClockHit, error: result.error, budgetNote: savedNote });
3107
- releaseWorktreeLock(cwd, id);
3108
- complete(id, lane);
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
- main(activeDirArg2, lane).catch((err) => {
3115
- const id = path16.basename(activeDirArg2);
3506
+ const id = path17.basename(activeDirArg2);
3507
+ main(activeDirArg2, lane, { reap: () => reapStarted(process.pid) }).catch((err) => {
3116
3508
  try {
3117
- appendStatus(id, lane, "failed", String(err instanceof Error ? err.message : err).slice(0, 500));
3118
- fs18.renameSync(activeDirArg2, path16.join(laneDirs(lane).done, id));
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
- runRunner
3526
+ reapStarted,
3527
+ runRunner,
3528
+ startedBy,
3529
+ stopPids
3130
3530
  };
3131
3531
  /*! Bundled license information:
3132
3532