lobstah 0.7.2 → 0.7.4

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