lobstah 0.7.1 → 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))
@@ -1290,7 +1324,7 @@ var DEFAULT_HELM = {
1290
1324
  armGraceSecs: 5
1291
1325
  };
1292
1326
  var DEFAULT_GLASS = { port: 4949 };
1293
- var DEFAULT_WATCH = { maxForksPerCycle: 3, autoRepair: true, conflicts: true, checks: true, maxRepairsPerPr: 2, maxRepairsWithoutProgress: 2, repairSettleSecs: 600, repairTrapWaitSecs: 600 };
1327
+ var DEFAULT_WATCH = { maxForksPerCycle: 3, autoRepair: true, conflicts: true, checks: true, maxRepairsPerPr: 2, maxRepairsWithoutProgress: 2, repairSettleSecs: 600, repairCooldownSecs: 0, repairTrapWaitSecs: 600 };
1294
1328
  var DEFAULT_LIMITS = {
1295
1329
  maxConcurrent: 2,
1296
1330
  choreConcurrent: 1,
@@ -1316,8 +1350,12 @@ 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")) : {};
1355
+ const watch = { ...DEFAULT_WATCH, ...raw.watch ?? {} };
1356
+ if (typeof watch.repairCooldownSecs !== "number" || !Number.isFinite(watch.repairCooldownSecs) || watch.repairCooldownSecs < 0) {
1357
+ throw new Error(`[watch].repairCooldownSecs must be a non-negative number in ${configPath()}`);
1358
+ }
1321
1359
  const readySettleSecs = raw.readySettleSecs ?? 600;
1322
1360
  if (typeof readySettleSecs !== "number" || !Number.isFinite(readySettleSecs) || readySettleSecs < 0) {
1323
1361
  throw new Error(`readySettleSecs must be a non-negative number in ${configPath()}`);
@@ -1338,7 +1376,8 @@ function loadConfig() {
1338
1376
  draftPr: r.draftPr === void 0 ? void 0 : Boolean(r.draftPr),
1339
1377
  checkpointOnStop: r.checkpointOnStop === void 0 ? void 0 : Boolean(r.checkpointOnStop),
1340
1378
  humanGateChecks: Array.isArray(r.humanGateChecks) ? r.humanGateChecks.map(String).filter(Boolean) : void 0,
1341
- briefHooks: parseBriefHooks(r.briefHooks)
1379
+ briefHooks: parseBriefHooks(r.briefHooks),
1380
+ poolKeep: Array.isArray(r.poolKeep) ? r.poolKeep.map(String).filter(Boolean) : void 0
1342
1381
  };
1343
1382
  }
1344
1383
  const groundsRaw = raw.grounds ?? {};
@@ -1353,8 +1392,9 @@ function loadConfig() {
1353
1392
  soak: parseSoak(raw.soak),
1354
1393
  helm: { ...DEFAULT_HELM, ...raw.helm ?? {} },
1355
1394
  glass: { ...DEFAULT_GLASS, ...raw.glass ?? {} },
1356
- watch: { ...DEFAULT_WATCH, ...raw.watch ?? {} },
1395
+ watch,
1357
1396
  grounds,
1397
+ pools: parsePools(raw.pools, repos),
1358
1398
  notifyCommand: raw.notifyCommand ? String(raw.notifyCommand) : void 0,
1359
1399
  notifyVerbs: Array.isArray(raw.notifyVerbs) ? raw.notifyVerbs.map(String) : void 0,
1360
1400
  remindSecs: raw.remindSecs !== void 0 ? Number(raw.remindSecs) : void 0,
@@ -1402,6 +1442,114 @@ function firstMeaningfulLine(...outputs) {
1402
1442
  return void 0;
1403
1443
  }
1404
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
+
1405
1553
  // packages/core/dist/pr.js
1406
1554
  function parsePrRef(s) {
1407
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());
@@ -1415,6 +1563,9 @@ var PR_VIEW_FIELDS = "title,state,isDraft,isCrossRepository,headRefOid,baseRefNa
1415
1563
  var PR_VIEW_FIELDS_NO_CHECKS = PR_VIEW_FIELDS.split(",").filter((f) => f !== "statusCheckRollup").join(",");
1416
1564
  var FORBIDDEN = /resource not accessible by (integration|personal access token)/i;
1417
1565
  function ghPrView(ref) {
1566
+ return cachedPrView(ref) ?? ghPrViewDirect(ref);
1567
+ }
1568
+ function ghPrViewDirect(ref) {
1418
1569
  const view1 = (fields2) => spawnSync("gh", ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", fields2], {
1419
1570
  encoding: "utf8",
1420
1571
  timeout: 6e4
@@ -1467,41 +1618,40 @@ function parseUnresolvedThreads(stdout) {
1467
1618
  }
1468
1619
  }
1469
1620
 
1470
- // packages/core/dist/prs.js
1471
- import * as fs8 from "node:fs";
1472
- import * as path6 from "node:path";
1473
- function prsDir() {
1474
- return path6.join(lobstahHome(), "prs");
1475
- }
1476
- function prRecordFile(key) {
1477
- const ref = parsePrRef(key);
1478
- if (!ref)
1479
- throw new Error(`not a PR key: ${key}`);
1480
- const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "_");
1481
- return path6.join(prsDir(), `${safe(ref.owner)}__${safe(ref.repo)}__${ref.number}.json`);
1482
- }
1483
- function readPr(key) {
1484
- try {
1485
- return JSON.parse(fs8.readFileSync(prRecordFile(key), "utf8"));
1486
- } catch {
1487
- return void 0;
1488
- }
1489
- }
1490
- function readPrs() {
1491
- let files;
1492
- try {
1493
- files = fs8.readdirSync(prsDir()).filter((f) => f.endsWith(".json"));
1494
- } catch {
1495
- return [];
1496
- }
1497
- 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, "_")}`);
1498
1631
  try {
1499
- const r = JSON.parse(fs8.readFileSync(path6.join(prsDir(), f), "utf8"));
1500
- return typeof r.key === "string" && typeof r.url === "string" ? [r] : [];
1632
+ fs12.writeFileSync(marker, "", { flag: "wx" });
1501
1633
  } catch {
1502
- return [];
1634
+ return void 0;
1503
1635
  }
1504
- });
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;
1505
1655
  }
1506
1656
 
1507
1657
  // packages/core/dist/attachments.js
@@ -1516,23 +1666,23 @@ function attachmentBlock(attachments) {
1516
1666
  }
1517
1667
 
1518
1668
  // packages/core/dist/worktrees.js
1519
- import * as fs9 from "node:fs";
1520
- import * as path7 from "node:path";
1669
+ import * as fs13 from "node:fs";
1670
+ import * as path11 from "node:path";
1521
1671
  var LANES = ["work", "chore"];
1522
1672
  function worktreePath(id) {
1523
- return path7.join(lobstahHome(), "worktrees", id);
1673
+ return path11.join(lobstahHome(), "worktrees", id);
1524
1674
  }
1525
- function readRecord(file) {
1675
+ function readRecord(file2) {
1526
1676
  try {
1527
- const r = JSON.parse(fs9.readFileSync(file, "utf8"));
1677
+ const r = JSON.parse(fs13.readFileSync(file2, "utf8"));
1528
1678
  return typeof r.path === "string" ? r : void 0;
1529
1679
  } catch {
1530
1680
  return void 0;
1531
1681
  }
1532
1682
  }
1533
1683
  function ownerOfPath(p) {
1534
- const rel = path7.relative(path7.join(lobstahHome(), "worktrees"), p);
1535
- 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))
1536
1686
  return void 0;
1537
1687
  return rel.split(/[\\/]/)[0];
1538
1688
  }
@@ -1545,7 +1695,7 @@ function dispatchWorktree(id, lane) {
1545
1695
  }
1546
1696
  const dirs = laneDirs(l);
1547
1697
  for (const dir of [dirs.active, dirs.done]) {
1548
- const rec = readRecord(path7.join(dir, id, "worktree.json"));
1698
+ const rec = readRecord(path11.join(dir, id, "worktree.json"));
1549
1699
  if (rec) {
1550
1700
  const owner = rec.of ?? ownerOfPath(rec.path) ?? id;
1551
1701
  return { path: rec.path, owner, reused: owner !== id };
@@ -1557,7 +1707,7 @@ function dispatchWorktree(id, lane) {
1557
1707
  function laneOf(id) {
1558
1708
  for (const l of LANES) {
1559
1709
  const d = laneDirs(l);
1560
- 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))) {
1561
1711
  return l;
1562
1712
  }
1563
1713
  }
@@ -1577,52 +1727,63 @@ function followUpAncestors(id, lane) {
1577
1727
  function chainWorktree(followUp) {
1578
1728
  for (const id of [followUp, ...followUpAncestors(followUp)]) {
1579
1729
  const wt = dispatchWorktree(id, laneOf(id));
1580
- if (fs9.existsSync(wt.path))
1730
+ if (fs13.existsSync(wt.path))
1581
1731
  return { ...wt, from: id };
1582
1732
  }
1583
1733
  return void 0;
1584
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
+ }
1585
1744
  function worktreeGitDir(wt) {
1586
- const dotGit = path7.join(wt, ".git");
1745
+ const dotGit = path11.join(wt, ".git");
1587
1746
  try {
1588
- const st = fs9.statSync(dotGit);
1747
+ const st = fs13.statSync(dotGit);
1589
1748
  if (st.isDirectory())
1590
1749
  return dotGit;
1591
- const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs9.readFileSync(dotGit, "utf8"));
1592
- 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;
1593
1752
  } catch {
1594
1753
  return void 0;
1595
1754
  }
1596
1755
  }
1597
1756
  function worktreeLockFile(wt) {
1598
1757
  const gitDir = worktreeGitDir(wt);
1599
- return gitDir ? path7.join(gitDir, "lobstah.lock") : void 0;
1758
+ return gitDir ? path11.join(gitDir, "lobstah.lock") : void 0;
1600
1759
  }
1601
1760
  function readWorktreeLock(wt) {
1602
- const file = worktreeLockFile(wt);
1603
- if (!file)
1761
+ const file2 = worktreeLockFile(wt);
1762
+ if (!file2)
1604
1763
  return void 0;
1605
1764
  try {
1606
- return JSON.parse(fs9.readFileSync(file, "utf8"));
1765
+ return JSON.parse(fs13.readFileSync(file2, "utf8"));
1607
1766
  } catch {
1608
1767
  return void 0;
1609
1768
  }
1610
1769
  }
1611
1770
  function lockIsLive(lock) {
1612
- 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));
1613
1774
  }
1614
1775
  function worktreeHolder(wt) {
1615
1776
  const lock = readWorktreeLock(wt);
1616
1777
  return lock && lockIsLive(lock) ? lock : void 0;
1617
1778
  }
1618
- function acquireWorktreeLock(wt, id, lane) {
1619
- const file = worktreeLockFile(wt);
1620
- const mine = { id, lane, pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() };
1621
- 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)
1622
1783
  return { id: "(no git dir)", lane, pid: 0, at: mine.at };
1623
1784
  for (let attempt = 0; attempt < 3; attempt++) {
1624
1785
  try {
1625
- fs9.writeFileSync(file, JSON.stringify(mine, null, 2), { flag: "wx" });
1786
+ fs13.writeFileSync(file2, JSON.stringify(mine, null, 2), { flag: "wx" });
1626
1787
  return void 0;
1627
1788
  } catch (err) {
1628
1789
  if (err.code !== "EEXIST")
@@ -1630,7 +1791,7 @@ function acquireWorktreeLock(wt, id, lane) {
1630
1791
  }
1631
1792
  let raw;
1632
1793
  try {
1633
- raw = fs9.readFileSync(file, "utf8");
1794
+ raw = fs13.readFileSync(file2, "utf8");
1634
1795
  } catch {
1635
1796
  continue;
1636
1797
  }
@@ -1641,14 +1802,14 @@ function acquireWorktreeLock(wt, id, lane) {
1641
1802
  held2 = void 0;
1642
1803
  }
1643
1804
  if (held2?.id === id) {
1644
- fs9.writeFileSync(file, JSON.stringify(mine, null, 2));
1805
+ fs13.writeFileSync(file2, JSON.stringify(mine, null, 2));
1645
1806
  return void 0;
1646
1807
  }
1647
1808
  if (held2 && lockIsLive(held2))
1648
1809
  return held2;
1649
1810
  try {
1650
- if (fs9.readFileSync(file, "utf8") === raw)
1651
- fs9.rmSync(file, { force: true });
1811
+ if (fs13.readFileSync(file2, "utf8") === raw)
1812
+ fs13.rmSync(file2, { force: true });
1652
1813
  } catch {
1653
1814
  }
1654
1815
  }
@@ -1656,60 +1817,205 @@ function acquireWorktreeLock(wt, id, lane) {
1656
1817
  return held ?? { id: "(contended)", lane, pid: 0, at: mine.at };
1657
1818
  }
1658
1819
  function releaseWorktreeLock(wt, id) {
1659
- const file = worktreeLockFile(wt);
1660
- if (!file)
1820
+ const file2 = worktreeLockFile(wt);
1821
+ if (!file2)
1661
1822
  return;
1662
1823
  if (readWorktreeLock(wt)?.id === id)
1663
- fs9.rmSync(file, { force: true });
1824
+ fs13.rmSync(file2, { force: true });
1664
1825
  }
1665
1826
 
1666
1827
  // packages/core/dist/disk.js
1828
+ import * as fs14 from "node:fs";
1667
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
+ }
1668
1837
 
1669
- // packages/core/dist/watch.js
1670
- import * as fs10 from "node:fs";
1671
- import * as path8 from "node:path";
1672
- function watchesDir() {
1673
- 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;
1674
1845
  }
1675
- function slug(key) {
1676
- 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);
1677
1849
  }
1678
- function watchPath(key) {
1679
- return path8.join(watchesDir(), `${slug(key)}.json`);
1850
+
1851
+ // packages/core/dist/pools.js
1852
+ function poolsRoot() {
1853
+ return path12.join(lobstahHome(), "pools");
1680
1854
  }
1681
- 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) {
1682
1887
  try {
1683
- return JSON.parse(fs10.readFileSync(watchPath(key), "utf8"));
1888
+ return JSON.parse(fs15.readFileSync(file2, "utf8"));
1684
1889
  } catch {
1685
1890
  return void 0;
1686
1891
  }
1687
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;
1688
1994
 
1689
1995
  // packages/core/dist/soak.js
1690
1996
  function regPath(trapId) {
1691
- return path9.join(soakingDir(), `${trapId}.json`);
1997
+ return path13.join(soakingDir(), `${trapId}.json`);
1692
1998
  }
1693
1999
  function readTrap(trapId) {
1694
2000
  try {
1695
- return JSON.parse(fs11.readFileSync(regPath(trapId), "utf8"));
2001
+ return JSON.parse(fs16.readFileSync(regPath(trapId), "utf8"));
1696
2002
  } catch {
1697
2003
  return void 0;
1698
2004
  }
1699
2005
  }
1700
2006
  function listTraps() {
1701
2007
  try {
1702
- 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");
1703
2009
  } catch {
1704
2010
  return [];
1705
2011
  }
1706
2012
  }
1707
2013
  function claimPath(id, lane) {
1708
- return path9.join(laneDirs(lane).active, id, "claim.json");
2014
+ return path13.join(laneDirs(lane).active, id, "claim.json");
1709
2015
  }
1710
2016
  function readSessionClaim(id, lane) {
1711
2017
  try {
1712
- return JSON.parse(fs11.readFileSync(claimPath(id, lane), "utf8"));
2018
+ return JSON.parse(fs16.readFileSync(claimPath(id, lane), "utf8"));
1713
2019
  } catch {
1714
2020
  return void 0;
1715
2021
  }
@@ -1718,7 +2024,7 @@ function readSessionClaim(id, lane) {
1718
2024
  // packages/core/dist/stats.js
1719
2025
  var DAILY_RETENTION_DAYS = 400;
1720
2026
  function statsPath() {
1721
- return path10.join(lobstahHome(), "stats.json");
2027
+ return path14.join(lobstahHome(), "stats.json");
1722
2028
  }
1723
2029
  function shiftDay(day, days) {
1724
2030
  const [y, m, d] = day.split("-").map(Number);
@@ -1731,7 +2037,7 @@ function localDay(at = Date.now()) {
1731
2037
  function readCatchFacts(stateDir, id) {
1732
2038
  let last;
1733
2039
  try {
1734
- 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")) {
1735
2041
  if (!line.trim())
1736
2042
  continue;
1737
2043
  try {
@@ -1747,7 +2053,7 @@ function readCatchFacts(stateDir, id) {
1747
2053
  let trap;
1748
2054
  let worker = workerProfile();
1749
2055
  try {
1750
- 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"));
1751
2057
  const to = evidence.deliveredTo;
1752
2058
  if (typeof to === "string" && to.startsWith("wt:"))
1753
2059
  trap = to;
@@ -1820,17 +2126,17 @@ function add(store, facts, now2) {
1820
2126
  }
1821
2127
  function backfillSources() {
1822
2128
  const live = ["work", "chore"].map((lane) => ({ dir: laneDirs(lane).state, live: true }));
1823
- 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());
1824
2130
  return [...live, ...backups];
1825
2131
  }
1826
2132
  function backfillStats(now2 = Date.now(), alreadyCounted) {
1827
2133
  const store = emptyStore(now2);
1828
2134
  const seen = /* @__PURE__ */ new Set();
1829
2135
  for (const { dir, live } of backfillSources()) {
1830
- for (const file of readDirIfPresent(dir).sort()) {
1831
- if (!file.endsWith(".status"))
2136
+ for (const file2 of readDirIfPresent(dir).sort()) {
2137
+ if (!file2.endsWith(".status"))
1832
2138
  continue;
1833
- const id = file.slice(0, -".status".length);
2139
+ const id = file2.slice(0, -".status".length);
1834
2140
  if (seen.has(id))
1835
2141
  continue;
1836
2142
  seen.add(id);
@@ -1852,10 +2158,10 @@ function backfillDaily(store, now2 = Date.now()) {
1852
2158
  const seen = /* @__PURE__ */ new Set();
1853
2159
  const into = { ...store, daily };
1854
2160
  for (const { dir, live } of backfillSources()) {
1855
- for (const file of readDirIfPresent(dir).sort()) {
1856
- if (!file.endsWith(".status"))
2161
+ for (const file2 of readDirIfPresent(dir).sort()) {
2162
+ if (!file2.endsWith(".status"))
1857
2163
  continue;
1858
- const id = file.slice(0, -".status".length);
2164
+ const id = file2.slice(0, -".status".length);
1859
2165
  if (seen.has(id))
1860
2166
  continue;
1861
2167
  seen.add(id);
@@ -1891,26 +2197,26 @@ function parseStore(text) {
1891
2197
  }
1892
2198
  var complete2 = (s) => !!s && s.daily !== void 0;
1893
2199
  function writeStore(store) {
1894
- const file = statsPath();
1895
- fs12.mkdirSync(path10.dirname(file), { recursive: true });
1896
- const tmp = `${file}.tmp-${process.pid}`;
1897
- fs12.writeFileSync(tmp, JSON.stringify(store));
1898
- 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);
1899
2205
  }
1900
2206
  function withStatsLock(action) {
1901
- fs12.mkdirSync(lobstahHome(), { recursive: true });
2207
+ fs17.mkdirSync(lobstahHome(), { recursive: true });
1902
2208
  const lock = `${statsPath()}.lock`;
1903
2209
  const deadline = Date.now() + 1e4;
1904
2210
  for (; ; ) {
1905
2211
  try {
1906
- fs12.mkdirSync(lock);
2212
+ fs17.mkdirSync(lock);
1907
2213
  break;
1908
2214
  } catch (err) {
1909
2215
  if (err.code !== "EEXIST")
1910
2216
  throw err;
1911
2217
  try {
1912
- if (Date.now() - fs12.statSync(lock).mtimeMs > 12e4)
1913
- fs12.rmdirSync(lock);
2218
+ if (Date.now() - fs17.statSync(lock).mtimeMs > 12e4)
2219
+ fs17.rmdirSync(lock);
1914
2220
  } catch {
1915
2221
  }
1916
2222
  if (Date.now() >= deadline)
@@ -1921,13 +2227,13 @@ function withStatsLock(action) {
1921
2227
  try {
1922
2228
  return action();
1923
2229
  } finally {
1924
- fs12.rmdirSync(lock);
2230
+ fs17.rmdirSync(lock);
1925
2231
  }
1926
2232
  }
1927
2233
  function loadOrBackfill(now2) {
1928
2234
  let text;
1929
2235
  try {
1930
- text = fs12.readFileSync(statsPath(), "utf8");
2236
+ text = fs17.readFileSync(statsPath(), "utf8");
1931
2237
  } catch (err) {
1932
2238
  if (err.code !== "ENOENT")
1933
2239
  throw err;
@@ -1954,7 +2260,7 @@ function loadOrBackfill(now2) {
1954
2260
  return store2;
1955
2261
  }
1956
2262
  if (text !== void 0)
1957
- fs12.renameSync(statsPath(), `${statsPath()}.unreadable-${now2}`);
2263
+ fs17.renameSync(statsPath(), `${statsPath()}.unreadable-${now2}`);
1958
2264
  const store = backfillStats(now2);
1959
2265
  writeStore(store);
1960
2266
  return store;
@@ -1997,58 +2303,58 @@ var ANSWER_TYPES = {
1997
2303
  var ANSWER_EXTENSIONS = Object.keys(ANSWER_TYPES);
1998
2304
 
1999
2305
  // packages/core/dist/inbox.js
2000
- import * as fs13 from "node:fs";
2001
- import * as path11 from "node:path";
2002
- 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");
2003
2309
  function inboxDir(id, lane) {
2004
- return path11.join(laneDirs(lane).inbox, id);
2310
+ return path15.join(laneDirs(lane).inbox, id);
2005
2311
  }
2006
2312
  function unhandled(id, lane) {
2007
2313
  const dir = inboxDir(id, lane);
2008
- 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") }));
2009
2315
  }
2010
- function acknowledge(id, lane, file) {
2316
+ function acknowledge(id, lane, file2) {
2011
2317
  const dir = inboxDir(id, lane);
2012
- fs13.mkdirSync(path11.join(dir, "handled"), { recursive: true });
2013
- 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));
2014
2320
  try {
2015
- 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)));
2016
2322
  } catch {
2017
2323
  }
2018
2324
  }
2019
2325
 
2020
2326
  // packages/core/dist/events.js
2021
- import * as fs14 from "node:fs";
2327
+ import * as fs19 from "node:fs";
2022
2328
  function appendEvent(id, lane, ev) {
2023
- fs14.appendFileSync(eventsPath(id, lane), `${JSON.stringify(ev)}
2329
+ fs19.appendFileSync(eventsPath(id, lane), `${JSON.stringify(ev)}
2024
2330
  `);
2025
2331
  }
2026
2332
  function touchEvents(id, lane, at = /* @__PURE__ */ new Date()) {
2027
- const file = eventsPath(id, lane);
2333
+ const file2 = eventsPath(id, lane);
2028
2334
  try {
2029
- fs14.utimesSync(file, at, at);
2335
+ fs19.utimesSync(file2, at, at);
2030
2336
  } catch {
2031
- fs14.appendFileSync(file, "");
2032
- fs14.utimesSync(file, at, at);
2337
+ fs19.appendFileSync(file2, "");
2338
+ fs19.utimesSync(file2, at, at);
2033
2339
  }
2034
2340
  }
2035
2341
 
2036
2342
  // packages/core/dist/harness.js
2037
- import * as fs15 from "node:fs";
2038
- import * as path12 from "node:path";
2343
+ import * as fs20 from "node:fs";
2344
+ import * as path16 from "node:path";
2039
2345
  import { createRequire } from "node:module";
2040
2346
  function resolveOnPath(bin, pathVar = process.env.PATH) {
2041
2347
  if (!pathVar)
2042
2348
  return void 0;
2043
2349
  const names = process.platform === "win32" ? [bin, `${bin}.exe`, `${bin}.cmd`, `${bin}.bat`] : [bin];
2044
- for (const dir of pathVar.split(path12.delimiter)) {
2350
+ for (const dir of pathVar.split(path16.delimiter)) {
2045
2351
  if (!dir)
2046
2352
  continue;
2047
2353
  for (const name of names) {
2048
- const candidate = path12.join(dir, name);
2354
+ const candidate = path16.join(dir, name);
2049
2355
  try {
2050
- fs15.accessSync(candidate, fs15.constants.X_OK);
2051
- if (fs15.statSync(candidate).isFile())
2356
+ fs20.accessSync(candidate, fs20.constants.X_OK);
2357
+ if (fs20.statSync(candidate).isFile())
2052
2358
  return candidate;
2053
2359
  } catch {
2054
2360
  }
@@ -2069,8 +2375,8 @@ function codexInvocation(argv) {
2069
2375
  } catch {
2070
2376
  }
2071
2377
  for (const dir of require_.resolve.paths("@openai/codex") ?? []) {
2072
- const cli = path12.join(dir, "@openai", "codex", "bin", "codex.js");
2073
- if (fs15.existsSync(cli))
2378
+ const cli = path16.join(dir, "@openai", "codex", "bin", "codex.js");
2379
+ if (fs20.existsSync(cli))
2074
2380
  return { file: process.execPath, argv: [cli, ...argv] };
2075
2381
  }
2076
2382
  return void 0;
@@ -2123,9 +2429,9 @@ function pushRule(branch, id = "<dispatch id>", mode = "rebase", also = []) {
2123
2429
  }
2124
2430
 
2125
2431
  // packages/core/dist/resume.js
2126
- import * as fs16 from "node:fs";
2432
+ import * as fs21 from "node:fs";
2127
2433
  import * as os3 from "node:os";
2128
- import * as path13 from "node:path";
2434
+ import * as path17 from "node:path";
2129
2435
  import { spawnSync as spawnSync2 } from "node:child_process";
2130
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;
2131
2437
  function harnessFromSessionId(sessionId) {
@@ -2136,7 +2442,7 @@ function claimOf(id, lane) {
2136
2442
  const dirs = laneDirs(lane);
2137
2443
  for (const dir of [dirs.active, dirs.done]) {
2138
2444
  try {
2139
- 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"));
2140
2446
  } catch {
2141
2447
  }
2142
2448
  }
@@ -2186,7 +2492,7 @@ function isUnresumable(error) {
2186
2492
  return error !== void 0 && UNRESUMABLE.test(error);
2187
2493
  }
2188
2494
  function worktreeProgress(worktree, trunk) {
2189
- if (!worktree || !fs16.existsSync(worktree))
2495
+ if (!worktree || !fs21.existsSync(worktree))
2190
2496
  return "No worktree progress recorded.";
2191
2497
  const git2 = (...a) => spawnSync2("git", a, { cwd: worktree, encoding: "utf8" }).stdout?.trim() ?? "";
2192
2498
  const commits = trunk ? git2("log", "--oneline", `origin/${trunk}..HEAD`) : git2("log", "--oneline", "-15");
@@ -2219,44 +2525,44 @@ function handoffNote(fromHarness, progress, why) {
2219
2525
  ${progress}`;
2220
2526
  }
2221
2527
  function codexHome(env = process.env) {
2222
- return env.CODEX_HOME ?? path13.join(os3.homedir(), ".codex");
2528
+ return env.CODEX_HOME ?? path17.join(os3.homedir(), ".codex");
2223
2529
  }
2224
2530
  function codexRolloutFile(threadId, home = codexHome()) {
2225
2531
  const suffix = `-${threadId}.jsonl`;
2226
2532
  const walk = (dir, depth) => {
2227
2533
  let entries;
2228
2534
  try {
2229
- entries = fs16.readdirSync(dir, { withFileTypes: true });
2535
+ entries = fs21.readdirSync(dir, { withFileTypes: true });
2230
2536
  } catch {
2231
2537
  return void 0;
2232
2538
  }
2233
2539
  for (const e of entries) {
2234
2540
  if (e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(suffix))
2235
- return path13.join(dir, e.name);
2541
+ return path17.join(dir, e.name);
2236
2542
  }
2237
2543
  if (depth === 0)
2238
2544
  return void 0;
2239
2545
  for (const e of entries.filter((x) => x.isDirectory()).sort((a, b) => b.name.localeCompare(a.name))) {
2240
- const hit = walk(path13.join(dir, e.name), depth - 1);
2546
+ const hit = walk(path17.join(dir, e.name), depth - 1);
2241
2547
  if (hit)
2242
2548
  return hit;
2243
2549
  }
2244
2550
  return void 0;
2245
2551
  };
2246
- 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);
2247
2553
  }
2248
2554
  function codexDesktopThread(threadId, home = codexHome()) {
2249
- const file = codexRolloutFile(threadId, home);
2250
- if (!file)
2555
+ const file2 = codexRolloutFile(threadId, home);
2556
+ if (!file2)
2251
2557
  return void 0;
2252
2558
  let head = "";
2253
2559
  try {
2254
- const fd = fs16.openSync(file, "r");
2560
+ const fd = fs21.openSync(file2, "r");
2255
2561
  try {
2256
2562
  const buf = Buffer.alloc(4096);
2257
- 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");
2258
2564
  } finally {
2259
- fs16.closeSync(fd);
2565
+ fs21.closeSync(fd);
2260
2566
  }
2261
2567
  } catch {
2262
2568
  return void 0;
@@ -2286,18 +2592,9 @@ function droppedModelNote(harness, dropped) {
2286
2592
  return `model ${dropped.model} is a ${dropped.owner} model \u2014 dropped, using ${harness}'s default`;
2287
2593
  }
2288
2594
 
2289
- // packages/core/dist/slots.js
2290
- function isTrapCatch(id, lane) {
2291
- return readSessionClaim(id, lane)?.by.startsWith("wt:") ?? false;
2292
- }
2293
- function isFinished(id, lane) {
2294
- const verb = readStatusLog(id, lane).at(-1)?.verb;
2295
- return verb !== void 0 && TERMINAL_VERBS.includes(verb);
2296
- }
2297
-
2298
2595
  // packages/runner/src/run.ts
2299
- import * as fs20 from "node:fs";
2300
- import * as path17 from "node:path";
2596
+ import * as fs26 from "node:fs";
2597
+ import * as path22 from "node:path";
2301
2598
  import { execFileSync as execFileSync2 } from "node:child_process";
2302
2599
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2303
2600
 
@@ -2331,7 +2628,7 @@ var AsyncQueue = class {
2331
2628
  return Promise.resolve({ value: item, done: false });
2332
2629
  if (this.closed)
2333
2630
  return Promise.resolve({ value: void 0, done: true });
2334
- return new Promise((resolve7) => this.waiters.push(resolve7));
2631
+ return new Promise((resolve9) => this.waiters.push(resolve9));
2335
2632
  }
2336
2633
  };
2337
2634
  }
@@ -2356,7 +2653,7 @@ var InputGate = class {
2356
2653
  return msg;
2357
2654
  if (this.ended)
2358
2655
  return void 0;
2359
- await new Promise((resolve7) => this.waiter = resolve7);
2656
+ await new Promise((resolve9) => this.waiter = resolve9);
2360
2657
  this.waiter = null;
2361
2658
  }
2362
2659
  }
@@ -2501,20 +2798,20 @@ function startClaudeCli(invocation, opts) {
2501
2798
  }
2502
2799
  }
2503
2800
  });
2504
- const done = new Promise((resolve7) => {
2801
+ const done = new Promise((resolve9) => {
2505
2802
  child.on("error", (err) => {
2506
2803
  events.push({ at: now(), type: "error", data: { message: err.message } });
2507
2804
  events.close();
2508
- resolve7({ sessionId, error: err.message });
2805
+ resolve9({ sessionId, error: err.message });
2509
2806
  });
2510
2807
  child.on("exit", (code) => {
2511
2808
  events.close();
2512
2809
  if (code === 0)
2513
- resolve7({ sessionId });
2810
+ resolve9({ sessionId });
2514
2811
  else {
2515
2812
  const detail = stderrTail.trim().split("\n").at(-1);
2516
2813
  const error = `claude exited ${code}${detail ? ` \u2014 ${detail}` : ""}`;
2517
- resolve7({ sessionId, error });
2814
+ resolve9({ sessionId, error });
2518
2815
  }
2519
2816
  });
2520
2817
  });
@@ -2593,25 +2890,25 @@ function createClaudeAdapter() {
2593
2890
  }
2594
2891
 
2595
2892
  // packages/adapters/dist/codex.js
2596
- import * as fs17 from "node:fs";
2893
+ import * as fs22 from "node:fs";
2597
2894
  import * as os4 from "node:os";
2598
- import * as path14 from "node:path";
2895
+ import * as path18 from "node:path";
2599
2896
  import { spawn as spawn2 } from "node:child_process";
2600
2897
  function isolatedCodexHome() {
2601
- const dir = path14.join(lobstahHome(), "codex-home");
2602
- fs17.mkdirSync(dir, { recursive: true });
2603
- const auth = path14.join(dir, "auth.json");
2604
- const realAuth = path14.join(os4.homedir(), ".codex", "auth.json");
2605
- 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)) {
2606
2903
  try {
2607
- fs17.symlinkSync(realAuth, auth);
2904
+ fs22.symlinkSync(realAuth, auth);
2608
2905
  } catch {
2609
- fs17.copyFileSync(realAuth, auth);
2906
+ fs22.copyFileSync(realAuth, auth);
2610
2907
  }
2611
2908
  }
2612
- const cfg = path14.join(dir, "config.toml");
2613
- if (!fs17.existsSync(cfg)) {
2614
- 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");
2615
2912
  }
2616
2913
  return dir;
2617
2914
  }
@@ -2663,7 +2960,7 @@ function startCodexCli(invoke, opts) {
2663
2960
  let current;
2664
2961
  let killed = false;
2665
2962
  function runTurn(prompt, resumeThread) {
2666
- return new Promise((resolve7) => {
2963
+ return new Promise((resolve9) => {
2667
2964
  const inv = invoke(codexExecArgs(opts, prompt, resumeThread));
2668
2965
  const child = spawn2(inv.file, inv.argv, {
2669
2966
  cwd: opts.cwd,
@@ -2694,14 +2991,14 @@ function startCodexCli(invoke, opts) {
2694
2991
  }
2695
2992
  }
2696
2993
  });
2697
- child.on("error", (err) => resolve7(err.message));
2994
+ child.on("error", (err) => resolve9(err.message));
2698
2995
  child.on("exit", (code) => {
2699
2996
  if (turnError)
2700
- return resolve7(turnError);
2997
+ return resolve9(turnError);
2701
2998
  if (code === 0 || killed)
2702
- return resolve7(void 0);
2999
+ return resolve9(void 0);
2703
3000
  const detail = stderrTail.trim().split("\n").at(-1);
2704
- resolve7(`codex exited ${code}${detail ? ` \u2014 ${detail}` : ""}`);
3001
+ resolve9(`codex exited ${code}${detail ? ` \u2014 ${detail}` : ""}`);
2705
3002
  });
2706
3003
  });
2707
3004
  }
@@ -2814,15 +3111,256 @@ function loadAdapter(name) {
2814
3111
  }
2815
3112
 
2816
3113
  // packages/worktree/dist/index.js
2817
- import * as fs18 from "node:fs";
2818
- import * as path15 from "node:path";
3114
+ import * as fs24 from "node:fs";
3115
+ import * as path20 from "node:path";
2819
3116
  import { createHash } from "node:crypto";
2820
- 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";
2821
3124
  import { promisify } from "node:util";
2822
3125
  var run = promisify(execFile);
2823
- 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);
2824
3362
  async function git(cwd, ...args) {
2825
- const { stdout } = await run("git", args, { cwd, env: process.env });
3363
+ const { stdout } = await run2("git", args, { cwd, env: process.env });
2826
3364
  return stdout.trim();
2827
3365
  }
2828
3366
  var LOCK_CONTENTION = [
@@ -2877,7 +3415,7 @@ ${gitStderr(err)}`, { cause: err });
2877
3415
  }
2878
3416
  var REPO_LOCK_WAIT_MS = 5 * 6e4;
2879
3417
  var repoQueues = /* @__PURE__ */ new Map();
2880
- function pidAlive(pid) {
3418
+ function pidAlive2(pid) {
2881
3419
  try {
2882
3420
  process.kill(pid, 0);
2883
3421
  return true;
@@ -2886,9 +3424,9 @@ function pidAlive(pid) {
2886
3424
  }
2887
3425
  }
2888
3426
  var BUSY = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
2889
- function takeRepoLock(file, mine) {
3427
+ function takeRepoLock(file2, mine) {
2890
3428
  try {
2891
- fs18.writeFileSync(file, mine, { flag: "wx" });
3429
+ fs24.writeFileSync(file2, mine, { flag: "wx" });
2892
3430
  return { state: "held" };
2893
3431
  } catch (err) {
2894
3432
  const code = err.code ?? "";
@@ -2898,8 +3436,8 @@ function takeRepoLock(file, mine) {
2898
3436
  let raw = "";
2899
3437
  let age = 0;
2900
3438
  try {
2901
- raw = fs18.readFileSync(file, "utf8");
2902
- age = Date.now() - fs18.statSync(file).mtimeMs;
3439
+ raw = fs24.readFileSync(file2, "utf8");
3440
+ age = Date.now() - fs24.statSync(file2).mtimeMs;
2903
3441
  } catch {
2904
3442
  return { state: "retry" };
2905
3443
  }
@@ -2909,31 +3447,31 @@ function takeRepoLock(file, mine) {
2909
3447
  } catch {
2910
3448
  holder = void 0;
2911
3449
  }
2912
- 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);
2913
3451
  if (stale) {
2914
3452
  try {
2915
- if (fs18.readFileSync(file, "utf8") === raw)
2916
- fs18.rmSync(file, { force: true });
3453
+ if (fs24.readFileSync(file2, "utf8") === raw)
3454
+ fs24.rmSync(file2, { force: true });
2917
3455
  } catch {
2918
3456
  }
2919
3457
  return { state: "retry" };
2920
3458
  }
2921
3459
  return { state: "wait", ...holder ? { holder: holder.pid } : {} };
2922
3460
  }
2923
- function releaseRepoLock(file, mine) {
3461
+ function releaseRepoLock(file2, mine) {
2924
3462
  try {
2925
- if (fs18.readFileSync(file, "utf8") === mine)
2926
- 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 });
2927
3465
  } catch {
2928
3466
  }
2929
3467
  }
2930
3468
  async function withRepoLock(common, fn) {
2931
- const file = path15.join(common, "lobstah-git.lock");
3469
+ const file2 = path20.join(common, "lobstah-git.lock");
2932
3470
  const mine = JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() });
2933
3471
  const deadline = Date.now() + REPO_LOCK_WAIT_MS;
2934
3472
  let held = false;
2935
3473
  while (Date.now() < deadline) {
2936
- const took = takeRepoLock(file, mine);
3474
+ const took = takeRepoLock(file2, mine);
2937
3475
  if (took.state === "held") {
2938
3476
  held = true;
2939
3477
  break;
@@ -2945,18 +3483,18 @@ async function withRepoLock(common, fn) {
2945
3483
  await fn();
2946
3484
  } finally {
2947
3485
  if (held)
2948
- releaseRepoLock(file, mine);
3486
+ releaseRepoLock(file2, mine);
2949
3487
  }
2950
3488
  }
2951
3489
  async function oneAtATime(cwd, fn) {
2952
3490
  const common = await git(cwd, "rev-parse", "--path-format=absolute", "--git-common-dir");
2953
3491
  const key = realpath(common);
2954
- const run2 = (repoQueues.get(key) ?? Promise.resolve()).then(() => withRepoLock(common, () => fn(common)));
2955
- const tail = run2.catch(() => {
3492
+ const run3 = (repoQueues.get(key) ?? Promise.resolve()).then(() => withRepoLock(common, () => fn(common)));
3493
+ const tail = run3.catch(() => {
2956
3494
  });
2957
3495
  repoQueues.set(key, tail);
2958
3496
  try {
2959
- await run2;
3497
+ await run3;
2960
3498
  } finally {
2961
3499
  if (repoQueues.get(key) === tail)
2962
3500
  repoQueues.delete(key);
@@ -2964,7 +3502,7 @@ async function oneAtATime(cwd, fn) {
2964
3502
  }
2965
3503
  function fetchStamps(common) {
2966
3504
  try {
2967
- 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"));
2968
3506
  } catch {
2969
3507
  return {};
2970
3508
  }
@@ -2978,18 +3516,18 @@ async function fetchShared(cwd, remote, ref) {
2978
3516
  const started = Date.now();
2979
3517
  await gitShared(cwd, "fetch", remote, ref);
2980
3518
  const stamps = { ...fetchStamps(common), [key]: started };
2981
- 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));
2982
3520
  });
2983
3521
  }
2984
3522
  async function allocate(repo, id, fromRemoteBranch = repo.trunk) {
2985
3523
  const dir = worktreePath(id);
2986
- if (fs18.existsSync(dir)) {
3524
+ if (fs24.existsSync(dir)) {
2987
3525
  throw new Error(`worktree for ${id} already exists at ${dir} \u2014 never allocate a second`);
2988
3526
  }
2989
- if (!fs18.existsSync(repo.path)) {
3527
+ if (!fs24.existsSync(repo.path)) {
2990
3528
  if (!repo.origin)
2991
3529
  throw new Error(`repo path ${repo.path} missing and no origin configured`);
2992
- await run("git", ["clone", repo.origin, repo.path], { env: process.env });
3530
+ await run2("git", ["clone", repo.origin, repo.path], { env: process.env });
2993
3531
  }
2994
3532
  await fetchShared(repo.path, "origin", fromRemoteBranch);
2995
3533
  await oneAtATime(repo.path, async () => {
@@ -3001,7 +3539,7 @@ async function allocate(repo, id, fromRemoteBranch = repo.trunk) {
3001
3539
  async function recoverWorktree(repo, id) {
3002
3540
  const dir = worktreePath(id);
3003
3541
  try {
3004
- if (fs18.statSync(path15.join(dir, ".git")).isFile()) {
3542
+ if (fs24.statSync(path20.join(dir, ".git")).isFile()) {
3005
3543
  const branch = await git(dir, "symbolic-ref", "--quiet", "HEAD");
3006
3544
  const root = await git(dir, "rev-parse", "--show-toplevel");
3007
3545
  const common = await git(dir, "rev-parse", "--path-format=absolute", "--git-common-dir");
@@ -3039,38 +3577,42 @@ function setupHash(repo, dir) {
3039
3577
  const h = createHash("sha256");
3040
3578
  h.update(JSON.stringify(repo.setup ?? []));
3041
3579
  for (const name of LOCKFILES) {
3042
- const file = path15.join(dir, name);
3043
- if (!fs18.existsSync(file))
3580
+ const file2 = path20.join(dir, name);
3581
+ if (!fs24.existsSync(file2))
3044
3582
  continue;
3045
3583
  h.update(`\0${name}\0`);
3046
- h.update(fs18.readFileSync(file));
3584
+ h.update(fs24.readFileSync(file2));
3047
3585
  }
3048
3586
  return h.digest("hex");
3049
3587
  }
3050
3588
  function setupRecordFile(dir) {
3051
3589
  const gitDir = worktreeGitDir(dir);
3052
- return gitDir ? path15.join(gitDir, "lobstah-setup.json") : void 0;
3590
+ return gitDir ? path20.join(gitDir, "lobstah-setup.json") : void 0;
3053
3591
  }
3054
3592
  function recordSetup(repo, dir) {
3055
- const file = setupRecordFile(dir);
3056
- if (file)
3057
- 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));
3058
3596
  }
3059
3597
  function recordedSetupHash(dir) {
3060
- const file = setupRecordFile(dir);
3598
+ const file2 = setupRecordFile(dir);
3061
3599
  try {
3062
- return file ? JSON.parse(fs18.readFileSync(file, "utf8")).hash : void 0;
3600
+ return file2 ? JSON.parse(fs24.readFileSync(file2, "utf8")).hash : void 0;
3063
3601
  } catch {
3064
3602
  return void 0;
3065
3603
  }
3066
3604
  }
3067
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
+ }
3068
3610
  function realpath(p) {
3069
3611
  let r;
3070
3612
  try {
3071
- r = fs18.realpathSync.native(p);
3613
+ r = fs24.realpathSync.native(p);
3072
3614
  } catch {
3073
- r = path15.resolve(p);
3615
+ r = path20.resolve(p);
3074
3616
  }
3075
3617
  return process.platform === "win32" ? r.toLowerCase() : r;
3076
3618
  }
@@ -3108,12 +3650,18 @@ async function chooseWorktree(input) {
3108
3650
  } catch {
3109
3651
  return { reuse: false, reason: "origin worktree is not a readable git checkout" };
3110
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
+ }
3111
3659
  const here = realpath(found.path);
3112
3660
  for (const t of listTraps()) {
3113
3661
  if (typeof t.worktree !== "string")
3114
3662
  continue;
3115
- const rel = path15.relative(here, realpath(t.worktree));
3116
- if (!rel.startsWith("..") && !path15.isAbsolute(rel))
3663
+ const rel = path20.relative(here, realpath(t.worktree));
3664
+ if (!rel.startsWith("..") && !path20.isAbsolute(rel))
3117
3665
  return { reuse: false, reason: "origin worktree is a trap\u2019s" };
3118
3666
  }
3119
3667
  for (const l of ["work", "chore"]) {
@@ -3140,6 +3688,14 @@ async function chooseWorktree(input) {
3140
3688
  const held = acquireWorktreeLock(found.path, id, lane);
3141
3689
  if (held)
3142
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
+ }
3143
3699
  return { reuse: true, path: found.path, owner: found.owner, from: found.from };
3144
3700
  }
3145
3701
  async function prepareReuse(repo, dir) {
@@ -3156,6 +3712,14 @@ async function collectEvidence(repo, dir) {
3156
3712
  const log = await git(dir, "log", "--oneline", `origin/${repo.trunk}..HEAD`);
3157
3713
  return { branch, commits: log ? log.split("\n") : [] };
3158
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
+ }
3159
3723
 
3160
3724
  // packages/runner/src/contract.ts
3161
3725
  function buildPrompt(brief, opts) {
@@ -3195,16 +3759,16 @@ async function* until(source, stop) {
3195
3759
  yield next.value;
3196
3760
  }
3197
3761
  }
3198
- var sleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
3199
- function deliver(run2, id, lane) {
3762
+ var sleep = (ms) => new Promise((resolve9) => setTimeout(resolve9, ms));
3763
+ function deliver(run3, id, lane) {
3200
3764
  const msgs = unhandled(id, lane);
3201
3765
  for (const m of msgs) {
3202
- run2.send(m.text);
3766
+ run3.send(m.text);
3203
3767
  acknowledge(id, lane, m.file);
3204
3768
  }
3205
3769
  return msgs.length;
3206
3770
  }
3207
- async function drive(run2, opts) {
3771
+ async function drive(run3, opts) {
3208
3772
  const {
3209
3773
  id,
3210
3774
  lane,
@@ -3225,12 +3789,12 @@ async function drive(run2, opts) {
3225
3789
  let cancelled = false;
3226
3790
  let activity = 0;
3227
3791
  let finished = false;
3228
- void run2.done.then(() => finished = true);
3792
+ void run3.done.then(() => finished = true);
3229
3793
  let killed = false;
3230
3794
  const kill = () => {
3231
3795
  if (killed) return;
3232
3796
  killed = true;
3233
- run2.kill();
3797
+ run3.kill();
3234
3798
  };
3235
3799
  const cancel = () => {
3236
3800
  cancelled = true;
@@ -3241,7 +3805,7 @@ async function drive(run2, opts) {
3241
3805
  let finalAt = 0;
3242
3806
  let exit;
3243
3807
  let abandon;
3244
- const abandoned = new Promise((resolve7) => abandon = resolve7);
3808
+ const abandoned = new Promise((resolve9) => abandon = resolve9);
3245
3809
  const clearExit = () => {
3246
3810
  if (!exit) return;
3247
3811
  clearInterval(exit.poll);
@@ -3269,7 +3833,7 @@ async function drive(run2, opts) {
3269
3833
  opts.onFinal?.();
3270
3834
  }
3271
3835
  finalAt = Date.now();
3272
- run2.end();
3836
+ run3.end();
3273
3837
  const poll = setInterval(() => {
3274
3838
  if (cancelRequested(id, lane)) stopHarness("cancel");
3275
3839
  }, Math.min(pollMs, exitGraceMs));
@@ -3286,7 +3850,7 @@ async function drive(run2, opts) {
3286
3850
  while (true) {
3287
3851
  if (stopped() || finished) return;
3288
3852
  if (cancelRequested(id, lane)) return cancel();
3289
- if (deliver(run2, id, lane) > 0) return answered(verb);
3853
+ if (deliver(run3, id, lane) > 0) return answered(verb);
3290
3854
  touchEvents(id, lane);
3291
3855
  await sleep(pollMs);
3292
3856
  }
@@ -3305,11 +3869,11 @@ async function drive(run2, opts) {
3305
3869
  if (!nudged) {
3306
3870
  nudged = true;
3307
3871
  runnerEvent({ nudged: "unreported" });
3308
- run2.send(unreportedNudge(id));
3872
+ run3.send(unreportedNudge(id));
3309
3873
  return;
3310
3874
  }
3311
3875
  appendStatus(id, lane, "failed", "ended without reporting a result, after being asked to");
3312
- run2.end();
3876
+ run3.end();
3313
3877
  };
3314
3878
  const holdOpen = (ms) => {
3315
3879
  releaseHold();
@@ -3328,7 +3892,7 @@ async function drive(run2, opts) {
3328
3892
  }, ms);
3329
3893
  hold = { heartbeat, expiry };
3330
3894
  };
3331
- for await (const ev of until(run2.events, abandoned)) {
3895
+ for await (const ev of until(run3.events, abandoned)) {
3332
3896
  record(ev);
3333
3897
  if (ev.type === "background") {
3334
3898
  liveBackground = Number(ev.data?.live ?? 0);
@@ -3358,7 +3922,7 @@ async function drive(run2, opts) {
3358
3922
  continue;
3359
3923
  }
3360
3924
  const waiting = lastVerb !== void 0 && WAITING_VERBS.includes(lastVerb);
3361
- if (deliver(run2, id, lane) > 0) {
3925
+ if (deliver(run3, id, lane) > 0) {
3362
3926
  if (waiting) answered(lastVerb);
3363
3927
  continue;
3364
3928
  }
@@ -3372,7 +3936,7 @@ async function drive(run2, opts) {
3372
3936
  }
3373
3937
  const subtype = ev.data?.subtype;
3374
3938
  if (subtype !== void 0 && subtype !== "success") {
3375
- run2.end();
3939
+ run3.end();
3376
3940
  continue;
3377
3941
  }
3378
3942
  if (liveBackground > 0) {
@@ -3522,18 +4086,18 @@ function startWallClock(opts) {
3522
4086
  }
3523
4087
 
3524
4088
  // packages/runner/src/remote.ts
3525
- import { execFile as execFile2 } from "node:child_process";
3526
- import { promisify as promisify2 } from "node:util";
4089
+ import { execFile as execFile3 } from "node:child_process";
4090
+ import { promisify as promisify3 } from "node:util";
3527
4091
  import { fileURLToPath } from "node:url";
3528
- import * as fs19 from "node:fs";
3529
- import * as path16 from "node:path";
3530
- var exec2 = promisify2(execFile2);
4092
+ import * as fs25 from "node:fs";
4093
+ import * as path21 from "node:path";
4094
+ var exec2 = promisify3(execFile3);
3531
4095
  function firstLine(err) {
3532
4096
  const stderr = err && typeof err === "object" && "stderr" in err ? String(err.stderr).trim() : "";
3533
4097
  return (stderr || (err instanceof Error ? err.message : String(err))).replace(/\s+/g, " ").slice(0, 240);
3534
4098
  }
3535
- function checkpointAllowed(file) {
3536
- const parts = file.toLowerCase().split("/");
4099
+ function checkpointAllowed(file2) {
4100
+ const parts = file2.toLowerCase().split("/");
3537
4101
  const base = parts.at(-1) ?? "";
3538
4102
  if (parts.some((p) => [".git", "node_modules", "dist", "build", "coverage", ".next", ".turbo"].includes(p))) return false;
3539
4103
  if (base === ".env" || base.startsWith(".env.") || base.endsWith(".pem") || base.endsWith(".key") || base.endsWith(".p12")) return false;
@@ -3543,23 +4107,23 @@ function checkpointAllowed(file) {
3543
4107
  function keepRemote(opts) {
3544
4108
  const { id, lane, cwd, trunk, policy, existingPrUrl } = opts;
3545
4109
  const command = async (bin, args, timeout = 12e4) => {
3546
- const file = bin === "gh" || bin === "lobstah" ? resolveOnPath(bin) ?? bin : bin;
3547
- return (await exec2(file, args, {
4110
+ const file2 = bin === "gh" || bin === "lobstah" ? resolveOnPath(bin) ?? bin : bin;
4111
+ return (await exec2(file2, args, {
3548
4112
  cwd,
3549
4113
  encoding: "utf8",
3550
4114
  timeout,
3551
4115
  maxBuffer: 4 * 1024 * 1024,
3552
- shell: process.platform === "win32" && /\.(?:cmd|bat)$/i.test(file)
4116
+ shell: process.platform === "win32" && /\.(?:cmd|bat)$/i.test(file2)
3553
4117
  })).stdout.trim();
3554
4118
  };
3555
4119
  const git2 = (...args) => command("git", args);
3556
4120
  const note = (message) => mergeEvidence(id, lane, { note: message.slice(0, 300) });
3557
4121
  if (existingPrUrl) mergeEvidence(id, lane, { prUrl: existingPrUrl });
3558
4122
  const registerWatch = async (url) => {
3559
- const root = path16.resolve(path16.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
3560
- 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");
3561
4125
  const args = ["watch", "add", url, "--for", id];
3562
- if (fs19.existsSync(localCli)) await command(process.execPath, [localCli, ...args], 15e3);
4126
+ if (fs25.existsSync(localCli)) await command(process.execPath, [localCli, ...args], 15e3);
3563
4127
  else await command("lobstah", args, 15e3);
3564
4128
  };
3565
4129
  let lastSeenHead;
@@ -3714,13 +4278,15 @@ var defaultDeps = {
3714
4278
  chooseWorktree,
3715
4279
  prepareReuse,
3716
4280
  collectEvidence,
4281
+ claimPool: claimPoolSlot,
4282
+ resetPool: resetPoolWorktree,
3717
4283
  reap: async () => 0,
3718
4284
  prState: livePrState
3719
4285
  };
3720
4286
  var DONE_WAIT_MS = 2e3;
3721
4287
  function within(p, ms, fallback) {
3722
4288
  let timer;
3723
- const late = new Promise((resolve7) => timer = setTimeout(() => resolve7(fallback), ms));
4289
+ const late = new Promise((resolve9) => timer = setTimeout(() => resolve9(fallback), ms));
3724
4290
  return Promise.race([p, late]).finally(() => clearTimeout(timer));
3725
4291
  }
3726
4292
  var short3 = (s) => s.slice(0, 8);
@@ -3738,16 +4304,16 @@ function coldNote(cold, cwd, trunk) {
3738
4304
  }
3739
4305
  async function main(activeDir, lane, seams = {}) {
3740
4306
  const deps = { ...defaultDeps, ...seams };
3741
- const id = path17.basename(activeDir);
4307
+ const id = path22.basename(activeDir);
3742
4308
  if (isTrapCatch(id, lane)) throw new Error(`refusing headless runner for trap catch ${id}`);
3743
4309
  const status = (verb, note) => appendStatus(id, lane, verb, note);
3744
4310
  const cfg = loadConfig();
3745
- 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"));
3746
4312
  const resolved = resolveDispatch(descriptor, cfg);
3747
4313
  const repo = cfg.repos[descriptor.repo];
3748
- const briefFile = path17.join(activeDir, "brief.md");
3749
- if (!fs20.existsSync(briefFile)) fs20.writeFileSync(briefFile, descriptor.brief);
3750
- 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");
3751
4317
  const attempts = Number(process.env.LOBSTAH_ATTEMPTS ?? "1");
3752
4318
  const wake = process.env.LOBSTAH_WAKE;
3753
4319
  const runnerInfo = {
@@ -3755,7 +4321,7 @@ async function main(activeDir, lane, seams = {}) {
3755
4321
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3756
4322
  attempts
3757
4323
  };
3758
- 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));
3759
4325
  if (descriptor.systemRepair && descriptor.pr?.url) {
3760
4326
  const state = deps.prState(descriptor.pr.url) ?? readPr(parsePrRef(descriptor.pr.url)?.key ?? "")?.state;
3761
4327
  if (state === "MERGED" || state === "CLOSED") {
@@ -3772,20 +4338,46 @@ async function main(activeDir, lane, seams = {}) {
3772
4338
  resolvedHarness: resolved.harness,
3773
4339
  envResume: process.env.LOBSTAH_RESUME
3774
4340
  });
3775
- const wtFile = path17.join(activeDir, "worktree.json");
4341
+ const wtFile = path22.join(activeDir, "worktree.json");
3776
4342
  let recorded;
3777
4343
  let recovered = false;
3778
4344
  let choice3;
3779
- if (fs20.existsSync(wtFile)) {
3780
- recorded = JSON.parse(fs20.readFileSync(wtFile, "utf8")).path;
3781
- if (!fs20.existsSync(recorded)) throw new Error(`recorded worktree ${recorded} is gone`);
3782
- } 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))) {
3783
4349
  recorded = await recoverWorktree(repo, id);
3784
4350
  recovered = true;
3785
4351
  } else if (descriptor.followUp && cfg.limits.reuseWorktree !== false) {
3786
4352
  choice3 = await deps.chooseWorktree({ id, lane, repoKey: descriptor.repo, repo, followUp: descriptor.followUp });
3787
4353
  }
3788
- 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;
3789
4381
  const firstModel = modelForHarness(plan.harness, resolved.model);
3790
4382
  const firstNote = [
3791
4383
  wake ? `woke from pause: ${wake}` : attempts > 1 ? `attempt ${attempts}` : void 0,
@@ -3808,7 +4400,7 @@ async function main(activeDir, lane, seams = {}) {
3808
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`);
3809
4401
  if (recovered) {
3810
4402
  await deps.prepareReuse(repo, cwd);
3811
- fs20.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
4403
+ fs26.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
3812
4404
  }
3813
4405
  } else if (choice3?.reuse) {
3814
4406
  cwd = choice3.path;
@@ -3819,14 +4411,19 @@ async function main(activeDir, lane, seams = {}) {
3819
4411
  type: "runner",
3820
4412
  data: { worktree: cwd, worktreeOf: choice3.owner, setupRan }
3821
4413
  });
3822
- 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));
3823
4419
  } else {
3824
4420
  cwd = await deps.allocate(repo, id, descriptor.systemRepair ? descriptor.pr?.headRefName : void 0);
3825
4421
  acquireWorktreeLock(cwd, id, lane);
3826
- fs20.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
4422
+ fs26.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
3827
4423
  }
3828
- const worktreeOf = JSON.parse(fs20.readFileSync(wtFile, "utf8")).of;
3829
- 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) } : {} });
3830
4427
  const existingPr = boundPr(descriptor, lane);
3831
4428
  if (existingPr) mergeEvidence(id, lane, { prUrl: existingPr.url });
3832
4429
  const remotePolicy = {
@@ -3856,14 +4453,14 @@ async function main(activeDir, lane, seams = {}) {
3856
4453
  const envNudge = [process.env.LOBSTAH_NUDGE, wakeNote()].filter(Boolean).join("\n\n") || void 0;
3857
4454
  const planNudge = plan.cold && !envNudge ? coldNote(plan.cold, cwd, repo.trunk) : void 0;
3858
4455
  const promptWith = (nudge) => buildPrompt(brief, { id, nudge, attachments: descriptor.attachments, existingPr });
3859
- const binDir = path17.resolve(path17.dirname(fileURLToPath2(import.meta.url)), "..", "..", "..", "bin");
3860
- 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;
3861
4458
  let wallClockHit = false;
3862
4459
  let current;
3863
- const wallFile = path17.join(activeDir, "wallclock.json");
4460
+ const wallFile = path22.join(activeDir, "wallclock.json");
3864
4461
  const wallState = (() => {
3865
4462
  try {
3866
- return JSON.parse(fs20.readFileSync(wallFile, "utf8"));
4463
+ return JSON.parse(fs26.readFileSync(wallFile, "utf8"));
3867
4464
  } catch {
3868
4465
  return { elapsedMs: 0, windowMs: resolved.limits.wallClockSecs * 1e3 };
3869
4466
  }
@@ -3882,8 +4479,8 @@ async function main(activeDir, lane, seams = {}) {
3882
4479
  ) * 1e3;
3883
4480
  const persistWall = (elapsedMs, windowMs) => {
3884
4481
  const tmp = `${wallFile}.tmp-${process.pid}`;
3885
- fs20.writeFileSync(tmp, JSON.stringify({ ...wallState, elapsedMs, windowMs }));
3886
- fs20.renameSync(tmp, wallFile);
4482
+ fs26.writeFileSync(tmp, JSON.stringify({ ...wallState, elapsedMs, windowMs }));
4483
+ fs26.renameSync(tmp, wallFile);
3887
4484
  };
3888
4485
  const madeProgress = () => {
3889
4486
  const activity = readActivity(id, lane);
@@ -3935,7 +4532,7 @@ async function main(activeDir, lane, seams = {}) {
3935
4532
  model: explicit ? modelForHarness(harness, resolved.model).model : void 0,
3936
4533
  config: { effort: explicit ? resolved.effort : null, permissionMode: explicit && harness === "claude" ? "bypassPermissions" : null }
3937
4534
  }) });
3938
- const run2 = await deps.loadAdapter(harness).start({
4535
+ const run3 = await deps.loadAdapter(harness).start({
3939
4536
  id,
3940
4537
  cwd,
3941
4538
  prompt,
@@ -3950,8 +4547,8 @@ async function main(activeDir, lane, seams = {}) {
3950
4547
  flags: resolved.flags,
3951
4548
  resumeSession
3952
4549
  });
3953
- current = run2;
3954
- const driven = await drive(run2, {
4550
+ current = run3;
4551
+ const driven = await drive(run3, {
3955
4552
  id,
3956
4553
  lane,
3957
4554
  cwd,
@@ -3973,7 +4570,7 @@ async function main(activeDir, lane, seams = {}) {
3973
4570
  }
3974
4571
  });
3975
4572
  }
3976
- 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;
3977
4574
  return { cancelled, activity, parked: parked === true, result };
3978
4575
  };
3979
4576
  let finalReport = false;
@@ -4140,13 +4737,13 @@ async function reapStarted(root = process.pid, graceMs) {
4140
4737
  var EXIT_LINGER_MS = 5e3;
4141
4738
  function runRunner(activeDirArg2, laneArg2) {
4142
4739
  const lane = laneArg2 === "chore" ? "chore" : "work";
4143
- const id = path18.basename(activeDirArg2);
4740
+ const id = path23.basename(activeDirArg2);
4144
4741
  main(activeDirArg2, lane, { reap: () => reapStarted(process.pid) }).catch((err) => {
4145
4742
  try {
4146
4743
  if (!isFinished(id, lane)) {
4147
4744
  appendStatus(id, lane, "failed", String(err instanceof Error ? err.message : err).slice(0, 500));
4148
4745
  }
4149
- fs21.renameSync(activeDirArg2, path18.join(laneDirs(lane).done, id));
4746
+ fs27.renameSync(activeDirArg2, path23.join(laneDirs(lane).done, id));
4150
4747
  } catch {
4151
4748
  }
4152
4749
  process.exitCode = 1;
@@ -4154,13 +4751,27 @@ function runRunner(activeDirArg2, laneArg2) {
4154
4751
  setTimeout(() => process.exit(), EXIT_LINGER_MS).unref();
4155
4752
  });
4156
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
+ }
4157
4765
  var [activeDirArg, laneArg] = process.argv.slice(2);
4158
- 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);
4159
4769
  export {
4160
4770
  listProcesses,
4161
4771
  main,
4162
4772
  planStart,
4163
4773
  reapStarted,
4774
+ runPoolWarm,
4164
4775
  runRunner,
4165
4776
  startedBy,
4166
4777
  stopPids