lobstah 0.6.8 → 0.6.10
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 +527 -424
- package/dist/runner.js +37 -11
- package/docs/configuration.md +2 -0
- package/docs/vocabulary.md +2 -2
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -1875,6 +1875,10 @@ function expandHome(p) {
|
|
|
1875
1875
|
function loadConfig() {
|
|
1876
1876
|
const file = configPath();
|
|
1877
1877
|
const raw = fs8.existsSync(file) ? parse(fs8.readFileSync(file, "utf8")) : {};
|
|
1878
|
+
const readySettleSecs = raw.readySettleSecs ?? 600;
|
|
1879
|
+
if (typeof readySettleSecs !== "number" || !Number.isFinite(readySettleSecs) || readySettleSecs < 0) {
|
|
1880
|
+
throw new Error(`readySettleSecs must be a non-negative number in ${configPath()}`);
|
|
1881
|
+
}
|
|
1878
1882
|
const reposRaw = raw.repos ?? {};
|
|
1879
1883
|
const repos = {};
|
|
1880
1884
|
for (const [key, r] of Object.entries(reposRaw)) {
|
|
@@ -1911,6 +1915,7 @@ function loadConfig() {
|
|
|
1911
1915
|
notifyCommand: raw.notifyCommand ? String(raw.notifyCommand) : void 0,
|
|
1912
1916
|
notifyVerbs: Array.isArray(raw.notifyVerbs) ? raw.notifyVerbs.map(String) : void 0,
|
|
1913
1917
|
remindSecs: raw.remindSecs !== void 0 ? Number(raw.remindSecs) : void 0,
|
|
1918
|
+
readySettleSecs,
|
|
1914
1919
|
attentionKinds: parseAttentionKinds(raw.attentionKinds)
|
|
1915
1920
|
};
|
|
1916
1921
|
}
|
|
@@ -1943,6 +1948,7 @@ var init_config = __esm({
|
|
|
1943
1948
|
DEFAULT_ATTENTION_KINDS = ["question", "decision", "pr:ready", "pr:review", "pr:conflict", "pr:checks"];
|
|
1944
1949
|
DEFAULT_SOAK = {
|
|
1945
1950
|
deferSecs: 90,
|
|
1951
|
+
claimIdleNoticeSecs: 180,
|
|
1946
1952
|
ttlSecs: 1800,
|
|
1947
1953
|
beat: true,
|
|
1948
1954
|
pausedTtlSecs: 86400,
|
|
@@ -2387,253 +2393,9 @@ var init_watch = __esm({
|
|
|
2387
2393
|
}
|
|
2388
2394
|
});
|
|
2389
2395
|
|
|
2390
|
-
// packages/core/dist/
|
|
2396
|
+
// packages/core/dist/activity.js
|
|
2391
2397
|
import * as fs11 from "node:fs";
|
|
2392
2398
|
import * as path11 from "node:path";
|
|
2393
|
-
function helmDir() {
|
|
2394
|
-
return path11.join(lobstahHome(), "helm");
|
|
2395
|
-
}
|
|
2396
|
-
function relievedDir() {
|
|
2397
|
-
return path11.join(helmDir(), ".relieved");
|
|
2398
|
-
}
|
|
2399
|
-
function helmPath(grounds) {
|
|
2400
|
-
return path11.join(helmDir(), `${grounds}.json`);
|
|
2401
|
-
}
|
|
2402
|
-
function atomicWrite(file, content) {
|
|
2403
|
-
fs11.mkdirSync(path11.dirname(file), { recursive: true });
|
|
2404
|
-
const tmp = `${file}.tmp-${process.pid}`;
|
|
2405
|
-
fs11.writeFileSync(tmp, content);
|
|
2406
|
-
fs11.renameSync(tmp, file);
|
|
2407
|
-
}
|
|
2408
|
-
function readHelm(grounds) {
|
|
2409
|
-
try {
|
|
2410
|
-
return JSON.parse(fs11.readFileSync(helmPath(grounds), "utf8"));
|
|
2411
|
-
} catch {
|
|
2412
|
-
return void 0;
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
function listHelms() {
|
|
2416
|
-
try {
|
|
2417
|
-
return fs11.readdirSync(helmDir()).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(fs11.readFileSync(path11.join(helmDir(), f), "utf8")));
|
|
2418
|
-
} catch {
|
|
2419
|
-
return [];
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
function helmOf(sessionId) {
|
|
2423
|
-
return listHelms().find((h) => h.sessionId === sessionId);
|
|
2424
|
-
}
|
|
2425
|
-
function liveHelms(ttlMs, now2 = Date.now()) {
|
|
2426
|
-
return listHelms().filter((h) => now2 - (Date.parse(h.heartbeatAt) || 0) <= ttlMs);
|
|
2427
|
-
}
|
|
2428
|
-
function helmGate(live, sessionId, grounds) {
|
|
2429
|
-
if (grounds !== void 0) {
|
|
2430
|
-
const holder = live.find((h) => h.grounds === grounds);
|
|
2431
|
-
if (holder && holder.sessionId !== sessionId) {
|
|
2432
|
-
return `grounds "${grounds}" is helmed by session ${holder.sessionId.slice(0, 8)} \u2014 its verbs are that session's alone. Run this from that session (--session), or take its helm (\`lobstah man helm --take\`).`;
|
|
2433
|
-
}
|
|
2434
|
-
if (holder)
|
|
2435
|
-
return void 0;
|
|
2436
|
-
}
|
|
2437
|
-
if (live.length === 0)
|
|
2438
|
-
return void 0;
|
|
2439
|
-
if (sessionId !== void 0 && live.some((h) => h.sessionId === sessionId))
|
|
2440
|
-
return void 0;
|
|
2441
|
-
const holders = live.map((h) => `${h.grounds}=${h.sessionId.slice(0, 8)}`).join(", ");
|
|
2442
|
-
return `the helm is claimed (${holders}) \u2014 this verb is reserved for the helm session. Run it there with --session <helm-session-id>, or take the helm (\`lobstah man helm --take\`).`;
|
|
2443
|
-
}
|
|
2444
|
-
function helmLabel(h) {
|
|
2445
|
-
if (h.label)
|
|
2446
|
-
return h.label;
|
|
2447
|
-
const where = h.cwd ? path11.basename(h.cwd) : void 0;
|
|
2448
|
-
if (!h.harness && !where)
|
|
2449
|
-
return `session ${h.sessionId.slice(0, 8)}`;
|
|
2450
|
-
return `${h.harness ?? "session"}${where ? ` @ ${where}` : ""}`;
|
|
2451
|
-
}
|
|
2452
|
-
function takeHelm(opts) {
|
|
2453
|
-
const now2 = opts.now ?? Date.now();
|
|
2454
|
-
const existing = readHelm(opts.grounds.name);
|
|
2455
|
-
if (existing && existing.sessionId !== opts.sessionId) {
|
|
2456
|
-
const fresh = now2 - (Date.parse(existing.heartbeatAt) || 0) <= opts.ttlMs;
|
|
2457
|
-
if (fresh && !opts.take)
|
|
2458
|
-
return { held: existing };
|
|
2459
|
-
atomicWrite(path11.join(relievedDir(), `${existing.sessionId}.json`), JSON.stringify({
|
|
2460
|
-
sessionId: existing.sessionId,
|
|
2461
|
-
grounds: opts.grounds.name,
|
|
2462
|
-
by: opts.sessionId,
|
|
2463
|
-
at: new Date(now2).toISOString()
|
|
2464
|
-
}, null, 2));
|
|
2465
|
-
}
|
|
2466
|
-
const iso = new Date(now2).toISOString();
|
|
2467
|
-
const same = existing?.sessionId === opts.sessionId;
|
|
2468
|
-
const live = existing !== void 0 && now2 - (Date.parse(existing.heartbeatAt) || 0) <= opts.ttlMs;
|
|
2469
|
-
const reg = {
|
|
2470
|
-
sessionId: opts.sessionId,
|
|
2471
|
-
grounds: opts.grounds.name,
|
|
2472
|
-
repos: opts.grounds.repos,
|
|
2473
|
-
signedOnAt: same ? existing.signedOnAt : iso,
|
|
2474
|
-
heartbeatAt: iso,
|
|
2475
|
-
// A new helm on empty grounds starts its cursor now: a notice delivered
|
|
2476
|
-
// to nobody while no helm was signed on is not news. A take from a live
|
|
2477
|
-
// helm, or the same session signing on again, keeps the cursor, so
|
|
2478
|
-
// nothing in flight is dropped.
|
|
2479
|
-
wakesFrom: same || live ? existing.wakesFrom : iso,
|
|
2480
|
-
// Identity refreshes on every sign-on; a re-sign without one keeps what
|
|
2481
|
-
// the registration already knows.
|
|
2482
|
-
harness: opts.identity?.harness ?? (same ? existing.harness : void 0),
|
|
2483
|
-
cwd: opts.identity?.cwd ?? (same ? existing.cwd : void 0),
|
|
2484
|
-
host: opts.identity?.host ?? (same ? existing.host : void 0),
|
|
2485
|
-
label: opts.identity?.label ?? (same ? existing.label : void 0),
|
|
2486
|
-
window: opts.identity?.window ?? (same ? existing.window : void 0),
|
|
2487
|
-
tookFrom: existing && existing.sessionId !== opts.sessionId ? { sessionId: existing.sessionId, at: iso } : existing?.tookFrom
|
|
2488
|
-
};
|
|
2489
|
-
atomicWrite(helmPath(opts.grounds.name), JSON.stringify(reg, null, 2));
|
|
2490
|
-
return { ok: reg };
|
|
2491
|
-
}
|
|
2492
|
-
function wakeFloorMs(h) {
|
|
2493
|
-
return h?.wakesFrom ? Date.parse(h.wakesFrom) || 0 : 0;
|
|
2494
|
-
}
|
|
2495
|
-
function noticeStands(n) {
|
|
2496
|
-
switch (n.kind) {
|
|
2497
|
-
case "disk-held":
|
|
2498
|
-
return readHold() !== void 0;
|
|
2499
|
-
case "watch-failing":
|
|
2500
|
-
case "watch-held":
|
|
2501
|
-
return listWatches().some((w) => (w.key === n.refId || w.owner === `dispatch:${n.refId}`) && (n.kind === "watch-failing" ? w.failingNoticed === true : true));
|
|
2502
|
-
case "trap-request":
|
|
2503
|
-
return n.refId !== void 0 && isOpenRequest(n.refId);
|
|
2504
|
-
default:
|
|
2505
|
-
return false;
|
|
2506
|
-
}
|
|
2507
|
-
}
|
|
2508
|
-
function noticeWakes(h) {
|
|
2509
|
-
const floor = wakeFloorMs(h);
|
|
2510
|
-
if (floor === 0)
|
|
2511
|
-
return void 0;
|
|
2512
|
-
return (n) => (Date.parse(n.at) || 0) >= floor || noticeStands(n);
|
|
2513
|
-
}
|
|
2514
|
-
function relieveHelm(sessionId) {
|
|
2515
|
-
const relieved = [];
|
|
2516
|
-
for (const h of listHelms()) {
|
|
2517
|
-
if (h.sessionId !== sessionId)
|
|
2518
|
-
continue;
|
|
2519
|
-
fs11.rmSync(helmPath(h.grounds), { force: true });
|
|
2520
|
-
relieved.push(h.grounds);
|
|
2521
|
-
}
|
|
2522
|
-
fs11.rmSync(path11.join(relievedDir(), `${sessionId}.json`), { force: true });
|
|
2523
|
-
return relieved;
|
|
2524
|
-
}
|
|
2525
|
-
function heartbeatHelm(sessionId, opts = {}) {
|
|
2526
|
-
const reg = helmOf(sessionId);
|
|
2527
|
-
if (!reg)
|
|
2528
|
-
return void 0;
|
|
2529
|
-
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
2530
|
-
const next = { ...reg, heartbeatAt: now2, ...opts.parked ? { parkedAt: now2 } : {} };
|
|
2531
|
-
atomicWrite(helmPath(reg.grounds), JSON.stringify(next, null, 2));
|
|
2532
|
-
return next;
|
|
2533
|
-
}
|
|
2534
|
-
function consumeRelievedNotice(sessionId) {
|
|
2535
|
-
const file = path11.join(relievedDir(), `${sessionId}.json`);
|
|
2536
|
-
try {
|
|
2537
|
-
const notice = JSON.parse(fs11.readFileSync(file, "utf8"));
|
|
2538
|
-
fs11.rmSync(file, { force: true });
|
|
2539
|
-
return notice;
|
|
2540
|
-
} catch {
|
|
2541
|
-
return void 0;
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
function groundsList(cfg) {
|
|
2545
|
-
const entries = Object.entries(cfg.grounds);
|
|
2546
|
-
if (entries.length === 0)
|
|
2547
|
-
return [{ name: "fleet", repos: Object.keys(cfg.repos) }];
|
|
2548
|
-
return entries.map(([name, g]) => ({ name, repos: g.repos }));
|
|
2549
|
-
}
|
|
2550
|
-
function groundsErrors(cfg) {
|
|
2551
|
-
const errors = [];
|
|
2552
|
-
const seen = /* @__PURE__ */ new Map();
|
|
2553
|
-
for (const [name, g] of Object.entries(cfg.grounds)) {
|
|
2554
|
-
for (const repo of g.repos) {
|
|
2555
|
-
if (cfg.repos[repo] === void 0)
|
|
2556
|
-
errors.push(`grounds.${name} names unknown repo "${repo}"`);
|
|
2557
|
-
const other = seen.get(repo);
|
|
2558
|
-
if (other !== void 0)
|
|
2559
|
-
errors.push(`repo "${repo}" is in both grounds.${other} and grounds.${name} \u2014 a repo belongs to at most one grounds`);
|
|
2560
|
-
else
|
|
2561
|
-
seen.set(repo, name);
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
2564
|
-
return errors;
|
|
2565
|
-
}
|
|
2566
|
-
function resolveGrounds(cfg, name) {
|
|
2567
|
-
const all = groundsList(cfg);
|
|
2568
|
-
if (name === void 0) {
|
|
2569
|
-
if (all.length === 1)
|
|
2570
|
-
return all[0];
|
|
2571
|
-
throw new Error(`multiple grounds configured \u2014 pass --grounds <${all.map((g) => g.name).join("|")}>`);
|
|
2572
|
-
}
|
|
2573
|
-
const found = all.find((g) => g.name === name);
|
|
2574
|
-
if (!found)
|
|
2575
|
-
throw new Error(`unknown grounds "${name}" (configured: ${all.map((g) => g.name).join(", ")})`);
|
|
2576
|
-
return found;
|
|
2577
|
-
}
|
|
2578
|
-
var isOpenRequest;
|
|
2579
|
-
var init_helm = __esm({
|
|
2580
|
-
"packages/core/dist/helm.js"() {
|
|
2581
|
-
"use strict";
|
|
2582
|
-
init_disk();
|
|
2583
|
-
init_paths();
|
|
2584
|
-
init_watch();
|
|
2585
|
-
init_requests();
|
|
2586
|
-
isOpenRequest = (id) => {
|
|
2587
|
-
const r = readRequest(id);
|
|
2588
|
-
return r !== void 0 && r.closedAt === void 0;
|
|
2589
|
-
};
|
|
2590
|
-
}
|
|
2591
|
-
});
|
|
2592
|
-
|
|
2593
|
-
// packages/core/dist/session-link.js
|
|
2594
|
-
function validSessionLink(value) {
|
|
2595
|
-
if (typeof value !== "string")
|
|
2596
|
-
return false;
|
|
2597
|
-
if (/[\x00-\x1f\x7f]/.test(value))
|
|
2598
|
-
return false;
|
|
2599
|
-
return /^claude:\/\/claude\.ai\/[A-Za-z0-9_/-]+$/.test(value) || /^vscode:\/\/anthropic\.claude-code\/open\?session=[A-Za-z0-9_-]+$/.test(value) || /^codex:\/\/threads\/[A-Za-z0-9-]+$/.test(value);
|
|
2600
|
-
}
|
|
2601
|
-
function linkMismatch(link, win) {
|
|
2602
|
-
if (!win)
|
|
2603
|
-
return void 0;
|
|
2604
|
-
const ep = win.entrypoint;
|
|
2605
|
-
const bundle = win.bundleId;
|
|
2606
|
-
if (link.startsWith("vscode://")) {
|
|
2607
|
-
if (ep !== void 0)
|
|
2608
|
-
return ep === "claude-vscode" ? void 0 : `a vscode:// link needs the VS Code extension; this session's entrypoint is ${ep}`;
|
|
2609
|
-
return bundle === void 0 || VSCODE_BUNDLE_RE.test(bundle) ? void 0 : `a vscode:// link needs the VS Code extension; this session runs in ${bundle}`;
|
|
2610
|
-
}
|
|
2611
|
-
if (link.startsWith("claude://")) {
|
|
2612
|
-
if (ep !== void 0) {
|
|
2613
|
-
return ep === "cli" || ep === "claude-vscode" || ep.startsWith("sdk-") ? `a claude:// link needs the Claude desktop app's Code tab; this session's entrypoint is ${ep}` : void 0;
|
|
2614
|
-
}
|
|
2615
|
-
if (bundle !== void 0 && bundle !== CLAUDE_DESKTOP_BUNDLE)
|
|
2616
|
-
return `a claude:// link needs the Claude desktop app; this session runs in ${bundle}`;
|
|
2617
|
-
return win.termProgram === "claude-desktop" ? "a claude:// link needs the Code tab; this session runs in the Claude desktop app's terminal" : void 0;
|
|
2618
|
-
}
|
|
2619
|
-
if (link.startsWith("codex://")) {
|
|
2620
|
-
return bundle === void 0 || bundle === CODEX_BUNDLE ? void 0 : `a codex:// link needs the Codex app; this session runs in ${bundle}`;
|
|
2621
|
-
}
|
|
2622
|
-
return void 0;
|
|
2623
|
-
}
|
|
2624
|
-
var VSCODE_BUNDLE_RE, CLAUDE_DESKTOP_BUNDLE, CODEX_BUNDLE;
|
|
2625
|
-
var init_session_link = __esm({
|
|
2626
|
-
"packages/core/dist/session-link.js"() {
|
|
2627
|
-
"use strict";
|
|
2628
|
-
VSCODE_BUNDLE_RE = /(?:VSCode|Cursor|windsurf)/;
|
|
2629
|
-
CLAUDE_DESKTOP_BUNDLE = "com.anthropic.claudefordesktop";
|
|
2630
|
-
CODEX_BUNDLE = "com.openai.codex";
|
|
2631
|
-
}
|
|
2632
|
-
});
|
|
2633
|
-
|
|
2634
|
-
// packages/core/dist/activity.js
|
|
2635
|
-
import * as fs12 from "node:fs";
|
|
2636
|
-
import * as path12 from "node:path";
|
|
2637
2399
|
function looksSecret(word) {
|
|
2638
2400
|
if (SECRET_PATTERNS.some((re) => re.test(word)))
|
|
2639
2401
|
return true;
|
|
@@ -2676,7 +2438,7 @@ function relativeTarget(p, root) {
|
|
|
2676
2438
|
if (!root)
|
|
2677
2439
|
return baseName(p);
|
|
2678
2440
|
const win = isWindowsPath(p) || isWindowsPath(root);
|
|
2679
|
-
const lib = win ?
|
|
2441
|
+
const lib = win ? path11.win32 : path11.posix;
|
|
2680
2442
|
if (!lib.isAbsolute(p))
|
|
2681
2443
|
return p.replace(/\\/g, "/");
|
|
2682
2444
|
const rel = lib.relative(win ? root : root.replace(/\\/g, "/"), p);
|
|
@@ -2743,18 +2505,18 @@ function activityFromEvent(ev, root) {
|
|
|
2743
2505
|
return void 0;
|
|
2744
2506
|
}
|
|
2745
2507
|
}
|
|
2746
|
-
function
|
|
2508
|
+
function atomicWrite(file, content) {
|
|
2747
2509
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
2748
|
-
|
|
2749
|
-
|
|
2510
|
+
fs11.writeFileSync(tmp, content);
|
|
2511
|
+
fs11.renameSync(tmp, file);
|
|
2750
2512
|
}
|
|
2751
2513
|
function writeActivity(id, lane, a) {
|
|
2752
2514
|
const safe = { at: a.at, kind: a.kind, summary: redactSummary(a.summary) };
|
|
2753
|
-
|
|
2515
|
+
atomicWrite(activityPath(id, lane), JSON.stringify(safe));
|
|
2754
2516
|
}
|
|
2755
2517
|
function readActivity(id, lane) {
|
|
2756
2518
|
try {
|
|
2757
|
-
const parsed = JSON.parse(
|
|
2519
|
+
const parsed = JSON.parse(fs11.readFileSync(activityPath(id, lane), "utf8"));
|
|
2758
2520
|
if (typeof parsed.at !== "string" || typeof parsed.summary !== "string")
|
|
2759
2521
|
return void 0;
|
|
2760
2522
|
if (!ACTIVITY_KINDS.includes(parsed.kind))
|
|
@@ -2865,7 +2627,7 @@ var init_activity = __esm({
|
|
|
2865
2627
|
});
|
|
2866
2628
|
|
|
2867
2629
|
// packages/core/dist/status.js
|
|
2868
|
-
import * as
|
|
2630
|
+
import * as fs12 from "node:fs";
|
|
2869
2631
|
function isVerb(v) {
|
|
2870
2632
|
return VERBS.includes(v);
|
|
2871
2633
|
}
|
|
@@ -2922,7 +2684,7 @@ function appendStatus(id, lane, verb, note, at, waiting, reported = false) {
|
|
|
2922
2684
|
...extra,
|
|
2923
2685
|
...reported ? { reported: true } : {}
|
|
2924
2686
|
};
|
|
2925
|
-
|
|
2687
|
+
fs12.appendFileSync(statusPath(id, lane), `${JSON.stringify(entry)}
|
|
2926
2688
|
`);
|
|
2927
2689
|
if (verb === "done") {
|
|
2928
2690
|
try {
|
|
@@ -2934,9 +2696,9 @@ function appendStatus(id, lane, verb, note, at, waiting, reported = false) {
|
|
|
2934
2696
|
}
|
|
2935
2697
|
function readStatusLog(id, lane) {
|
|
2936
2698
|
const file = statusPath(id, lane);
|
|
2937
|
-
if (!
|
|
2699
|
+
if (!fs12.existsSync(file))
|
|
2938
2700
|
return [];
|
|
2939
|
-
return
|
|
2701
|
+
return fs12.readFileSync(file, "utf8").split("\n").filter(Boolean).flatMap((line) => {
|
|
2940
2702
|
try {
|
|
2941
2703
|
const parsed = JSON.parse(line);
|
|
2942
2704
|
return isVerb(parsed.verb) ? [parsed] : [];
|
|
@@ -2994,12 +2756,12 @@ var init_status = __esm({
|
|
|
2994
2756
|
});
|
|
2995
2757
|
|
|
2996
2758
|
// packages/core/dist/queue.js
|
|
2997
|
-
import * as
|
|
2998
|
-
import * as
|
|
2999
|
-
function
|
|
2759
|
+
import * as fs13 from "node:fs";
|
|
2760
|
+
import * as path12 from "node:path";
|
|
2761
|
+
function atomicWrite2(file, content) {
|
|
3000
2762
|
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
3001
|
-
|
|
3002
|
-
|
|
2763
|
+
fs13.writeFileSync(tmp, content);
|
|
2764
|
+
fs13.renameSync(tmp, file);
|
|
3003
2765
|
}
|
|
3004
2766
|
function enqueue(d, lane = "work") {
|
|
3005
2767
|
if (!d.id || !d.repo || !d.brief) {
|
|
@@ -3011,11 +2773,11 @@ function enqueue(d, lane = "work") {
|
|
|
3011
2773
|
...inherited?.length ? { attachments: inherited } : {},
|
|
3012
2774
|
queuedAt: d.queuedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3013
2775
|
};
|
|
3014
|
-
|
|
2776
|
+
atomicWrite2(path12.join(laneDirs(lane).queue, `${d.id}.json`), JSON.stringify(descriptor, null, 2));
|
|
3015
2777
|
}
|
|
3016
2778
|
function mtimeIfPresent(file) {
|
|
3017
2779
|
try {
|
|
3018
|
-
return
|
|
2780
|
+
return fs13.statSync(file).mtimeMs;
|
|
3019
2781
|
} catch (e) {
|
|
3020
2782
|
if (e.code === "ENOENT")
|
|
3021
2783
|
return void 0;
|
|
@@ -3025,22 +2787,22 @@ function mtimeIfPresent(file) {
|
|
|
3025
2787
|
function pendingIds(lane) {
|
|
3026
2788
|
const dir = laneDirs(lane).queue;
|
|
3027
2789
|
return readDirIfPresent(dir).filter((f) => f.endsWith(".json")).flatMap((f) => {
|
|
3028
|
-
const m = mtimeIfPresent(
|
|
2790
|
+
const m = mtimeIfPresent(path12.join(dir, f));
|
|
3029
2791
|
return m === void 0 ? [] : [{ f, m }];
|
|
3030
2792
|
}).sort((a, b) => a.m - b.m).map(({ f }) => f.slice(0, -".json".length));
|
|
3031
2793
|
}
|
|
3032
2794
|
function queuedDescriptor(id, lane) {
|
|
3033
2795
|
try {
|
|
3034
|
-
return JSON.parse(
|
|
2796
|
+
return JSON.parse(fs13.readFileSync(path12.join(laneDirs(lane).queue, `${id}.json`), "utf8"));
|
|
3035
2797
|
} catch {
|
|
3036
2798
|
return void 0;
|
|
3037
2799
|
}
|
|
3038
2800
|
}
|
|
3039
2801
|
function queuedAt(id, lane) {
|
|
3040
|
-
const file =
|
|
2802
|
+
const file = path12.join(laneDirs(lane).queue, `${id}.json`);
|
|
3041
2803
|
let stat;
|
|
3042
2804
|
try {
|
|
3043
|
-
stat =
|
|
2805
|
+
stat = fs13.statSync(file);
|
|
3044
2806
|
} catch {
|
|
3045
2807
|
return void 0;
|
|
3046
2808
|
}
|
|
@@ -3055,17 +2817,17 @@ function claimNext(lane, skip) {
|
|
|
3055
2817
|
if (!d || skip(d))
|
|
3056
2818
|
continue;
|
|
3057
2819
|
}
|
|
3058
|
-
const activeDir =
|
|
2820
|
+
const activeDir = path12.join(dirs.active, id);
|
|
3059
2821
|
try {
|
|
3060
|
-
|
|
2822
|
+
fs13.mkdirSync(activeDir);
|
|
3061
2823
|
} catch {
|
|
3062
2824
|
continue;
|
|
3063
2825
|
}
|
|
3064
2826
|
try {
|
|
3065
|
-
|
|
2827
|
+
fs13.renameSync(path12.join(dirs.queue, `${id}.json`), path12.join(activeDir, "descriptor.json"));
|
|
3066
2828
|
return id;
|
|
3067
2829
|
} catch {
|
|
3068
|
-
|
|
2830
|
+
fs13.rmdirSync(activeDir, { recursive: true });
|
|
3069
2831
|
continue;
|
|
3070
2832
|
}
|
|
3071
2833
|
}
|
|
@@ -3073,19 +2835,19 @@ function claimNext(lane, skip) {
|
|
|
3073
2835
|
}
|
|
3074
2836
|
function requeue(id, lane) {
|
|
3075
2837
|
const dirs = laneDirs(lane);
|
|
3076
|
-
const activeDir =
|
|
3077
|
-
|
|
3078
|
-
|
|
2838
|
+
const activeDir = path12.join(dirs.active, id);
|
|
2839
|
+
fs13.renameSync(path12.join(activeDir, "descriptor.json"), path12.join(dirs.queue, `${id}.json`));
|
|
2840
|
+
fs13.rmSync(activeDir, { recursive: true, force: true });
|
|
3079
2841
|
}
|
|
3080
2842
|
function storedDescriptor(id, lane) {
|
|
3081
2843
|
const dirs = laneDirs(lane);
|
|
3082
2844
|
for (const file of [
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
2845
|
+
path12.join(dirs.queue, `${id}.json`),
|
|
2846
|
+
path12.join(dirs.active, id, "descriptor.json"),
|
|
2847
|
+
path12.join(dirs.done, id, "descriptor.json")
|
|
3086
2848
|
]) {
|
|
3087
2849
|
try {
|
|
3088
|
-
return JSON.parse(
|
|
2850
|
+
return JSON.parse(fs13.readFileSync(file, "utf8"));
|
|
3089
2851
|
} catch {
|
|
3090
2852
|
}
|
|
3091
2853
|
}
|
|
@@ -3096,30 +2858,30 @@ function activeIds(lane) {
|
|
|
3096
2858
|
}
|
|
3097
2859
|
function complete(id, lane) {
|
|
3098
2860
|
const dirs = laneDirs(lane);
|
|
3099
|
-
|
|
2861
|
+
fs13.renameSync(path12.join(dirs.active, id), path12.join(dirs.done, id));
|
|
3100
2862
|
}
|
|
3101
2863
|
function requestCancel(id, lane) {
|
|
3102
|
-
|
|
2864
|
+
fs13.writeFileSync(path12.join(laneDirs(lane).active, id, "cancel"), (/* @__PURE__ */ new Date()).toISOString());
|
|
3103
2865
|
}
|
|
3104
2866
|
function cancelQueued(id, lane) {
|
|
3105
2867
|
const dirs = laneDirs(lane);
|
|
3106
|
-
const doneDir =
|
|
2868
|
+
const doneDir = path12.join(dirs.done, id);
|
|
3107
2869
|
try {
|
|
3108
|
-
|
|
2870
|
+
fs13.mkdirSync(doneDir);
|
|
3109
2871
|
} catch {
|
|
3110
2872
|
return false;
|
|
3111
2873
|
}
|
|
3112
2874
|
try {
|
|
3113
|
-
|
|
2875
|
+
fs13.renameSync(path12.join(dirs.queue, `${id}.json`), path12.join(doneDir, "descriptor.json"));
|
|
3114
2876
|
} catch {
|
|
3115
|
-
|
|
2877
|
+
fs13.rmdirSync(doneDir, { recursive: true });
|
|
3116
2878
|
return false;
|
|
3117
2879
|
}
|
|
3118
2880
|
appendStatus(id, lane, "failed", "cancelled before claim");
|
|
3119
2881
|
return true;
|
|
3120
2882
|
}
|
|
3121
2883
|
function cancelRequested(id, lane) {
|
|
3122
|
-
return
|
|
2884
|
+
return fs13.existsSync(path12.join(laneDirs(lane).active, id, "cancel"));
|
|
3123
2885
|
}
|
|
3124
2886
|
var init_queue = __esm({
|
|
3125
2887
|
"packages/core/dist/queue.js"() {
|
|
@@ -3129,11 +2891,52 @@ var init_queue = __esm({
|
|
|
3129
2891
|
}
|
|
3130
2892
|
});
|
|
3131
2893
|
|
|
2894
|
+
// packages/core/dist/session-link.js
|
|
2895
|
+
function validSessionLink(value) {
|
|
2896
|
+
if (typeof value !== "string")
|
|
2897
|
+
return false;
|
|
2898
|
+
if (/[\x00-\x1f\x7f]/.test(value))
|
|
2899
|
+
return false;
|
|
2900
|
+
return /^claude:\/\/claude\.ai\/[A-Za-z0-9_/-]+$/.test(value) || /^vscode:\/\/anthropic\.claude-code\/open\?session=[A-Za-z0-9_-]+$/.test(value) || /^codex:\/\/threads\/[A-Za-z0-9-]+$/.test(value);
|
|
2901
|
+
}
|
|
2902
|
+
function linkMismatch(link, win) {
|
|
2903
|
+
if (!win)
|
|
2904
|
+
return void 0;
|
|
2905
|
+
const ep = win.entrypoint;
|
|
2906
|
+
const bundle = win.bundleId;
|
|
2907
|
+
if (link.startsWith("vscode://")) {
|
|
2908
|
+
if (ep !== void 0)
|
|
2909
|
+
return ep === "claude-vscode" ? void 0 : `a vscode:// link needs the VS Code extension; this session's entrypoint is ${ep}`;
|
|
2910
|
+
return bundle === void 0 || VSCODE_BUNDLE_RE.test(bundle) ? void 0 : `a vscode:// link needs the VS Code extension; this session runs in ${bundle}`;
|
|
2911
|
+
}
|
|
2912
|
+
if (link.startsWith("claude://")) {
|
|
2913
|
+
if (ep !== void 0) {
|
|
2914
|
+
return ep === "cli" || ep === "claude-vscode" || ep.startsWith("sdk-") ? `a claude:// link needs the Claude desktop app's Code tab; this session's entrypoint is ${ep}` : void 0;
|
|
2915
|
+
}
|
|
2916
|
+
if (bundle !== void 0 && bundle !== CLAUDE_DESKTOP_BUNDLE)
|
|
2917
|
+
return `a claude:// link needs the Claude desktop app; this session runs in ${bundle}`;
|
|
2918
|
+
return win.termProgram === "claude-desktop" ? "a claude:// link needs the Code tab; this session runs in the Claude desktop app's terminal" : void 0;
|
|
2919
|
+
}
|
|
2920
|
+
if (link.startsWith("codex://")) {
|
|
2921
|
+
return bundle === void 0 || bundle === CODEX_BUNDLE ? void 0 : `a codex:// link needs the Codex app; this session runs in ${bundle}`;
|
|
2922
|
+
}
|
|
2923
|
+
return void 0;
|
|
2924
|
+
}
|
|
2925
|
+
var VSCODE_BUNDLE_RE, CLAUDE_DESKTOP_BUNDLE, CODEX_BUNDLE;
|
|
2926
|
+
var init_session_link = __esm({
|
|
2927
|
+
"packages/core/dist/session-link.js"() {
|
|
2928
|
+
"use strict";
|
|
2929
|
+
VSCODE_BUNDLE_RE = /(?:VSCode|Cursor|windsurf)/;
|
|
2930
|
+
CLAUDE_DESKTOP_BUNDLE = "com.anthropic.claudefordesktop";
|
|
2931
|
+
CODEX_BUNDLE = "com.openai.codex";
|
|
2932
|
+
}
|
|
2933
|
+
});
|
|
2934
|
+
|
|
3132
2935
|
// packages/core/dist/evidence.js
|
|
3133
|
-
import * as
|
|
2936
|
+
import * as fs14 from "node:fs";
|
|
3134
2937
|
function readEvidence(id, lane) {
|
|
3135
2938
|
try {
|
|
3136
|
-
return JSON.parse(
|
|
2939
|
+
return JSON.parse(fs14.readFileSync(evidencePath(id, lane), "utf8"));
|
|
3137
2940
|
} catch {
|
|
3138
2941
|
return {};
|
|
3139
2942
|
}
|
|
@@ -3141,8 +2944,8 @@ function readEvidence(id, lane) {
|
|
|
3141
2944
|
function mergeEvidence(id, lane, patch) {
|
|
3142
2945
|
const file = evidencePath(id, lane);
|
|
3143
2946
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
3144
|
-
|
|
3145
|
-
|
|
2947
|
+
fs14.writeFileSync(tmp, JSON.stringify({ ...readEvidence(id, lane), ...patch }, null, 2));
|
|
2948
|
+
fs14.renameSync(tmp, file);
|
|
3146
2949
|
}
|
|
3147
2950
|
var init_evidence = __esm({
|
|
3148
2951
|
"packages/core/dist/evidence.js"() {
|
|
@@ -3152,8 +2955,8 @@ var init_evidence = __esm({
|
|
|
3152
2955
|
});
|
|
3153
2956
|
|
|
3154
2957
|
// packages/core/dist/pushes.js
|
|
3155
|
-
import * as
|
|
3156
|
-
import * as
|
|
2958
|
+
import * as fs15 from "node:fs";
|
|
2959
|
+
import * as path13 from "node:path";
|
|
3157
2960
|
function words(segment) {
|
|
3158
2961
|
const out = [];
|
|
3159
2962
|
const re = /"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g;
|
|
@@ -3205,18 +3008,18 @@ function gitPushTargets(command) {
|
|
|
3205
3008
|
return found ? [...new Set(targets)] : void 0;
|
|
3206
3009
|
}
|
|
3207
3010
|
function gitDirAt(dir) {
|
|
3208
|
-
let at =
|
|
3011
|
+
let at = path13.resolve(dir);
|
|
3209
3012
|
for (; ; ) {
|
|
3210
|
-
const dotGit =
|
|
3013
|
+
const dotGit = path13.join(at, ".git");
|
|
3211
3014
|
try {
|
|
3212
|
-
const stat =
|
|
3015
|
+
const stat = fs15.statSync(dotGit);
|
|
3213
3016
|
if (stat.isDirectory())
|
|
3214
3017
|
return dotGit;
|
|
3215
|
-
const m = /^gitdir:\s*(.+)$/m.exec(
|
|
3216
|
-
return m ?
|
|
3018
|
+
const m = /^gitdir:\s*(.+)$/m.exec(fs15.readFileSync(dotGit, "utf8"));
|
|
3019
|
+
return m ? path13.resolve(at, m[1].trim()) : void 0;
|
|
3217
3020
|
} catch {
|
|
3218
3021
|
}
|
|
3219
|
-
const up =
|
|
3022
|
+
const up = path13.dirname(at);
|
|
3220
3023
|
if (up === at)
|
|
3221
3024
|
return void 0;
|
|
3222
3025
|
at = up;
|
|
@@ -3227,7 +3030,7 @@ function currentBranchAt(dir) {
|
|
|
3227
3030
|
if (!gitDir)
|
|
3228
3031
|
return void 0;
|
|
3229
3032
|
try {
|
|
3230
|
-
const m = /^ref:\s*refs\/heads\/(.+)$/m.exec(
|
|
3033
|
+
const m = /^ref:\s*refs\/heads\/(.+)$/m.exec(fs15.readFileSync(path13.join(gitDir, "HEAD"), "utf8"));
|
|
3231
3034
|
return m ? m[1].trim() : void 0;
|
|
3232
3035
|
} catch {
|
|
3233
3036
|
return void 0;
|
|
@@ -3256,18 +3059,18 @@ var init_pushes = __esm({
|
|
|
3256
3059
|
});
|
|
3257
3060
|
|
|
3258
3061
|
// packages/core/dist/trap-names.js
|
|
3259
|
-
import * as
|
|
3260
|
-
import * as
|
|
3062
|
+
import * as fs16 from "node:fs";
|
|
3063
|
+
import * as path14 from "node:path";
|
|
3261
3064
|
import { randomInt } from "node:crypto";
|
|
3262
3065
|
function namesDir() {
|
|
3263
|
-
return
|
|
3066
|
+
return path14.join(lobstahHome(), "trap-names");
|
|
3264
3067
|
}
|
|
3265
3068
|
function nameFile(name) {
|
|
3266
|
-
return
|
|
3069
|
+
return path14.join(namesDir(), `${name}.json`);
|
|
3267
3070
|
}
|
|
3268
3071
|
function knownTrapNames() {
|
|
3269
3072
|
try {
|
|
3270
|
-
return
|
|
3073
|
+
return fs16.readdirSync(namesDir()).filter((file) => file.endsWith(".json")).map((file) => file.slice(0, -5)).filter((name) => TRAP_NAME_RE.test(name)).sort();
|
|
3271
3074
|
} catch {
|
|
3272
3075
|
return [];
|
|
3273
3076
|
}
|
|
@@ -3276,7 +3079,7 @@ function trapIdForName(name) {
|
|
|
3276
3079
|
if (!TRAP_NAME_RE.test(name))
|
|
3277
3080
|
return void 0;
|
|
3278
3081
|
try {
|
|
3279
|
-
const id = JSON.parse(
|
|
3082
|
+
const id = JSON.parse(fs16.readFileSync(nameFile(name), "utf8")).trapId;
|
|
3280
3083
|
return typeof id === "string" ? id : void 0;
|
|
3281
3084
|
} catch {
|
|
3282
3085
|
return void 0;
|
|
@@ -3286,7 +3089,7 @@ function trapNameForId(trapId) {
|
|
|
3286
3089
|
return knownTrapNames().find((name) => trapIdForName(name) === trapId);
|
|
3287
3090
|
}
|
|
3288
3091
|
function reserveTrapName(trapId, requested, start = randomInt(TRAP_FIRST_WORDS.length * TRAP_LAST_WORDS.length)) {
|
|
3289
|
-
|
|
3092
|
+
fs16.mkdirSync(namesDir(), { recursive: true });
|
|
3290
3093
|
const old = knownTrapNames().find((name) => trapIdForName(name) === trapId);
|
|
3291
3094
|
if (requested !== void 0 && !TRAP_NAME_RE.test(requested)) {
|
|
3292
3095
|
throw new Error(`invalid trap name "${requested}" \u2014 use two lowercase words of 2\u20138 letters joined by one hyphen`);
|
|
@@ -3302,9 +3105,9 @@ function reserveTrapName(trapId, requested, start = randomInt(TRAP_FIRST_WORDS.l
|
|
|
3302
3105
|
if (name === old)
|
|
3303
3106
|
return name;
|
|
3304
3107
|
try {
|
|
3305
|
-
|
|
3108
|
+
fs16.writeFileSync(nameFile(name), JSON.stringify({ trapId }), { flag: "wx" });
|
|
3306
3109
|
if (old)
|
|
3307
|
-
|
|
3110
|
+
fs16.rmSync(nameFile(old), { force: true });
|
|
3308
3111
|
return name;
|
|
3309
3112
|
} catch (err) {
|
|
3310
3113
|
if (err.code !== "EEXIST")
|
|
@@ -3459,22 +3262,22 @@ var init_trap_names = __esm({
|
|
|
3459
3262
|
});
|
|
3460
3263
|
|
|
3461
3264
|
// packages/core/dist/worktrees.js
|
|
3462
|
-
import * as
|
|
3463
|
-
import * as
|
|
3265
|
+
import * as fs17 from "node:fs";
|
|
3266
|
+
import * as path15 from "node:path";
|
|
3464
3267
|
function worktreePath(id) {
|
|
3465
|
-
return
|
|
3268
|
+
return path15.join(lobstahHome(), "worktrees", id);
|
|
3466
3269
|
}
|
|
3467
3270
|
function readRecord(file) {
|
|
3468
3271
|
try {
|
|
3469
|
-
const r = JSON.parse(
|
|
3272
|
+
const r = JSON.parse(fs17.readFileSync(file, "utf8"));
|
|
3470
3273
|
return typeof r.path === "string" ? r : void 0;
|
|
3471
3274
|
} catch {
|
|
3472
3275
|
return void 0;
|
|
3473
3276
|
}
|
|
3474
3277
|
}
|
|
3475
3278
|
function ownerOfPath(p) {
|
|
3476
|
-
const rel =
|
|
3477
|
-
if (!rel || rel.startsWith("..") ||
|
|
3279
|
+
const rel = path15.relative(path15.join(lobstahHome(), "worktrees"), p);
|
|
3280
|
+
if (!rel || rel.startsWith("..") || path15.isAbsolute(rel))
|
|
3478
3281
|
return void 0;
|
|
3479
3282
|
return rel.split(/[\\/]/)[0];
|
|
3480
3283
|
}
|
|
@@ -3487,7 +3290,7 @@ function dispatchWorktree(id, lane) {
|
|
|
3487
3290
|
}
|
|
3488
3291
|
const dirs = laneDirs(l);
|
|
3489
3292
|
for (const dir of [dirs.active, dirs.done]) {
|
|
3490
|
-
const rec = readRecord(
|
|
3293
|
+
const rec = readRecord(path15.join(dir, id, "worktree.json"));
|
|
3491
3294
|
if (rec) {
|
|
3492
3295
|
const owner = rec.of ?? ownerOfPath(rec.path) ?? id;
|
|
3493
3296
|
return { path: rec.path, owner, reused: owner !== id };
|
|
@@ -3499,7 +3302,7 @@ function dispatchWorktree(id, lane) {
|
|
|
3499
3302
|
function laneOf(id) {
|
|
3500
3303
|
for (const l of LANES) {
|
|
3501
3304
|
const d = laneDirs(l);
|
|
3502
|
-
if (
|
|
3305
|
+
if (fs17.existsSync(path15.join(d.queue, `${id}.json`)) || fs17.existsSync(path15.join(d.active, id)) || fs17.existsSync(path15.join(d.done, id))) {
|
|
3503
3306
|
return l;
|
|
3504
3307
|
}
|
|
3505
3308
|
}
|
|
@@ -3519,39 +3322,39 @@ function followUpAncestors(id, lane) {
|
|
|
3519
3322
|
function chainWorktree(followUp) {
|
|
3520
3323
|
for (const id of [followUp, ...followUpAncestors(followUp)]) {
|
|
3521
3324
|
const wt = dispatchWorktree(id, laneOf(id));
|
|
3522
|
-
if (
|
|
3325
|
+
if (fs17.existsSync(wt.path))
|
|
3523
3326
|
return { ...wt, from: id };
|
|
3524
3327
|
}
|
|
3525
3328
|
return void 0;
|
|
3526
3329
|
}
|
|
3527
3330
|
function worktreeGitDir(wt) {
|
|
3528
|
-
const dotGit =
|
|
3331
|
+
const dotGit = path15.join(wt, ".git");
|
|
3529
3332
|
try {
|
|
3530
|
-
const st =
|
|
3333
|
+
const st = fs17.statSync(dotGit);
|
|
3531
3334
|
if (st.isDirectory())
|
|
3532
3335
|
return dotGit;
|
|
3533
|
-
const m = /^gitdir:\s*(.+?)\s*$/m.exec(
|
|
3534
|
-
return m ?
|
|
3336
|
+
const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs17.readFileSync(dotGit, "utf8"));
|
|
3337
|
+
return m ? path15.resolve(wt, m[1]) : void 0;
|
|
3535
3338
|
} catch {
|
|
3536
3339
|
return void 0;
|
|
3537
3340
|
}
|
|
3538
3341
|
}
|
|
3539
3342
|
function worktreeLockFile(wt) {
|
|
3540
3343
|
const gitDir = worktreeGitDir(wt);
|
|
3541
|
-
return gitDir ?
|
|
3344
|
+
return gitDir ? path15.join(gitDir, "lobstah.lock") : void 0;
|
|
3542
3345
|
}
|
|
3543
3346
|
function readWorktreeLock(wt) {
|
|
3544
3347
|
const file = worktreeLockFile(wt);
|
|
3545
3348
|
if (!file)
|
|
3546
3349
|
return void 0;
|
|
3547
3350
|
try {
|
|
3548
|
-
return JSON.parse(
|
|
3351
|
+
return JSON.parse(fs17.readFileSync(file, "utf8"));
|
|
3549
3352
|
} catch {
|
|
3550
3353
|
return void 0;
|
|
3551
3354
|
}
|
|
3552
3355
|
}
|
|
3553
3356
|
function lockIsLive(lock) {
|
|
3554
|
-
return
|
|
3357
|
+
return fs17.existsSync(path15.join(laneDirs(lock.lane).active, lock.id));
|
|
3555
3358
|
}
|
|
3556
3359
|
function worktreeHolder(wt) {
|
|
3557
3360
|
const lock = readWorktreeLock(wt);
|
|
@@ -3564,7 +3367,7 @@ function acquireWorktreeLock(wt, id, lane) {
|
|
|
3564
3367
|
return { id: "(no git dir)", lane, pid: 0, at: mine.at };
|
|
3565
3368
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
3566
3369
|
try {
|
|
3567
|
-
|
|
3370
|
+
fs17.writeFileSync(file, JSON.stringify(mine, null, 2), { flag: "wx" });
|
|
3568
3371
|
return void 0;
|
|
3569
3372
|
} catch (err) {
|
|
3570
3373
|
if (err.code !== "EEXIST")
|
|
@@ -3572,7 +3375,7 @@ function acquireWorktreeLock(wt, id, lane) {
|
|
|
3572
3375
|
}
|
|
3573
3376
|
let raw;
|
|
3574
3377
|
try {
|
|
3575
|
-
raw =
|
|
3378
|
+
raw = fs17.readFileSync(file, "utf8");
|
|
3576
3379
|
} catch {
|
|
3577
3380
|
continue;
|
|
3578
3381
|
}
|
|
@@ -3583,14 +3386,14 @@ function acquireWorktreeLock(wt, id, lane) {
|
|
|
3583
3386
|
held2 = void 0;
|
|
3584
3387
|
}
|
|
3585
3388
|
if (held2?.id === id) {
|
|
3586
|
-
|
|
3389
|
+
fs17.writeFileSync(file, JSON.stringify(mine, null, 2));
|
|
3587
3390
|
return void 0;
|
|
3588
3391
|
}
|
|
3589
3392
|
if (held2 && lockIsLive(held2))
|
|
3590
3393
|
return held2;
|
|
3591
3394
|
try {
|
|
3592
|
-
if (
|
|
3593
|
-
|
|
3395
|
+
if (fs17.readFileSync(file, "utf8") === raw)
|
|
3396
|
+
fs17.rmSync(file, { force: true });
|
|
3594
3397
|
} catch {
|
|
3595
3398
|
}
|
|
3596
3399
|
}
|
|
@@ -3602,20 +3405,20 @@ function releaseWorktreeLock(wt, id) {
|
|
|
3602
3405
|
if (!file)
|
|
3603
3406
|
return;
|
|
3604
3407
|
if (readWorktreeLock(wt)?.id === id)
|
|
3605
|
-
|
|
3408
|
+
fs17.rmSync(file, { force: true });
|
|
3606
3409
|
}
|
|
3607
3410
|
function releaseDispatchLock(dispatchDir, id) {
|
|
3608
|
-
const rec = readRecord(
|
|
3411
|
+
const rec = readRecord(path15.join(dispatchDir, "worktree.json"));
|
|
3609
3412
|
if (rec)
|
|
3610
3413
|
releaseWorktreeLock(rec.path, id);
|
|
3611
3414
|
}
|
|
3612
3415
|
function keptWorktreesPath() {
|
|
3613
|
-
return
|
|
3416
|
+
return path15.join(lobstahHome(), "release-kept.json");
|
|
3614
3417
|
}
|
|
3615
3418
|
function readKeptWorktrees() {
|
|
3616
3419
|
try {
|
|
3617
|
-
const list2 = JSON.parse(
|
|
3618
|
-
return Array.isArray(list2) ? list2.filter((k) =>
|
|
3420
|
+
const list2 = JSON.parse(fs17.readFileSync(keptWorktreesPath(), "utf8"));
|
|
3421
|
+
return Array.isArray(list2) ? list2.filter((k) => fs17.existsSync(worktreePath(k.id))) : [];
|
|
3619
3422
|
} catch {
|
|
3620
3423
|
return [];
|
|
3621
3424
|
}
|
|
@@ -3623,12 +3426,12 @@ function readKeptWorktrees() {
|
|
|
3623
3426
|
function writeKeptWorktrees(list2) {
|
|
3624
3427
|
const file = keptWorktreesPath();
|
|
3625
3428
|
if (list2.length === 0) {
|
|
3626
|
-
|
|
3429
|
+
fs17.rmSync(file, { force: true });
|
|
3627
3430
|
return;
|
|
3628
3431
|
}
|
|
3629
3432
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
3630
|
-
|
|
3631
|
-
|
|
3433
|
+
fs17.writeFileSync(tmp, JSON.stringify(list2, null, 2));
|
|
3434
|
+
fs17.renameSync(tmp, file);
|
|
3632
3435
|
}
|
|
3633
3436
|
var LANES;
|
|
3634
3437
|
var init_worktrees = __esm({
|
|
@@ -3642,8 +3445,8 @@ var init_worktrees = __esm({
|
|
|
3642
3445
|
});
|
|
3643
3446
|
|
|
3644
3447
|
// packages/core/dist/worktree-safety.js
|
|
3645
|
-
import * as
|
|
3646
|
-
import * as
|
|
3448
|
+
import * as fs18 from "node:fs";
|
|
3449
|
+
import * as path16 from "node:path";
|
|
3647
3450
|
import { execFileSync } from "node:child_process";
|
|
3648
3451
|
function git(dir, ...args) {
|
|
3649
3452
|
return execFileSync("git", args, { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 1e4 });
|
|
@@ -3687,22 +3490,22 @@ function removeGhostWorktree(dir) {
|
|
|
3687
3490
|
const safety = inspectTrapWorktree(dir);
|
|
3688
3491
|
if (safety.reason)
|
|
3689
3492
|
return { ...safety, removed: false };
|
|
3690
|
-
const anchor =
|
|
3493
|
+
const anchor = path16.join(dir, ".lobstah-trap");
|
|
3691
3494
|
let saved;
|
|
3692
3495
|
try {
|
|
3693
3496
|
const common = git(dir, "rev-parse", "--path-format=absolute", "--git-common-dir").trim();
|
|
3694
3497
|
const own = git(dir, "rev-parse", "--path-format=absolute", "--git-dir").trim();
|
|
3695
3498
|
if (common === own)
|
|
3696
3499
|
return { ...safety, removed: false, reason: "not a linked worktree" };
|
|
3697
|
-
if (
|
|
3698
|
-
saved =
|
|
3699
|
-
|
|
3500
|
+
if (fs18.existsSync(anchor)) {
|
|
3501
|
+
saved = fs18.readFileSync(anchor);
|
|
3502
|
+
fs18.unlinkSync(anchor);
|
|
3700
3503
|
}
|
|
3701
|
-
git(
|
|
3504
|
+
git(path16.dirname(common), "--git-dir", common, "worktree", "remove", dir);
|
|
3702
3505
|
return { ...safety, removed: true };
|
|
3703
3506
|
} catch {
|
|
3704
|
-
if (saved &&
|
|
3705
|
-
|
|
3507
|
+
if (saved && fs18.existsSync(dir))
|
|
3508
|
+
fs18.writeFileSync(anchor, saved);
|
|
3706
3509
|
return { ...safety, removed: false, reason: "git worktree remove refused" };
|
|
3707
3510
|
}
|
|
3708
3511
|
}
|
|
@@ -3713,20 +3516,20 @@ var init_worktree_safety = __esm({
|
|
|
3713
3516
|
});
|
|
3714
3517
|
|
|
3715
3518
|
// packages/core/dist/trap-start.js
|
|
3716
|
-
import * as
|
|
3717
|
-
import * as
|
|
3519
|
+
import * as fs19 from "node:fs";
|
|
3520
|
+
import * as path17 from "node:path";
|
|
3718
3521
|
import { createHash as createHash3, randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
|
|
3719
3522
|
function startPath(trapId) {
|
|
3720
|
-
return
|
|
3523
|
+
return path17.join(soakingDir(), `${trapId}.starting`);
|
|
3721
3524
|
}
|
|
3722
3525
|
function ticketPath(trapId) {
|
|
3723
|
-
return
|
|
3526
|
+
return path17.join(soakingDir(), `${trapId}.ticket`);
|
|
3724
3527
|
}
|
|
3725
3528
|
function readReservationTicket(trapId) {
|
|
3726
3529
|
if (!/^[0-9a-f]{8}$/.test(trapId))
|
|
3727
3530
|
return void 0;
|
|
3728
3531
|
try {
|
|
3729
|
-
const ticket =
|
|
3532
|
+
const ticket = fs19.readFileSync(ticketPath(trapId), "utf8").trim();
|
|
3730
3533
|
return TRAP_TICKET_RE.exec(ticket)?.[1] === trapId ? ticket : void 0;
|
|
3731
3534
|
} catch {
|
|
3732
3535
|
return void 0;
|
|
@@ -3744,17 +3547,17 @@ function trapStartCommands(repoPath, ticket, harness) {
|
|
|
3744
3547
|
return harness === "claude" || harness === "codex" ? all.filter((c) => c.harness === harness) : all;
|
|
3745
3548
|
}
|
|
3746
3549
|
function writeReservation(r) {
|
|
3747
|
-
|
|
3550
|
+
fs19.mkdirSync(soakingDir(), { recursive: true });
|
|
3748
3551
|
const file = startPath(r.trapId);
|
|
3749
3552
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
3750
|
-
|
|
3751
|
-
|
|
3553
|
+
fs19.writeFileSync(tmp, JSON.stringify(r, null, 2));
|
|
3554
|
+
fs19.renameSync(tmp, file);
|
|
3752
3555
|
}
|
|
3753
3556
|
function readReservation(trapId) {
|
|
3754
3557
|
if (!/^[0-9a-f]{8}$/.test(trapId))
|
|
3755
3558
|
return void 0;
|
|
3756
3559
|
try {
|
|
3757
|
-
const parsed = JSON.parse(
|
|
3560
|
+
const parsed = JSON.parse(fs19.readFileSync(startPath(trapId), "utf8"));
|
|
3758
3561
|
return typeof parsed.trapId === "string" && parsed.trapId === trapId ? parsed : void 0;
|
|
3759
3562
|
} catch {
|
|
3760
3563
|
return void 0;
|
|
@@ -3763,7 +3566,7 @@ function readReservation(trapId) {
|
|
|
3763
3566
|
function listReservations() {
|
|
3764
3567
|
let files;
|
|
3765
3568
|
try {
|
|
3766
|
-
files =
|
|
3569
|
+
files = fs19.readdirSync(soakingDir()).filter((f) => f.endsWith(".starting"));
|
|
3767
3570
|
} catch {
|
|
3768
3571
|
return [];
|
|
3769
3572
|
}
|
|
@@ -3794,7 +3597,7 @@ function reserveTrap(opts) {
|
|
|
3794
3597
|
...opts.request ? { request: opts.request } : {}
|
|
3795
3598
|
};
|
|
3796
3599
|
writeReservation(reservation);
|
|
3797
|
-
|
|
3600
|
+
fs19.writeFileSync(ticketPath(trapId), ticket, { mode: 384 });
|
|
3798
3601
|
postNotice({
|
|
3799
3602
|
kind: "trap-starting",
|
|
3800
3603
|
text: `trap ${trapLabel(reservation)} reserved for repo ${opts.repo} \u2014 starting, sign-on due by ${reservation.deadline}; address work with \`--for ${name}\``,
|
|
@@ -3818,8 +3621,8 @@ function reservationForTicket(ticket) {
|
|
|
3818
3621
|
function dropReservation(trapId) {
|
|
3819
3622
|
const r = readReservation(trapId);
|
|
3820
3623
|
if (r)
|
|
3821
|
-
|
|
3822
|
-
|
|
3624
|
+
fs19.rmSync(startPath(trapId), { force: true });
|
|
3625
|
+
fs19.rmSync(ticketPath(trapId), { force: true });
|
|
3823
3626
|
return r;
|
|
3824
3627
|
}
|
|
3825
3628
|
function withdrawReservation(trapId, by) {
|
|
@@ -3875,27 +3678,27 @@ var init_trap_start = __esm({
|
|
|
3875
3678
|
});
|
|
3876
3679
|
|
|
3877
3680
|
// packages/core/dist/soak.js
|
|
3878
|
-
import * as
|
|
3879
|
-
import * as
|
|
3681
|
+
import * as fs20 from "node:fs";
|
|
3682
|
+
import * as path18 from "node:path";
|
|
3880
3683
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
3881
3684
|
function regPath(trapId) {
|
|
3882
|
-
return
|
|
3685
|
+
return path18.join(soakingDir(), `${trapId}.json`);
|
|
3883
3686
|
}
|
|
3884
|
-
function
|
|
3687
|
+
function atomicWrite3(file, content) {
|
|
3885
3688
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
3886
|
-
|
|
3887
|
-
|
|
3689
|
+
fs20.writeFileSync(tmp, content);
|
|
3690
|
+
fs20.renameSync(tmp, file);
|
|
3888
3691
|
}
|
|
3889
3692
|
function readTrapAnchor(worktree) {
|
|
3890
3693
|
try {
|
|
3891
|
-
const parsed = JSON.parse(
|
|
3694
|
+
const parsed = JSON.parse(fs20.readFileSync(path18.join(worktree, TRAP_FILE), "utf8"));
|
|
3892
3695
|
return typeof parsed.trapId === "string" ? parsed : void 0;
|
|
3893
3696
|
} catch {
|
|
3894
3697
|
return void 0;
|
|
3895
3698
|
}
|
|
3896
3699
|
}
|
|
3897
3700
|
function writeTrapAnchor(worktree, anchor) {
|
|
3898
|
-
|
|
3701
|
+
atomicWrite3(path18.join(worktree, TRAP_FILE), `${JSON.stringify(anchor, null, 2)}
|
|
3899
3702
|
`);
|
|
3900
3703
|
}
|
|
3901
3704
|
function trapIdAt(worktree) {
|
|
@@ -3915,12 +3718,12 @@ function newTrapId() {
|
|
|
3915
3718
|
function findAnchored(root, match) {
|
|
3916
3719
|
let names;
|
|
3917
3720
|
try {
|
|
3918
|
-
names =
|
|
3721
|
+
names = fs20.readdirSync(root);
|
|
3919
3722
|
} catch {
|
|
3920
3723
|
return void 0;
|
|
3921
3724
|
}
|
|
3922
3725
|
for (const name of names.sort()) {
|
|
3923
|
-
const dir =
|
|
3726
|
+
const dir = path18.join(root, name);
|
|
3924
3727
|
const anchor = readTrapAnchor(dir);
|
|
3925
3728
|
if (anchor && match(anchor))
|
|
3926
3729
|
return dir;
|
|
@@ -3935,7 +3738,7 @@ function anchoredWorktree(root, trapId) {
|
|
|
3935
3738
|
}
|
|
3936
3739
|
function readTrap(trapId) {
|
|
3937
3740
|
try {
|
|
3938
|
-
return JSON.parse(
|
|
3741
|
+
return JSON.parse(fs20.readFileSync(regPath(trapId), "utf8"));
|
|
3939
3742
|
} catch {
|
|
3940
3743
|
return void 0;
|
|
3941
3744
|
}
|
|
@@ -3976,7 +3779,7 @@ function unknownTrapMessage(address) {
|
|
|
3976
3779
|
}
|
|
3977
3780
|
function listTraps() {
|
|
3978
3781
|
try {
|
|
3979
|
-
return
|
|
3782
|
+
return fs20.readdirSync(soakingDir()).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(fs20.readFileSync(path18.join(soakingDir(), f), "utf8"))).filter((r) => typeof r.trapId === "string");
|
|
3980
3783
|
} catch {
|
|
3981
3784
|
return [];
|
|
3982
3785
|
}
|
|
@@ -4031,12 +3834,12 @@ function signOnTrap(opts) {
|
|
|
4031
3834
|
...sameSession && prior.titleSet ? { titleSet: prior.titleSet, titleSetAt: prior.titleSetAt } : {},
|
|
4032
3835
|
...sameSession && prior.titlePending ? { titlePending: prior.titlePending } : {}
|
|
4033
3836
|
};
|
|
4034
|
-
|
|
3837
|
+
atomicWrite3(regPath(trapId), JSON.stringify(reg, null, 2));
|
|
4035
3838
|
releaseSignedOff(trapId);
|
|
4036
3839
|
if (!prior) {
|
|
4037
3840
|
postNotice({
|
|
4038
3841
|
kind: "trap-signed-on",
|
|
4039
|
-
text: `trap ${trapLabel(reg)} signed on (${opts.harness}, ${opts.repo ?? "no repo"}, ${
|
|
3842
|
+
text: `trap ${trapLabel(reg)} signed on (${opts.harness}, ${opts.repo ?? "no repo"}, ${path18.basename(opts.worktree)}) \u2014 address work with \`--for ${name}\``,
|
|
4040
3843
|
refId: trapId,
|
|
4041
3844
|
repo: opts.repo,
|
|
4042
3845
|
by: opts.sessionId
|
|
@@ -4046,14 +3849,14 @@ function signOnTrap(opts) {
|
|
|
4046
3849
|
}
|
|
4047
3850
|
function readSignedOff(trapId) {
|
|
4048
3851
|
try {
|
|
4049
|
-
return JSON.parse(
|
|
3852
|
+
return JSON.parse(fs20.readFileSync(signedOffPath(trapId), "utf8"));
|
|
4050
3853
|
} catch {
|
|
4051
3854
|
return void 0;
|
|
4052
3855
|
}
|
|
4053
3856
|
}
|
|
4054
3857
|
function listSignedOff() {
|
|
4055
3858
|
try {
|
|
4056
|
-
return
|
|
3859
|
+
return fs20.readdirSync(signedOffDir()).filter((f) => f.endsWith(".json")).flatMap((f) => {
|
|
4057
3860
|
const s = readSignedOff(f.slice(0, -".json".length));
|
|
4058
3861
|
return s ? [s] : [];
|
|
4059
3862
|
});
|
|
@@ -4062,7 +3865,7 @@ function listSignedOff() {
|
|
|
4062
3865
|
}
|
|
4063
3866
|
}
|
|
4064
3867
|
function releaseSignedOff(trapId) {
|
|
4065
|
-
|
|
3868
|
+
fs20.rmSync(signedOffPath(trapId), { force: true });
|
|
4066
3869
|
}
|
|
4067
3870
|
function recentlySignedOff(trapId, graceMs, now2 = Date.now()) {
|
|
4068
3871
|
const s = readSignedOff(trapId);
|
|
@@ -4072,14 +3875,14 @@ function stowTrap(trapId, reason = "signed off", by) {
|
|
|
4072
3875
|
const reg = readTrap(trapId);
|
|
4073
3876
|
if (!reg)
|
|
4074
3877
|
return void 0;
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
3878
|
+
fs20.mkdirSync(signedOffDir(), { recursive: true });
|
|
3879
|
+
atomicWrite3(signedOffPath(trapId), JSON.stringify({ trapId, name: reg.name, worktree: reg.worktree, at: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
3880
|
+
fs20.rmSync(regPath(trapId), { force: true });
|
|
3881
|
+
fs20.rmSync(beatPath(trapId), { force: true });
|
|
3882
|
+
fs20.rmSync(prProbePath(trapId), { force: true });
|
|
4080
3883
|
postNotice({
|
|
4081
3884
|
kind: "trap-stowed",
|
|
4082
|
-
text: `trap ${trapLabel(reg)} ${reason} (${
|
|
3885
|
+
text: `trap ${trapLabel(reg)} ${reason} (${path18.basename(reg.worktree)}) \u2014 re-soaking that worktree restores the address`,
|
|
4083
3886
|
refId: trapId,
|
|
4084
3887
|
repo: reg.repo,
|
|
4085
3888
|
by
|
|
@@ -4095,7 +3898,7 @@ function askTrapTitle(trapId, title) {
|
|
|
4095
3898
|
delete next.titlePending;
|
|
4096
3899
|
else
|
|
4097
3900
|
next.titlePending = title;
|
|
4098
|
-
|
|
3901
|
+
atomicWrite3(regPath(trapId), JSON.stringify(next, null, 2));
|
|
4099
3902
|
return next;
|
|
4100
3903
|
}
|
|
4101
3904
|
function countTitleReminder(trapId) {
|
|
@@ -4103,7 +3906,7 @@ function countTitleReminder(trapId) {
|
|
|
4103
3906
|
if (!reg?.titlePending)
|
|
4104
3907
|
return void 0;
|
|
4105
3908
|
const n = (reg.titleReminders ?? 0) + 1;
|
|
4106
|
-
|
|
3909
|
+
atomicWrite3(regPath(trapId), JSON.stringify({ ...reg, titleReminders: n }, null, 2));
|
|
4107
3910
|
return n;
|
|
4108
3911
|
}
|
|
4109
3912
|
function confirmTrapTitle(trapId, now2 = Date.now()) {
|
|
@@ -4112,7 +3915,7 @@ function confirmTrapTitle(trapId, now2 = Date.now()) {
|
|
|
4112
3915
|
return void 0;
|
|
4113
3916
|
const { titlePending, titleReminders: _reminders, ...rest } = reg;
|
|
4114
3917
|
const next = { ...rest, titleSet: titlePending ?? reg.titleSet ?? reg.name, titleSetAt: new Date(now2).toISOString() };
|
|
4115
|
-
|
|
3918
|
+
atomicWrite3(regPath(trapId), JSON.stringify(next, null, 2));
|
|
4116
3919
|
return next;
|
|
4117
3920
|
}
|
|
4118
3921
|
function heartbeatTrap(trapId, opts = {}) {
|
|
@@ -4127,7 +3930,7 @@ function heartbeatTrap(trapId, opts = {}) {
|
|
|
4127
3930
|
...opts.parked ? { parkedAt: (/* @__PURE__ */ new Date()).toISOString() } : {},
|
|
4128
3931
|
claimed: opts.claimed === null ? void 0 : opts.claimed ?? reg.claimed
|
|
4129
3932
|
};
|
|
4130
|
-
|
|
3933
|
+
atomicWrite3(regPath(trapId), JSON.stringify(next, null, 2));
|
|
4131
3934
|
if (firstPark) {
|
|
4132
3935
|
postNotice({
|
|
4133
3936
|
kind: "trap-listening",
|
|
@@ -4140,11 +3943,11 @@ function heartbeatTrap(trapId, opts = {}) {
|
|
|
4140
3943
|
return next;
|
|
4141
3944
|
}
|
|
4142
3945
|
function claimPath(id, lane) {
|
|
4143
|
-
return
|
|
3946
|
+
return path18.join(laneDirs(lane).active, id, "claim.json");
|
|
4144
3947
|
}
|
|
4145
3948
|
function readSessionClaim(id, lane) {
|
|
4146
3949
|
try {
|
|
4147
|
-
return JSON.parse(
|
|
3950
|
+
return JSON.parse(fs20.readFileSync(claimPath(id, lane), "utf8"));
|
|
4148
3951
|
} catch {
|
|
4149
3952
|
return void 0;
|
|
4150
3953
|
}
|
|
@@ -4156,24 +3959,66 @@ function hasOpenCatch(reg) {
|
|
|
4156
3959
|
if (!reg.claimed)
|
|
4157
3960
|
return false;
|
|
4158
3961
|
const lane = laneOf(reg.claimed);
|
|
4159
|
-
if (!lane || !
|
|
3962
|
+
if (!lane || !fs20.existsSync(path18.join(laneDirs(lane).active, reg.claimed)))
|
|
4160
3963
|
return false;
|
|
4161
3964
|
const last = readStatusLog(reg.claimed, lane).at(-1)?.verb;
|
|
4162
3965
|
return last === void 0 || !TERMINAL_VERBS.includes(last);
|
|
4163
3966
|
}
|
|
3967
|
+
function unreportedTrapBait(reg) {
|
|
3968
|
+
if (!hasOpenCatch(reg))
|
|
3969
|
+
return void 0;
|
|
3970
|
+
const id = reg.claimed;
|
|
3971
|
+
const lane = laneOf(id);
|
|
3972
|
+
const claim = readSessionClaim(id, lane);
|
|
3973
|
+
if (!claim || claim.by !== `wt:${reg.trapId}`)
|
|
3974
|
+
return void 0;
|
|
3975
|
+
const log = readStatusLog(id, lane);
|
|
3976
|
+
let receipt = -1;
|
|
3977
|
+
for (let i = 0; i < log.length; i++) {
|
|
3978
|
+
const s = log[i];
|
|
3979
|
+
if (s.at === claim.at && s.note === `claimed by ${claim.by}` && !s.reported)
|
|
3980
|
+
receipt = i;
|
|
3981
|
+
}
|
|
3982
|
+
const reports = receipt >= 0 ? log.slice(receipt + 1) : log.filter((s) => Date.parse(s.at) >= Date.parse(claim.at));
|
|
3983
|
+
if (reports.some((s) => s.reported))
|
|
3984
|
+
return void 0;
|
|
3985
|
+
try {
|
|
3986
|
+
const descriptor = JSON.parse(fs20.readFileSync(path18.join(laneDirs(lane).active, id, "descriptor.json"), "utf8"));
|
|
3987
|
+
return { id, lane, descriptor, claim };
|
|
3988
|
+
} catch {
|
|
3989
|
+
return void 0;
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
function noticeIdleTrapClaims(now2 = Date.now(), graceMs = DEFAULT_SOAK.claimIdleNoticeSecs * 1e3) {
|
|
3993
|
+
for (const reg of listTraps()) {
|
|
3994
|
+
const bait = unreportedTrapBait(reg);
|
|
3995
|
+
if (!bait || cancelRequested(bait.id, bait.lane))
|
|
3996
|
+
continue;
|
|
3997
|
+
const age2 = now2 - Date.parse(bait.claim.at);
|
|
3998
|
+
if (!Number.isFinite(age2) || age2 < graceMs)
|
|
3999
|
+
continue;
|
|
4000
|
+
postNotice({
|
|
4001
|
+
kind: "trap-claim-idle",
|
|
4002
|
+
refId: bait.id,
|
|
4003
|
+
repo: bait.descriptor.repo,
|
|
4004
|
+
text: `${trapLabel(reg)} claimed ${bait.id} ${Math.floor(age2 / 1e3)}s ago but has not reported. The brief will be re-delivered at its next park; check that the session is awake.`,
|
|
4005
|
+
dedupeKey: `trap-claim-idle-${bait.id}-${bait.claim.at}`
|
|
4006
|
+
});
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4164
4009
|
function releaseCatch(reg) {
|
|
4165
4010
|
const id = reg.claimed;
|
|
4166
4011
|
if (!id)
|
|
4167
4012
|
return {};
|
|
4168
4013
|
const lane = laneOf(id);
|
|
4169
|
-
if (!lane || !
|
|
4014
|
+
if (!lane || !fs20.existsSync(path18.join(laneDirs(lane).active, id)))
|
|
4170
4015
|
return {};
|
|
4171
4016
|
const last = readStatusLog(id, lane).at(-1)?.verb;
|
|
4172
4017
|
if (last && TERMINAL_VERBS.includes(last)) {
|
|
4173
4018
|
complete(id, lane);
|
|
4174
4019
|
return { finalized: id };
|
|
4175
4020
|
}
|
|
4176
|
-
|
|
4021
|
+
fs20.rmSync(path18.join(laneDirs(lane).active, id, "claim.json"), { force: true });
|
|
4177
4022
|
if (cancelRequested(id, lane)) {
|
|
4178
4023
|
appendStatus(id, lane, "failed", "cancelled by request; claimant gone, work preserved");
|
|
4179
4024
|
complete(id, lane);
|
|
@@ -4195,7 +4040,7 @@ function claimBait(reg) {
|
|
|
4195
4040
|
const id = claimNext(lane, (d) => !pass(d));
|
|
4196
4041
|
if (!id)
|
|
4197
4042
|
continue;
|
|
4198
|
-
const descriptor = JSON.parse(
|
|
4043
|
+
const descriptor = JSON.parse(fs20.readFileSync(path18.join(laneDirs(lane).active, id, "descriptor.json"), "utf8"));
|
|
4199
4044
|
const claim = {
|
|
4200
4045
|
by: mine,
|
|
4201
4046
|
sessionId: reg.sessionId,
|
|
@@ -4203,7 +4048,7 @@ function claimBait(reg) {
|
|
|
4203
4048
|
worktree: reg.worktree,
|
|
4204
4049
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4205
4050
|
};
|
|
4206
|
-
|
|
4051
|
+
atomicWrite3(claimPath(id, lane), JSON.stringify(claim, null, 2));
|
|
4207
4052
|
appendStatus(id, lane, "working", `claimed by ${mine}`, claim.at);
|
|
4208
4053
|
mergeEvidence(id, lane, { sessionId: reg.sessionId, harness: reg.harness, deliveredTo: mine, deliveredAt: claim.at });
|
|
4209
4054
|
heartbeatTrap(reg.trapId, { claimed: id, parked: true });
|
|
@@ -4258,13 +4103,13 @@ function sweepGhostTraps(ttlMs, now2 = Date.now(), pausedTtlMs = 864e5) {
|
|
|
4258
4103
|
}
|
|
4259
4104
|
}
|
|
4260
4105
|
const released = releaseCatch(reg);
|
|
4261
|
-
const removal = reg.createdWorktree &&
|
|
4106
|
+
const removal = reg.createdWorktree && fs20.existsSync(reg.worktree) ? removeGhostWorktree(reg.worktree) : void 0;
|
|
4262
4107
|
const catchNote = released.requeued ? ", catch requeued" : released.finalized ? ", catch finalized" : "";
|
|
4263
4108
|
const worktreeNote = removal ? `; worktree ${removal.removed ? "removed" : "kept"}: ${reg.worktree}; branch ${removal.branch}; modified files ${removal.modifiedFiles ?? "unknown"}; unpushed commits ${removal.unpushedCommits ?? "unknown"}${removal.reason ? ` (${removal.reason})` : ""}` : "; worktree kept; re-soaking restores the same address";
|
|
4264
4109
|
actions.push({ trapId: reg.trapId, ...released, ...pauseExpired ? { pauseExpired } : {}, ...removal ? { worktree: removal.removed ? "removed" : "kept" } : {} });
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4110
|
+
fs20.rmSync(regPath(reg.trapId), { force: true });
|
|
4111
|
+
fs20.rmSync(beatPath(reg.trapId), { force: true });
|
|
4112
|
+
fs20.rmSync(prProbePath(reg.trapId), { force: true });
|
|
4268
4113
|
postNotice({
|
|
4269
4114
|
kind: "trap-ghosted",
|
|
4270
4115
|
text: (pauseExpired ? `trap ${trapLabel(reg)} ghosted: its pause expired (paused on ${reg.claimed.slice(0, 8)} past --until or [soak].pausedTtlSecs, and quiet since)` : `trap ${trapLabel(reg)} ghosted (went quiet mid-watch)`) + ` \u2014 registration removed${catchNote}${worktreeNote}`,
|
|
@@ -4299,7 +4144,7 @@ function baitBrief(id, d) {
|
|
|
4299
4144
|
`You are a lobstah worker session and have been assigned dispatch ${id}.`,
|
|
4300
4145
|
"",
|
|
4301
4146
|
"Do the work in THIS worktree on a fresh branch (branch first, never on the checked-out state directly).",
|
|
4302
|
-
`
|
|
4147
|
+
`Acknowledge this assignment now with \`lobstah report ${id} working "<note>"\`, then report at milestones, check \`lobstah inbox ${id}\` at natural checkpoints, and finish with \`lobstah report ${id} done "<note>" [--pr <url>]\` (or \`failed\`).`,
|
|
4303
4148
|
"A needs-decision or blocked report queues your question to the human; the answer arrives in this dispatch's inbox.",
|
|
4304
4149
|
`Before you wait on something outside lobstah (a human review, a PR review, a deploy), report \`lobstah report ${id} paused "<note>" --waiting-on review|pr|deploy|person|external --link <url>\`.`,
|
|
4305
4150
|
"After EVERY report, run `lobstah soak --wait` again \u2014 it delivers inbox answers and your next assignment. Never end your turn without it unless you are signing off (`lobstah stow`).",
|
|
@@ -4311,40 +4156,40 @@ function baitBrief(id, d) {
|
|
|
4311
4156
|
].join("\n");
|
|
4312
4157
|
}
|
|
4313
4158
|
function beatPath(trapId) {
|
|
4314
|
-
return
|
|
4159
|
+
return path18.join(soakingDir(), `${trapId}.beat`);
|
|
4315
4160
|
}
|
|
4316
4161
|
function readBeat(trapId) {
|
|
4317
4162
|
try {
|
|
4318
|
-
const parsed = JSON.parse(
|
|
4163
|
+
const parsed = JSON.parse(fs20.readFileSync(beatPath(trapId), "utf8"));
|
|
4319
4164
|
return typeof parsed.at === "string" ? parsed : void 0;
|
|
4320
4165
|
} catch {
|
|
4321
4166
|
return void 0;
|
|
4322
4167
|
}
|
|
4323
4168
|
}
|
|
4324
4169
|
function prProbePath(trapId) {
|
|
4325
|
-
return
|
|
4170
|
+
return path18.join(soakingDir(), `${trapId}.prprobe`);
|
|
4326
4171
|
}
|
|
4327
4172
|
function readTrapPrProbe(trapId) {
|
|
4328
4173
|
try {
|
|
4329
|
-
const parsed = JSON.parse(
|
|
4174
|
+
const parsed = JSON.parse(fs20.readFileSync(prProbePath(trapId), "utf8"));
|
|
4330
4175
|
return typeof parsed.dispatch === "string" && typeof parsed.checkedAt === "string" ? parsed : void 0;
|
|
4331
4176
|
} catch {
|
|
4332
4177
|
return void 0;
|
|
4333
4178
|
}
|
|
4334
4179
|
}
|
|
4335
4180
|
function writeTrapPrProbe(trapId, probe) {
|
|
4336
|
-
|
|
4181
|
+
atomicWrite3(prProbePath(trapId), JSON.stringify(probe));
|
|
4337
4182
|
}
|
|
4338
4183
|
function trapLastSeen(reg) {
|
|
4339
4184
|
return Math.max(msOf(reg.heartbeatAt), msOf(readBeat(reg.trapId)?.at ?? ""));
|
|
4340
4185
|
}
|
|
4341
4186
|
function trapIdAbove(dir) {
|
|
4342
|
-
let at =
|
|
4187
|
+
let at = path18.resolve(dir);
|
|
4343
4188
|
for (; ; ) {
|
|
4344
4189
|
const id = trapIdAt(at);
|
|
4345
4190
|
if (id !== void 0)
|
|
4346
4191
|
return id;
|
|
4347
|
-
const up =
|
|
4192
|
+
const up = path18.dirname(at);
|
|
4348
4193
|
if (up === at)
|
|
4349
4194
|
return void 0;
|
|
4350
4195
|
at = up;
|
|
@@ -4366,7 +4211,7 @@ function beatTrap(input) {
|
|
|
4366
4211
|
if (now2 - last < (input.throttleMs ?? 3e4))
|
|
4367
4212
|
return { beat: false, reason: "throttled" };
|
|
4368
4213
|
const at = new Date(now2).toISOString();
|
|
4369
|
-
|
|
4214
|
+
atomicWrite3(beatPath(trapId), JSON.stringify({ at, sessionId: reg.sessionId }));
|
|
4370
4215
|
if (!hasOpenCatch(reg))
|
|
4371
4216
|
return { beat: true, trapId };
|
|
4372
4217
|
const name = input.toolName || "tool";
|
|
@@ -4391,16 +4236,229 @@ var init_soak = __esm({
|
|
|
4391
4236
|
init_worktrees();
|
|
4392
4237
|
init_worktree_safety();
|
|
4393
4238
|
init_trap_start();
|
|
4239
|
+
init_config();
|
|
4394
4240
|
TRAP_FILE = ".lobstah-trap";
|
|
4395
4241
|
TRAP_ANCHOR_FILE = TRAP_FILE;
|
|
4396
4242
|
TRAP_ADDRESS_RE = /\bwt:([a-z0-9][a-z0-9-]*)/g;
|
|
4397
|
-
signedOffDir = () =>
|
|
4398
|
-
signedOffPath = (trapId) =>
|
|
4243
|
+
signedOffDir = () => path18.join(path18.dirname(soakingDir()), "signed-off");
|
|
4244
|
+
signedOffPath = (trapId) => path18.join(signedOffDir(), `${trapId}.json`);
|
|
4399
4245
|
TITLE_REMINDERS = 3;
|
|
4400
4246
|
msOf = (iso) => Date.parse(iso) || 0;
|
|
4401
4247
|
}
|
|
4402
4248
|
});
|
|
4403
4249
|
|
|
4250
|
+
// packages/core/dist/helm.js
|
|
4251
|
+
import * as fs21 from "node:fs";
|
|
4252
|
+
import * as path19 from "node:path";
|
|
4253
|
+
function helmDir() {
|
|
4254
|
+
return path19.join(lobstahHome(), "helm");
|
|
4255
|
+
}
|
|
4256
|
+
function relievedDir() {
|
|
4257
|
+
return path19.join(helmDir(), ".relieved");
|
|
4258
|
+
}
|
|
4259
|
+
function helmPath(grounds) {
|
|
4260
|
+
return path19.join(helmDir(), `${grounds}.json`);
|
|
4261
|
+
}
|
|
4262
|
+
function atomicWrite4(file, content) {
|
|
4263
|
+
fs21.mkdirSync(path19.dirname(file), { recursive: true });
|
|
4264
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
4265
|
+
fs21.writeFileSync(tmp, content);
|
|
4266
|
+
fs21.renameSync(tmp, file);
|
|
4267
|
+
}
|
|
4268
|
+
function readHelm(grounds) {
|
|
4269
|
+
try {
|
|
4270
|
+
return JSON.parse(fs21.readFileSync(helmPath(grounds), "utf8"));
|
|
4271
|
+
} catch {
|
|
4272
|
+
return void 0;
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
function listHelms() {
|
|
4276
|
+
try {
|
|
4277
|
+
return fs21.readdirSync(helmDir()).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(fs21.readFileSync(path19.join(helmDir(), f), "utf8")));
|
|
4278
|
+
} catch {
|
|
4279
|
+
return [];
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
function helmOf(sessionId) {
|
|
4283
|
+
return listHelms().find((h) => h.sessionId === sessionId);
|
|
4284
|
+
}
|
|
4285
|
+
function liveHelms(ttlMs, now2 = Date.now()) {
|
|
4286
|
+
return listHelms().filter((h) => now2 - (Date.parse(h.heartbeatAt) || 0) <= ttlMs);
|
|
4287
|
+
}
|
|
4288
|
+
function helmGate(live, sessionId, grounds) {
|
|
4289
|
+
if (grounds !== void 0) {
|
|
4290
|
+
const holder = live.find((h) => h.grounds === grounds);
|
|
4291
|
+
if (holder && holder.sessionId !== sessionId) {
|
|
4292
|
+
return `grounds "${grounds}" is helmed by session ${holder.sessionId.slice(0, 8)} \u2014 its verbs are that session's alone. Run this from that session (--session), or take its helm (\`lobstah man helm --take\`).`;
|
|
4293
|
+
}
|
|
4294
|
+
if (holder)
|
|
4295
|
+
return void 0;
|
|
4296
|
+
}
|
|
4297
|
+
if (live.length === 0)
|
|
4298
|
+
return void 0;
|
|
4299
|
+
if (sessionId !== void 0 && live.some((h) => h.sessionId === sessionId))
|
|
4300
|
+
return void 0;
|
|
4301
|
+
const holders = live.map((h) => `${h.grounds}=${h.sessionId.slice(0, 8)}`).join(", ");
|
|
4302
|
+
return `the helm is claimed (${holders}) \u2014 this verb is reserved for the helm session. Run it there with --session <helm-session-id>, or take the helm (\`lobstah man helm --take\`).`;
|
|
4303
|
+
}
|
|
4304
|
+
function helmLabel(h) {
|
|
4305
|
+
if (h.label)
|
|
4306
|
+
return h.label;
|
|
4307
|
+
const where = h.cwd ? path19.basename(h.cwd) : void 0;
|
|
4308
|
+
if (!h.harness && !where)
|
|
4309
|
+
return `session ${h.sessionId.slice(0, 8)}`;
|
|
4310
|
+
return `${h.harness ?? "session"}${where ? ` @ ${where}` : ""}`;
|
|
4311
|
+
}
|
|
4312
|
+
function takeHelm(opts) {
|
|
4313
|
+
const now2 = opts.now ?? Date.now();
|
|
4314
|
+
const existing = readHelm(opts.grounds.name);
|
|
4315
|
+
if (existing && existing.sessionId !== opts.sessionId) {
|
|
4316
|
+
const fresh = now2 - (Date.parse(existing.heartbeatAt) || 0) <= opts.ttlMs;
|
|
4317
|
+
if (fresh && !opts.take)
|
|
4318
|
+
return { held: existing };
|
|
4319
|
+
atomicWrite4(path19.join(relievedDir(), `${existing.sessionId}.json`), JSON.stringify({
|
|
4320
|
+
sessionId: existing.sessionId,
|
|
4321
|
+
grounds: opts.grounds.name,
|
|
4322
|
+
by: opts.sessionId,
|
|
4323
|
+
at: new Date(now2).toISOString()
|
|
4324
|
+
}, null, 2));
|
|
4325
|
+
}
|
|
4326
|
+
const iso = new Date(now2).toISOString();
|
|
4327
|
+
const same = existing?.sessionId === opts.sessionId;
|
|
4328
|
+
const live = existing !== void 0 && now2 - (Date.parse(existing.heartbeatAt) || 0) <= opts.ttlMs;
|
|
4329
|
+
const reg = {
|
|
4330
|
+
sessionId: opts.sessionId,
|
|
4331
|
+
grounds: opts.grounds.name,
|
|
4332
|
+
repos: opts.grounds.repos,
|
|
4333
|
+
signedOnAt: same ? existing.signedOnAt : iso,
|
|
4334
|
+
heartbeatAt: iso,
|
|
4335
|
+
// A new helm on empty grounds starts its cursor now: a notice delivered
|
|
4336
|
+
// to nobody while no helm was signed on is not news. A take from a live
|
|
4337
|
+
// helm, or the same session signing on again, keeps the cursor, so
|
|
4338
|
+
// nothing in flight is dropped.
|
|
4339
|
+
wakesFrom: same || live ? existing.wakesFrom : iso,
|
|
4340
|
+
// Identity refreshes on every sign-on; a re-sign without one keeps what
|
|
4341
|
+
// the registration already knows.
|
|
4342
|
+
harness: opts.identity?.harness ?? (same ? existing.harness : void 0),
|
|
4343
|
+
cwd: opts.identity?.cwd ?? (same ? existing.cwd : void 0),
|
|
4344
|
+
host: opts.identity?.host ?? (same ? existing.host : void 0),
|
|
4345
|
+
label: opts.identity?.label ?? (same ? existing.label : void 0),
|
|
4346
|
+
window: opts.identity?.window ?? (same ? existing.window : void 0),
|
|
4347
|
+
tookFrom: existing && existing.sessionId !== opts.sessionId ? { sessionId: existing.sessionId, at: iso } : existing?.tookFrom
|
|
4348
|
+
};
|
|
4349
|
+
atomicWrite4(helmPath(opts.grounds.name), JSON.stringify(reg, null, 2));
|
|
4350
|
+
return { ok: reg };
|
|
4351
|
+
}
|
|
4352
|
+
function wakeFloorMs(h) {
|
|
4353
|
+
return h?.wakesFrom ? Date.parse(h.wakesFrom) || 0 : 0;
|
|
4354
|
+
}
|
|
4355
|
+
function noticeStands(n) {
|
|
4356
|
+
switch (n.kind) {
|
|
4357
|
+
case "disk-held":
|
|
4358
|
+
return readHold() !== void 0;
|
|
4359
|
+
case "watch-failing":
|
|
4360
|
+
case "watch-held":
|
|
4361
|
+
return listWatches().some((w) => (w.key === n.refId || w.owner === `dispatch:${n.refId}`) && (n.kind === "watch-failing" ? w.failingNoticed === true : true));
|
|
4362
|
+
case "trap-request":
|
|
4363
|
+
return n.refId !== void 0 && isOpenRequest(n.refId);
|
|
4364
|
+
case "trap-claim-idle":
|
|
4365
|
+
return listTraps().some((reg) => {
|
|
4366
|
+
if (reg.claimed !== n.refId)
|
|
4367
|
+
return false;
|
|
4368
|
+
const bait = unreportedTrapBait(reg);
|
|
4369
|
+
return bait !== void 0 && !cancelRequested(bait.id, bait.lane) && Date.parse(bait.claim.at) <= Date.parse(n.at);
|
|
4370
|
+
});
|
|
4371
|
+
default:
|
|
4372
|
+
return false;
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
function noticeWakes(h) {
|
|
4376
|
+
const floor = wakeFloorMs(h);
|
|
4377
|
+
if (floor === 0)
|
|
4378
|
+
return void 0;
|
|
4379
|
+
return (n) => (Date.parse(n.at) || 0) >= floor || noticeStands(n);
|
|
4380
|
+
}
|
|
4381
|
+
function relieveHelm(sessionId) {
|
|
4382
|
+
const relieved = [];
|
|
4383
|
+
for (const h of listHelms()) {
|
|
4384
|
+
if (h.sessionId !== sessionId)
|
|
4385
|
+
continue;
|
|
4386
|
+
fs21.rmSync(helmPath(h.grounds), { force: true });
|
|
4387
|
+
relieved.push(h.grounds);
|
|
4388
|
+
}
|
|
4389
|
+
fs21.rmSync(path19.join(relievedDir(), `${sessionId}.json`), { force: true });
|
|
4390
|
+
return relieved;
|
|
4391
|
+
}
|
|
4392
|
+
function heartbeatHelm(sessionId, opts = {}) {
|
|
4393
|
+
const reg = helmOf(sessionId);
|
|
4394
|
+
if (!reg)
|
|
4395
|
+
return void 0;
|
|
4396
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
4397
|
+
const next = { ...reg, heartbeatAt: now2, ...opts.parked ? { parkedAt: now2 } : {} };
|
|
4398
|
+
atomicWrite4(helmPath(reg.grounds), JSON.stringify(next, null, 2));
|
|
4399
|
+
return next;
|
|
4400
|
+
}
|
|
4401
|
+
function consumeRelievedNotice(sessionId) {
|
|
4402
|
+
const file = path19.join(relievedDir(), `${sessionId}.json`);
|
|
4403
|
+
try {
|
|
4404
|
+
const notice = JSON.parse(fs21.readFileSync(file, "utf8"));
|
|
4405
|
+
fs21.rmSync(file, { force: true });
|
|
4406
|
+
return notice;
|
|
4407
|
+
} catch {
|
|
4408
|
+
return void 0;
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
function groundsList(cfg) {
|
|
4412
|
+
const entries = Object.entries(cfg.grounds);
|
|
4413
|
+
if (entries.length === 0)
|
|
4414
|
+
return [{ name: "fleet", repos: Object.keys(cfg.repos) }];
|
|
4415
|
+
return entries.map(([name, g]) => ({ name, repos: g.repos }));
|
|
4416
|
+
}
|
|
4417
|
+
function groundsErrors(cfg) {
|
|
4418
|
+
const errors = [];
|
|
4419
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4420
|
+
for (const [name, g] of Object.entries(cfg.grounds)) {
|
|
4421
|
+
for (const repo of g.repos) {
|
|
4422
|
+
if (cfg.repos[repo] === void 0)
|
|
4423
|
+
errors.push(`grounds.${name} names unknown repo "${repo}"`);
|
|
4424
|
+
const other = seen.get(repo);
|
|
4425
|
+
if (other !== void 0)
|
|
4426
|
+
errors.push(`repo "${repo}" is in both grounds.${other} and grounds.${name} \u2014 a repo belongs to at most one grounds`);
|
|
4427
|
+
else
|
|
4428
|
+
seen.set(repo, name);
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
return errors;
|
|
4432
|
+
}
|
|
4433
|
+
function resolveGrounds(cfg, name) {
|
|
4434
|
+
const all = groundsList(cfg);
|
|
4435
|
+
if (name === void 0) {
|
|
4436
|
+
if (all.length === 1)
|
|
4437
|
+
return all[0];
|
|
4438
|
+
throw new Error(`multiple grounds configured \u2014 pass --grounds <${all.map((g) => g.name).join("|")}>`);
|
|
4439
|
+
}
|
|
4440
|
+
const found = all.find((g) => g.name === name);
|
|
4441
|
+
if (!found)
|
|
4442
|
+
throw new Error(`unknown grounds "${name}" (configured: ${all.map((g) => g.name).join(", ")})`);
|
|
4443
|
+
return found;
|
|
4444
|
+
}
|
|
4445
|
+
var isOpenRequest;
|
|
4446
|
+
var init_helm = __esm({
|
|
4447
|
+
"packages/core/dist/helm.js"() {
|
|
4448
|
+
"use strict";
|
|
4449
|
+
init_disk();
|
|
4450
|
+
init_paths();
|
|
4451
|
+
init_watch();
|
|
4452
|
+
init_requests();
|
|
4453
|
+
init_queue();
|
|
4454
|
+
init_soak();
|
|
4455
|
+
isOpenRequest = (id) => {
|
|
4456
|
+
const r = readRequest(id);
|
|
4457
|
+
return r !== void 0 && r.closedAt === void 0;
|
|
4458
|
+
};
|
|
4459
|
+
}
|
|
4460
|
+
});
|
|
4461
|
+
|
|
4404
4462
|
// packages/core/dist/inbox.js
|
|
4405
4463
|
import * as fs22 from "node:fs";
|
|
4406
4464
|
import * as path20 from "node:path";
|
|
@@ -5067,7 +5125,7 @@ function normalizeChecks(rollup) {
|
|
|
5067
5125
|
}
|
|
5068
5126
|
return [...latest.values()].map((v) => v.check).sort((a, b) => a.name.localeCompare(b.name));
|
|
5069
5127
|
}
|
|
5070
|
-
function prStandingKinds(pr) {
|
|
5128
|
+
function prStandingKinds(pr, settle2) {
|
|
5071
5129
|
if (pr.state !== "OPEN")
|
|
5072
5130
|
return [];
|
|
5073
5131
|
const out = [];
|
|
@@ -5081,8 +5139,11 @@ function prStandingKinds(pr) {
|
|
|
5081
5139
|
out.push("pr:checks");
|
|
5082
5140
|
if (isConflicting(pr.mergeStateStatus))
|
|
5083
5141
|
out.push("pr:conflict");
|
|
5084
|
-
if (!pr.checks.unknown && !pr.draft && !review && isMergeable(pr.mergeStateStatus) && failed === 0 && pending === 0 && (pr.reviewDecision === "APPROVED" || total > 0))
|
|
5085
|
-
|
|
5142
|
+
if (!pr.checks.unknown && !pr.draft && !review && isMergeable(pr.mergeStateStatus) && failed === 0 && pending === 0 && (pr.reviewDecision === "APPROVED" || total > 0)) {
|
|
5143
|
+
const since = Date.parse(pr.standingSince?.["pr:ready"] ?? pr.observedAt);
|
|
5144
|
+
if (!settle2 || settle2.readySettleSecs === 0 || settle2.now >= since + settle2.readySettleSecs * 1e3)
|
|
5145
|
+
out.push("pr:ready");
|
|
5146
|
+
}
|
|
5086
5147
|
return out;
|
|
5087
5148
|
}
|
|
5088
5149
|
function prReview(view) {
|
|
@@ -5438,7 +5499,7 @@ function upsertPr(pr, dispatchId) {
|
|
|
5438
5499
|
const merged = { ...before ?? {}, ...pr, failingChecks: pr.failingChecks };
|
|
5439
5500
|
const standingSince = {};
|
|
5440
5501
|
for (const kind of prStandingKinds(merged)) {
|
|
5441
|
-
standingSince[kind] = before?.standingSince?.[kind] ?? pr.observedAt;
|
|
5502
|
+
standingSince[kind] = kind === "pr:ready" && before?.headSha !== pr.headSha ? pr.observedAt : before?.standingSince?.[kind] ?? pr.observedAt;
|
|
5442
5503
|
}
|
|
5443
5504
|
const after = {
|
|
5444
5505
|
...merged,
|
|
@@ -6185,6 +6246,21 @@ async function allocate(repo, id, fromRemoteBranch = repo.trunk) {
|
|
|
6185
6246
|
await runSetup(repo, dir);
|
|
6186
6247
|
return dir;
|
|
6187
6248
|
}
|
|
6249
|
+
async function recoverWorktree(repo, id) {
|
|
6250
|
+
const dir = worktreePath(id);
|
|
6251
|
+
try {
|
|
6252
|
+
if (fs31.statSync(path27.join(dir, ".git")).isFile()) {
|
|
6253
|
+
const branch = await git2(dir, "symbolic-ref", "--quiet", "HEAD");
|
|
6254
|
+
const root = await git2(dir, "rev-parse", "--show-toplevel");
|
|
6255
|
+
const common = await git2(dir, "rev-parse", "--path-format=absolute", "--git-common-dir");
|
|
6256
|
+
const mine = await git2(repo.path, "rev-parse", "--path-format=absolute", "--git-common-dir");
|
|
6257
|
+
if (branch === `refs/heads/lobstah/${id}` && realpath(root) === realpath(dir) && realpath(common) === realpath(mine))
|
|
6258
|
+
return dir;
|
|
6259
|
+
}
|
|
6260
|
+
} catch {
|
|
6261
|
+
}
|
|
6262
|
+
throw new Error(`unrecorded worktree at ${dir} for ${id} \u2014 ownership not proven (expected lobstah/${id} in ${repo.path}); inspect and preserve it, then relocate the conflicting checkout before retrying`);
|
|
6263
|
+
}
|
|
6188
6264
|
async function runSetup(repo, dir) {
|
|
6189
6265
|
for (const cmd of repo.setup ?? []) {
|
|
6190
6266
|
await shell(cmd, { cwd: dir, env: { ...process.env, ...repo.env ?? {} } });
|
|
@@ -7723,17 +7799,19 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
7723
7799
|
});
|
|
7724
7800
|
const wtFile = path64.join(activeDir, "worktree.json");
|
|
7725
7801
|
let recorded;
|
|
7802
|
+
let recovered = false;
|
|
7726
7803
|
let choice;
|
|
7727
7804
|
if (fs77.existsSync(wtFile)) {
|
|
7728
7805
|
recorded = JSON.parse(fs77.readFileSync(wtFile, "utf8")).path;
|
|
7729
7806
|
if (!fs77.existsSync(recorded))
|
|
7730
7807
|
throw new Error(`recorded worktree ${recorded} is gone`);
|
|
7731
7808
|
} else if (fs77.existsSync(worktreePath(id))) {
|
|
7732
|
-
|
|
7809
|
+
recorded = await recoverWorktree(repo, id);
|
|
7810
|
+
recovered = true;
|
|
7733
7811
|
} else if (descriptor.followUp && cfg.limits.reuseWorktree !== false) {
|
|
7734
7812
|
choice = await deps.chooseWorktree({ id, lane, repoKey: descriptor.repo, repo, followUp: descriptor.followUp });
|
|
7735
7813
|
}
|
|
7736
|
-
const worktreeNote = choice ? choice.reuse ? `reusing worktree of ${short3(choice.from)}` : `fresh worktree (${choice.reason})` : void 0;
|
|
7814
|
+
const worktreeNote = choice ? choice.reuse ? `reusing worktree of ${short3(choice.from)}` : `fresh worktree (${choice.reason})` : recovered ? "recovered own unrecorded worktree" : void 0;
|
|
7737
7815
|
const firstModel = modelForHarness(plan.harness, resolved.model);
|
|
7738
7816
|
const firstNote = [
|
|
7739
7817
|
wake ? `woke from pause: ${wake}` : attempts > 1 ? `attempt ${attempts}` : void 0,
|
|
@@ -7752,7 +7830,13 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
7752
7830
|
let cwd;
|
|
7753
7831
|
if (recorded) {
|
|
7754
7832
|
cwd = recorded;
|
|
7755
|
-
acquireWorktreeLock(cwd, id, lane);
|
|
7833
|
+
const held = acquireWorktreeLock(cwd, id, lane);
|
|
7834
|
+
if (held)
|
|
7835
|
+
throw new Error(`worktree at ${cwd} is in use by ${held.id} \u2014 wait for that dispatch to release it before retrying`);
|
|
7836
|
+
if (recovered) {
|
|
7837
|
+
await deps.prepareReuse(repo, cwd);
|
|
7838
|
+
fs77.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
|
|
7839
|
+
}
|
|
7756
7840
|
} else if (choice?.reuse) {
|
|
7757
7841
|
cwd = choice.path;
|
|
7758
7842
|
const { setupRan } = await deps.prepareReuse(repo, cwd);
|
|
@@ -8693,6 +8777,7 @@ function tick(log = () => {
|
|
|
8693
8777
|
for (const t of bounceExpiredSignOffs(signOffGraceMs))
|
|
8694
8778
|
log(`trap wt:${t} did not re-soak within the sign-off grace \u2014 its messages bounced to the helm`);
|
|
8695
8779
|
noticeOrphanedBait(Date.now(), signOffGraceMs);
|
|
8780
|
+
noticeIdleTrapClaims(now2, cfg.soak.claimIdleNoticeSecs * 1e3);
|
|
8696
8781
|
const workSkip = daemonSkip(listTraps(), cfg.soak.deferSecs * 1e3);
|
|
8697
8782
|
const choreSkip = (d) => d.for !== void 0 && (!d.systemRepair?.trapWaitUntil || Date.now() < Date.parse(d.systemRepair.trapWaitUntil));
|
|
8698
8783
|
const skipFor = (lane) => lane === "work" ? workSkip : choreSkip;
|
|
@@ -10379,8 +10464,7 @@ function removeAck(key) {
|
|
|
10379
10464
|
}
|
|
10380
10465
|
var sha2 = (v) => createHash7("sha1").update(JSON.stringify(v)).digest("hex").slice(0, 16);
|
|
10381
10466
|
function prStateHash(pr) {
|
|
10382
|
-
|
|
10383
|
-
if (readySince) return sha2({ "pr:ready": readySince });
|
|
10467
|
+
if (prStandingKinds(pr).includes("pr:ready")) return prReadyStateHash(pr.headSha);
|
|
10384
10468
|
return sha2({
|
|
10385
10469
|
headSha: pr.headSha,
|
|
10386
10470
|
state: pr.state,
|
|
@@ -10393,15 +10477,18 @@ function prStateHash(pr) {
|
|
|
10393
10477
|
repair: pr.repair ? { headSha: pr.repair.headSha, kind: pr.repair.kind, status: pr.repair.status, reason: pr.repair.reason } : void 0
|
|
10394
10478
|
});
|
|
10395
10479
|
}
|
|
10480
|
+
var prReadyStateHash = (headSha) => sha2({ "pr:ready": headSha });
|
|
10396
10481
|
function currentAck(key, stateHash) {
|
|
10397
10482
|
const a = readAck(key);
|
|
10398
10483
|
return a && a.stateHash === stateHash ? a : void 0;
|
|
10399
10484
|
}
|
|
10400
10485
|
function pruneStaleAcks(standing) {
|
|
10401
|
-
const
|
|
10486
|
+
const items = new Map(standing.map((s) => [s.key, s]));
|
|
10402
10487
|
const pruned = [];
|
|
10403
10488
|
for (const a of listAcks()) {
|
|
10404
|
-
const
|
|
10489
|
+
const item = items.get(a.key);
|
|
10490
|
+
if (a.kind === "pr:ready" && item?.headSha && a.stateHash === prReadyStateHash(item.headSha)) continue;
|
|
10491
|
+
const h = item?.stateHash;
|
|
10405
10492
|
if (h !== void 0 && h !== a.stateHash && removeAck(a.key)) pruned.push(a.key);
|
|
10406
10493
|
}
|
|
10407
10494
|
return pruned;
|
|
@@ -11578,7 +11665,7 @@ function prAttention(now2, observed = observedPrs(), cfg = loadConfig()) {
|
|
|
11578
11665
|
);
|
|
11579
11666
|
const out = [];
|
|
11580
11667
|
for (const { id, lane, pr, dispatch } of observed) {
|
|
11581
|
-
const kinds = prKinds(pr).filter(
|
|
11668
|
+
const kinds = prKinds(pr, { readySettleSecs: cfg.readySettleSecs, now: now2 }).filter(
|
|
11582
11669
|
(kind) => kind !== "pr:ready" || !readyBlockedByStack(
|
|
11583
11670
|
pr,
|
|
11584
11671
|
observed.map((x) => x.pr)
|
|
@@ -11594,7 +11681,8 @@ function prAttention(now2, observed = observedPrs(), cfg = loadConfig()) {
|
|
|
11594
11681
|
if (!human.show) continue;
|
|
11595
11682
|
const repairCannotAct = human.reason !== void 0 && !["auto-repair is off", "not owned by lobstah", "no active PR watch"].includes(human.reason);
|
|
11596
11683
|
if (!repairCannotAct && onTheHook(kind, chain, { reviewRounds, watchFollowUp })) continue;
|
|
11597
|
-
const
|
|
11684
|
+
const conditionSince = pr.standingSince?.[kind] ?? pr.observedAt;
|
|
11685
|
+
const standingSince = kind === "pr:ready" ? new Date(Date.parse(conditionSince) + cfg.readySettleSecs * 1e3).toISOString() : conditionSince;
|
|
11598
11686
|
out.push({
|
|
11599
11687
|
kind,
|
|
11600
11688
|
key: ref?.key ?? pr.url,
|
|
@@ -12325,6 +12413,7 @@ function renderDigest(d) {
|
|
|
12325
12413
|
}
|
|
12326
12414
|
|
|
12327
12415
|
// apps/cli/src/charter.ts
|
|
12416
|
+
var HELM_REMINDER = "Helm reminder: any question or choice for the human goes on a card with lobstah man ask, not only in chat.";
|
|
12328
12417
|
function charter(g) {
|
|
12329
12418
|
const repos = g.repos.length > 0 ? g.repos.join(", ") : "(no repos configured)";
|
|
12330
12419
|
return `the helm charter \u2014 grounds "${g.name}" (${repos})
|
|
@@ -12335,6 +12424,7 @@ Role:
|
|
|
12335
12424
|
- Triage incoming work. Dispatch it. Review each catch. Decide requeue or cancel.
|
|
12336
12425
|
- Do not do the work yourself. Write a self-contained brief and dispatch it.
|
|
12337
12426
|
- Answer a worker's question when you can decide. Send the answer. Ask the human with \`lobstah man ask\` when the choice is theirs.
|
|
12427
|
+
- ${HELM_REMINDER}
|
|
12338
12428
|
|
|
12339
12429
|
Fences:
|
|
12340
12430
|
- The daemon claims, spawns, and restarts workers. Do not supervise a running catch.
|
|
@@ -16612,7 +16702,7 @@ var COMMANDS = {
|
|
|
16612
16702
|
"man:init": { flags: { "--shared": {}, "--global": {}, "--marker": {} } },
|
|
16613
16703
|
"man:haul": { flags: { "--timeout": { value: "<secs>" }, "--park": {} } },
|
|
16614
16704
|
"man:brief": { flags: {} },
|
|
16615
|
-
hook: { flags: {}, subverbs: ["session-start", "stop", "post-tool-use", "session-end"], maxPositionals: 1 },
|
|
16705
|
+
hook: { flags: {}, subverbs: ["session-start", "user-prompt-submit", "stop", "post-tool-use", "session-end"], maxPositionals: 1 },
|
|
16616
16706
|
"man:file": {
|
|
16617
16707
|
flags: { "--attach": { value: "<file>", repeatable: true }, "--title": { value: "<text>" }, "--grounds": { value: "<name>" }, "--session": { value: "<id>" } },
|
|
16618
16708
|
positionals: "<file.md>"
|
|
@@ -16770,7 +16860,8 @@ Stores the answer; the helm's man wait receives it as decision-answer.`,
|
|
|
16770
16860
|
"man:brief": `SessionStart-hook entry point: announce the session id and fleet state into
|
|
16771
16861
|
the conversation.`,
|
|
16772
16862
|
hook: `The plugin hook entry points. Each detects the session's role (helm, trap, or
|
|
16773
|
-
neither): session-start runs man brief,
|
|
16863
|
+
neither): session-start runs man brief, user-prompt-submit adds a helm card reminder,
|
|
16864
|
+
stop runs man haul (--park, --timeout),
|
|
16774
16865
|
post-tool-use runs soak beat, session-end runs stow --quiet. The older commands
|
|
16775
16866
|
stay as aliases.`,
|
|
16776
16867
|
__runner: `Internal: run one dispatch inside the compiled binary (the daemon re-execs
|
|
@@ -17008,7 +17099,7 @@ lobstah man (orchestrator sessions \u2014 bare \`lobstah man\` prints the manual
|
|
|
17008
17099
|
~/.claude/settings.json with --global (any
|
|
17009
17100
|
directory with a .lobstah-man file then
|
|
17010
17101
|
parks); --marker touches .lobstah-man.
|
|
17011
|
-
hook session-start|stop|post-tool-use|session-end
|
|
17102
|
+
hook session-start|user-prompt-submit|stop|post-tool-use|session-end
|
|
17012
17103
|
the plugin hook entry points: each
|
|
17013
17104
|
detects the session's role (helm, trap,
|
|
17014
17105
|
or neither). session-start = man brief,
|
|
@@ -17156,8 +17247,10 @@ async function soakPark(trapId, timeout, plain = false) {
|
|
|
17156
17247
|
);
|
|
17157
17248
|
return "woke";
|
|
17158
17249
|
}
|
|
17159
|
-
|
|
17160
|
-
|
|
17250
|
+
const pending = unreportedTrapBait(reg);
|
|
17251
|
+
const instruction = unhandled(id, lane).length > 0 ? `New instruction for your dispatch ${id} \u2014 read it with \`lobstah inbox ${id}\`, act on it, and keep reporting.` : void 0;
|
|
17252
|
+
if (pending || instruction) {
|
|
17253
|
+
block([instruction, pending ? baitBrief(id, pending.descriptor) : void 0].filter(Boolean).join("\n\n"));
|
|
17161
17254
|
return "woke";
|
|
17162
17255
|
}
|
|
17163
17256
|
} else {
|
|
@@ -17354,6 +17447,16 @@ var HOOK_COMMANDS = {
|
|
|
17354
17447
|
};
|
|
17355
17448
|
async function mainCli() {
|
|
17356
17449
|
let [cmd, ...args] = process.argv.slice(2);
|
|
17450
|
+
if (cmd === "hook" && args[0] === "user-prompt-submit") {
|
|
17451
|
+
try {
|
|
17452
|
+
const hook = readHookStdin();
|
|
17453
|
+
if (hook?.session_id && !trapBySession(hook.session_id) && helmOf(hook.session_id)) {
|
|
17454
|
+
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: HELM_REMINDER } }));
|
|
17455
|
+
}
|
|
17456
|
+
} catch {
|
|
17457
|
+
}
|
|
17458
|
+
return;
|
|
17459
|
+
}
|
|
17357
17460
|
if (cmd === "soak" && args[0] === "beat" || cmd === "hook" && args[0] === "post-tool-use") {
|
|
17358
17461
|
runBeat();
|
|
17359
17462
|
return;
|