lobstah 0.7.2 → 0.7.3

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 fs21 from "node:fs";
3
- import * as path18 from "node:path";
2
+ import * as fs27 from "node:fs";
3
+ import * as path23 from "node:path";
4
4
 
5
5
  // packages/core/dist/types.js
6
6
  var VERBS = ["working", "needs-decision", "blocked", "paused", "done", "failed"];
@@ -9,8 +9,8 @@ var WAITING_ON = ["review", "pr", "deploy", "person", "external"];
9
9
  var WAITING_ON_VERBS = ["paused", "needs-decision", "blocked"];
10
10
 
11
11
  // packages/core/dist/stats.js
12
- import * as fs12 from "node:fs";
13
- import * as path10 from "node:path";
12
+ import * as fs17 from "node:fs";
13
+ import * as path14 from "node:path";
14
14
 
15
15
  // packages/core/dist/paths.js
16
16
  import * as os from "node:os";
@@ -149,8 +149,8 @@ function sanitizeWorker(value) {
149
149
  }
150
150
 
151
151
  // packages/core/dist/soak.js
152
- import * as fs11 from "node:fs";
153
- import * as path9 from "node:path";
152
+ import * as fs16 from "node:fs";
153
+ import * as path13 from "node:path";
154
154
 
155
155
  // packages/core/dist/queue.js
156
156
  import * as fs4 from "node:fs";
@@ -175,8 +175,8 @@ var SECRET_PATTERNS = [
175
175
  function looksSecret(word) {
176
176
  if (SECRET_PATTERNS.some((re) => re.test(word)))
177
177
  return true;
178
- for (const run2 of word.match(/[A-Za-z0-9+/_=-]{24,}/g) ?? []) {
179
- if (/[0-9]/.test(run2) && /[A-Za-z]/.test(run2))
178
+ for (const run3 of word.match(/[A-Za-z0-9+/_=-]{24,}/g) ?? []) {
179
+ if (/[0-9]/.test(run3) && /[A-Za-z]/.test(run3))
180
180
  return true;
181
181
  }
182
182
  return false;
@@ -235,9 +235,9 @@ function toolTarget(input) {
235
235
  return void 0;
236
236
  const i = input;
237
237
  const str = (k) => typeof i[k] === "string" && i[k].length > 0 ? i[k] : void 0;
238
- const file = str("file_path") ?? str("notebook_path") ?? str("path");
239
- if (file)
240
- return file;
238
+ const file2 = str("file_path") ?? str("notebook_path") ?? str("path");
239
+ if (file2)
240
+ return file2;
241
241
  const command = str("command") ?? (Array.isArray(i.command) ? i.command.map(String).join(" ") : void 0);
242
242
  if (command)
243
243
  return commandWord(command);
@@ -282,10 +282,10 @@ function activityFromEvent(ev, root) {
282
282
  return void 0;
283
283
  }
284
284
  }
285
- function atomicWrite(file, content) {
286
- const tmp = `${file}.tmp-${process.pid}`;
285
+ function atomicWrite(file2, content) {
286
+ const tmp = `${file2}.tmp-${process.pid}`;
287
287
  fs2.writeFileSync(tmp, content);
288
- fs2.renameSync(tmp, file);
288
+ fs2.renameSync(tmp, file2);
289
289
  }
290
290
  function writeActivity(id, lane, a) {
291
291
  const safe = { at: a.at, kind: a.kind, summary: redactSummary(a.summary) };
@@ -436,10 +436,10 @@ function appendStatus(id, lane, verb, note, at, waiting, reported = false) {
436
436
  return entry;
437
437
  }
438
438
  function readStatusLog(id, lane) {
439
- const file = statusPath(id, lane);
440
- if (!fs3.existsSync(file))
439
+ const file2 = statusPath(id, lane);
440
+ if (!fs3.existsSync(file2))
441
441
  return [];
442
- return fs3.readFileSync(file, "utf8").split("\n").filter(Boolean).flatMap((line) => {
442
+ return fs3.readFileSync(file2, "utf8").split("\n").filter(Boolean).flatMap((line) => {
443
443
  try {
444
444
  const parsed = JSON.parse(line);
445
445
  return isVerb(parsed.verb) ? [parsed] : [];
@@ -453,15 +453,21 @@ function pausedWaiting(entry) {
453
453
  }
454
454
 
455
455
  // packages/core/dist/queue.js
456
+ function requeue(id, lane) {
457
+ const dirs = laneDirs(lane);
458
+ const activeDir = path3.join(dirs.active, id);
459
+ fs4.renameSync(path3.join(activeDir, "descriptor.json"), path3.join(dirs.queue, `${id}.json`));
460
+ fs4.rmSync(activeDir, { recursive: true, force: true });
461
+ }
456
462
  function storedDescriptor(id, lane) {
457
463
  const dirs = laneDirs(lane);
458
- for (const file of [
464
+ for (const file2 of [
459
465
  path3.join(dirs.queue, `${id}.json`),
460
466
  path3.join(dirs.active, id, "descriptor.json"),
461
467
  path3.join(dirs.done, id, "descriptor.json")
462
468
  ]) {
463
469
  try {
464
- return JSON.parse(fs4.readFileSync(file, "utf8"));
470
+ return JSON.parse(fs4.readFileSync(file2, "utf8"));
465
471
  } catch {
466
472
  }
467
473
  }
@@ -488,10 +494,10 @@ function readEvidence(id, lane) {
488
494
  }
489
495
  }
490
496
  function mergeEvidence(id, lane, patch) {
491
- const file = evidencePath(id, lane);
492
- const tmp = `${file}.tmp-${process.pid}`;
497
+ const file2 = evidencePath(id, lane);
498
+ const tmp = `${file2}.tmp-${process.pid}`;
493
499
  fs5.writeFileSync(tmp, JSON.stringify({ ...readEvidence(id, lane), ...patch }, null, 2));
494
- fs5.renameSync(tmp, file);
500
+ fs5.renameSync(tmp, file2);
495
501
  }
496
502
 
497
503
  // packages/core/dist/pushes.js
@@ -591,6 +597,10 @@ function recordPush(id, lane, branches, at = (/* @__PURE__ */ new Date()).toISOS
591
597
  mergeEvidence(id, lane, { pushes });
592
598
  }
593
599
 
600
+ // packages/core/dist/notices.js
601
+ import fs12 from "node:fs";
602
+ import * as path10 from "node:path";
603
+
594
604
  // packages/core/dist/config.js
595
605
  import * as fs7 from "node:fs";
596
606
  import * as os2 from "node:os";
@@ -1242,6 +1252,30 @@ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
1242
1252
  }
1243
1253
 
1244
1254
  // packages/core/dist/config.js
1255
+ var POOL_NAME = /^[A-Za-z0-9_-]+$/;
1256
+ function parsePools(raw, repos) {
1257
+ const out = {};
1258
+ if (raw === void 0)
1259
+ return out;
1260
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
1261
+ throw new Error(`[pools] must be a table of pools in ${configPath()}`);
1262
+ for (const [name, p] of Object.entries(raw)) {
1263
+ const where = `[pools.${name}] in ${configPath()}`;
1264
+ if (!POOL_NAME.test(name))
1265
+ throw new Error(`${where}: a pool name may use letters, digits, "-" and "_" only`);
1266
+ const repo = String(p?.repo ?? "");
1267
+ if (!repos[repo])
1268
+ throw new Error(`${where}: repo "${repo}" is not a configured [repos.<key>]`);
1269
+ const size = p.size ?? 1;
1270
+ if (typeof size !== "number" || !Number.isInteger(size) || size < 1)
1271
+ throw new Error(`${where}: size must be a positive integer`);
1272
+ const overflow = p.overflow ?? "headless";
1273
+ if (overflow !== "headless" && overflow !== "queue")
1274
+ throw new Error(`${where}: overflow must be "headless" or "queue"`);
1275
+ out[name] = { repo, size, overflow };
1276
+ }
1277
+ return out;
1278
+ }
1245
1279
  var BRIEF_KINDS = ["conflict", "checks", "review", "ciFix", "rebase"];
1246
1280
  function parseBriefHooks(raw) {
1247
1281
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
@@ -1316,8 +1350,8 @@ function expandHome(p) {
1316
1350
  return p.startsWith("~/") ? path5.join(os2.homedir(), p.slice(2)) : p;
1317
1351
  }
1318
1352
  function loadConfig() {
1319
- const file = configPath();
1320
- const raw = fs7.existsSync(file) ? parse(fs7.readFileSync(file, "utf8")) : {};
1353
+ const file2 = configPath();
1354
+ const raw = fs7.existsSync(file2) ? parse(fs7.readFileSync(file2, "utf8")) : {};
1321
1355
  const watch = { ...DEFAULT_WATCH, ...raw.watch ?? {} };
1322
1356
  if (typeof watch.repairCooldownSecs !== "number" || !Number.isFinite(watch.repairCooldownSecs) || watch.repairCooldownSecs < 0) {
1323
1357
  throw new Error(`[watch].repairCooldownSecs must be a non-negative number in ${configPath()}`);
@@ -1342,7 +1376,8 @@ function loadConfig() {
1342
1376
  draftPr: r.draftPr === void 0 ? void 0 : Boolean(r.draftPr),
1343
1377
  checkpointOnStop: r.checkpointOnStop === void 0 ? void 0 : Boolean(r.checkpointOnStop),
1344
1378
  humanGateChecks: Array.isArray(r.humanGateChecks) ? r.humanGateChecks.map(String).filter(Boolean) : void 0,
1345
- briefHooks: parseBriefHooks(r.briefHooks)
1379
+ briefHooks: parseBriefHooks(r.briefHooks),
1380
+ poolKeep: Array.isArray(r.poolKeep) ? r.poolKeep.map(String).filter(Boolean) : void 0
1346
1381
  };
1347
1382
  }
1348
1383
  const groundsRaw = raw.grounds ?? {};
@@ -1359,6 +1394,7 @@ function loadConfig() {
1359
1394
  glass: { ...DEFAULT_GLASS, ...raw.glass ?? {} },
1360
1395
  watch,
1361
1396
  grounds,
1397
+ pools: parsePools(raw.pools, repos),
1362
1398
  notifyCommand: raw.notifyCommand ? String(raw.notifyCommand) : void 0,
1363
1399
  notifyVerbs: Array.isArray(raw.notifyVerbs) ? raw.notifyVerbs.map(String) : void 0,
1364
1400
  remindSecs: raw.remindSecs !== void 0 ? Number(raw.remindSecs) : void 0,
@@ -1406,6 +1442,114 @@ function firstMeaningfulLine(...outputs) {
1406
1442
  return void 0;
1407
1443
  }
1408
1444
 
1445
+ // packages/core/dist/pr-poll.js
1446
+ import * as fs11 from "node:fs";
1447
+ import * as path9 from "node:path";
1448
+
1449
+ // packages/core/dist/prs.js
1450
+ import * as fs8 from "node:fs";
1451
+ import * as path6 from "node:path";
1452
+ function prsDir() {
1453
+ return path6.join(lobstahHome(), "prs");
1454
+ }
1455
+ function prRecordFile(key) {
1456
+ const ref = parsePrRef(key);
1457
+ if (!ref)
1458
+ throw new Error(`not a PR key: ${key}`);
1459
+ const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "_");
1460
+ return path6.join(prsDir(), `${safe(ref.owner)}__${safe(ref.repo)}__${ref.number}.json`);
1461
+ }
1462
+ function readPr(key) {
1463
+ try {
1464
+ return JSON.parse(fs8.readFileSync(prRecordFile(key), "utf8"));
1465
+ } catch {
1466
+ return void 0;
1467
+ }
1468
+ }
1469
+ function readPrs() {
1470
+ let files;
1471
+ try {
1472
+ files = fs8.readdirSync(prsDir()).filter((f) => f.endsWith(".json"));
1473
+ } catch {
1474
+ return [];
1475
+ }
1476
+ return files.flatMap((f) => {
1477
+ try {
1478
+ const r = JSON.parse(fs8.readFileSync(path6.join(prsDir(), f), "utf8"));
1479
+ return typeof r.key === "string" && typeof r.url === "string" ? [r] : [];
1480
+ } catch {
1481
+ return [];
1482
+ }
1483
+ });
1484
+ }
1485
+
1486
+ // packages/core/dist/watch.js
1487
+ import * as fs10 from "node:fs";
1488
+ import * as path8 from "node:path";
1489
+
1490
+ // packages/core/dist/github-budget.js
1491
+ import * as fs9 from "node:fs";
1492
+ import * as path7 from "node:path";
1493
+ var file = () => path7.join(lobstahHome(), "github-budget.json");
1494
+ function read() {
1495
+ try {
1496
+ return JSON.parse(fs9.readFileSync(file(), "utf8"));
1497
+ } catch {
1498
+ return { resources: {} };
1499
+ }
1500
+ }
1501
+ function githubBlockedUntil(now2 = Date.now()) {
1502
+ const s = read();
1503
+ const zeros = Object.values(s.resources).filter((b) => b.remaining === 0 && b.reset > now2).map((b) => b.reset);
1504
+ const until2 = Math.max(s.retryAt ?? 0, ...zeros);
1505
+ return until2 > now2 ? until2 : void 0;
1506
+ }
1507
+
1508
+ // packages/core/dist/watch.js
1509
+ function watchesDir() {
1510
+ return path8.join(lobstahHome(), "watches");
1511
+ }
1512
+ function slug(key) {
1513
+ return key.replace(/[^A-Za-z0-9._-]+/g, "-");
1514
+ }
1515
+ function watchPath(key) {
1516
+ return path8.join(watchesDir(), `${slug(key)}.json`);
1517
+ }
1518
+ function readWatch(key) {
1519
+ try {
1520
+ return JSON.parse(fs10.readFileSync(watchPath(key), "utf8"));
1521
+ } catch {
1522
+ return void 0;
1523
+ }
1524
+ }
1525
+
1526
+ // packages/core/dist/pr-poll.js
1527
+ var cycleFile = (repo) => path9.join(lobstahHome(), "pr-polls", `${repo.replace(/[^\w.-]/g, "_")}.json`);
1528
+ var readCycle = (repo) => {
1529
+ try {
1530
+ return JSON.parse(fs11.readFileSync(cycleFile(repo), "utf8"));
1531
+ } catch {
1532
+ return void 0;
1533
+ }
1534
+ };
1535
+ var repoOf = (ref) => `${ref.owner}/${ref.repo}`;
1536
+ function cachedPrView(ref) {
1537
+ if (process.env.LOBSTAH_PR_BATCH !== "1")
1538
+ return void 0;
1539
+ const blocked = githubBlockedUntil();
1540
+ if (blocked)
1541
+ throw new Error(`GitHub rate limit reached; retry after ${new Date(blocked).toISOString()}`);
1542
+ const cycle = readCycle(repoOf(ref));
1543
+ if (!cycle)
1544
+ return void 0;
1545
+ if (cycle.error)
1546
+ throw new Error(cycle.error);
1547
+ const cached = cycle.snapshots[ref.key];
1548
+ if (cached?.error)
1549
+ throw new Error(cached.error);
1550
+ return cached?.view;
1551
+ }
1552
+
1409
1553
  // packages/core/dist/pr.js
1410
1554
  function parsePrRef(s) {
1411
1555
  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());
@@ -1419,6 +1563,9 @@ var PR_VIEW_FIELDS = "title,state,isDraft,isCrossRepository,headRefOid,baseRefNa
1419
1563
  var PR_VIEW_FIELDS_NO_CHECKS = PR_VIEW_FIELDS.split(",").filter((f) => f !== "statusCheckRollup").join(",");
1420
1564
  var FORBIDDEN = /resource not accessible by (integration|personal access token)/i;
1421
1565
  function ghPrView(ref) {
1566
+ return cachedPrView(ref) ?? ghPrViewDirect(ref);
1567
+ }
1568
+ function ghPrViewDirect(ref) {
1422
1569
  const view1 = (fields2) => spawnSync("gh", ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", fields2], {
1423
1570
  encoding: "utf8",
1424
1571
  timeout: 6e4
@@ -1471,41 +1618,40 @@ function parseUnresolvedThreads(stdout) {
1471
1618
  }
1472
1619
  }
1473
1620
 
1474
- // packages/core/dist/prs.js
1475
- import * as fs8 from "node:fs";
1476
- import * as path6 from "node:path";
1477
- function prsDir() {
1478
- return path6.join(lobstahHome(), "prs");
1479
- }
1480
- function prRecordFile(key) {
1481
- const ref = parsePrRef(key);
1482
- if (!ref)
1483
- throw new Error(`not a PR key: ${key}`);
1484
- const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "_");
1485
- return path6.join(prsDir(), `${safe(ref.owner)}__${safe(ref.repo)}__${ref.number}.json`);
1486
- }
1487
- function readPr(key) {
1488
- try {
1489
- return JSON.parse(fs8.readFileSync(prRecordFile(key), "utf8"));
1490
- } catch {
1491
- return void 0;
1492
- }
1493
- }
1494
- function readPrs() {
1495
- let files;
1496
- try {
1497
- files = fs8.readdirSync(prsDir()).filter((f) => f.endsWith(".json"));
1498
- } catch {
1499
- return [];
1500
- }
1501
- return files.flatMap((f) => {
1621
+ // packages/core/dist/notices.js
1622
+ function noticesDir() {
1623
+ return path10.join(lobstahHome(), "notices");
1624
+ }
1625
+ var counter = 0;
1626
+ function postNotice(n) {
1627
+ const dir = noticesDir();
1628
+ fs12.mkdirSync(dir, { recursive: true });
1629
+ if (n.dedupeKey) {
1630
+ const marker = path10.join(dir, `.dedupe-${n.dedupeKey.replace(/[^A-Za-z0-9_-]/g, "_")}`);
1502
1631
  try {
1503
- const r = JSON.parse(fs8.readFileSync(path6.join(prsDir(), f), "utf8"));
1504
- return typeof r.key === "string" && typeof r.url === "string" ? [r] : [];
1632
+ fs12.writeFileSync(marker, "", { flag: "wx" });
1505
1633
  } catch {
1506
- return [];
1634
+ return void 0;
1507
1635
  }
1508
- });
1636
+ }
1637
+ const seq = `${String(Date.now()).padStart(15, "0")}-${process.pid}-${String(counter++).padStart(8, "0")}`;
1638
+ const notice = {
1639
+ seq,
1640
+ kind: n.kind,
1641
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1642
+ text: n.text,
1643
+ refId: n.refId,
1644
+ ...n.url ? { url: n.url } : {},
1645
+ repo: n.repo,
1646
+ by: n.by,
1647
+ ...n.quiet ? { quiet: true } : {},
1648
+ ...n.stateHash ? { stateHash: n.stateHash } : {}
1649
+ };
1650
+ const file2 = path10.join(dir, `${seq}.json`);
1651
+ const tmp = `${file2}.tmp`;
1652
+ fs12.writeFileSync(tmp, JSON.stringify(notice, null, 2));
1653
+ fs12.renameSync(tmp, file2);
1654
+ return notice;
1509
1655
  }
1510
1656
 
1511
1657
  // packages/core/dist/attachments.js
@@ -1520,23 +1666,23 @@ function attachmentBlock(attachments) {
1520
1666
  }
1521
1667
 
1522
1668
  // packages/core/dist/worktrees.js
1523
- import * as fs9 from "node:fs";
1524
- import * as path7 from "node:path";
1669
+ import * as fs13 from "node:fs";
1670
+ import * as path11 from "node:path";
1525
1671
  var LANES = ["work", "chore"];
1526
1672
  function worktreePath(id) {
1527
- return path7.join(lobstahHome(), "worktrees", id);
1673
+ return path11.join(lobstahHome(), "worktrees", id);
1528
1674
  }
1529
- function readRecord(file) {
1675
+ function readRecord(file2) {
1530
1676
  try {
1531
- const r = JSON.parse(fs9.readFileSync(file, "utf8"));
1677
+ const r = JSON.parse(fs13.readFileSync(file2, "utf8"));
1532
1678
  return typeof r.path === "string" ? r : void 0;
1533
1679
  } catch {
1534
1680
  return void 0;
1535
1681
  }
1536
1682
  }
1537
1683
  function ownerOfPath(p) {
1538
- const rel = path7.relative(path7.join(lobstahHome(), "worktrees"), p);
1539
- if (!rel || rel.startsWith("..") || path7.isAbsolute(rel))
1684
+ const rel = path11.relative(path11.join(lobstahHome(), "worktrees"), p);
1685
+ if (!rel || rel.startsWith("..") || path11.isAbsolute(rel))
1540
1686
  return void 0;
1541
1687
  return rel.split(/[\\/]/)[0];
1542
1688
  }
@@ -1549,7 +1695,7 @@ function dispatchWorktree(id, lane) {
1549
1695
  }
1550
1696
  const dirs = laneDirs(l);
1551
1697
  for (const dir of [dirs.active, dirs.done]) {
1552
- const rec = readRecord(path7.join(dir, id, "worktree.json"));
1698
+ const rec = readRecord(path11.join(dir, id, "worktree.json"));
1553
1699
  if (rec) {
1554
1700
  const owner = rec.of ?? ownerOfPath(rec.path) ?? id;
1555
1701
  return { path: rec.path, owner, reused: owner !== id };
@@ -1561,7 +1707,7 @@ function dispatchWorktree(id, lane) {
1561
1707
  function laneOf(id) {
1562
1708
  for (const l of LANES) {
1563
1709
  const d = laneDirs(l);
1564
- if (fs9.existsSync(path7.join(d.queue, `${id}.json`)) || fs9.existsSync(path7.join(d.active, id)) || fs9.existsSync(path7.join(d.done, id))) {
1710
+ if (fs13.existsSync(path11.join(d.queue, `${id}.json`)) || fs13.existsSync(path11.join(d.active, id)) || fs13.existsSync(path11.join(d.done, id))) {
1565
1711
  return l;
1566
1712
  }
1567
1713
  }
@@ -1581,52 +1727,63 @@ function followUpAncestors(id, lane) {
1581
1727
  function chainWorktree(followUp) {
1582
1728
  for (const id of [followUp, ...followUpAncestors(followUp)]) {
1583
1729
  const wt = dispatchWorktree(id, laneOf(id));
1584
- if (fs9.existsSync(wt.path))
1730
+ if (fs13.existsSync(wt.path))
1585
1731
  return { ...wt, from: id };
1586
1732
  }
1587
1733
  return void 0;
1588
1734
  }
1735
+ var WARM_LOCK_MAX_MS = 15 * 6e4;
1736
+ function processAlive(pid) {
1737
+ try {
1738
+ process.kill(pid, 0);
1739
+ return true;
1740
+ } catch (err) {
1741
+ return err.code === "EPERM";
1742
+ }
1743
+ }
1589
1744
  function worktreeGitDir(wt) {
1590
- const dotGit = path7.join(wt, ".git");
1745
+ const dotGit = path11.join(wt, ".git");
1591
1746
  try {
1592
- const st = fs9.statSync(dotGit);
1747
+ const st = fs13.statSync(dotGit);
1593
1748
  if (st.isDirectory())
1594
1749
  return dotGit;
1595
- const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs9.readFileSync(dotGit, "utf8"));
1596
- return m ? path7.resolve(wt, m[1]) : void 0;
1750
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs13.readFileSync(dotGit, "utf8"));
1751
+ return m ? path11.resolve(wt, m[1]) : void 0;
1597
1752
  } catch {
1598
1753
  return void 0;
1599
1754
  }
1600
1755
  }
1601
1756
  function worktreeLockFile(wt) {
1602
1757
  const gitDir = worktreeGitDir(wt);
1603
- return gitDir ? path7.join(gitDir, "lobstah.lock") : void 0;
1758
+ return gitDir ? path11.join(gitDir, "lobstah.lock") : void 0;
1604
1759
  }
1605
1760
  function readWorktreeLock(wt) {
1606
- const file = worktreeLockFile(wt);
1607
- if (!file)
1761
+ const file2 = worktreeLockFile(wt);
1762
+ if (!file2)
1608
1763
  return void 0;
1609
1764
  try {
1610
- return JSON.parse(fs9.readFileSync(file, "utf8"));
1765
+ return JSON.parse(fs13.readFileSync(file2, "utf8"));
1611
1766
  } catch {
1612
1767
  return void 0;
1613
1768
  }
1614
1769
  }
1615
1770
  function lockIsLive(lock) {
1616
- return fs9.existsSync(path7.join(laneDirs(lock.lane).active, lock.id));
1771
+ if (lock.kind === "warm")
1772
+ return processAlive(lock.pid) && Date.now() - (Date.parse(lock.at) || 0) < WARM_LOCK_MAX_MS;
1773
+ return fs13.existsSync(path11.join(laneDirs(lock.lane).active, lock.id));
1617
1774
  }
1618
1775
  function worktreeHolder(wt) {
1619
1776
  const lock = readWorktreeLock(wt);
1620
1777
  return lock && lockIsLive(lock) ? lock : void 0;
1621
1778
  }
1622
- function acquireWorktreeLock(wt, id, lane) {
1623
- const file = worktreeLockFile(wt);
1624
- const mine = { id, lane, pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() };
1625
- if (!file)
1779
+ function acquireWorktreeLock(wt, id, lane, kind) {
1780
+ const file2 = worktreeLockFile(wt);
1781
+ const mine = { id, lane, pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString(), ...kind ? { kind } : {} };
1782
+ if (!file2)
1626
1783
  return { id: "(no git dir)", lane, pid: 0, at: mine.at };
1627
1784
  for (let attempt = 0; attempt < 3; attempt++) {
1628
1785
  try {
1629
- fs9.writeFileSync(file, JSON.stringify(mine, null, 2), { flag: "wx" });
1786
+ fs13.writeFileSync(file2, JSON.stringify(mine, null, 2), { flag: "wx" });
1630
1787
  return void 0;
1631
1788
  } catch (err) {
1632
1789
  if (err.code !== "EEXIST")
@@ -1634,7 +1791,7 @@ function acquireWorktreeLock(wt, id, lane) {
1634
1791
  }
1635
1792
  let raw;
1636
1793
  try {
1637
- raw = fs9.readFileSync(file, "utf8");
1794
+ raw = fs13.readFileSync(file2, "utf8");
1638
1795
  } catch {
1639
1796
  continue;
1640
1797
  }
@@ -1645,14 +1802,14 @@ function acquireWorktreeLock(wt, id, lane) {
1645
1802
  held2 = void 0;
1646
1803
  }
1647
1804
  if (held2?.id === id) {
1648
- fs9.writeFileSync(file, JSON.stringify(mine, null, 2));
1805
+ fs13.writeFileSync(file2, JSON.stringify(mine, null, 2));
1649
1806
  return void 0;
1650
1807
  }
1651
1808
  if (held2 && lockIsLive(held2))
1652
1809
  return held2;
1653
1810
  try {
1654
- if (fs9.readFileSync(file, "utf8") === raw)
1655
- fs9.rmSync(file, { force: true });
1811
+ if (fs13.readFileSync(file2, "utf8") === raw)
1812
+ fs13.rmSync(file2, { force: true });
1656
1813
  } catch {
1657
1814
  }
1658
1815
  }
@@ -1660,60 +1817,205 @@ function acquireWorktreeLock(wt, id, lane) {
1660
1817
  return held ?? { id: "(contended)", lane, pid: 0, at: mine.at };
1661
1818
  }
1662
1819
  function releaseWorktreeLock(wt, id) {
1663
- const file = worktreeLockFile(wt);
1664
- if (!file)
1820
+ const file2 = worktreeLockFile(wt);
1821
+ if (!file2)
1665
1822
  return;
1666
1823
  if (readWorktreeLock(wt)?.id === id)
1667
- fs9.rmSync(file, { force: true });
1824
+ fs13.rmSync(file2, { force: true });
1668
1825
  }
1669
1826
 
1670
1827
  // packages/core/dist/disk.js
1828
+ import * as fs14 from "node:fs";
1671
1829
  var GB = 1024 ** 3;
1830
+ var statfsFreeBytes = (dir) => {
1831
+ const s = fs14.statfsSync(dir);
1832
+ return Number(s.bavail) * Number(s.bsize);
1833
+ };
1834
+ function formatGB(bytes) {
1835
+ return `${(bytes / GB).toFixed(1).replace(/\.0$/, "")} GB`;
1836
+ }
1672
1837
 
1673
- // packages/core/dist/watch.js
1674
- import * as fs10 from "node:fs";
1675
- import * as path8 from "node:path";
1676
- function watchesDir() {
1677
- return path8.join(lobstahHome(), "watches");
1838
+ // packages/core/dist/pools.js
1839
+ import * as fs15 from "node:fs";
1840
+ import * as path12 from "node:path";
1841
+
1842
+ // packages/core/dist/slots.js
1843
+ function isTrapCatch(id, lane) {
1844
+ return readSessionClaim(id, lane)?.by.startsWith("wt:") ?? false;
1678
1845
  }
1679
- function slug(key) {
1680
- return key.replace(/[^A-Za-z0-9._-]+/g, "-");
1846
+ function isFinished(id, lane) {
1847
+ const verb = readStatusLog(id, lane).at(-1)?.verb;
1848
+ return verb !== void 0 && TERMINAL_VERBS.includes(verb);
1681
1849
  }
1682
- function watchPath(key) {
1683
- return path8.join(watchesDir(), `${slug(key)}.json`);
1850
+
1851
+ // packages/core/dist/pools.js
1852
+ function poolsRoot() {
1853
+ return path12.join(lobstahHome(), "pools");
1684
1854
  }
1685
- function readWatch(key) {
1855
+ function poolDir(pool) {
1856
+ return path12.join(poolsRoot(), pool);
1857
+ }
1858
+ function poolSlotPath(pool, slot) {
1859
+ return path12.join(poolDir(pool), String(slot));
1860
+ }
1861
+ function slotsDir(pool) {
1862
+ return path12.join(poolDir(pool), ".slots");
1863
+ }
1864
+ function poolSlotOf(p) {
1865
+ const real = (x) => {
1866
+ try {
1867
+ return fs15.realpathSync.native(x);
1868
+ } catch {
1869
+ return path12.resolve(x);
1870
+ }
1871
+ };
1872
+ for (const [rootDir, dir] of [[poolsRoot(), path12.resolve(p)], [real(poolsRoot()), real(p)]]) {
1873
+ const rel = path12.relative(rootDir, dir);
1874
+ if (!rel || rel.startsWith("..") || path12.isAbsolute(rel))
1875
+ continue;
1876
+ const parts = rel.split(/[\\/]/);
1877
+ if (parts.length !== 2 || !/^[1-9][0-9]*$/.test(parts[1]))
1878
+ continue;
1879
+ return { pool: parts[0], slot: Number(parts[1]) };
1880
+ }
1881
+ return void 0;
1882
+ }
1883
+ function poolSlotLabel(pool, slot) {
1884
+ return `${pool}/${slot}`;
1885
+ }
1886
+ function readJson(file2) {
1686
1887
  try {
1687
- return JSON.parse(fs10.readFileSync(watchPath(key), "utf8"));
1888
+ return JSON.parse(fs15.readFileSync(file2, "utf8"));
1688
1889
  } catch {
1689
1890
  return void 0;
1690
1891
  }
1691
1892
  }
1893
+ function writeJson(file2, value) {
1894
+ fs15.mkdirSync(path12.dirname(file2), { recursive: true });
1895
+ const tmp = `${file2}.tmp-${process.pid}-${Date.now()}`;
1896
+ fs15.writeFileSync(tmp, JSON.stringify(value, null, 2));
1897
+ fs15.renameSync(tmp, file2);
1898
+ }
1899
+ var metaFile = (pool, slot) => path12.join(slotsDir(pool), `${slot}.json`);
1900
+ var claimFile = (pool, slot) => path12.join(slotsDir(pool), `${slot}.claim.json`);
1901
+ var outFile = (pool, slot) => path12.join(slotsDir(pool), `${slot}.out.json`);
1902
+ function readSlotMeta(pool, slot) {
1903
+ return readJson(metaFile(pool, slot)) ?? {};
1904
+ }
1905
+ function writeSlotMeta(pool, slot, meta) {
1906
+ writeJson(metaFile(pool, slot), meta);
1907
+ }
1908
+ function readSlotClaim(pool, slot) {
1909
+ return readJson(claimFile(pool, slot));
1910
+ }
1911
+ function writeSlotClaim(pool, slot, claim) {
1912
+ writeJson(claimFile(pool, slot), claim);
1913
+ }
1914
+ function readSlotOut(pool, slot) {
1915
+ return readJson(outFile(pool, slot));
1916
+ }
1917
+ function writeSlotOut(pool, slot, out) {
1918
+ writeJson(outFile(pool, slot), out);
1919
+ }
1920
+ function clearSlotOut(pool, slot) {
1921
+ fs15.rmSync(outFile(pool, slot), { force: true });
1922
+ }
1923
+ function dispatchHolder(dir) {
1924
+ const lock = readWorktreeLock(dir);
1925
+ return lock && lock.kind !== "warm" && lockIsLive(lock) ? lock : void 0;
1926
+ }
1927
+ function poolSlotView(pool, slot) {
1928
+ const dir = poolSlotPath(pool, slot);
1929
+ const lastClaim = readSlotClaim(pool, slot)?.id;
1930
+ const base = { slot, path: dir, ...lastClaim ? { lastClaim } : {} };
1931
+ if (!fs15.existsSync(dir)) {
1932
+ const meta2 = readSlotMeta(pool, slot);
1933
+ return { ...base, state: "missing", ...meta2.error ? { reason: meta2.error } : {} };
1934
+ }
1935
+ const holder = dispatchHolder(dir);
1936
+ if (holder)
1937
+ return { ...base, state: "claimed", dispatch: holder.id };
1938
+ const out = readSlotOut(pool, slot);
1939
+ if (out)
1940
+ return { ...base, state: "out", reason: out.reason, ...out.dispatch ? { dispatch: out.dispatch } : {} };
1941
+ const meta = readSlotMeta(pool, slot);
1942
+ if (!meta.readyAt)
1943
+ return { ...base, state: "warming", ...meta.error ? { reason: meta.error } : {} };
1944
+ return { ...base, state: "free" };
1945
+ }
1946
+ var warmStateFile = (pool) => path12.join(poolDir(pool), ".warm-state.json");
1947
+ var warmLockFile = (pool) => path12.join(poolDir(pool), ".warm.lock");
1948
+ var POOL_WARM_MAX_MS = 60 * 6e4;
1949
+ function readPoolWarmState(pool) {
1950
+ return readJson(warmStateFile(pool)) ?? {};
1951
+ }
1952
+ function writePoolWarmState(pool, state) {
1953
+ writeJson(warmStateFile(pool), state);
1954
+ }
1955
+ function pidAlive(pid) {
1956
+ try {
1957
+ process.kill(pid, 0);
1958
+ return true;
1959
+ } catch (err) {
1960
+ return err.code === "EPERM";
1961
+ }
1962
+ }
1963
+ function poolWarmRunning(pool, now2 = Date.now()) {
1964
+ const lock = readJson(warmLockFile(pool));
1965
+ return !!lock && pidAlive(lock.pid) && now2 - (Date.parse(lock.at) || 0) < POOL_WARM_MAX_MS;
1966
+ }
1967
+ function takePoolWarmLock(pool) {
1968
+ const file2 = warmLockFile(pool);
1969
+ fs15.mkdirSync(path12.dirname(file2), { recursive: true });
1970
+ const mine = JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() });
1971
+ for (let attempt = 0; attempt < 2; attempt++) {
1972
+ try {
1973
+ fs15.writeFileSync(file2, mine, { flag: "wx" });
1974
+ return () => {
1975
+ try {
1976
+ if (fs15.readFileSync(file2, "utf8") === mine)
1977
+ fs15.rmSync(file2, { force: true });
1978
+ } catch {
1979
+ }
1980
+ };
1981
+ } catch (err) {
1982
+ if (err.code !== "EEXIST")
1983
+ throw err;
1984
+ }
1985
+ if (poolWarmRunning(pool))
1986
+ return void 0;
1987
+ fs15.rmSync(file2, { force: true });
1988
+ }
1989
+ return void 0;
1990
+ }
1991
+ var POOL_RETRY_MS = 10 * 6e4;
1992
+ var POOL_FETCH_MS = 10 * 6e4;
1993
+ var POOL_RECHECK_MS = 5 * 6e4;
1692
1994
 
1693
1995
  // packages/core/dist/soak.js
1694
1996
  function regPath(trapId) {
1695
- return path9.join(soakingDir(), `${trapId}.json`);
1997
+ return path13.join(soakingDir(), `${trapId}.json`);
1696
1998
  }
1697
1999
  function readTrap(trapId) {
1698
2000
  try {
1699
- return JSON.parse(fs11.readFileSync(regPath(trapId), "utf8"));
2001
+ return JSON.parse(fs16.readFileSync(regPath(trapId), "utf8"));
1700
2002
  } catch {
1701
2003
  return void 0;
1702
2004
  }
1703
2005
  }
1704
2006
  function listTraps() {
1705
2007
  try {
1706
- return fs11.readdirSync(soakingDir()).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(fs11.readFileSync(path9.join(soakingDir(), f), "utf8"))).filter((r) => typeof r.trapId === "string");
2008
+ return fs16.readdirSync(soakingDir()).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(fs16.readFileSync(path13.join(soakingDir(), f), "utf8"))).filter((r) => typeof r.trapId === "string");
1707
2009
  } catch {
1708
2010
  return [];
1709
2011
  }
1710
2012
  }
1711
2013
  function claimPath(id, lane) {
1712
- return path9.join(laneDirs(lane).active, id, "claim.json");
2014
+ return path13.join(laneDirs(lane).active, id, "claim.json");
1713
2015
  }
1714
2016
  function readSessionClaim(id, lane) {
1715
2017
  try {
1716
- return JSON.parse(fs11.readFileSync(claimPath(id, lane), "utf8"));
2018
+ return JSON.parse(fs16.readFileSync(claimPath(id, lane), "utf8"));
1717
2019
  } catch {
1718
2020
  return void 0;
1719
2021
  }
@@ -1722,7 +2024,7 @@ function readSessionClaim(id, lane) {
1722
2024
  // packages/core/dist/stats.js
1723
2025
  var DAILY_RETENTION_DAYS = 400;
1724
2026
  function statsPath() {
1725
- return path10.join(lobstahHome(), "stats.json");
2027
+ return path14.join(lobstahHome(), "stats.json");
1726
2028
  }
1727
2029
  function shiftDay(day, days) {
1728
2030
  const [y, m, d] = day.split("-").map(Number);
@@ -1735,7 +2037,7 @@ function localDay(at = Date.now()) {
1735
2037
  function readCatchFacts(stateDir, id) {
1736
2038
  let last;
1737
2039
  try {
1738
- for (const line of fs12.readFileSync(path10.join(stateDir, `${id}.status`), "utf8").split("\n")) {
2040
+ for (const line of fs17.readFileSync(path14.join(stateDir, `${id}.status`), "utf8").split("\n")) {
1739
2041
  if (!line.trim())
1740
2042
  continue;
1741
2043
  try {
@@ -1751,7 +2053,7 @@ function readCatchFacts(stateDir, id) {
1751
2053
  let trap;
1752
2054
  let worker = workerProfile();
1753
2055
  try {
1754
- const evidence = JSON.parse(fs12.readFileSync(path10.join(stateDir, `${id}.evidence`), "utf8"));
2056
+ const evidence = JSON.parse(fs17.readFileSync(path14.join(stateDir, `${id}.evidence`), "utf8"));
1755
2057
  const to = evidence.deliveredTo;
1756
2058
  if (typeof to === "string" && to.startsWith("wt:"))
1757
2059
  trap = to;
@@ -1824,17 +2126,17 @@ function add(store, facts, now2) {
1824
2126
  }
1825
2127
  function backfillSources() {
1826
2128
  const live = ["work", "chore"].map((lane) => ({ dir: laneDirs(lane).state, live: true }));
1827
- const backups = readDirIfPresent(lobstahHome()).filter((name) => name.startsWith("state-backup-")).sort().map((name) => ({ dir: path10.join(lobstahHome(), name), live: false })).filter((s) => fs12.statSync(s.dir).isDirectory());
2129
+ const backups = readDirIfPresent(lobstahHome()).filter((name) => name.startsWith("state-backup-")).sort().map((name) => ({ dir: path14.join(lobstahHome(), name), live: false })).filter((s) => fs17.statSync(s.dir).isDirectory());
1828
2130
  return [...live, ...backups];
1829
2131
  }
1830
2132
  function backfillStats(now2 = Date.now(), alreadyCounted) {
1831
2133
  const store = emptyStore(now2);
1832
2134
  const seen = /* @__PURE__ */ new Set();
1833
2135
  for (const { dir, live } of backfillSources()) {
1834
- for (const file of readDirIfPresent(dir).sort()) {
1835
- if (!file.endsWith(".status"))
2136
+ for (const file2 of readDirIfPresent(dir).sort()) {
2137
+ if (!file2.endsWith(".status"))
1836
2138
  continue;
1837
- const id = file.slice(0, -".status".length);
2139
+ const id = file2.slice(0, -".status".length);
1838
2140
  if (seen.has(id))
1839
2141
  continue;
1840
2142
  seen.add(id);
@@ -1856,10 +2158,10 @@ function backfillDaily(store, now2 = Date.now()) {
1856
2158
  const seen = /* @__PURE__ */ new Set();
1857
2159
  const into = { ...store, daily };
1858
2160
  for (const { dir, live } of backfillSources()) {
1859
- for (const file of readDirIfPresent(dir).sort()) {
1860
- if (!file.endsWith(".status"))
2161
+ for (const file2 of readDirIfPresent(dir).sort()) {
2162
+ if (!file2.endsWith(".status"))
1861
2163
  continue;
1862
- const id = file.slice(0, -".status".length);
2164
+ const id = file2.slice(0, -".status".length);
1863
2165
  if (seen.has(id))
1864
2166
  continue;
1865
2167
  seen.add(id);
@@ -1895,26 +2197,26 @@ function parseStore(text) {
1895
2197
  }
1896
2198
  var complete2 = (s) => !!s && s.daily !== void 0;
1897
2199
  function writeStore(store) {
1898
- const file = statsPath();
1899
- fs12.mkdirSync(path10.dirname(file), { recursive: true });
1900
- const tmp = `${file}.tmp-${process.pid}`;
1901
- fs12.writeFileSync(tmp, JSON.stringify(store));
1902
- fs12.renameSync(tmp, file);
2200
+ const file2 = statsPath();
2201
+ fs17.mkdirSync(path14.dirname(file2), { recursive: true });
2202
+ const tmp = `${file2}.tmp-${process.pid}`;
2203
+ fs17.writeFileSync(tmp, JSON.stringify(store));
2204
+ fs17.renameSync(tmp, file2);
1903
2205
  }
1904
2206
  function withStatsLock(action) {
1905
- fs12.mkdirSync(lobstahHome(), { recursive: true });
2207
+ fs17.mkdirSync(lobstahHome(), { recursive: true });
1906
2208
  const lock = `${statsPath()}.lock`;
1907
2209
  const deadline = Date.now() + 1e4;
1908
2210
  for (; ; ) {
1909
2211
  try {
1910
- fs12.mkdirSync(lock);
2212
+ fs17.mkdirSync(lock);
1911
2213
  break;
1912
2214
  } catch (err) {
1913
2215
  if (err.code !== "EEXIST")
1914
2216
  throw err;
1915
2217
  try {
1916
- if (Date.now() - fs12.statSync(lock).mtimeMs > 12e4)
1917
- fs12.rmdirSync(lock);
2218
+ if (Date.now() - fs17.statSync(lock).mtimeMs > 12e4)
2219
+ fs17.rmdirSync(lock);
1918
2220
  } catch {
1919
2221
  }
1920
2222
  if (Date.now() >= deadline)
@@ -1925,13 +2227,13 @@ function withStatsLock(action) {
1925
2227
  try {
1926
2228
  return action();
1927
2229
  } finally {
1928
- fs12.rmdirSync(lock);
2230
+ fs17.rmdirSync(lock);
1929
2231
  }
1930
2232
  }
1931
2233
  function loadOrBackfill(now2) {
1932
2234
  let text;
1933
2235
  try {
1934
- text = fs12.readFileSync(statsPath(), "utf8");
2236
+ text = fs17.readFileSync(statsPath(), "utf8");
1935
2237
  } catch (err) {
1936
2238
  if (err.code !== "ENOENT")
1937
2239
  throw err;
@@ -1958,7 +2260,7 @@ function loadOrBackfill(now2) {
1958
2260
  return store2;
1959
2261
  }
1960
2262
  if (text !== void 0)
1961
- fs12.renameSync(statsPath(), `${statsPath()}.unreadable-${now2}`);
2263
+ fs17.renameSync(statsPath(), `${statsPath()}.unreadable-${now2}`);
1962
2264
  const store = backfillStats(now2);
1963
2265
  writeStore(store);
1964
2266
  return store;
@@ -2001,58 +2303,58 @@ var ANSWER_TYPES = {
2001
2303
  var ANSWER_EXTENSIONS = Object.keys(ANSWER_TYPES);
2002
2304
 
2003
2305
  // packages/core/dist/inbox.js
2004
- import * as fs13 from "node:fs";
2005
- import * as path11 from "node:path";
2006
- var metaName = (file) => file.replace(/\.msg$/, ".meta.json");
2306
+ import * as fs18 from "node:fs";
2307
+ import * as path15 from "node:path";
2308
+ var metaName = (file2) => file2.replace(/\.msg$/, ".meta.json");
2007
2309
  function inboxDir(id, lane) {
2008
- return path11.join(laneDirs(lane).inbox, id);
2310
+ return path15.join(laneDirs(lane).inbox, id);
2009
2311
  }
2010
2312
  function unhandled(id, lane) {
2011
2313
  const dir = inboxDir(id, lane);
2012
- return readDirIfPresent(dir).filter((f) => f.endsWith(".msg")).sort().map((f) => ({ file: f, text: fs13.readFileSync(path11.join(dir, f), "utf8") }));
2314
+ return readDirIfPresent(dir).filter((f) => f.endsWith(".msg")).sort().map((f) => ({ file: f, text: fs18.readFileSync(path15.join(dir, f), "utf8") }));
2013
2315
  }
2014
- function acknowledge(id, lane, file) {
2316
+ function acknowledge(id, lane, file2) {
2015
2317
  const dir = inboxDir(id, lane);
2016
- fs13.mkdirSync(path11.join(dir, "handled"), { recursive: true });
2017
- fs13.renameSync(path11.join(dir, file), path11.join(dir, "handled", file));
2318
+ fs18.mkdirSync(path15.join(dir, "handled"), { recursive: true });
2319
+ fs18.renameSync(path15.join(dir, file2), path15.join(dir, "handled", file2));
2018
2320
  try {
2019
- fs13.renameSync(path11.join(dir, metaName(file)), path11.join(dir, "handled", metaName(file)));
2321
+ fs18.renameSync(path15.join(dir, metaName(file2)), path15.join(dir, "handled", metaName(file2)));
2020
2322
  } catch {
2021
2323
  }
2022
2324
  }
2023
2325
 
2024
2326
  // packages/core/dist/events.js
2025
- import * as fs14 from "node:fs";
2327
+ import * as fs19 from "node:fs";
2026
2328
  function appendEvent(id, lane, ev) {
2027
- fs14.appendFileSync(eventsPath(id, lane), `${JSON.stringify(ev)}
2329
+ fs19.appendFileSync(eventsPath(id, lane), `${JSON.stringify(ev)}
2028
2330
  `);
2029
2331
  }
2030
2332
  function touchEvents(id, lane, at = /* @__PURE__ */ new Date()) {
2031
- const file = eventsPath(id, lane);
2333
+ const file2 = eventsPath(id, lane);
2032
2334
  try {
2033
- fs14.utimesSync(file, at, at);
2335
+ fs19.utimesSync(file2, at, at);
2034
2336
  } catch {
2035
- fs14.appendFileSync(file, "");
2036
- fs14.utimesSync(file, at, at);
2337
+ fs19.appendFileSync(file2, "");
2338
+ fs19.utimesSync(file2, at, at);
2037
2339
  }
2038
2340
  }
2039
2341
 
2040
2342
  // packages/core/dist/harness.js
2041
- import * as fs15 from "node:fs";
2042
- import * as path12 from "node:path";
2343
+ import * as fs20 from "node:fs";
2344
+ import * as path16 from "node:path";
2043
2345
  import { createRequire } from "node:module";
2044
2346
  function resolveOnPath(bin, pathVar = process.env.PATH) {
2045
2347
  if (!pathVar)
2046
2348
  return void 0;
2047
2349
  const names = process.platform === "win32" ? [bin, `${bin}.exe`, `${bin}.cmd`, `${bin}.bat`] : [bin];
2048
- for (const dir of pathVar.split(path12.delimiter)) {
2350
+ for (const dir of pathVar.split(path16.delimiter)) {
2049
2351
  if (!dir)
2050
2352
  continue;
2051
2353
  for (const name of names) {
2052
- const candidate = path12.join(dir, name);
2354
+ const candidate = path16.join(dir, name);
2053
2355
  try {
2054
- fs15.accessSync(candidate, fs15.constants.X_OK);
2055
- if (fs15.statSync(candidate).isFile())
2356
+ fs20.accessSync(candidate, fs20.constants.X_OK);
2357
+ if (fs20.statSync(candidate).isFile())
2056
2358
  return candidate;
2057
2359
  } catch {
2058
2360
  }
@@ -2073,8 +2375,8 @@ function codexInvocation(argv) {
2073
2375
  } catch {
2074
2376
  }
2075
2377
  for (const dir of require_.resolve.paths("@openai/codex") ?? []) {
2076
- const cli = path12.join(dir, "@openai", "codex", "bin", "codex.js");
2077
- if (fs15.existsSync(cli))
2378
+ const cli = path16.join(dir, "@openai", "codex", "bin", "codex.js");
2379
+ if (fs20.existsSync(cli))
2078
2380
  return { file: process.execPath, argv: [cli, ...argv] };
2079
2381
  }
2080
2382
  return void 0;
@@ -2127,9 +2429,9 @@ function pushRule(branch, id = "<dispatch id>", mode = "rebase", also = []) {
2127
2429
  }
2128
2430
 
2129
2431
  // packages/core/dist/resume.js
2130
- import * as fs16 from "node:fs";
2432
+ import * as fs21 from "node:fs";
2131
2433
  import * as os3 from "node:os";
2132
- import * as path13 from "node:path";
2434
+ import * as path17 from "node:path";
2133
2435
  import { spawnSync as spawnSync2 } from "node:child_process";
2134
2436
  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;
2135
2437
  function harnessFromSessionId(sessionId) {
@@ -2140,7 +2442,7 @@ function claimOf(id, lane) {
2140
2442
  const dirs = laneDirs(lane);
2141
2443
  for (const dir of [dirs.active, dirs.done]) {
2142
2444
  try {
2143
- return JSON.parse(fs16.readFileSync(path13.join(dir, id, "claim.json"), "utf8"));
2445
+ return JSON.parse(fs21.readFileSync(path17.join(dir, id, "claim.json"), "utf8"));
2144
2446
  } catch {
2145
2447
  }
2146
2448
  }
@@ -2190,7 +2492,7 @@ function isUnresumable(error) {
2190
2492
  return error !== void 0 && UNRESUMABLE.test(error);
2191
2493
  }
2192
2494
  function worktreeProgress(worktree, trunk) {
2193
- if (!worktree || !fs16.existsSync(worktree))
2495
+ if (!worktree || !fs21.existsSync(worktree))
2194
2496
  return "No worktree progress recorded.";
2195
2497
  const git2 = (...a) => spawnSync2("git", a, { cwd: worktree, encoding: "utf8" }).stdout?.trim() ?? "";
2196
2498
  const commits = trunk ? git2("log", "--oneline", `origin/${trunk}..HEAD`) : git2("log", "--oneline", "-15");
@@ -2223,44 +2525,44 @@ function handoffNote(fromHarness, progress, why) {
2223
2525
  ${progress}`;
2224
2526
  }
2225
2527
  function codexHome(env = process.env) {
2226
- return env.CODEX_HOME ?? path13.join(os3.homedir(), ".codex");
2528
+ return env.CODEX_HOME ?? path17.join(os3.homedir(), ".codex");
2227
2529
  }
2228
2530
  function codexRolloutFile(threadId, home = codexHome()) {
2229
2531
  const suffix = `-${threadId}.jsonl`;
2230
2532
  const walk = (dir, depth) => {
2231
2533
  let entries;
2232
2534
  try {
2233
- entries = fs16.readdirSync(dir, { withFileTypes: true });
2535
+ entries = fs21.readdirSync(dir, { withFileTypes: true });
2234
2536
  } catch {
2235
2537
  return void 0;
2236
2538
  }
2237
2539
  for (const e of entries) {
2238
2540
  if (e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(suffix))
2239
- return path13.join(dir, e.name);
2541
+ return path17.join(dir, e.name);
2240
2542
  }
2241
2543
  if (depth === 0)
2242
2544
  return void 0;
2243
2545
  for (const e of entries.filter((x) => x.isDirectory()).sort((a, b) => b.name.localeCompare(a.name))) {
2244
- const hit = walk(path13.join(dir, e.name), depth - 1);
2546
+ const hit = walk(path17.join(dir, e.name), depth - 1);
2245
2547
  if (hit)
2246
2548
  return hit;
2247
2549
  }
2248
2550
  return void 0;
2249
2551
  };
2250
- return walk(path13.join(home, "sessions"), 3) ?? walk(path13.join(home, "archived_sessions"), 0);
2552
+ return walk(path17.join(home, "sessions"), 3) ?? walk(path17.join(home, "archived_sessions"), 0);
2251
2553
  }
2252
2554
  function codexDesktopThread(threadId, home = codexHome()) {
2253
- const file = codexRolloutFile(threadId, home);
2254
- if (!file)
2555
+ const file2 = codexRolloutFile(threadId, home);
2556
+ if (!file2)
2255
2557
  return void 0;
2256
2558
  let head = "";
2257
2559
  try {
2258
- const fd = fs16.openSync(file, "r");
2560
+ const fd = fs21.openSync(file2, "r");
2259
2561
  try {
2260
2562
  const buf = Buffer.alloc(4096);
2261
- head = buf.subarray(0, fs16.readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
2563
+ head = buf.subarray(0, fs21.readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
2262
2564
  } finally {
2263
- fs16.closeSync(fd);
2565
+ fs21.closeSync(fd);
2264
2566
  }
2265
2567
  } catch {
2266
2568
  return void 0;
@@ -2290,18 +2592,9 @@ function droppedModelNote(harness, dropped) {
2290
2592
  return `model ${dropped.model} is a ${dropped.owner} model \u2014 dropped, using ${harness}'s default`;
2291
2593
  }
2292
2594
 
2293
- // packages/core/dist/slots.js
2294
- function isTrapCatch(id, lane) {
2295
- return readSessionClaim(id, lane)?.by.startsWith("wt:") ?? false;
2296
- }
2297
- function isFinished(id, lane) {
2298
- const verb = readStatusLog(id, lane).at(-1)?.verb;
2299
- return verb !== void 0 && TERMINAL_VERBS.includes(verb);
2300
- }
2301
-
2302
2595
  // packages/runner/src/run.ts
2303
- import * as fs20 from "node:fs";
2304
- import * as path17 from "node:path";
2596
+ import * as fs26 from "node:fs";
2597
+ import * as path22 from "node:path";
2305
2598
  import { execFileSync as execFileSync2 } from "node:child_process";
2306
2599
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2307
2600
 
@@ -2335,7 +2628,7 @@ var AsyncQueue = class {
2335
2628
  return Promise.resolve({ value: item, done: false });
2336
2629
  if (this.closed)
2337
2630
  return Promise.resolve({ value: void 0, done: true });
2338
- return new Promise((resolve7) => this.waiters.push(resolve7));
2631
+ return new Promise((resolve9) => this.waiters.push(resolve9));
2339
2632
  }
2340
2633
  };
2341
2634
  }
@@ -2360,7 +2653,7 @@ var InputGate = class {
2360
2653
  return msg;
2361
2654
  if (this.ended)
2362
2655
  return void 0;
2363
- await new Promise((resolve7) => this.waiter = resolve7);
2656
+ await new Promise((resolve9) => this.waiter = resolve9);
2364
2657
  this.waiter = null;
2365
2658
  }
2366
2659
  }
@@ -2505,20 +2798,20 @@ function startClaudeCli(invocation, opts) {
2505
2798
  }
2506
2799
  }
2507
2800
  });
2508
- const done = new Promise((resolve7) => {
2801
+ const done = new Promise((resolve9) => {
2509
2802
  child.on("error", (err) => {
2510
2803
  events.push({ at: now(), type: "error", data: { message: err.message } });
2511
2804
  events.close();
2512
- resolve7({ sessionId, error: err.message });
2805
+ resolve9({ sessionId, error: err.message });
2513
2806
  });
2514
2807
  child.on("exit", (code) => {
2515
2808
  events.close();
2516
2809
  if (code === 0)
2517
- resolve7({ sessionId });
2810
+ resolve9({ sessionId });
2518
2811
  else {
2519
2812
  const detail = stderrTail.trim().split("\n").at(-1);
2520
2813
  const error = `claude exited ${code}${detail ? ` \u2014 ${detail}` : ""}`;
2521
- resolve7({ sessionId, error });
2814
+ resolve9({ sessionId, error });
2522
2815
  }
2523
2816
  });
2524
2817
  });
@@ -2597,25 +2890,25 @@ function createClaudeAdapter() {
2597
2890
  }
2598
2891
 
2599
2892
  // packages/adapters/dist/codex.js
2600
- import * as fs17 from "node:fs";
2893
+ import * as fs22 from "node:fs";
2601
2894
  import * as os4 from "node:os";
2602
- import * as path14 from "node:path";
2895
+ import * as path18 from "node:path";
2603
2896
  import { spawn as spawn2 } from "node:child_process";
2604
2897
  function isolatedCodexHome() {
2605
- const dir = path14.join(lobstahHome(), "codex-home");
2606
- fs17.mkdirSync(dir, { recursive: true });
2607
- const auth = path14.join(dir, "auth.json");
2608
- const realAuth = path14.join(os4.homedir(), ".codex", "auth.json");
2609
- if (!fs17.existsSync(auth) && fs17.existsSync(realAuth)) {
2898
+ const dir = path18.join(lobstahHome(), "codex-home");
2899
+ fs22.mkdirSync(dir, { recursive: true });
2900
+ const auth = path18.join(dir, "auth.json");
2901
+ const realAuth = path18.join(os4.homedir(), ".codex", "auth.json");
2902
+ if (!fs22.existsSync(auth) && fs22.existsSync(realAuth)) {
2610
2903
  try {
2611
- fs17.symlinkSync(realAuth, auth);
2904
+ fs22.symlinkSync(realAuth, auth);
2612
2905
  } catch {
2613
- fs17.copyFileSync(realAuth, auth);
2906
+ fs22.copyFileSync(realAuth, auth);
2614
2907
  }
2615
2908
  }
2616
- const cfg = path14.join(dir, "config.toml");
2617
- if (!fs17.existsSync(cfg)) {
2618
- fs17.writeFileSync(cfg, "# lobstah-managed: auth inherited from ~/.codex, interactive config not\n");
2909
+ const cfg = path18.join(dir, "config.toml");
2910
+ if (!fs22.existsSync(cfg)) {
2911
+ fs22.writeFileSync(cfg, "# lobstah-managed: auth inherited from ~/.codex, interactive config not\n");
2619
2912
  }
2620
2913
  return dir;
2621
2914
  }
@@ -2667,7 +2960,7 @@ function startCodexCli(invoke, opts) {
2667
2960
  let current;
2668
2961
  let killed = false;
2669
2962
  function runTurn(prompt, resumeThread) {
2670
- return new Promise((resolve7) => {
2963
+ return new Promise((resolve9) => {
2671
2964
  const inv = invoke(codexExecArgs(opts, prompt, resumeThread));
2672
2965
  const child = spawn2(inv.file, inv.argv, {
2673
2966
  cwd: opts.cwd,
@@ -2698,14 +2991,14 @@ function startCodexCli(invoke, opts) {
2698
2991
  }
2699
2992
  }
2700
2993
  });
2701
- child.on("error", (err) => resolve7(err.message));
2994
+ child.on("error", (err) => resolve9(err.message));
2702
2995
  child.on("exit", (code) => {
2703
2996
  if (turnError)
2704
- return resolve7(turnError);
2997
+ return resolve9(turnError);
2705
2998
  if (code === 0 || killed)
2706
- return resolve7(void 0);
2999
+ return resolve9(void 0);
2707
3000
  const detail = stderrTail.trim().split("\n").at(-1);
2708
- resolve7(`codex exited ${code}${detail ? ` \u2014 ${detail}` : ""}`);
3001
+ resolve9(`codex exited ${code}${detail ? ` \u2014 ${detail}` : ""}`);
2709
3002
  });
2710
3003
  });
2711
3004
  }
@@ -2818,15 +3111,256 @@ function loadAdapter(name) {
2818
3111
  }
2819
3112
 
2820
3113
  // packages/worktree/dist/index.js
2821
- import * as fs18 from "node:fs";
2822
- import * as path15 from "node:path";
3114
+ import * as fs24 from "node:fs";
3115
+ import * as path20 from "node:path";
2823
3116
  import { createHash } from "node:crypto";
2824
- import { exec, execFile, execFileSync } from "node:child_process";
3117
+ import { exec, execFile as execFile2, execFileSync } from "node:child_process";
3118
+ import { promisify as promisify2 } from "node:util";
3119
+
3120
+ // packages/worktree/dist/pool.js
3121
+ import * as fs23 from "node:fs";
3122
+ import * as path19 from "node:path";
3123
+ import { execFile } from "node:child_process";
2825
3124
  import { promisify } from "node:util";
2826
3125
  var run = promisify(execFile);
2827
- var shell = promisify(exec);
3126
+ var DEFAULT_POOL_KEEP = [
3127
+ // dependencies
3128
+ "node_modules/",
3129
+ ".pnpm-store/",
3130
+ ".venv/",
3131
+ "vendor/bundle/",
3132
+ "Pods/",
3133
+ // build caches
3134
+ ".turbo/",
3135
+ ".next/cache/",
3136
+ ".nx/cache/",
3137
+ ".cache/",
3138
+ "*.tsbuildinfo",
3139
+ "target/",
3140
+ ".gradle/",
3141
+ ".build/",
3142
+ // local env files
3143
+ ".env*",
3144
+ ".dev.vars"
3145
+ ];
3146
+ var PRODUCTION_ENV_PATHSPEC = ":(glob,icase)**/.env*prod*";
3147
+ function poolKeep(repo) {
3148
+ return [.../* @__PURE__ */ new Set([...DEFAULT_POOL_KEEP, ...repo.poolKeep ?? []])];
3149
+ }
3150
+ async function poolUnsafe(repo, dir) {
3151
+ const status = await tryGit(dir, "status", "--porcelain");
3152
+ if (!status.ok)
3153
+ return `cannot read git status: ${status.err.split("\n")[0]}`;
3154
+ if (!onlyScratch(status.out, repo.scratch)) {
3155
+ const lines = status.out.split("\n").filter((l) => l.trim() !== "");
3156
+ const paths = lines.map((l) => l.slice(3));
3157
+ return `uncommitted changes in ${lines.length} file(s): ${paths.slice(0, 3).join(", ")}${paths.length > 3 ? `, and ${paths.length - 3} more` : ""}`;
3158
+ }
3159
+ const unpushed = await tryGit(dir, "rev-list", "--count", "HEAD", "--not", "--remotes");
3160
+ if (!unpushed.ok)
3161
+ return `cannot tell which commits are on a remote: ${unpushed.err.split("\n")[0]}`;
3162
+ const n = Number(unpushed.out);
3163
+ if (!Number.isFinite(n))
3164
+ return "cannot tell which commits are on a remote";
3165
+ if (n > 0)
3166
+ return `${n} commit(s) on no remote branch`;
3167
+ return void 0;
3168
+ }
3169
+ function takeOut(name, pool, slot, reason, dispatch, notice = true) {
3170
+ const label = poolSlotLabel(name, slot);
3171
+ writeSlotOut(name, slot, { reason, at: (/* @__PURE__ */ new Date()).toISOString(), ...dispatch ? { dispatch } : {} });
3172
+ if (!notice)
3173
+ return;
3174
+ postNotice({
3175
+ kind: "pool-out",
3176
+ text: `pool worktree ${label} (${poolSlotPath(name, slot)}) is out of rotation: ${reason}` + (dispatch ? ` (last used by ${dispatch.slice(0, 8)})` : "") + " \u2014 commit and push or discard the work there; the pool takes it back once it is clean and pushed",
3177
+ refId: label,
3178
+ repo: pool.repo
3179
+ });
3180
+ }
3181
+ function trapIn(dir) {
3182
+ const real = (p) => {
3183
+ try {
3184
+ return fs23.realpathSync.native(p);
3185
+ } catch {
3186
+ return path19.resolve(p);
3187
+ }
3188
+ };
3189
+ const here = real(dir);
3190
+ return listTraps().some((t) => {
3191
+ if (typeof t.worktree !== "string")
3192
+ return false;
3193
+ const rel = path19.relative(here, real(t.worktree));
3194
+ return rel === "" || !rel.startsWith("..") && !path19.isAbsolute(rel);
3195
+ });
3196
+ }
3197
+ async function claimPoolSlot(input) {
3198
+ const { name, pool, repo, id, lane } = input;
3199
+ const refused = [];
3200
+ for (let slot = 1; slot <= pool.size; slot++) {
3201
+ const dir = poolSlotPath(name, slot);
3202
+ if (fs23.existsSync(dir) && readWorktreeLock(dir)?.id === id && readWorktreeLock(dir)?.kind !== "warm") {
3203
+ writeSlotClaim(name, slot, { id, lane, at: (/* @__PURE__ */ new Date()).toISOString() });
3204
+ return { claimed: true, dir, slot, label: poolSlotLabel(name, slot) };
3205
+ }
3206
+ }
3207
+ const deadline = Date.now() + (input.warmWaitMs ?? 6e4);
3208
+ for (; ; ) {
3209
+ let warmHeld = false;
3210
+ for (let slot = 1; slot <= pool.size; slot++) {
3211
+ if (poolSlotView(name, slot).state !== "free")
3212
+ continue;
3213
+ const dir = poolSlotPath(name, slot);
3214
+ if (trapIn(dir))
3215
+ continue;
3216
+ const held = acquireWorktreeLock(dir, id, lane);
3217
+ if (held) {
3218
+ if (held.kind === "warm")
3219
+ warmHeld = true;
3220
+ continue;
3221
+ }
3222
+ const why = await poolUnsafe(repo, dir);
3223
+ if (why) {
3224
+ takeOut(name, pool, slot, why, readSlotClaim(name, slot)?.id);
3225
+ releaseWorktreeLock(dir, id);
3226
+ refused.push(`${poolSlotLabel(name, slot)}: ${why}`);
3227
+ continue;
3228
+ }
3229
+ writeSlotClaim(name, slot, { id, lane, at: (/* @__PURE__ */ new Date()).toISOString() });
3230
+ return { claimed: true, dir, slot, label: poolSlotLabel(name, slot) };
3231
+ }
3232
+ if (!warmHeld || Date.now() >= deadline)
3233
+ return { claimed: false, refused };
3234
+ await new Promise((r) => setTimeout(r, 500));
3235
+ }
3236
+ }
3237
+ async function resetPoolWorktree(repo, dir, id, fromRemoteBranch = repo.trunk) {
3238
+ await fetchShared(repo.path, "origin", fromRemoteBranch);
3239
+ await oneAtATime(repo.path, async () => {
3240
+ await gitShared(dir, "checkout", "--quiet", "--force", "--no-track", "-B", `lobstah/${id}`, `origin/${fromRemoteBranch}`);
3241
+ });
3242
+ const keep = poolKeep(repo);
3243
+ await run("git", ["clean", "-ffdxq", ...keep.flatMap((p) => ["-e", p])], { cwd: dir, env: process.env });
3244
+ const dirs = keep.filter((p) => p.endsWith("/"));
3245
+ await run("git", ["clean", "-ffdxq", ...dirs.flatMap((p) => ["-e", p]), "--", PRODUCTION_ENV_PATHSPEC], { cwd: dir, env: process.env });
3246
+ await runSetup(repo, dir);
3247
+ }
3248
+ var WARM_ID = "pool-warm";
3249
+ async function createSlot(repo, dir) {
3250
+ fs23.mkdirSync(path19.dirname(dir), { recursive: true });
3251
+ await oneAtATime(repo.path, async () => {
3252
+ await tryGit(repo.path, "worktree", "prune");
3253
+ await gitShared(repo.path, "worktree", "add", "--detach", dir, `origin/${repo.trunk}`);
3254
+ });
3255
+ await runSetup(repo, dir);
3256
+ }
3257
+ async function warmPool(name, opts = {}) {
3258
+ const log = opts.log ?? (() => {
3259
+ });
3260
+ const now2 = () => new Date(opts.now?.() ?? Date.now()).toISOString();
3261
+ const cfg = loadConfig();
3262
+ const pool = cfg.pools[name];
3263
+ if (!pool) {
3264
+ log(`pool ${name}: not configured`);
3265
+ return true;
3266
+ }
3267
+ const repo = cfg.repos[pool.repo];
3268
+ const release = takePoolWarmLock(name);
3269
+ if (!release)
3270
+ return false;
3271
+ try {
3272
+ if (!fs23.existsSync(repo.path)) {
3273
+ if (!repo.origin)
3274
+ throw new Error(`repo path ${repo.path} missing and no origin configured`);
3275
+ await run("git", ["clone", repo.origin, repo.path], { env: process.env });
3276
+ }
3277
+ await fetchShared(repo.path, "origin", repo.trunk);
3278
+ writePoolWarmState(name, { ...readPoolWarmState(name), fetchedAt: now2() });
3279
+ const need = (cfg.limits.minFreeGB ?? 0) * GB;
3280
+ const read2 = opts.freeBytes ?? statfsFreeBytes;
3281
+ for (let slot = 1; slot <= pool.size; slot++) {
3282
+ const dir = poolSlotPath(name, slot);
3283
+ const label = poolSlotLabel(name, slot);
3284
+ const meta = readSlotMeta(name, slot);
3285
+ if (!fs23.existsSync(dir)) {
3286
+ fs23.mkdirSync(poolsRoot(), { recursive: true });
3287
+ let free = Infinity;
3288
+ try {
3289
+ free = need > 0 ? read2(poolsRoot()) : Infinity;
3290
+ } catch {
3291
+ }
3292
+ if (free < need) {
3293
+ const error = `not created: ${formatGB(free)} free, needs ${formatGB(need)} ([limits].minFreeGB)`;
3294
+ writeSlotMeta(name, slot, { ...meta, error, errorAt: now2() });
3295
+ log(`pool ${label}: ${error}`);
3296
+ continue;
3297
+ }
3298
+ try {
3299
+ await createSlot(repo, dir);
3300
+ writeSlotMeta(name, slot, { readyAt: now2() });
3301
+ log(`pool ${label}: created at ${dir}`);
3302
+ } catch (err) {
3303
+ const error = `create failed: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`.slice(0, 300);
3304
+ writeSlotMeta(name, slot, { ...meta, error, errorAt: now2() });
3305
+ log(`pool ${label}: ${error}`);
3306
+ }
3307
+ continue;
3308
+ }
3309
+ if (trapIn(dir))
3310
+ continue;
3311
+ const held = acquireWorktreeLock(dir, WARM_ID, "work", "warm");
3312
+ if (held)
3313
+ continue;
3314
+ try {
3315
+ if (!meta.readyAt) {
3316
+ try {
3317
+ await runSetup(repo, dir);
3318
+ writeSlotMeta(name, slot, { readyAt: now2() });
3319
+ log(`pool ${label}: setup finished`);
3320
+ } catch (err) {
3321
+ const error = `setup failed: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`.slice(0, 300);
3322
+ writeSlotMeta(name, slot, { ...meta, error, errorAt: now2() });
3323
+ log(`pool ${label}: ${error}`);
3324
+ }
3325
+ continue;
3326
+ }
3327
+ const claim = readSlotClaim(name, slot);
3328
+ const out = readSlotOut(name, slot);
3329
+ if (!out && (!claim || claim.id === meta.verifiedClaim))
3330
+ continue;
3331
+ const why = await poolUnsafe(repo, dir);
3332
+ if (why) {
3333
+ if (!out) {
3334
+ takeOut(name, pool, slot, why, claim?.id);
3335
+ log(`pool ${label}: out of rotation \u2014 ${why}`);
3336
+ } else if (out.reason !== why) {
3337
+ takeOut(name, pool, slot, why, out.dispatch ?? claim?.id, false);
3338
+ }
3339
+ writeSlotMeta(name, slot, { ...meta, checkedAt: now2() });
3340
+ continue;
3341
+ }
3342
+ if (out) {
3343
+ clearSlotOut(name, slot);
3344
+ postNotice({ kind: "pool-returned", text: `pool worktree ${label} is back in rotation: clean and pushed`, refId: label, repo: pool.repo, quiet: true });
3345
+ log(`pool ${label}: back in rotation`);
3346
+ }
3347
+ writeSlotMeta(name, slot, { ...meta, ...claim ? { verifiedClaim: claim.id } : {}, checkedAt: now2() });
3348
+ } finally {
3349
+ releaseWorktreeLock(dir, WARM_ID);
3350
+ }
3351
+ }
3352
+ writePoolWarmState(name, { ...readPoolWarmState(name), finishedAt: now2() });
3353
+ return true;
3354
+ } finally {
3355
+ release();
3356
+ }
3357
+ }
3358
+
3359
+ // packages/worktree/dist/index.js
3360
+ var run2 = promisify2(execFile2);
3361
+ var shell = promisify2(exec);
2828
3362
  async function git(cwd, ...args) {
2829
- const { stdout } = await run("git", args, { cwd, env: process.env });
3363
+ const { stdout } = await run2("git", args, { cwd, env: process.env });
2830
3364
  return stdout.trim();
2831
3365
  }
2832
3366
  var LOCK_CONTENTION = [
@@ -2881,7 +3415,7 @@ ${gitStderr(err)}`, { cause: err });
2881
3415
  }
2882
3416
  var REPO_LOCK_WAIT_MS = 5 * 6e4;
2883
3417
  var repoQueues = /* @__PURE__ */ new Map();
2884
- function pidAlive(pid) {
3418
+ function pidAlive2(pid) {
2885
3419
  try {
2886
3420
  process.kill(pid, 0);
2887
3421
  return true;
@@ -2890,9 +3424,9 @@ function pidAlive(pid) {
2890
3424
  }
2891
3425
  }
2892
3426
  var BUSY = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
2893
- function takeRepoLock(file, mine) {
3427
+ function takeRepoLock(file2, mine) {
2894
3428
  try {
2895
- fs18.writeFileSync(file, mine, { flag: "wx" });
3429
+ fs24.writeFileSync(file2, mine, { flag: "wx" });
2896
3430
  return { state: "held" };
2897
3431
  } catch (err) {
2898
3432
  const code = err.code ?? "";
@@ -2902,8 +3436,8 @@ function takeRepoLock(file, mine) {
2902
3436
  let raw = "";
2903
3437
  let age = 0;
2904
3438
  try {
2905
- raw = fs18.readFileSync(file, "utf8");
2906
- age = Date.now() - fs18.statSync(file).mtimeMs;
3439
+ raw = fs24.readFileSync(file2, "utf8");
3440
+ age = Date.now() - fs24.statSync(file2).mtimeMs;
2907
3441
  } catch {
2908
3442
  return { state: "retry" };
2909
3443
  }
@@ -2913,31 +3447,31 @@ function takeRepoLock(file, mine) {
2913
3447
  } catch {
2914
3448
  holder = void 0;
2915
3449
  }
2916
- const stale = age > REPO_LOCK_WAIT_MS || (holder ? !pidAlive(holder.pid) : age > 5e3);
3450
+ const stale = age > REPO_LOCK_WAIT_MS || (holder ? !pidAlive2(holder.pid) : age > 5e3);
2917
3451
  if (stale) {
2918
3452
  try {
2919
- if (fs18.readFileSync(file, "utf8") === raw)
2920
- fs18.rmSync(file, { force: true });
3453
+ if (fs24.readFileSync(file2, "utf8") === raw)
3454
+ fs24.rmSync(file2, { force: true });
2921
3455
  } catch {
2922
3456
  }
2923
3457
  return { state: "retry" };
2924
3458
  }
2925
3459
  return { state: "wait", ...holder ? { holder: holder.pid } : {} };
2926
3460
  }
2927
- function releaseRepoLock(file, mine) {
3461
+ function releaseRepoLock(file2, mine) {
2928
3462
  try {
2929
- if (fs18.readFileSync(file, "utf8") === mine)
2930
- fs18.rmSync(file, { force: true, recursive: true, maxRetries: 5 });
3463
+ if (fs24.readFileSync(file2, "utf8") === mine)
3464
+ fs24.rmSync(file2, { force: true, recursive: true, maxRetries: 5 });
2931
3465
  } catch {
2932
3466
  }
2933
3467
  }
2934
3468
  async function withRepoLock(common, fn) {
2935
- const file = path15.join(common, "lobstah-git.lock");
3469
+ const file2 = path20.join(common, "lobstah-git.lock");
2936
3470
  const mine = JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() });
2937
3471
  const deadline = Date.now() + REPO_LOCK_WAIT_MS;
2938
3472
  let held = false;
2939
3473
  while (Date.now() < deadline) {
2940
- const took = takeRepoLock(file, mine);
3474
+ const took = takeRepoLock(file2, mine);
2941
3475
  if (took.state === "held") {
2942
3476
  held = true;
2943
3477
  break;
@@ -2949,18 +3483,18 @@ async function withRepoLock(common, fn) {
2949
3483
  await fn();
2950
3484
  } finally {
2951
3485
  if (held)
2952
- releaseRepoLock(file, mine);
3486
+ releaseRepoLock(file2, mine);
2953
3487
  }
2954
3488
  }
2955
3489
  async function oneAtATime(cwd, fn) {
2956
3490
  const common = await git(cwd, "rev-parse", "--path-format=absolute", "--git-common-dir");
2957
3491
  const key = realpath(common);
2958
- const run2 = (repoQueues.get(key) ?? Promise.resolve()).then(() => withRepoLock(common, () => fn(common)));
2959
- const tail = run2.catch(() => {
3492
+ const run3 = (repoQueues.get(key) ?? Promise.resolve()).then(() => withRepoLock(common, () => fn(common)));
3493
+ const tail = run3.catch(() => {
2960
3494
  });
2961
3495
  repoQueues.set(key, tail);
2962
3496
  try {
2963
- await run2;
3497
+ await run3;
2964
3498
  } finally {
2965
3499
  if (repoQueues.get(key) === tail)
2966
3500
  repoQueues.delete(key);
@@ -2968,7 +3502,7 @@ async function oneAtATime(cwd, fn) {
2968
3502
  }
2969
3503
  function fetchStamps(common) {
2970
3504
  try {
2971
- return JSON.parse(fs18.readFileSync(path15.join(common, "lobstah-fetch.json"), "utf8"));
3505
+ return JSON.parse(fs24.readFileSync(path20.join(common, "lobstah-fetch.json"), "utf8"));
2972
3506
  } catch {
2973
3507
  return {};
2974
3508
  }
@@ -2982,18 +3516,18 @@ async function fetchShared(cwd, remote, ref) {
2982
3516
  const started = Date.now();
2983
3517
  await gitShared(cwd, "fetch", remote, ref);
2984
3518
  const stamps = { ...fetchStamps(common), [key]: started };
2985
- fs18.writeFileSync(path15.join(common, "lobstah-fetch.json"), JSON.stringify(stamps, null, 2));
3519
+ fs24.writeFileSync(path20.join(common, "lobstah-fetch.json"), JSON.stringify(stamps, null, 2));
2986
3520
  });
2987
3521
  }
2988
3522
  async function allocate(repo, id, fromRemoteBranch = repo.trunk) {
2989
3523
  const dir = worktreePath(id);
2990
- if (fs18.existsSync(dir)) {
3524
+ if (fs24.existsSync(dir)) {
2991
3525
  throw new Error(`worktree for ${id} already exists at ${dir} \u2014 never allocate a second`);
2992
3526
  }
2993
- if (!fs18.existsSync(repo.path)) {
3527
+ if (!fs24.existsSync(repo.path)) {
2994
3528
  if (!repo.origin)
2995
3529
  throw new Error(`repo path ${repo.path} missing and no origin configured`);
2996
- await run("git", ["clone", repo.origin, repo.path], { env: process.env });
3530
+ await run2("git", ["clone", repo.origin, repo.path], { env: process.env });
2997
3531
  }
2998
3532
  await fetchShared(repo.path, "origin", fromRemoteBranch);
2999
3533
  await oneAtATime(repo.path, async () => {
@@ -3005,7 +3539,7 @@ async function allocate(repo, id, fromRemoteBranch = repo.trunk) {
3005
3539
  async function recoverWorktree(repo, id) {
3006
3540
  const dir = worktreePath(id);
3007
3541
  try {
3008
- if (fs18.statSync(path15.join(dir, ".git")).isFile()) {
3542
+ if (fs24.statSync(path20.join(dir, ".git")).isFile()) {
3009
3543
  const branch = await git(dir, "symbolic-ref", "--quiet", "HEAD");
3010
3544
  const root = await git(dir, "rev-parse", "--show-toplevel");
3011
3545
  const common = await git(dir, "rev-parse", "--path-format=absolute", "--git-common-dir");
@@ -3043,38 +3577,42 @@ function setupHash(repo, dir) {
3043
3577
  const h = createHash("sha256");
3044
3578
  h.update(JSON.stringify(repo.setup ?? []));
3045
3579
  for (const name of LOCKFILES) {
3046
- const file = path15.join(dir, name);
3047
- if (!fs18.existsSync(file))
3580
+ const file2 = path20.join(dir, name);
3581
+ if (!fs24.existsSync(file2))
3048
3582
  continue;
3049
3583
  h.update(`\0${name}\0`);
3050
- h.update(fs18.readFileSync(file));
3584
+ h.update(fs24.readFileSync(file2));
3051
3585
  }
3052
3586
  return h.digest("hex");
3053
3587
  }
3054
3588
  function setupRecordFile(dir) {
3055
3589
  const gitDir = worktreeGitDir(dir);
3056
- return gitDir ? path15.join(gitDir, "lobstah-setup.json") : void 0;
3590
+ return gitDir ? path20.join(gitDir, "lobstah-setup.json") : void 0;
3057
3591
  }
3058
3592
  function recordSetup(repo, dir) {
3059
- const file = setupRecordFile(dir);
3060
- if (file)
3061
- fs18.writeFileSync(file, JSON.stringify({ hash: setupHash(repo, dir), at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
3593
+ const file2 = setupRecordFile(dir);
3594
+ if (file2)
3595
+ fs24.writeFileSync(file2, JSON.stringify({ hash: setupHash(repo, dir), at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
3062
3596
  }
3063
3597
  function recordedSetupHash(dir) {
3064
- const file = setupRecordFile(dir);
3598
+ const file2 = setupRecordFile(dir);
3065
3599
  try {
3066
- return file ? JSON.parse(fs18.readFileSync(file, "utf8")).hash : void 0;
3600
+ return file2 ? JSON.parse(fs24.readFileSync(file2, "utf8")).hash : void 0;
3067
3601
  } catch {
3068
3602
  return void 0;
3069
3603
  }
3070
3604
  }
3071
3605
  var short = (s) => s.slice(0, 8);
3606
+ function poolReused(slot, by) {
3607
+ const label = poolSlotLabel(slot.pool, slot.slot);
3608
+ return by ? `origin pool worktree ${label} was reused by ${short(by)}` : `origin pool worktree ${label} cannot be traced to it`;
3609
+ }
3072
3610
  function realpath(p) {
3073
3611
  let r;
3074
3612
  try {
3075
- r = fs18.realpathSync.native(p);
3613
+ r = fs24.realpathSync.native(p);
3076
3614
  } catch {
3077
- r = path15.resolve(p);
3615
+ r = path20.resolve(p);
3078
3616
  }
3079
3617
  return process.platform === "win32" ? r.toLowerCase() : r;
3080
3618
  }
@@ -3112,12 +3650,18 @@ async function chooseWorktree(input) {
3112
3650
  } catch {
3113
3651
  return { reuse: false, reason: "origin worktree is not a readable git checkout" };
3114
3652
  }
3653
+ const slot = poolSlotOf(found.path);
3654
+ if (slot) {
3655
+ const claim = readSlotClaim(slot.pool, slot.slot);
3656
+ if (claim?.id !== found.from && claim?.id !== id)
3657
+ return { reuse: false, reason: poolReused(slot, claim?.id) };
3658
+ }
3115
3659
  const here = realpath(found.path);
3116
3660
  for (const t of listTraps()) {
3117
3661
  if (typeof t.worktree !== "string")
3118
3662
  continue;
3119
- const rel = path15.relative(here, realpath(t.worktree));
3120
- if (!rel.startsWith("..") && !path15.isAbsolute(rel))
3663
+ const rel = path20.relative(here, realpath(t.worktree));
3664
+ if (!rel.startsWith("..") && !path20.isAbsolute(rel))
3121
3665
  return { reuse: false, reason: "origin worktree is a trap\u2019s" };
3122
3666
  }
3123
3667
  for (const l of ["work", "chore"]) {
@@ -3144,6 +3688,14 @@ async function chooseWorktree(input) {
3144
3688
  const held = acquireWorktreeLock(found.path, id, lane);
3145
3689
  if (held)
3146
3690
  return { reuse: false, reason: `origin worktree is in use by ${short(held.id)}` };
3691
+ if (slot) {
3692
+ const claim = readSlotClaim(slot.pool, slot.slot);
3693
+ if (claim?.id !== found.from && claim?.id !== id) {
3694
+ releaseWorktreeLock(found.path, id);
3695
+ return { reuse: false, reason: poolReused(slot, claim?.id) };
3696
+ }
3697
+ writeSlotClaim(slot.pool, slot.slot, { id, lane, at: (/* @__PURE__ */ new Date()).toISOString() });
3698
+ }
3147
3699
  return { reuse: true, path: found.path, owner: found.owner, from: found.from };
3148
3700
  }
3149
3701
  async function prepareReuse(repo, dir) {
@@ -3160,6 +3712,14 @@ async function collectEvidence(repo, dir) {
3160
3712
  const log = await git(dir, "log", "--oneline", `origin/${repo.trunk}..HEAD`);
3161
3713
  return { branch, commits: log ? log.split("\n") : [] };
3162
3714
  }
3715
+ async function tryGit(cwd, ...args) {
3716
+ try {
3717
+ const { stdout, stderr } = await run2("git", args, { cwd, env: process.env });
3718
+ return { ok: true, out: stdout.trimEnd(), err: stderr.trim() };
3719
+ } catch (e) {
3720
+ return { ok: false, out: (e.stdout ?? "").trim(), err: gitStderr(e) };
3721
+ }
3722
+ }
3163
3723
 
3164
3724
  // packages/runner/src/contract.ts
3165
3725
  function buildPrompt(brief, opts) {
@@ -3199,16 +3759,16 @@ async function* until(source, stop) {
3199
3759
  yield next.value;
3200
3760
  }
3201
3761
  }
3202
- var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
3203
- function deliver(run2, id, lane) {
3762
+ var sleep = (ms) => new Promise((resolve9) => setTimeout(resolve9, ms));
3763
+ function deliver(run3, id, lane) {
3204
3764
  const msgs = unhandled(id, lane);
3205
3765
  for (const m of msgs) {
3206
- run2.send(m.text);
3766
+ run3.send(m.text);
3207
3767
  acknowledge(id, lane, m.file);
3208
3768
  }
3209
3769
  return msgs.length;
3210
3770
  }
3211
- async function drive(run2, opts) {
3771
+ async function drive(run3, opts) {
3212
3772
  const {
3213
3773
  id,
3214
3774
  lane,
@@ -3229,12 +3789,12 @@ async function drive(run2, opts) {
3229
3789
  let cancelled = false;
3230
3790
  let activity = 0;
3231
3791
  let finished = false;
3232
- void run2.done.then(() => finished = true);
3792
+ void run3.done.then(() => finished = true);
3233
3793
  let killed = false;
3234
3794
  const kill = () => {
3235
3795
  if (killed) return;
3236
3796
  killed = true;
3237
- run2.kill();
3797
+ run3.kill();
3238
3798
  };
3239
3799
  const cancel = () => {
3240
3800
  cancelled = true;
@@ -3245,7 +3805,7 @@ async function drive(run2, opts) {
3245
3805
  let finalAt = 0;
3246
3806
  let exit;
3247
3807
  let abandon;
3248
- const abandoned = new Promise((resolve7) => abandon = resolve7);
3808
+ const abandoned = new Promise((resolve9) => abandon = resolve9);
3249
3809
  const clearExit = () => {
3250
3810
  if (!exit) return;
3251
3811
  clearInterval(exit.poll);
@@ -3273,7 +3833,7 @@ async function drive(run2, opts) {
3273
3833
  opts.onFinal?.();
3274
3834
  }
3275
3835
  finalAt = Date.now();
3276
- run2.end();
3836
+ run3.end();
3277
3837
  const poll = setInterval(() => {
3278
3838
  if (cancelRequested(id, lane)) stopHarness("cancel");
3279
3839
  }, Math.min(pollMs, exitGraceMs));
@@ -3290,7 +3850,7 @@ async function drive(run2, opts) {
3290
3850
  while (true) {
3291
3851
  if (stopped() || finished) return;
3292
3852
  if (cancelRequested(id, lane)) return cancel();
3293
- if (deliver(run2, id, lane) > 0) return answered(verb);
3853
+ if (deliver(run3, id, lane) > 0) return answered(verb);
3294
3854
  touchEvents(id, lane);
3295
3855
  await sleep(pollMs);
3296
3856
  }
@@ -3309,11 +3869,11 @@ async function drive(run2, opts) {
3309
3869
  if (!nudged) {
3310
3870
  nudged = true;
3311
3871
  runnerEvent({ nudged: "unreported" });
3312
- run2.send(unreportedNudge(id));
3872
+ run3.send(unreportedNudge(id));
3313
3873
  return;
3314
3874
  }
3315
3875
  appendStatus(id, lane, "failed", "ended without reporting a result, after being asked to");
3316
- run2.end();
3876
+ run3.end();
3317
3877
  };
3318
3878
  const holdOpen = (ms) => {
3319
3879
  releaseHold();
@@ -3332,7 +3892,7 @@ async function drive(run2, opts) {
3332
3892
  }, ms);
3333
3893
  hold = { heartbeat, expiry };
3334
3894
  };
3335
- for await (const ev of until(run2.events, abandoned)) {
3895
+ for await (const ev of until(run3.events, abandoned)) {
3336
3896
  record(ev);
3337
3897
  if (ev.type === "background") {
3338
3898
  liveBackground = Number(ev.data?.live ?? 0);
@@ -3362,7 +3922,7 @@ async function drive(run2, opts) {
3362
3922
  continue;
3363
3923
  }
3364
3924
  const waiting = lastVerb !== void 0 && WAITING_VERBS.includes(lastVerb);
3365
- if (deliver(run2, id, lane) > 0) {
3925
+ if (deliver(run3, id, lane) > 0) {
3366
3926
  if (waiting) answered(lastVerb);
3367
3927
  continue;
3368
3928
  }
@@ -3376,7 +3936,7 @@ async function drive(run2, opts) {
3376
3936
  }
3377
3937
  const subtype = ev.data?.subtype;
3378
3938
  if (subtype !== void 0 && subtype !== "success") {
3379
- run2.end();
3939
+ run3.end();
3380
3940
  continue;
3381
3941
  }
3382
3942
  if (liveBackground > 0) {
@@ -3526,18 +4086,18 @@ function startWallClock(opts) {
3526
4086
  }
3527
4087
 
3528
4088
  // packages/runner/src/remote.ts
3529
- import { execFile as execFile2 } from "node:child_process";
3530
- import { promisify as promisify2 } from "node:util";
4089
+ import { execFile as execFile3 } from "node:child_process";
4090
+ import { promisify as promisify3 } from "node:util";
3531
4091
  import { fileURLToPath } from "node:url";
3532
- import * as fs19 from "node:fs";
3533
- import * as path16 from "node:path";
3534
- var exec2 = promisify2(execFile2);
4092
+ import * as fs25 from "node:fs";
4093
+ import * as path21 from "node:path";
4094
+ var exec2 = promisify3(execFile3);
3535
4095
  function firstLine(err) {
3536
4096
  const stderr = err && typeof err === "object" && "stderr" in err ? String(err.stderr).trim() : "";
3537
4097
  return (stderr || (err instanceof Error ? err.message : String(err))).replace(/\s+/g, " ").slice(0, 240);
3538
4098
  }
3539
- function checkpointAllowed(file) {
3540
- const parts = file.toLowerCase().split("/");
4099
+ function checkpointAllowed(file2) {
4100
+ const parts = file2.toLowerCase().split("/");
3541
4101
  const base = parts.at(-1) ?? "";
3542
4102
  if (parts.some((p) => [".git", "node_modules", "dist", "build", "coverage", ".next", ".turbo"].includes(p))) return false;
3543
4103
  if (base === ".env" || base.startsWith(".env.") || base.endsWith(".pem") || base.endsWith(".key") || base.endsWith(".p12")) return false;
@@ -3547,23 +4107,23 @@ function checkpointAllowed(file) {
3547
4107
  function keepRemote(opts) {
3548
4108
  const { id, lane, cwd, trunk, policy, existingPrUrl } = opts;
3549
4109
  const command = async (bin, args, timeout = 12e4) => {
3550
- const file = bin === "gh" || bin === "lobstah" ? resolveOnPath(bin) ?? bin : bin;
3551
- return (await exec2(file, args, {
4110
+ const file2 = bin === "gh" || bin === "lobstah" ? resolveOnPath(bin) ?? bin : bin;
4111
+ return (await exec2(file2, args, {
3552
4112
  cwd,
3553
4113
  encoding: "utf8",
3554
4114
  timeout,
3555
4115
  maxBuffer: 4 * 1024 * 1024,
3556
- shell: process.platform === "win32" && /\.(?:cmd|bat)$/i.test(file)
4116
+ shell: process.platform === "win32" && /\.(?:cmd|bat)$/i.test(file2)
3557
4117
  })).stdout.trim();
3558
4118
  };
3559
4119
  const git2 = (...args) => command("git", args);
3560
4120
  const note = (message) => mergeEvidence(id, lane, { note: message.slice(0, 300) });
3561
4121
  if (existingPrUrl) mergeEvidence(id, lane, { prUrl: existingPrUrl });
3562
4122
  const registerWatch = async (url) => {
3563
- const root = path16.resolve(path16.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
3564
- const localCli = path16.join(root, "apps", "cli", "dist", "main.js");
4123
+ const root = path21.resolve(path21.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
4124
+ const localCli = path21.join(root, "apps", "cli", "dist", "main.js");
3565
4125
  const args = ["watch", "add", url, "--for", id];
3566
- if (fs19.existsSync(localCli)) await command(process.execPath, [localCli, ...args], 15e3);
4126
+ if (fs25.existsSync(localCli)) await command(process.execPath, [localCli, ...args], 15e3);
3567
4127
  else await command("lobstah", args, 15e3);
3568
4128
  };
3569
4129
  let lastSeenHead;
@@ -3718,13 +4278,15 @@ var defaultDeps = {
3718
4278
  chooseWorktree,
3719
4279
  prepareReuse,
3720
4280
  collectEvidence,
4281
+ claimPool: claimPoolSlot,
4282
+ resetPool: resetPoolWorktree,
3721
4283
  reap: async () => 0,
3722
4284
  prState: livePrState
3723
4285
  };
3724
4286
  var DONE_WAIT_MS = 2e3;
3725
4287
  function within(p, ms, fallback) {
3726
4288
  let timer;
3727
- const late = new Promise((resolve7) => timer = setTimeout(() => resolve7(fallback), ms));
4289
+ const late = new Promise((resolve9) => timer = setTimeout(() => resolve9(fallback), ms));
3728
4290
  return Promise.race([p, late]).finally(() => clearTimeout(timer));
3729
4291
  }
3730
4292
  var short3 = (s) => s.slice(0, 8);
@@ -3742,16 +4304,16 @@ function coldNote(cold, cwd, trunk) {
3742
4304
  }
3743
4305
  async function main(activeDir, lane, seams = {}) {
3744
4306
  const deps = { ...defaultDeps, ...seams };
3745
- const id = path17.basename(activeDir);
4307
+ const id = path22.basename(activeDir);
3746
4308
  if (isTrapCatch(id, lane)) throw new Error(`refusing headless runner for trap catch ${id}`);
3747
4309
  const status = (verb, note) => appendStatus(id, lane, verb, note);
3748
4310
  const cfg = loadConfig();
3749
- const descriptor = JSON.parse(fs20.readFileSync(path17.join(activeDir, "descriptor.json"), "utf8"));
4311
+ const descriptor = JSON.parse(fs26.readFileSync(path22.join(activeDir, "descriptor.json"), "utf8"));
3750
4312
  const resolved = resolveDispatch(descriptor, cfg);
3751
4313
  const repo = cfg.repos[descriptor.repo];
3752
- const briefFile = path17.join(activeDir, "brief.md");
3753
- if (!fs20.existsSync(briefFile)) fs20.writeFileSync(briefFile, descriptor.brief);
3754
- const brief = fs20.readFileSync(briefFile, "utf8");
4314
+ const briefFile = path22.join(activeDir, "brief.md");
4315
+ if (!fs26.existsSync(briefFile)) fs26.writeFileSync(briefFile, descriptor.brief);
4316
+ const brief = fs26.readFileSync(briefFile, "utf8");
3755
4317
  const attempts = Number(process.env.LOBSTAH_ATTEMPTS ?? "1");
3756
4318
  const wake = process.env.LOBSTAH_WAKE;
3757
4319
  const runnerInfo = {
@@ -3759,7 +4321,7 @@ async function main(activeDir, lane, seams = {}) {
3759
4321
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3760
4322
  attempts
3761
4323
  };
3762
- fs20.writeFileSync(path17.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
4324
+ fs26.writeFileSync(path22.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
3763
4325
  if (descriptor.systemRepair && descriptor.pr?.url) {
3764
4326
  const state = deps.prState(descriptor.pr.url) ?? readPr(parsePrRef(descriptor.pr.url)?.key ?? "")?.state;
3765
4327
  if (state === "MERGED" || state === "CLOSED") {
@@ -3776,20 +4338,46 @@ async function main(activeDir, lane, seams = {}) {
3776
4338
  resolvedHarness: resolved.harness,
3777
4339
  envResume: process.env.LOBSTAH_RESUME
3778
4340
  });
3779
- const wtFile = path17.join(activeDir, "worktree.json");
4341
+ const wtFile = path22.join(activeDir, "worktree.json");
3780
4342
  let recorded;
3781
4343
  let recovered = false;
3782
4344
  let choice3;
3783
- if (fs20.existsSync(wtFile)) {
3784
- recorded = JSON.parse(fs20.readFileSync(wtFile, "utf8")).path;
3785
- if (!fs20.existsSync(recorded)) throw new Error(`recorded worktree ${recorded} is gone`);
3786
- } else if (fs20.existsSync(worktreePath(id))) {
4345
+ if (fs26.existsSync(wtFile)) {
4346
+ recorded = JSON.parse(fs26.readFileSync(wtFile, "utf8")).path;
4347
+ if (!fs26.existsSync(recorded)) throw new Error(`recorded worktree ${recorded} is gone`);
4348
+ } else if (fs26.existsSync(worktreePath(id))) {
3787
4349
  recorded = await recoverWorktree(repo, id);
3788
4350
  recovered = true;
3789
4351
  } else if (descriptor.followUp && cfg.limits.reuseWorktree !== false) {
3790
4352
  choice3 = await deps.chooseWorktree({ id, lane, repoKey: descriptor.repo, repo, followUp: descriptor.followUp });
3791
4353
  }
3792
- const worktreeNote = choice3 ? choice3.reuse ? `reusing worktree of ${short3(choice3.from)}` : `fresh worktree (${choice3.reason})` : recovered ? "recovered own unrecorded worktree" : void 0;
4354
+ let poolClaim;
4355
+ let poolNote;
4356
+ if (!recorded && !choice3?.reuse && descriptor.pool) {
4357
+ const pool = cfg.pools[descriptor.pool];
4358
+ if (!pool) throw new Error(`unknown pool "${descriptor.pool}" \u2014 add [pools.${descriptor.pool}] to ${configPath()}`);
4359
+ if (pool.repo !== descriptor.repo) throw new Error(`pool ${descriptor.pool} serves repo ${pool.repo}, not ${descriptor.repo}`);
4360
+ const got = await deps.claimPool({ name: descriptor.pool, pool, repo, id, lane });
4361
+ if (got.claimed) {
4362
+ poolClaim = got;
4363
+ poolNote = `pool worktree ${got.label}`;
4364
+ } else if (pool.overflow === "queue") {
4365
+ if (cancelRequested(id, lane)) {
4366
+ status("failed", `cancelled by request while waiting for a pool ${descriptor.pool} worktree`);
4367
+ complete(id, lane);
4368
+ return;
4369
+ }
4370
+ requeue(id, lane);
4371
+ console.log(`[runner] pool ${descriptor.pool} has no free worktree: back in the queue`);
4372
+ return;
4373
+ } else {
4374
+ poolNote = `pool ${descriptor.pool} full${got.refused.length ? ` (${got.refused.join("; ")})` : ""}: cold worktree`;
4375
+ }
4376
+ }
4377
+ const worktreeNote = [
4378
+ choice3 ? choice3.reuse ? `reusing worktree of ${short3(choice3.from)}` : `fresh worktree (${choice3.reason})` : recovered ? "recovered own unrecorded worktree" : void 0,
4379
+ poolNote
4380
+ ].filter(Boolean).join("; ") || void 0;
3793
4381
  const firstModel = modelForHarness(plan.harness, resolved.model);
3794
4382
  const firstNote = [
3795
4383
  wake ? `woke from pause: ${wake}` : attempts > 1 ? `attempt ${attempts}` : void 0,
@@ -3812,7 +4400,7 @@ async function main(activeDir, lane, seams = {}) {
3812
4400
  if (held) throw new Error(`worktree at ${cwd} is in use by ${held.id} \u2014 wait for that dispatch to release it before retrying`);
3813
4401
  if (recovered) {
3814
4402
  await deps.prepareReuse(repo, cwd);
3815
- fs20.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
4403
+ fs26.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
3816
4404
  }
3817
4405
  } else if (choice3?.reuse) {
3818
4406
  cwd = choice3.path;
@@ -3823,14 +4411,19 @@ async function main(activeDir, lane, seams = {}) {
3823
4411
  type: "runner",
3824
4412
  data: { worktree: cwd, worktreeOf: choice3.owner, setupRan }
3825
4413
  });
3826
- fs20.writeFileSync(wtFile, JSON.stringify({ path: cwd, of: choice3.owner }, null, 2));
4414
+ fs26.writeFileSync(wtFile, JSON.stringify({ path: cwd, of: choice3.owner }, null, 2));
4415
+ } else if (poolClaim) {
4416
+ cwd = poolClaim.dir;
4417
+ await deps.resetPool(repo, cwd, id, descriptor.systemRepair ? descriptor.pr?.headRefName : void 0);
4418
+ fs26.writeFileSync(wtFile, JSON.stringify({ path: cwd, pool: poolClaim.label }, null, 2));
3827
4419
  } else {
3828
4420
  cwd = await deps.allocate(repo, id, descriptor.systemRepair ? descriptor.pr?.headRefName : void 0);
3829
4421
  acquireWorktreeLock(cwd, id, lane);
3830
- fs20.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
4422
+ fs26.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
3831
4423
  }
3832
- const worktreeOf = JSON.parse(fs20.readFileSync(wtFile, "utf8")).of;
3833
- mergeEvidence(id, lane, { worktree: cwd, worktreeOf });
4424
+ const worktreeOf = JSON.parse(fs26.readFileSync(wtFile, "utf8")).of;
4425
+ const inPool = poolSlotOf(cwd);
4426
+ mergeEvidence(id, lane, { worktree: cwd, worktreeOf, ...inPool ? { pool: poolSlotLabel(inPool.pool, inPool.slot) } : {} });
3834
4427
  const existingPr = boundPr(descriptor, lane);
3835
4428
  if (existingPr) mergeEvidence(id, lane, { prUrl: existingPr.url });
3836
4429
  const remotePolicy = {
@@ -3860,14 +4453,14 @@ async function main(activeDir, lane, seams = {}) {
3860
4453
  const envNudge = [process.env.LOBSTAH_NUDGE, wakeNote()].filter(Boolean).join("\n\n") || void 0;
3861
4454
  const planNudge = plan.cold && !envNudge ? coldNote(plan.cold, cwd, repo.trunk) : void 0;
3862
4455
  const promptWith = (nudge) => buildPrompt(brief, { id, nudge, attachments: descriptor.attachments, existingPr });
3863
- const binDir = path17.resolve(path17.dirname(fileURLToPath2(import.meta.url)), "..", "..", "..", "bin");
3864
- const workerPath = fs20.existsSync(path17.join(binDir, "lobstah")) ? `${binDir}${path17.delimiter}${process.env.PATH ?? ""}` : void 0;
4456
+ const binDir = path22.resolve(path22.dirname(fileURLToPath2(import.meta.url)), "..", "..", "..", "bin");
4457
+ const workerPath = fs26.existsSync(path22.join(binDir, "lobstah")) ? `${binDir}${path22.delimiter}${process.env.PATH ?? ""}` : void 0;
3865
4458
  let wallClockHit = false;
3866
4459
  let current;
3867
- const wallFile = path17.join(activeDir, "wallclock.json");
4460
+ const wallFile = path22.join(activeDir, "wallclock.json");
3868
4461
  const wallState = (() => {
3869
4462
  try {
3870
- return JSON.parse(fs20.readFileSync(wallFile, "utf8"));
4463
+ return JSON.parse(fs26.readFileSync(wallFile, "utf8"));
3871
4464
  } catch {
3872
4465
  return { elapsedMs: 0, windowMs: resolved.limits.wallClockSecs * 1e3 };
3873
4466
  }
@@ -3886,8 +4479,8 @@ async function main(activeDir, lane, seams = {}) {
3886
4479
  ) * 1e3;
3887
4480
  const persistWall = (elapsedMs, windowMs) => {
3888
4481
  const tmp = `${wallFile}.tmp-${process.pid}`;
3889
- fs20.writeFileSync(tmp, JSON.stringify({ ...wallState, elapsedMs, windowMs }));
3890
- fs20.renameSync(tmp, wallFile);
4482
+ fs26.writeFileSync(tmp, JSON.stringify({ ...wallState, elapsedMs, windowMs }));
4483
+ fs26.renameSync(tmp, wallFile);
3891
4484
  };
3892
4485
  const madeProgress = () => {
3893
4486
  const activity = readActivity(id, lane);
@@ -3939,7 +4532,7 @@ async function main(activeDir, lane, seams = {}) {
3939
4532
  model: explicit ? modelForHarness(harness, resolved.model).model : void 0,
3940
4533
  config: { effort: explicit ? resolved.effort : null, permissionMode: explicit && harness === "claude" ? "bypassPermissions" : null }
3941
4534
  }) });
3942
- const run2 = await deps.loadAdapter(harness).start({
4535
+ const run3 = await deps.loadAdapter(harness).start({
3943
4536
  id,
3944
4537
  cwd,
3945
4538
  prompt,
@@ -3954,8 +4547,8 @@ async function main(activeDir, lane, seams = {}) {
3954
4547
  flags: resolved.flags,
3955
4548
  resumeSession
3956
4549
  });
3957
- current = run2;
3958
- const driven = await drive(run2, {
4550
+ current = run3;
4551
+ const driven = await drive(run3, {
3959
4552
  id,
3960
4553
  lane,
3961
4554
  cwd,
@@ -3977,7 +4570,7 @@ async function main(activeDir, lane, seams = {}) {
3977
4570
  }
3978
4571
  });
3979
4572
  }
3980
- const result = driven.final || parked ? await within(run2.done, DONE_WAIT_MS, {}) : await run2.done;
4573
+ const result = driven.final || parked ? await within(run3.done, DONE_WAIT_MS, {}) : await run3.done;
3981
4574
  return { cancelled, activity, parked: parked === true, result };
3982
4575
  };
3983
4576
  let finalReport = false;
@@ -4144,13 +4737,13 @@ async function reapStarted(root = process.pid, graceMs) {
4144
4737
  var EXIT_LINGER_MS = 5e3;
4145
4738
  function runRunner(activeDirArg2, laneArg2) {
4146
4739
  const lane = laneArg2 === "chore" ? "chore" : "work";
4147
- const id = path18.basename(activeDirArg2);
4740
+ const id = path23.basename(activeDirArg2);
4148
4741
  main(activeDirArg2, lane, { reap: () => reapStarted(process.pid) }).catch((err) => {
4149
4742
  try {
4150
4743
  if (!isFinished(id, lane)) {
4151
4744
  appendStatus(id, lane, "failed", String(err instanceof Error ? err.message : err).slice(0, 500));
4152
4745
  }
4153
- fs21.renameSync(activeDirArg2, path18.join(laneDirs(lane).done, id));
4746
+ fs27.renameSync(activeDirArg2, path23.join(laneDirs(lane).done, id));
4154
4747
  } catch {
4155
4748
  }
4156
4749
  process.exitCode = 1;
@@ -4158,13 +4751,27 @@ function runRunner(activeDirArg2, laneArg2) {
4158
4751
  setTimeout(() => process.exit(), EXIT_LINGER_MS).unref();
4159
4752
  });
4160
4753
  }
4754
+ function runPoolWarm(name) {
4755
+ const log = (m) => console.log(`[pool-warm] ${(/* @__PURE__ */ new Date()).toISOString()} ${m}`);
4756
+ warmPool(name, { log }).then((ran) => {
4757
+ if (!ran) log(`pool ${name}: another warm-up is running`);
4758
+ }).catch((err) => {
4759
+ log(`pool ${name}: warm-up failed: ${err instanceof Error ? err.message : String(err)}`);
4760
+ process.exitCode = 1;
4761
+ }).finally(() => {
4762
+ setTimeout(() => process.exit(), EXIT_LINGER_MS).unref();
4763
+ });
4764
+ }
4161
4765
  var [activeDirArg, laneArg] = process.argv.slice(2);
4162
- if (activeDirArg && activeDirArg !== "__runner") runRunner(activeDirArg, laneArg);
4766
+ if (activeDirArg === "--pool-warm") {
4767
+ if (laneArg) runPoolWarm(laneArg);
4768
+ } else if (activeDirArg && activeDirArg !== "__runner" && activeDirArg !== "__pool-warm") runRunner(activeDirArg, laneArg);
4163
4769
  export {
4164
4770
  listProcesses,
4165
4771
  main,
4166
4772
  planStart,
4167
4773
  reapStarted,
4774
+ runPoolWarm,
4168
4775
  runRunner,
4169
4776
  startedBy,
4170
4777
  stopPids