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