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