lobstah 0.5.11 → 0.5.13

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