lobstah 0.5.12 → 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,6 +1,6 @@
1
1
  // packages/runner/src/index.ts
2
- import * as fs16 from "node:fs";
3
- import * as path14 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"];
@@ -401,32 +401,65 @@ function mergeEvidence(id, lane, patch) {
401
401
  }
402
402
 
403
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
404
408
  import * as fs6 from "node:fs";
405
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
+ }
436
+ }
437
+
438
+ // packages/core/dist/inbox.js
406
439
  var metaName = (file) => file.replace(/\.msg$/, ".meta.json");
407
440
  function inboxDir(id, lane) {
408
- return path4.join(laneDirs(lane).inbox, id);
441
+ return path5.join(laneDirs(lane).inbox, id);
409
442
  }
410
443
  function unhandled(id, lane) {
411
444
  const dir = inboxDir(id, lane);
412
- if (!fs6.existsSync(dir))
445
+ if (!fs7.existsSync(dir))
413
446
  return [];
414
- return fs6.readdirSync(dir).filter((f) => f.endsWith(".msg")).sort().map((f) => ({ file: f, text: fs6.readFileSync(path4.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") }));
415
448
  }
416
449
  function acknowledge(id, lane, file) {
417
450
  const dir = inboxDir(id, lane);
418
- fs6.mkdirSync(path4.join(dir, "handled"), { recursive: true });
419
- fs6.renameSync(path4.join(dir, file), path4.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));
420
453
  try {
421
- fs6.renameSync(path4.join(dir, metaName(file)), path4.join(dir, "handled", metaName(file)));
454
+ fs7.renameSync(path5.join(dir, metaName(file)), path5.join(dir, "handled", metaName(file)));
422
455
  } catch {
423
456
  }
424
457
  }
425
458
 
426
459
  // packages/core/dist/config.js
427
- import * as fs7 from "node:fs";
460
+ import * as fs8 from "node:fs";
428
461
  import * as os2 from "node:os";
429
- import * as path5 from "node:path";
462
+ import * as path6 from "node:path";
430
463
 
431
464
  // node_modules/.pnpm/smol-toml@1.8.0/node_modules/smol-toml/dist/date.js
432
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;
@@ -1120,14 +1153,14 @@ var DEFAULT_LIMITS = {
1120
1153
  releaseOnMerge: false
1121
1154
  };
1122
1155
  function configPath() {
1123
- return path5.join(lobstahHome(), "config.toml");
1156
+ return path6.join(lobstahHome(), "config.toml");
1124
1157
  }
1125
1158
  function expandHome(p) {
1126
- return p.startsWith("~/") ? path5.join(os2.homedir(), p.slice(2)) : p;
1159
+ return p.startsWith("~/") ? path6.join(os2.homedir(), p.slice(2)) : p;
1127
1160
  }
1128
1161
  function loadConfig() {
1129
1162
  const file = configPath();
1130
- const raw = fs7.existsSync(file) ? parse(fs7.readFileSync(file, "utf8")) : {};
1163
+ const raw = fs8.existsSync(file) ? parse(fs8.readFileSync(file, "utf8")) : {};
1131
1164
  const reposRaw = raw.repos ?? {};
1132
1165
  const repos = {};
1133
1166
  for (const [key, r] of Object.entries(reposRaw)) {
@@ -1185,21 +1218,21 @@ function resolveDispatch(d, cfg) {
1185
1218
  }
1186
1219
 
1187
1220
  // packages/core/dist/harness.js
1188
- import * as fs8 from "node:fs";
1189
- import * as path6 from "node:path";
1221
+ import * as fs9 from "node:fs";
1222
+ import * as path7 from "node:path";
1190
1223
  import { createRequire } from "node:module";
1191
1224
  function resolveOnPath(bin, pathVar = process.env.PATH) {
1192
1225
  if (!pathVar)
1193
1226
  return void 0;
1194
1227
  const names = process.platform === "win32" ? [bin, `${bin}.exe`, `${bin}.cmd`, `${bin}.bat`] : [bin];
1195
- for (const dir of pathVar.split(path6.delimiter)) {
1228
+ for (const dir of pathVar.split(path7.delimiter)) {
1196
1229
  if (!dir)
1197
1230
  continue;
1198
1231
  for (const name of names) {
1199
- const candidate = path6.join(dir, name);
1232
+ const candidate = path7.join(dir, name);
1200
1233
  try {
1201
- fs8.accessSync(candidate, fs8.constants.X_OK);
1202
- if (fs8.statSync(candidate).isFile())
1234
+ fs9.accessSync(candidate, fs9.constants.X_OK);
1235
+ if (fs9.statSync(candidate).isFile())
1203
1236
  return candidate;
1204
1237
  } catch {
1205
1238
  }
@@ -1220,39 +1253,28 @@ function codexInvocation(argv) {
1220
1253
  } catch {
1221
1254
  }
1222
1255
  for (const dir of require_.resolve.paths("@openai/codex") ?? []) {
1223
- const cli = path6.join(dir, "@openai", "codex", "bin", "codex.js");
1224
- if (fs8.existsSync(cli))
1256
+ const cli = path7.join(dir, "@openai", "codex", "bin", "codex.js");
1257
+ if (fs9.existsSync(cli))
1225
1258
  return { file: process.execPath, argv: [cli, ...argv] };
1226
1259
  }
1227
1260
  return void 0;
1228
1261
  }
1229
1262
 
1230
- // packages/core/dist/soak.js
1231
- import * as fs9 from "node:fs";
1232
- import * as path7 from "node:path";
1233
- function regPath(trapId) {
1234
- return path7.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");
1235
1268
  }
1236
- function readTrap(trapId) {
1237
- try {
1238
- return JSON.parse(fs9.readFileSync(regPath(trapId), "utf8"));
1239
- } catch {
1240
- return void 0;
1241
- }
1269
+ function slug(key) {
1270
+ return key.replace(/[^A-Za-z0-9._-]+/g, "-");
1242
1271
  }
1243
- function listTraps() {
1244
- try {
1245
- return fs9.readdirSync(soakingDir()).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(fs9.readFileSync(path7.join(soakingDir(), f), "utf8"))).filter((r) => typeof r.trapId === "string");
1246
- } catch {
1247
- return [];
1248
- }
1272
+ function watchPath(key) {
1273
+ return path8.join(watchesDir(), `${slug(key)}.json`);
1249
1274
  }
1250
- function claimPath(id, lane) {
1251
- return path7.join(laneDirs(lane).active, id, "claim.json");
1252
- }
1253
- function readSessionClaim(id, lane) {
1275
+ function readWatch(key) {
1254
1276
  try {
1255
- return JSON.parse(fs9.readFileSync(claimPath(id, lane), "utf8"));
1277
+ return JSON.parse(fs10.readFileSync(watchPath(key), "utf8"));
1256
1278
  } catch {
1257
1279
  return void 0;
1258
1280
  }
@@ -1268,13 +1290,70 @@ var COMPILED_BINARY = (() => {
1268
1290
  })();
1269
1291
 
1270
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
+ }
1271
1301
  var PR_VIEW_FIELDS = "state,isDraft,headRefOid,baseRefName,headRefName,mergeStateStatus,reviewDecision,statusCheckRollup,mergedAt,closedAt,updatedAt,reviews";
1272
1302
  var PR_VIEW_FIELDS_NO_CHECKS = PR_VIEW_FIELDS.split(",").filter((f) => f !== "statusCheckRollup").join(",");
1273
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
+
1274
1353
  // packages/core/dist/resume.js
1275
- import * as fs10 from "node:fs";
1354
+ import * as fs12 from "node:fs";
1276
1355
  import * as os3 from "node:os";
1277
- import * as path8 from "node:path";
1356
+ import * as path10 from "node:path";
1278
1357
  import { spawnSync } from "node:child_process";
1279
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;
1280
1359
  function harnessFromSessionId(sessionId) {
@@ -1285,7 +1364,7 @@ function claimOf(id, lane) {
1285
1364
  const dirs = laneDirs(lane);
1286
1365
  for (const dir of [dirs.active, dirs.done]) {
1287
1366
  try {
1288
- return JSON.parse(fs10.readFileSync(path8.join(dir, id, "claim.json"), "utf8"));
1367
+ return JSON.parse(fs12.readFileSync(path10.join(dir, id, "claim.json"), "utf8"));
1289
1368
  } catch {
1290
1369
  }
1291
1370
  }
@@ -1335,7 +1414,7 @@ function isUnresumable(error) {
1335
1414
  return error !== void 0 && UNRESUMABLE.test(error);
1336
1415
  }
1337
1416
  function worktreeProgress(worktree, trunk) {
1338
- if (!worktree || !fs10.existsSync(worktree))
1417
+ if (!worktree || !fs12.existsSync(worktree))
1339
1418
  return "No worktree progress recorded.";
1340
1419
  const git2 = (...a) => spawnSync("git", a, { cwd: worktree, encoding: "utf8" }).stdout?.trim() ?? "";
1341
1420
  const commits = trunk ? git2("log", "--oneline", `origin/${trunk}..HEAD`) : git2("log", "--oneline", "-15");
@@ -1368,31 +1447,31 @@ function handoffNote(fromHarness, progress, why) {
1368
1447
  ${progress}`;
1369
1448
  }
1370
1449
  function codexHome(env = process.env) {
1371
- return env.CODEX_HOME ?? path8.join(os3.homedir(), ".codex");
1450
+ return env.CODEX_HOME ?? path10.join(os3.homedir(), ".codex");
1372
1451
  }
1373
1452
  function codexRolloutFile(threadId, home = codexHome()) {
1374
1453
  const suffix = `-${threadId}.jsonl`;
1375
1454
  const walk = (dir, depth) => {
1376
1455
  let entries;
1377
1456
  try {
1378
- entries = fs10.readdirSync(dir, { withFileTypes: true });
1457
+ entries = fs12.readdirSync(dir, { withFileTypes: true });
1379
1458
  } catch {
1380
1459
  return void 0;
1381
1460
  }
1382
1461
  for (const e of entries) {
1383
1462
  if (e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(suffix))
1384
- return path8.join(dir, e.name);
1463
+ return path10.join(dir, e.name);
1385
1464
  }
1386
1465
  if (depth === 0)
1387
1466
  return void 0;
1388
1467
  for (const e of entries.filter((x) => x.isDirectory()).sort((a, b) => b.name.localeCompare(a.name))) {
1389
- const hit = walk(path8.join(dir, e.name), depth - 1);
1468
+ const hit = walk(path10.join(dir, e.name), depth - 1);
1390
1469
  if (hit)
1391
1470
  return hit;
1392
1471
  }
1393
1472
  return void 0;
1394
1473
  };
1395
- return walk(path8.join(home, "sessions"), 3) ?? walk(path8.join(home, "archived_sessions"), 0);
1474
+ return walk(path10.join(home, "sessions"), 3) ?? walk(path10.join(home, "archived_sessions"), 0);
1396
1475
  }
1397
1476
  function codexDesktopThread(threadId, home = codexHome()) {
1398
1477
  const file = codexRolloutFile(threadId, home);
@@ -1400,12 +1479,12 @@ function codexDesktopThread(threadId, home = codexHome()) {
1400
1479
  return void 0;
1401
1480
  let head = "";
1402
1481
  try {
1403
- const fd = fs10.openSync(file, "r");
1482
+ const fd = fs12.openSync(file, "r");
1404
1483
  try {
1405
1484
  const buf = Buffer.alloc(4096);
1406
- head = buf.subarray(0, fs10.readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
1485
+ head = buf.subarray(0, fs12.readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
1407
1486
  } finally {
1408
- fs10.closeSync(fd);
1487
+ fs12.closeSync(fd);
1409
1488
  }
1410
1489
  } catch {
1411
1490
  return void 0;
@@ -1436,23 +1515,23 @@ function droppedModelNote(harness, dropped) {
1436
1515
  }
1437
1516
 
1438
1517
  // packages/core/dist/worktrees.js
1439
- import * as fs11 from "node:fs";
1440
- import * as path9 from "node:path";
1518
+ import * as fs13 from "node:fs";
1519
+ import * as path11 from "node:path";
1441
1520
  var LANES = ["work", "chore"];
1442
1521
  function worktreePath(id) {
1443
- return path9.join(lobstahHome(), "worktrees", id);
1522
+ return path11.join(lobstahHome(), "worktrees", id);
1444
1523
  }
1445
1524
  function readRecord(file) {
1446
1525
  try {
1447
- const r = JSON.parse(fs11.readFileSync(file, "utf8"));
1526
+ const r = JSON.parse(fs13.readFileSync(file, "utf8"));
1448
1527
  return typeof r.path === "string" ? r : void 0;
1449
1528
  } catch {
1450
1529
  return void 0;
1451
1530
  }
1452
1531
  }
1453
1532
  function ownerOfPath(p) {
1454
- const rel = path9.relative(path9.join(lobstahHome(), "worktrees"), p);
1455
- if (!rel || rel.startsWith("..") || path9.isAbsolute(rel))
1533
+ const rel = path11.relative(path11.join(lobstahHome(), "worktrees"), p);
1534
+ if (!rel || rel.startsWith("..") || path11.isAbsolute(rel))
1456
1535
  return void 0;
1457
1536
  return rel.split(/[\\/]/)[0];
1458
1537
  }
@@ -1465,7 +1544,7 @@ function dispatchWorktree(id, lane) {
1465
1544
  }
1466
1545
  const dirs = laneDirs(l);
1467
1546
  for (const dir of [dirs.active, dirs.done]) {
1468
- const rec = readRecord(path9.join(dir, id, "worktree.json"));
1547
+ const rec = readRecord(path11.join(dir, id, "worktree.json"));
1469
1548
  if (rec) {
1470
1549
  const owner = rec.of ?? ownerOfPath(rec.path) ?? id;
1471
1550
  return { path: rec.path, owner, reused: owner !== id };
@@ -1477,7 +1556,7 @@ function dispatchWorktree(id, lane) {
1477
1556
  function laneOf2(id) {
1478
1557
  for (const l of LANES) {
1479
1558
  const d = laneDirs(l);
1480
- if (fs11.existsSync(path9.join(d.queue, `${id}.json`)) || fs11.existsSync(path9.join(d.active, id)) || fs11.existsSync(path9.join(d.done, id))) {
1559
+ if (fs13.existsSync(path11.join(d.queue, `${id}.json`)) || fs13.existsSync(path11.join(d.active, id)) || fs13.existsSync(path11.join(d.done, id))) {
1481
1560
  return l;
1482
1561
  }
1483
1562
  }
@@ -1497,39 +1576,39 @@ function followUpAncestors(id, lane) {
1497
1576
  function chainWorktree(followUp) {
1498
1577
  for (const id of [followUp, ...followUpAncestors(followUp)]) {
1499
1578
  const wt = dispatchWorktree(id, laneOf2(id));
1500
- if (fs11.existsSync(wt.path))
1579
+ if (fs13.existsSync(wt.path))
1501
1580
  return { ...wt, from: id };
1502
1581
  }
1503
1582
  return void 0;
1504
1583
  }
1505
1584
  function worktreeGitDir(wt) {
1506
- const dotGit = path9.join(wt, ".git");
1585
+ const dotGit = path11.join(wt, ".git");
1507
1586
  try {
1508
- const st = fs11.statSync(dotGit);
1587
+ const st = fs13.statSync(dotGit);
1509
1588
  if (st.isDirectory())
1510
1589
  return dotGit;
1511
- const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs11.readFileSync(dotGit, "utf8"));
1512
- return m ? path9.resolve(wt, m[1]) : void 0;
1590
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs13.readFileSync(dotGit, "utf8"));
1591
+ return m ? path11.resolve(wt, m[1]) : void 0;
1513
1592
  } catch {
1514
1593
  return void 0;
1515
1594
  }
1516
1595
  }
1517
1596
  function worktreeLockFile(wt) {
1518
1597
  const gitDir = worktreeGitDir(wt);
1519
- return gitDir ? path9.join(gitDir, "lobstah.lock") : void 0;
1598
+ return gitDir ? path11.join(gitDir, "lobstah.lock") : void 0;
1520
1599
  }
1521
1600
  function readWorktreeLock(wt) {
1522
1601
  const file = worktreeLockFile(wt);
1523
1602
  if (!file)
1524
1603
  return void 0;
1525
1604
  try {
1526
- return JSON.parse(fs11.readFileSync(file, "utf8"));
1605
+ return JSON.parse(fs13.readFileSync(file, "utf8"));
1527
1606
  } catch {
1528
1607
  return void 0;
1529
1608
  }
1530
1609
  }
1531
1610
  function lockIsLive(lock) {
1532
- return fs11.existsSync(path9.join(laneDirs(lock.lane).active, lock.id));
1611
+ return fs13.existsSync(path11.join(laneDirs(lock.lane).active, lock.id));
1533
1612
  }
1534
1613
  function worktreeHolder(wt) {
1535
1614
  const lock = readWorktreeLock(wt);
@@ -1542,7 +1621,7 @@ function acquireWorktreeLock(wt, id, lane) {
1542
1621
  return { id: "(no git dir)", lane, pid: 0, at: mine.at };
1543
1622
  for (let attempt = 0; attempt < 3; attempt++) {
1544
1623
  try {
1545
- fs11.writeFileSync(file, JSON.stringify(mine, null, 2), { flag: "wx" });
1624
+ fs13.writeFileSync(file, JSON.stringify(mine, null, 2), { flag: "wx" });
1546
1625
  return void 0;
1547
1626
  } catch (err) {
1548
1627
  if (err.code !== "EEXIST")
@@ -1550,7 +1629,7 @@ function acquireWorktreeLock(wt, id, lane) {
1550
1629
  }
1551
1630
  let raw;
1552
1631
  try {
1553
- raw = fs11.readFileSync(file, "utf8");
1632
+ raw = fs13.readFileSync(file, "utf8");
1554
1633
  } catch {
1555
1634
  continue;
1556
1635
  }
@@ -1561,14 +1640,14 @@ function acquireWorktreeLock(wt, id, lane) {
1561
1640
  held2 = void 0;
1562
1641
  }
1563
1642
  if (held2?.id === id) {
1564
- fs11.writeFileSync(file, JSON.stringify(mine, null, 2));
1643
+ fs13.writeFileSync(file, JSON.stringify(mine, null, 2));
1565
1644
  return void 0;
1566
1645
  }
1567
1646
  if (held2 && lockIsLive(held2))
1568
1647
  return held2;
1569
1648
  try {
1570
- if (fs11.readFileSync(file, "utf8") === raw)
1571
- fs11.rmSync(file, { force: true });
1649
+ if (fs13.readFileSync(file, "utf8") === raw)
1650
+ fs13.rmSync(file, { force: true });
1572
1651
  } catch {
1573
1652
  }
1574
1653
  }
@@ -1580,7 +1659,7 @@ function releaseWorktreeLock(wt, id) {
1580
1659
  if (!file)
1581
1660
  return;
1582
1661
  if (readWorktreeLock(wt)?.id === id)
1583
- fs11.rmSync(file, { force: true });
1662
+ fs13.rmSync(file, { force: true });
1584
1663
  }
1585
1664
 
1586
1665
  // packages/core/dist/slots.js
@@ -1589,8 +1668,8 @@ function isTrapCatch(id, lane) {
1589
1668
  }
1590
1669
 
1591
1670
  // packages/runner/src/run.ts
1592
- import * as fs15 from "node:fs";
1593
- import * as path13 from "node:path";
1671
+ import * as fs17 from "node:fs";
1672
+ import * as path15 from "node:path";
1594
1673
  import { execFileSync } from "node:child_process";
1595
1674
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1596
1675
 
@@ -1884,25 +1963,25 @@ function createClaudeAdapter() {
1884
1963
  }
1885
1964
 
1886
1965
  // packages/adapters/dist/codex.js
1887
- import * as fs12 from "node:fs";
1966
+ import * as fs14 from "node:fs";
1888
1967
  import * as os4 from "node:os";
1889
- import * as path10 from "node:path";
1968
+ import * as path12 from "node:path";
1890
1969
  import { spawn as spawn2 } from "node:child_process";
1891
1970
  function isolatedCodexHome() {
1892
- const dir = path10.join(lobstahHome(), "codex-home");
1893
- fs12.mkdirSync(dir, { recursive: true });
1894
- const auth = path10.join(dir, "auth.json");
1895
- const realAuth = path10.join(os4.homedir(), ".codex", "auth.json");
1896
- if (!fs12.existsSync(auth) && fs12.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)) {
1897
1976
  try {
1898
- fs12.symlinkSync(realAuth, auth);
1977
+ fs14.symlinkSync(realAuth, auth);
1899
1978
  } catch {
1900
- fs12.copyFileSync(realAuth, auth);
1979
+ fs14.copyFileSync(realAuth, auth);
1901
1980
  }
1902
1981
  }
1903
- const cfg = path10.join(dir, "config.toml");
1904
- if (!fs12.existsSync(cfg)) {
1905
- fs12.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");
1906
1985
  }
1907
1986
  return dir;
1908
1987
  }
@@ -2103,8 +2182,8 @@ function loadAdapter(name) {
2103
2182
  }
2104
2183
 
2105
2184
  // packages/worktree/dist/index.js
2106
- import * as fs13 from "node:fs";
2107
- import * as path11 from "node:path";
2185
+ import * as fs15 from "node:fs";
2186
+ import * as path13 from "node:path";
2108
2187
  import { createHash } from "node:crypto";
2109
2188
  import { exec, execFile } from "node:child_process";
2110
2189
  import { promisify } from "node:util";
@@ -2161,10 +2240,10 @@ ${gitStderr(err)}`, { cause: err });
2161
2240
  }
2162
2241
  async function allocate(repo, id) {
2163
2242
  const dir = worktreePath(id);
2164
- if (fs13.existsSync(dir)) {
2243
+ if (fs15.existsSync(dir)) {
2165
2244
  throw new Error(`worktree for ${id} already exists at ${dir} \u2014 never allocate a second`);
2166
2245
  }
2167
- if (!fs13.existsSync(repo.path)) {
2246
+ if (!fs15.existsSync(repo.path)) {
2168
2247
  if (!repo.origin)
2169
2248
  throw new Error(`repo path ${repo.path} missing and no origin configured`);
2170
2249
  await run("git", ["clone", repo.origin, repo.path], { env: process.env });
@@ -2200,38 +2279,40 @@ function setupHash(repo, dir) {
2200
2279
  const h = createHash("sha256");
2201
2280
  h.update(JSON.stringify(repo.setup ?? []));
2202
2281
  for (const name of LOCKFILES) {
2203
- const file = path11.join(dir, name);
2204
- if (!fs13.existsSync(file))
2282
+ const file = path13.join(dir, name);
2283
+ if (!fs15.existsSync(file))
2205
2284
  continue;
2206
2285
  h.update(`\0${name}\0`);
2207
- h.update(fs13.readFileSync(file));
2286
+ h.update(fs15.readFileSync(file));
2208
2287
  }
2209
2288
  return h.digest("hex");
2210
2289
  }
2211
2290
  function setupRecordFile(dir) {
2212
2291
  const gitDir = worktreeGitDir(dir);
2213
- return gitDir ? path11.join(gitDir, "lobstah-setup.json") : void 0;
2292
+ return gitDir ? path13.join(gitDir, "lobstah-setup.json") : void 0;
2214
2293
  }
2215
2294
  function recordSetup(repo, dir) {
2216
2295
  const file = setupRecordFile(dir);
2217
2296
  if (file)
2218
- fs13.writeFileSync(file, JSON.stringify({ hash: setupHash(repo, dir), at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
2297
+ fs15.writeFileSync(file, JSON.stringify({ hash: setupHash(repo, dir), at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
2219
2298
  }
2220
2299
  function recordedSetupHash(dir) {
2221
2300
  const file = setupRecordFile(dir);
2222
2301
  try {
2223
- return file ? JSON.parse(fs13.readFileSync(file, "utf8")).hash : void 0;
2302
+ return file ? JSON.parse(fs15.readFileSync(file, "utf8")).hash : void 0;
2224
2303
  } catch {
2225
2304
  return void 0;
2226
2305
  }
2227
2306
  }
2228
2307
  var short = (s) => s.slice(0, 8);
2229
2308
  function realpath(p) {
2309
+ let r;
2230
2310
  try {
2231
- return fs13.realpathSync(p);
2311
+ r = fs15.realpathSync.native(p);
2232
2312
  } catch {
2233
- return path11.resolve(p);
2313
+ r = path13.resolve(p);
2234
2314
  }
2315
+ return process.platform === "win32" ? r.toLowerCase() : r;
2235
2316
  }
2236
2317
  function norm(p) {
2237
2318
  return p.replace(/^"(.*)"$/, "$1").replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
@@ -2271,8 +2352,8 @@ async function chooseWorktree(input) {
2271
2352
  for (const t of listTraps()) {
2272
2353
  if (typeof t.worktree !== "string")
2273
2354
  continue;
2274
- const rel = path11.relative(here, realpath(t.worktree));
2275
- if (!rel.startsWith("..") && !path11.isAbsolute(rel))
2355
+ const rel = path13.relative(here, realpath(t.worktree));
2356
+ if (!rel.startsWith("..") && !path13.isAbsolute(rel))
2276
2357
  return { reuse: false, reason: "origin worktree is a trap\u2019s" };
2277
2358
  }
2278
2359
  for (const l of ["work", "chore"]) {
@@ -2325,9 +2406,11 @@ function buildPrompt(brief, opts) {
2325
2406
  reporting,
2326
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.`,
2327
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.`,
2328
- `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.`,
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.`,
2329
2410
  `--- BRIEF ---`,
2330
- 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
2331
2414
  ];
2332
2415
  if (opts.attachments?.length) parts.push(attachmentBlock(opts.attachments));
2333
2416
  if (opts.nudge) parts.push(`--- SUPERVISOR NOTE ---`, opts.nudge);
@@ -2607,8 +2690,8 @@ function startWallClock(opts) {
2607
2690
  import { execFile as execFile2 } from "node:child_process";
2608
2691
  import { promisify as promisify2 } from "node:util";
2609
2692
  import { fileURLToPath } from "node:url";
2610
- import * as fs14 from "node:fs";
2611
- import * as path12 from "node:path";
2693
+ import * as fs16 from "node:fs";
2694
+ import * as path14 from "node:path";
2612
2695
  var exec2 = promisify2(execFile2);
2613
2696
  function firstLine(err) {
2614
2697
  const stderr = err && typeof err === "object" && "stderr" in err ? String(err.stderr).trim() : "";
@@ -2623,9 +2706,9 @@ function checkpointAllowed(file) {
2623
2706
  return true;
2624
2707
  }
2625
2708
  function keepRemote(opts) {
2626
- const { id, lane, cwd, trunk, policy } = opts;
2709
+ const { id, lane, cwd, trunk, policy, existingPrUrl } = opts;
2627
2710
  const command = async (bin, args, timeout = 12e4) => {
2628
- const file = bin === "gh" ? resolveOnPath("gh") ?? bin : bin;
2711
+ const file = bin === "gh" || bin === "lobstah" ? resolveOnPath(bin) ?? bin : bin;
2629
2712
  return (await exec2(file, args, {
2630
2713
  cwd,
2631
2714
  encoding: "utf8",
@@ -2636,6 +2719,14 @@ function keepRemote(opts) {
2636
2719
  };
2637
2720
  const git2 = (...args) => command("git", args);
2638
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
+ };
2639
2730
  let lastSeenHead;
2640
2731
  let pushedHead;
2641
2732
  let busy;
@@ -2645,6 +2736,10 @@ function keepRemote(opts) {
2645
2736
  let pushProblem;
2646
2737
  let draftProblem;
2647
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;
2648
2743
  const branchName = async () => {
2649
2744
  const branch = await git2("symbolic-ref", "--quiet", "--short", "HEAD").catch(() => "");
2650
2745
  if (!branch || branch === trunk || branch === `origin/${trunk}`) return void 0;
@@ -2683,12 +2778,11 @@ function keepRemote(opts) {
2683
2778
  opened = true;
2684
2779
  draftProblem = void 0;
2685
2780
  mergeEvidence(id, lane, { prUrl: match[0] });
2686
- const bin = path12.resolve(path12.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "bin", "lobstah");
2687
- await command(fs14.existsSync(bin) ? bin : "lobstah", ["watch", "add", match[0], "--for", id], 15e3).catch(() => {
2781
+ await registerWatch(match[0]).catch(() => {
2688
2782
  });
2689
2783
  };
2690
2784
  const check = async () => {
2691
- if (!policy.pushEarly || trapClaimed()) return;
2785
+ if (!policy.pushEarly || trapClaimed() || existingPrUrl) return;
2692
2786
  const branch = await branchName();
2693
2787
  if (!branch) return;
2694
2788
  const head = await git2("rev-parse", "HEAD");
@@ -2727,6 +2821,7 @@ function keepRemote(opts) {
2727
2821
  await busy;
2728
2822
  await check().catch((err) => note(`final push unavailable: ${firstLine(err)}`));
2729
2823
  await opening;
2824
+ await watching;
2730
2825
  },
2731
2826
  saveBeforeStop: async () => {
2732
2827
  stopped = true;
@@ -2757,9 +2852,10 @@ function keepRemote(opts) {
2757
2852
  }
2758
2853
  await check().catch((err) => note(`final push unavailable: ${firstLine(err)}`));
2759
2854
  await opening;
2855
+ await watching;
2760
2856
  const head = await git2("rev-parse", "--short", "HEAD").catch(() => "unknown");
2761
2857
  const pr = readEvidence(id, lane).prUrl;
2762
- const message = `${saved}; ${branch}@${head}${pr ? `; draft PR ${pr}` : ""}${pushProblem ? `; ${pushProblem}` : ""}${draftProblem ? `; ${draftProblem}` : ""}`;
2858
+ const message = `${saved}; ${branch}@${head}${pr ? `; ${existingPrUrl ? "existing" : "draft"} PR ${pr}` : ""}${pushProblem ? `; ${pushProblem}` : ""}${draftProblem ? `; ${draftProblem}` : ""}`;
2763
2859
  note(message);
2764
2860
  return message;
2765
2861
  }
@@ -2776,23 +2872,23 @@ function coldNote(cold, cwd, trunk) {
2776
2872
  }
2777
2873
  async function main(activeDir, lane, seams = {}) {
2778
2874
  const deps = { ...defaultDeps, ...seams };
2779
- const id = path13.basename(activeDir);
2875
+ const id = path15.basename(activeDir);
2780
2876
  if (isTrapCatch(id, lane)) throw new Error(`refusing headless runner for trap catch ${id}`);
2781
2877
  const status = (verb, note) => appendStatus(id, lane, verb, note);
2782
2878
  const cfg = loadConfig();
2783
- const descriptor = JSON.parse(fs15.readFileSync(path13.join(activeDir, "descriptor.json"), "utf8"));
2879
+ const descriptor = JSON.parse(fs17.readFileSync(path15.join(activeDir, "descriptor.json"), "utf8"));
2784
2880
  const resolved = resolveDispatch(descriptor, cfg);
2785
2881
  const repo = cfg.repos[descriptor.repo];
2786
- const briefFile = path13.join(activeDir, "brief.md");
2787
- if (!fs15.existsSync(briefFile)) fs15.writeFileSync(briefFile, descriptor.brief);
2788
- const brief = fs15.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");
2789
2885
  const attempts = Number(process.env.LOBSTAH_ATTEMPTS ?? "1");
2790
2886
  const runnerInfo = {
2791
2887
  pid: process.pid,
2792
2888
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2793
2889
  attempts
2794
2890
  };
2795
- fs15.writeFileSync(path13.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
2891
+ fs17.writeFileSync(path15.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
2796
2892
  const plan = planStart({
2797
2893
  id,
2798
2894
  lane,
@@ -2801,13 +2897,13 @@ async function main(activeDir, lane, seams = {}) {
2801
2897
  resolvedHarness: resolved.harness,
2802
2898
  envResume: process.env.LOBSTAH_RESUME
2803
2899
  });
2804
- const wtFile = path13.join(activeDir, "worktree.json");
2900
+ const wtFile = path15.join(activeDir, "worktree.json");
2805
2901
  let recorded;
2806
2902
  let choice;
2807
- if (fs15.existsSync(wtFile)) {
2808
- recorded = JSON.parse(fs15.readFileSync(wtFile, "utf8")).path;
2809
- if (!fs15.existsSync(recorded)) throw new Error(`recorded worktree ${recorded} is gone`);
2810
- } else if (fs15.existsSync(worktreePath(id))) {
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))) {
2811
2907
  throw new Error(`unrecorded worktree already exists for ${id} \u2014 refusing to proceed`);
2812
2908
  } else if (descriptor.followUp && cfg.limits.reuseWorktree !== false) {
2813
2909
  choice = await deps.chooseWorktree({ id, lane, repoKey: descriptor.repo, repo, followUp: descriptor.followUp });
@@ -2841,14 +2937,16 @@ async function main(activeDir, lane, seams = {}) {
2841
2937
  type: "runner",
2842
2938
  data: { worktree: cwd, worktreeOf: choice.owner, setupRan }
2843
2939
  });
2844
- fs15.writeFileSync(wtFile, JSON.stringify({ path: cwd, of: choice.owner }, null, 2));
2940
+ fs17.writeFileSync(wtFile, JSON.stringify({ path: cwd, of: choice.owner }, null, 2));
2845
2941
  } else {
2846
2942
  cwd = await deps.allocate(repo, id);
2847
2943
  acquireWorktreeLock(cwd, id, lane);
2848
- fs15.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
2944
+ fs17.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
2849
2945
  }
2850
- const worktreeOf = JSON.parse(fs15.readFileSync(wtFile, "utf8")).of;
2946
+ const worktreeOf = JSON.parse(fs17.readFileSync(wtFile, "utf8")).of;
2851
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 });
2852
2950
  const remotePolicy = {
2853
2951
  pushEarly: repo.pushEarly ?? cfg.limits.pushEarly,
2854
2952
  draftPr: repo.draftPr ?? cfg.limits.draftPr,
@@ -2861,19 +2959,20 @@ async function main(activeDir, lane, seams = {}) {
2861
2959
  cwd,
2862
2960
  trunk: repo.trunk,
2863
2961
  title: descriptor.brief.split("\n")[0]?.trim() || `Lobstah dispatch ${short3(id)}`,
2864
- policy: remotePolicy
2962
+ policy: remotePolicy,
2963
+ existingPrUrl: existingPr?.url
2865
2964
  });
2866
2965
  const envNudge = process.env.LOBSTAH_NUDGE;
2867
2966
  const planNudge = plan.cold && !envNudge ? coldNote(plan.cold, cwd, repo.trunk) : void 0;
2868
- const promptWith = (nudge) => buildPrompt(brief, { id, nudge, attachments: descriptor.attachments });
2869
- const binDir = path13.resolve(path13.dirname(fileURLToPath2(import.meta.url)), "..", "..", "..", "bin");
2870
- const workerPath = fs15.existsSync(path13.join(binDir, "lobstah")) ? `${binDir}${path13.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;
2871
2970
  let wallClockHit = false;
2872
2971
  let current;
2873
- const wallFile = path13.join(activeDir, "wallclock.json");
2972
+ const wallFile = path15.join(activeDir, "wallclock.json");
2874
2973
  const wallState = (() => {
2875
2974
  try {
2876
- return JSON.parse(fs15.readFileSync(wallFile, "utf8"));
2975
+ return JSON.parse(fs17.readFileSync(wallFile, "utf8"));
2877
2976
  } catch {
2878
2977
  return { elapsedMs: 0, windowMs: resolved.limits.wallClockSecs * 1e3 };
2879
2978
  }
@@ -2892,8 +2991,8 @@ async function main(activeDir, lane, seams = {}) {
2892
2991
  ) * 1e3;
2893
2992
  const persistWall = (elapsedMs, windowMs) => {
2894
2993
  const tmp = `${wallFile}.tmp-${process.pid}`;
2895
- fs15.writeFileSync(tmp, JSON.stringify({ ...wallState, elapsedMs, windowMs }));
2896
- fs15.renameSync(tmp, wallFile);
2994
+ fs17.writeFileSync(tmp, JSON.stringify({ ...wallState, elapsedMs, windowMs }));
2995
+ fs17.renameSync(tmp, wallFile);
2897
2996
  };
2898
2997
  const madeProgress = () => {
2899
2998
  const activity = readActivity(id, lane);
@@ -3013,10 +3112,10 @@ async function main(activeDir, lane, seams = {}) {
3013
3112
  function runRunner(activeDirArg2, laneArg2) {
3014
3113
  const lane = laneArg2 === "chore" ? "chore" : "work";
3015
3114
  main(activeDirArg2, lane).catch((err) => {
3016
- const id = path14.basename(activeDirArg2);
3115
+ const id = path16.basename(activeDirArg2);
3017
3116
  try {
3018
3117
  appendStatus(id, lane, "failed", String(err instanceof Error ? err.message : err).slice(0, 500));
3019
- fs16.renameSync(activeDirArg2, path14.join(laneDirs(lane).done, id));
3118
+ fs18.renameSync(activeDirArg2, path16.join(laneDirs(lane).done, id));
3020
3119
  } catch {
3021
3120
  }
3022
3121
  process.exitCode = 1;