lobstah 0.6.8 → 0.6.9

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 CHANGED
@@ -1943,6 +1943,7 @@ var init_config = __esm({
1943
1943
  DEFAULT_ATTENTION_KINDS = ["question", "decision", "pr:ready", "pr:review", "pr:conflict", "pr:checks"];
1944
1944
  DEFAULT_SOAK = {
1945
1945
  deferSecs: 90,
1946
+ claimIdleNoticeSecs: 180,
1946
1947
  ttlSecs: 1800,
1947
1948
  beat: true,
1948
1949
  pausedTtlSecs: 86400,
@@ -2387,253 +2388,9 @@ var init_watch = __esm({
2387
2388
  }
2388
2389
  });
2389
2390
 
2390
- // packages/core/dist/helm.js
2391
+ // packages/core/dist/activity.js
2391
2392
  import * as fs11 from "node:fs";
2392
2393
  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
2394
  function looksSecret(word) {
2638
2395
  if (SECRET_PATTERNS.some((re) => re.test(word)))
2639
2396
  return true;
@@ -2676,7 +2433,7 @@ function relativeTarget(p, root) {
2676
2433
  if (!root)
2677
2434
  return baseName(p);
2678
2435
  const win = isWindowsPath(p) || isWindowsPath(root);
2679
- const lib = win ? path12.win32 : path12.posix;
2436
+ const lib = win ? path11.win32 : path11.posix;
2680
2437
  if (!lib.isAbsolute(p))
2681
2438
  return p.replace(/\\/g, "/");
2682
2439
  const rel = lib.relative(win ? root : root.replace(/\\/g, "/"), p);
@@ -2743,18 +2500,18 @@ function activityFromEvent(ev, root) {
2743
2500
  return void 0;
2744
2501
  }
2745
2502
  }
2746
- function atomicWrite2(file, content) {
2503
+ function atomicWrite(file, content) {
2747
2504
  const tmp = `${file}.tmp-${process.pid}`;
2748
- fs12.writeFileSync(tmp, content);
2749
- fs12.renameSync(tmp, file);
2505
+ fs11.writeFileSync(tmp, content);
2506
+ fs11.renameSync(tmp, file);
2750
2507
  }
2751
2508
  function writeActivity(id, lane, a) {
2752
2509
  const safe = { at: a.at, kind: a.kind, summary: redactSummary(a.summary) };
2753
- atomicWrite2(activityPath(id, lane), JSON.stringify(safe));
2510
+ atomicWrite(activityPath(id, lane), JSON.stringify(safe));
2754
2511
  }
2755
2512
  function readActivity(id, lane) {
2756
2513
  try {
2757
- const parsed = JSON.parse(fs12.readFileSync(activityPath(id, lane), "utf8"));
2514
+ const parsed = JSON.parse(fs11.readFileSync(activityPath(id, lane), "utf8"));
2758
2515
  if (typeof parsed.at !== "string" || typeof parsed.summary !== "string")
2759
2516
  return void 0;
2760
2517
  if (!ACTIVITY_KINDS.includes(parsed.kind))
@@ -2865,7 +2622,7 @@ var init_activity = __esm({
2865
2622
  });
2866
2623
 
2867
2624
  // packages/core/dist/status.js
2868
- import * as fs13 from "node:fs";
2625
+ import * as fs12 from "node:fs";
2869
2626
  function isVerb(v) {
2870
2627
  return VERBS.includes(v);
2871
2628
  }
@@ -2922,7 +2679,7 @@ function appendStatus(id, lane, verb, note, at, waiting, reported = false) {
2922
2679
  ...extra,
2923
2680
  ...reported ? { reported: true } : {}
2924
2681
  };
2925
- fs13.appendFileSync(statusPath(id, lane), `${JSON.stringify(entry)}
2682
+ fs12.appendFileSync(statusPath(id, lane), `${JSON.stringify(entry)}
2926
2683
  `);
2927
2684
  if (verb === "done") {
2928
2685
  try {
@@ -2934,9 +2691,9 @@ function appendStatus(id, lane, verb, note, at, waiting, reported = false) {
2934
2691
  }
2935
2692
  function readStatusLog(id, lane) {
2936
2693
  const file = statusPath(id, lane);
2937
- if (!fs13.existsSync(file))
2694
+ if (!fs12.existsSync(file))
2938
2695
  return [];
2939
- return fs13.readFileSync(file, "utf8").split("\n").filter(Boolean).flatMap((line) => {
2696
+ return fs12.readFileSync(file, "utf8").split("\n").filter(Boolean).flatMap((line) => {
2940
2697
  try {
2941
2698
  const parsed = JSON.parse(line);
2942
2699
  return isVerb(parsed.verb) ? [parsed] : [];
@@ -2994,12 +2751,12 @@ var init_status = __esm({
2994
2751
  });
2995
2752
 
2996
2753
  // packages/core/dist/queue.js
2997
- import * as fs14 from "node:fs";
2998
- import * as path13 from "node:path";
2999
- function atomicWrite3(file, content) {
2754
+ import * as fs13 from "node:fs";
2755
+ import * as path12 from "node:path";
2756
+ function atomicWrite2(file, content) {
3000
2757
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
3001
- fs14.writeFileSync(tmp, content);
3002
- fs14.renameSync(tmp, file);
2758
+ fs13.writeFileSync(tmp, content);
2759
+ fs13.renameSync(tmp, file);
3003
2760
  }
3004
2761
  function enqueue(d, lane = "work") {
3005
2762
  if (!d.id || !d.repo || !d.brief) {
@@ -3011,11 +2768,11 @@ function enqueue(d, lane = "work") {
3011
2768
  ...inherited?.length ? { attachments: inherited } : {},
3012
2769
  queuedAt: d.queuedAt ?? (/* @__PURE__ */ new Date()).toISOString()
3013
2770
  };
3014
- atomicWrite3(path13.join(laneDirs(lane).queue, `${d.id}.json`), JSON.stringify(descriptor, null, 2));
2771
+ atomicWrite2(path12.join(laneDirs(lane).queue, `${d.id}.json`), JSON.stringify(descriptor, null, 2));
3015
2772
  }
3016
2773
  function mtimeIfPresent(file) {
3017
2774
  try {
3018
- return fs14.statSync(file).mtimeMs;
2775
+ return fs13.statSync(file).mtimeMs;
3019
2776
  } catch (e) {
3020
2777
  if (e.code === "ENOENT")
3021
2778
  return void 0;
@@ -3025,22 +2782,22 @@ function mtimeIfPresent(file) {
3025
2782
  function pendingIds(lane) {
3026
2783
  const dir = laneDirs(lane).queue;
3027
2784
  return readDirIfPresent(dir).filter((f) => f.endsWith(".json")).flatMap((f) => {
3028
- const m = mtimeIfPresent(path13.join(dir, f));
2785
+ const m = mtimeIfPresent(path12.join(dir, f));
3029
2786
  return m === void 0 ? [] : [{ f, m }];
3030
2787
  }).sort((a, b) => a.m - b.m).map(({ f }) => f.slice(0, -".json".length));
3031
2788
  }
3032
2789
  function queuedDescriptor(id, lane) {
3033
2790
  try {
3034
- return JSON.parse(fs14.readFileSync(path13.join(laneDirs(lane).queue, `${id}.json`), "utf8"));
2791
+ return JSON.parse(fs13.readFileSync(path12.join(laneDirs(lane).queue, `${id}.json`), "utf8"));
3035
2792
  } catch {
3036
2793
  return void 0;
3037
2794
  }
3038
2795
  }
3039
2796
  function queuedAt(id, lane) {
3040
- const file = path13.join(laneDirs(lane).queue, `${id}.json`);
2797
+ const file = path12.join(laneDirs(lane).queue, `${id}.json`);
3041
2798
  let stat;
3042
2799
  try {
3043
- stat = fs14.statSync(file);
2800
+ stat = fs13.statSync(file);
3044
2801
  } catch {
3045
2802
  return void 0;
3046
2803
  }
@@ -3055,17 +2812,17 @@ function claimNext(lane, skip) {
3055
2812
  if (!d || skip(d))
3056
2813
  continue;
3057
2814
  }
3058
- const activeDir = path13.join(dirs.active, id);
2815
+ const activeDir = path12.join(dirs.active, id);
3059
2816
  try {
3060
- fs14.mkdirSync(activeDir);
2817
+ fs13.mkdirSync(activeDir);
3061
2818
  } catch {
3062
2819
  continue;
3063
2820
  }
3064
2821
  try {
3065
- fs14.renameSync(path13.join(dirs.queue, `${id}.json`), path13.join(activeDir, "descriptor.json"));
2822
+ fs13.renameSync(path12.join(dirs.queue, `${id}.json`), path12.join(activeDir, "descriptor.json"));
3066
2823
  return id;
3067
2824
  } catch {
3068
- fs14.rmdirSync(activeDir, { recursive: true });
2825
+ fs13.rmdirSync(activeDir, { recursive: true });
3069
2826
  continue;
3070
2827
  }
3071
2828
  }
@@ -3073,19 +2830,19 @@ function claimNext(lane, skip) {
3073
2830
  }
3074
2831
  function requeue(id, lane) {
3075
2832
  const dirs = laneDirs(lane);
3076
- const activeDir = path13.join(dirs.active, id);
3077
- fs14.renameSync(path13.join(activeDir, "descriptor.json"), path13.join(dirs.queue, `${id}.json`));
3078
- fs14.rmSync(activeDir, { recursive: true, force: true });
2833
+ const activeDir = path12.join(dirs.active, id);
2834
+ fs13.renameSync(path12.join(activeDir, "descriptor.json"), path12.join(dirs.queue, `${id}.json`));
2835
+ fs13.rmSync(activeDir, { recursive: true, force: true });
3079
2836
  }
3080
2837
  function storedDescriptor(id, lane) {
3081
2838
  const dirs = laneDirs(lane);
3082
2839
  for (const file of [
3083
- path13.join(dirs.queue, `${id}.json`),
3084
- path13.join(dirs.active, id, "descriptor.json"),
3085
- path13.join(dirs.done, id, "descriptor.json")
2840
+ path12.join(dirs.queue, `${id}.json`),
2841
+ path12.join(dirs.active, id, "descriptor.json"),
2842
+ path12.join(dirs.done, id, "descriptor.json")
3086
2843
  ]) {
3087
2844
  try {
3088
- return JSON.parse(fs14.readFileSync(file, "utf8"));
2845
+ return JSON.parse(fs13.readFileSync(file, "utf8"));
3089
2846
  } catch {
3090
2847
  }
3091
2848
  }
@@ -3096,44 +2853,85 @@ function activeIds(lane) {
3096
2853
  }
3097
2854
  function complete(id, lane) {
3098
2855
  const dirs = laneDirs(lane);
3099
- fs14.renameSync(path13.join(dirs.active, id), path13.join(dirs.done, id));
2856
+ fs13.renameSync(path12.join(dirs.active, id), path12.join(dirs.done, id));
3100
2857
  }
3101
2858
  function requestCancel(id, lane) {
3102
- fs14.writeFileSync(path13.join(laneDirs(lane).active, id, "cancel"), (/* @__PURE__ */ new Date()).toISOString());
2859
+ fs13.writeFileSync(path12.join(laneDirs(lane).active, id, "cancel"), (/* @__PURE__ */ new Date()).toISOString());
3103
2860
  }
3104
2861
  function cancelQueued(id, lane) {
3105
2862
  const dirs = laneDirs(lane);
3106
- const doneDir = path13.join(dirs.done, id);
2863
+ const doneDir = path12.join(dirs.done, id);
3107
2864
  try {
3108
- fs14.mkdirSync(doneDir);
2865
+ fs13.mkdirSync(doneDir);
3109
2866
  } catch {
3110
2867
  return false;
3111
2868
  }
3112
2869
  try {
3113
- fs14.renameSync(path13.join(dirs.queue, `${id}.json`), path13.join(doneDir, "descriptor.json"));
2870
+ fs13.renameSync(path12.join(dirs.queue, `${id}.json`), path12.join(doneDir, "descriptor.json"));
3114
2871
  } catch {
3115
- fs14.rmdirSync(doneDir, { recursive: true });
2872
+ fs13.rmdirSync(doneDir, { recursive: true });
3116
2873
  return false;
3117
2874
  }
3118
2875
  appendStatus(id, lane, "failed", "cancelled before claim");
3119
2876
  return true;
3120
2877
  }
3121
2878
  function cancelRequested(id, lane) {
3122
- return fs14.existsSync(path13.join(laneDirs(lane).active, id, "cancel"));
2879
+ return fs13.existsSync(path12.join(laneDirs(lane).active, id, "cancel"));
2880
+ }
2881
+ var init_queue = __esm({
2882
+ "packages/core/dist/queue.js"() {
2883
+ "use strict";
2884
+ init_paths();
2885
+ init_status();
2886
+ }
2887
+ });
2888
+
2889
+ // packages/core/dist/session-link.js
2890
+ function validSessionLink(value) {
2891
+ if (typeof value !== "string")
2892
+ return false;
2893
+ if (/[\x00-\x1f\x7f]/.test(value))
2894
+ return false;
2895
+ 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);
2896
+ }
2897
+ function linkMismatch(link, win) {
2898
+ if (!win)
2899
+ return void 0;
2900
+ const ep = win.entrypoint;
2901
+ const bundle = win.bundleId;
2902
+ if (link.startsWith("vscode://")) {
2903
+ if (ep !== void 0)
2904
+ return ep === "claude-vscode" ? void 0 : `a vscode:// link needs the VS Code extension; this session's entrypoint is ${ep}`;
2905
+ return bundle === void 0 || VSCODE_BUNDLE_RE.test(bundle) ? void 0 : `a vscode:// link needs the VS Code extension; this session runs in ${bundle}`;
2906
+ }
2907
+ if (link.startsWith("claude://")) {
2908
+ if (ep !== void 0) {
2909
+ 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;
2910
+ }
2911
+ if (bundle !== void 0 && bundle !== CLAUDE_DESKTOP_BUNDLE)
2912
+ return `a claude:// link needs the Claude desktop app; this session runs in ${bundle}`;
2913
+ return win.termProgram === "claude-desktop" ? "a claude:// link needs the Code tab; this session runs in the Claude desktop app's terminal" : void 0;
2914
+ }
2915
+ if (link.startsWith("codex://")) {
2916
+ return bundle === void 0 || bundle === CODEX_BUNDLE ? void 0 : `a codex:// link needs the Codex app; this session runs in ${bundle}`;
2917
+ }
2918
+ return void 0;
3123
2919
  }
3124
- var init_queue = __esm({
3125
- "packages/core/dist/queue.js"() {
2920
+ var VSCODE_BUNDLE_RE, CLAUDE_DESKTOP_BUNDLE, CODEX_BUNDLE;
2921
+ var init_session_link = __esm({
2922
+ "packages/core/dist/session-link.js"() {
3126
2923
  "use strict";
3127
- init_paths();
3128
- init_status();
2924
+ VSCODE_BUNDLE_RE = /(?:VSCode|Cursor|windsurf)/;
2925
+ CLAUDE_DESKTOP_BUNDLE = "com.anthropic.claudefordesktop";
2926
+ CODEX_BUNDLE = "com.openai.codex";
3129
2927
  }
3130
2928
  });
3131
2929
 
3132
2930
  // packages/core/dist/evidence.js
3133
- import * as fs15 from "node:fs";
2931
+ import * as fs14 from "node:fs";
3134
2932
  function readEvidence(id, lane) {
3135
2933
  try {
3136
- return JSON.parse(fs15.readFileSync(evidencePath(id, lane), "utf8"));
2934
+ return JSON.parse(fs14.readFileSync(evidencePath(id, lane), "utf8"));
3137
2935
  } catch {
3138
2936
  return {};
3139
2937
  }
@@ -3141,8 +2939,8 @@ function readEvidence(id, lane) {
3141
2939
  function mergeEvidence(id, lane, patch) {
3142
2940
  const file = evidencePath(id, lane);
3143
2941
  const tmp = `${file}.tmp-${process.pid}`;
3144
- fs15.writeFileSync(tmp, JSON.stringify({ ...readEvidence(id, lane), ...patch }, null, 2));
3145
- fs15.renameSync(tmp, file);
2942
+ fs14.writeFileSync(tmp, JSON.stringify({ ...readEvidence(id, lane), ...patch }, null, 2));
2943
+ fs14.renameSync(tmp, file);
3146
2944
  }
3147
2945
  var init_evidence = __esm({
3148
2946
  "packages/core/dist/evidence.js"() {
@@ -3152,8 +2950,8 @@ var init_evidence = __esm({
3152
2950
  });
3153
2951
 
3154
2952
  // packages/core/dist/pushes.js
3155
- import * as fs16 from "node:fs";
3156
- import * as path14 from "node:path";
2953
+ import * as fs15 from "node:fs";
2954
+ import * as path13 from "node:path";
3157
2955
  function words(segment) {
3158
2956
  const out = [];
3159
2957
  const re = /"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S+)/g;
@@ -3205,18 +3003,18 @@ function gitPushTargets(command) {
3205
3003
  return found ? [...new Set(targets)] : void 0;
3206
3004
  }
3207
3005
  function gitDirAt(dir) {
3208
- let at = path14.resolve(dir);
3006
+ let at = path13.resolve(dir);
3209
3007
  for (; ; ) {
3210
- const dotGit = path14.join(at, ".git");
3008
+ const dotGit = path13.join(at, ".git");
3211
3009
  try {
3212
- const stat = fs16.statSync(dotGit);
3010
+ const stat = fs15.statSync(dotGit);
3213
3011
  if (stat.isDirectory())
3214
3012
  return dotGit;
3215
- const m = /^gitdir:\s*(.+)$/m.exec(fs16.readFileSync(dotGit, "utf8"));
3216
- return m ? path14.resolve(at, m[1].trim()) : void 0;
3013
+ const m = /^gitdir:\s*(.+)$/m.exec(fs15.readFileSync(dotGit, "utf8"));
3014
+ return m ? path13.resolve(at, m[1].trim()) : void 0;
3217
3015
  } catch {
3218
3016
  }
3219
- const up = path14.dirname(at);
3017
+ const up = path13.dirname(at);
3220
3018
  if (up === at)
3221
3019
  return void 0;
3222
3020
  at = up;
@@ -3227,7 +3025,7 @@ function currentBranchAt(dir) {
3227
3025
  if (!gitDir)
3228
3026
  return void 0;
3229
3027
  try {
3230
- const m = /^ref:\s*refs\/heads\/(.+)$/m.exec(fs16.readFileSync(path14.join(gitDir, "HEAD"), "utf8"));
3028
+ const m = /^ref:\s*refs\/heads\/(.+)$/m.exec(fs15.readFileSync(path13.join(gitDir, "HEAD"), "utf8"));
3231
3029
  return m ? m[1].trim() : void 0;
3232
3030
  } catch {
3233
3031
  return void 0;
@@ -3256,18 +3054,18 @@ var init_pushes = __esm({
3256
3054
  });
3257
3055
 
3258
3056
  // packages/core/dist/trap-names.js
3259
- import * as fs17 from "node:fs";
3260
- import * as path15 from "node:path";
3057
+ import * as fs16 from "node:fs";
3058
+ import * as path14 from "node:path";
3261
3059
  import { randomInt } from "node:crypto";
3262
3060
  function namesDir() {
3263
- return path15.join(lobstahHome(), "trap-names");
3061
+ return path14.join(lobstahHome(), "trap-names");
3264
3062
  }
3265
3063
  function nameFile(name) {
3266
- return path15.join(namesDir(), `${name}.json`);
3064
+ return path14.join(namesDir(), `${name}.json`);
3267
3065
  }
3268
3066
  function knownTrapNames() {
3269
3067
  try {
3270
- return fs17.readdirSync(namesDir()).filter((file) => file.endsWith(".json")).map((file) => file.slice(0, -5)).filter((name) => TRAP_NAME_RE.test(name)).sort();
3068
+ return fs16.readdirSync(namesDir()).filter((file) => file.endsWith(".json")).map((file) => file.slice(0, -5)).filter((name) => TRAP_NAME_RE.test(name)).sort();
3271
3069
  } catch {
3272
3070
  return [];
3273
3071
  }
@@ -3276,7 +3074,7 @@ function trapIdForName(name) {
3276
3074
  if (!TRAP_NAME_RE.test(name))
3277
3075
  return void 0;
3278
3076
  try {
3279
- const id = JSON.parse(fs17.readFileSync(nameFile(name), "utf8")).trapId;
3077
+ const id = JSON.parse(fs16.readFileSync(nameFile(name), "utf8")).trapId;
3280
3078
  return typeof id === "string" ? id : void 0;
3281
3079
  } catch {
3282
3080
  return void 0;
@@ -3286,7 +3084,7 @@ function trapNameForId(trapId) {
3286
3084
  return knownTrapNames().find((name) => trapIdForName(name) === trapId);
3287
3085
  }
3288
3086
  function reserveTrapName(trapId, requested, start = randomInt(TRAP_FIRST_WORDS.length * TRAP_LAST_WORDS.length)) {
3289
- fs17.mkdirSync(namesDir(), { recursive: true });
3087
+ fs16.mkdirSync(namesDir(), { recursive: true });
3290
3088
  const old = knownTrapNames().find((name) => trapIdForName(name) === trapId);
3291
3089
  if (requested !== void 0 && !TRAP_NAME_RE.test(requested)) {
3292
3090
  throw new Error(`invalid trap name "${requested}" \u2014 use two lowercase words of 2\u20138 letters joined by one hyphen`);
@@ -3302,9 +3100,9 @@ function reserveTrapName(trapId, requested, start = randomInt(TRAP_FIRST_WORDS.l
3302
3100
  if (name === old)
3303
3101
  return name;
3304
3102
  try {
3305
- fs17.writeFileSync(nameFile(name), JSON.stringify({ trapId }), { flag: "wx" });
3103
+ fs16.writeFileSync(nameFile(name), JSON.stringify({ trapId }), { flag: "wx" });
3306
3104
  if (old)
3307
- fs17.rmSync(nameFile(old), { force: true });
3105
+ fs16.rmSync(nameFile(old), { force: true });
3308
3106
  return name;
3309
3107
  } catch (err) {
3310
3108
  if (err.code !== "EEXIST")
@@ -3459,22 +3257,22 @@ var init_trap_names = __esm({
3459
3257
  });
3460
3258
 
3461
3259
  // packages/core/dist/worktrees.js
3462
- import * as fs18 from "node:fs";
3463
- import * as path16 from "node:path";
3260
+ import * as fs17 from "node:fs";
3261
+ import * as path15 from "node:path";
3464
3262
  function worktreePath(id) {
3465
- return path16.join(lobstahHome(), "worktrees", id);
3263
+ return path15.join(lobstahHome(), "worktrees", id);
3466
3264
  }
3467
3265
  function readRecord(file) {
3468
3266
  try {
3469
- const r = JSON.parse(fs18.readFileSync(file, "utf8"));
3267
+ const r = JSON.parse(fs17.readFileSync(file, "utf8"));
3470
3268
  return typeof r.path === "string" ? r : void 0;
3471
3269
  } catch {
3472
3270
  return void 0;
3473
3271
  }
3474
3272
  }
3475
3273
  function ownerOfPath(p) {
3476
- const rel = path16.relative(path16.join(lobstahHome(), "worktrees"), p);
3477
- if (!rel || rel.startsWith("..") || path16.isAbsolute(rel))
3274
+ const rel = path15.relative(path15.join(lobstahHome(), "worktrees"), p);
3275
+ if (!rel || rel.startsWith("..") || path15.isAbsolute(rel))
3478
3276
  return void 0;
3479
3277
  return rel.split(/[\\/]/)[0];
3480
3278
  }
@@ -3487,7 +3285,7 @@ function dispatchWorktree(id, lane) {
3487
3285
  }
3488
3286
  const dirs = laneDirs(l);
3489
3287
  for (const dir of [dirs.active, dirs.done]) {
3490
- const rec = readRecord(path16.join(dir, id, "worktree.json"));
3288
+ const rec = readRecord(path15.join(dir, id, "worktree.json"));
3491
3289
  if (rec) {
3492
3290
  const owner = rec.of ?? ownerOfPath(rec.path) ?? id;
3493
3291
  return { path: rec.path, owner, reused: owner !== id };
@@ -3499,7 +3297,7 @@ function dispatchWorktree(id, lane) {
3499
3297
  function laneOf(id) {
3500
3298
  for (const l of LANES) {
3501
3299
  const d = laneDirs(l);
3502
- if (fs18.existsSync(path16.join(d.queue, `${id}.json`)) || fs18.existsSync(path16.join(d.active, id)) || fs18.existsSync(path16.join(d.done, id))) {
3300
+ if (fs17.existsSync(path15.join(d.queue, `${id}.json`)) || fs17.existsSync(path15.join(d.active, id)) || fs17.existsSync(path15.join(d.done, id))) {
3503
3301
  return l;
3504
3302
  }
3505
3303
  }
@@ -3519,39 +3317,39 @@ function followUpAncestors(id, lane) {
3519
3317
  function chainWorktree(followUp) {
3520
3318
  for (const id of [followUp, ...followUpAncestors(followUp)]) {
3521
3319
  const wt = dispatchWorktree(id, laneOf(id));
3522
- if (fs18.existsSync(wt.path))
3320
+ if (fs17.existsSync(wt.path))
3523
3321
  return { ...wt, from: id };
3524
3322
  }
3525
3323
  return void 0;
3526
3324
  }
3527
3325
  function worktreeGitDir(wt) {
3528
- const dotGit = path16.join(wt, ".git");
3326
+ const dotGit = path15.join(wt, ".git");
3529
3327
  try {
3530
- const st = fs18.statSync(dotGit);
3328
+ const st = fs17.statSync(dotGit);
3531
3329
  if (st.isDirectory())
3532
3330
  return dotGit;
3533
- const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs18.readFileSync(dotGit, "utf8"));
3534
- return m ? path16.resolve(wt, m[1]) : void 0;
3331
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs17.readFileSync(dotGit, "utf8"));
3332
+ return m ? path15.resolve(wt, m[1]) : void 0;
3535
3333
  } catch {
3536
3334
  return void 0;
3537
3335
  }
3538
3336
  }
3539
3337
  function worktreeLockFile(wt) {
3540
3338
  const gitDir = worktreeGitDir(wt);
3541
- return gitDir ? path16.join(gitDir, "lobstah.lock") : void 0;
3339
+ return gitDir ? path15.join(gitDir, "lobstah.lock") : void 0;
3542
3340
  }
3543
3341
  function readWorktreeLock(wt) {
3544
3342
  const file = worktreeLockFile(wt);
3545
3343
  if (!file)
3546
3344
  return void 0;
3547
3345
  try {
3548
- return JSON.parse(fs18.readFileSync(file, "utf8"));
3346
+ return JSON.parse(fs17.readFileSync(file, "utf8"));
3549
3347
  } catch {
3550
3348
  return void 0;
3551
3349
  }
3552
3350
  }
3553
3351
  function lockIsLive(lock) {
3554
- return fs18.existsSync(path16.join(laneDirs(lock.lane).active, lock.id));
3352
+ return fs17.existsSync(path15.join(laneDirs(lock.lane).active, lock.id));
3555
3353
  }
3556
3354
  function worktreeHolder(wt) {
3557
3355
  const lock = readWorktreeLock(wt);
@@ -3564,7 +3362,7 @@ function acquireWorktreeLock(wt, id, lane) {
3564
3362
  return { id: "(no git dir)", lane, pid: 0, at: mine.at };
3565
3363
  for (let attempt = 0; attempt < 3; attempt++) {
3566
3364
  try {
3567
- fs18.writeFileSync(file, JSON.stringify(mine, null, 2), { flag: "wx" });
3365
+ fs17.writeFileSync(file, JSON.stringify(mine, null, 2), { flag: "wx" });
3568
3366
  return void 0;
3569
3367
  } catch (err) {
3570
3368
  if (err.code !== "EEXIST")
@@ -3572,7 +3370,7 @@ function acquireWorktreeLock(wt, id, lane) {
3572
3370
  }
3573
3371
  let raw;
3574
3372
  try {
3575
- raw = fs18.readFileSync(file, "utf8");
3373
+ raw = fs17.readFileSync(file, "utf8");
3576
3374
  } catch {
3577
3375
  continue;
3578
3376
  }
@@ -3583,14 +3381,14 @@ function acquireWorktreeLock(wt, id, lane) {
3583
3381
  held2 = void 0;
3584
3382
  }
3585
3383
  if (held2?.id === id) {
3586
- fs18.writeFileSync(file, JSON.stringify(mine, null, 2));
3384
+ fs17.writeFileSync(file, JSON.stringify(mine, null, 2));
3587
3385
  return void 0;
3588
3386
  }
3589
3387
  if (held2 && lockIsLive(held2))
3590
3388
  return held2;
3591
3389
  try {
3592
- if (fs18.readFileSync(file, "utf8") === raw)
3593
- fs18.rmSync(file, { force: true });
3390
+ if (fs17.readFileSync(file, "utf8") === raw)
3391
+ fs17.rmSync(file, { force: true });
3594
3392
  } catch {
3595
3393
  }
3596
3394
  }
@@ -3602,20 +3400,20 @@ function releaseWorktreeLock(wt, id) {
3602
3400
  if (!file)
3603
3401
  return;
3604
3402
  if (readWorktreeLock(wt)?.id === id)
3605
- fs18.rmSync(file, { force: true });
3403
+ fs17.rmSync(file, { force: true });
3606
3404
  }
3607
3405
  function releaseDispatchLock(dispatchDir, id) {
3608
- const rec = readRecord(path16.join(dispatchDir, "worktree.json"));
3406
+ const rec = readRecord(path15.join(dispatchDir, "worktree.json"));
3609
3407
  if (rec)
3610
3408
  releaseWorktreeLock(rec.path, id);
3611
3409
  }
3612
3410
  function keptWorktreesPath() {
3613
- return path16.join(lobstahHome(), "release-kept.json");
3411
+ return path15.join(lobstahHome(), "release-kept.json");
3614
3412
  }
3615
3413
  function readKeptWorktrees() {
3616
3414
  try {
3617
- const list2 = JSON.parse(fs18.readFileSync(keptWorktreesPath(), "utf8"));
3618
- return Array.isArray(list2) ? list2.filter((k) => fs18.existsSync(worktreePath(k.id))) : [];
3415
+ const list2 = JSON.parse(fs17.readFileSync(keptWorktreesPath(), "utf8"));
3416
+ return Array.isArray(list2) ? list2.filter((k) => fs17.existsSync(worktreePath(k.id))) : [];
3619
3417
  } catch {
3620
3418
  return [];
3621
3419
  }
@@ -3623,12 +3421,12 @@ function readKeptWorktrees() {
3623
3421
  function writeKeptWorktrees(list2) {
3624
3422
  const file = keptWorktreesPath();
3625
3423
  if (list2.length === 0) {
3626
- fs18.rmSync(file, { force: true });
3424
+ fs17.rmSync(file, { force: true });
3627
3425
  return;
3628
3426
  }
3629
3427
  const tmp = `${file}.tmp-${process.pid}`;
3630
- fs18.writeFileSync(tmp, JSON.stringify(list2, null, 2));
3631
- fs18.renameSync(tmp, file);
3428
+ fs17.writeFileSync(tmp, JSON.stringify(list2, null, 2));
3429
+ fs17.renameSync(tmp, file);
3632
3430
  }
3633
3431
  var LANES;
3634
3432
  var init_worktrees = __esm({
@@ -3642,8 +3440,8 @@ var init_worktrees = __esm({
3642
3440
  });
3643
3441
 
3644
3442
  // packages/core/dist/worktree-safety.js
3645
- import * as fs19 from "node:fs";
3646
- import * as path17 from "node:path";
3443
+ import * as fs18 from "node:fs";
3444
+ import * as path16 from "node:path";
3647
3445
  import { execFileSync } from "node:child_process";
3648
3446
  function git(dir, ...args) {
3649
3447
  return execFileSync("git", args, { cwd: dir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 1e4 });
@@ -3687,22 +3485,22 @@ function removeGhostWorktree(dir) {
3687
3485
  const safety = inspectTrapWorktree(dir);
3688
3486
  if (safety.reason)
3689
3487
  return { ...safety, removed: false };
3690
- const anchor = path17.join(dir, ".lobstah-trap");
3488
+ const anchor = path16.join(dir, ".lobstah-trap");
3691
3489
  let saved;
3692
3490
  try {
3693
3491
  const common = git(dir, "rev-parse", "--path-format=absolute", "--git-common-dir").trim();
3694
3492
  const own = git(dir, "rev-parse", "--path-format=absolute", "--git-dir").trim();
3695
3493
  if (common === own)
3696
3494
  return { ...safety, removed: false, reason: "not a linked worktree" };
3697
- if (fs19.existsSync(anchor)) {
3698
- saved = fs19.readFileSync(anchor);
3699
- fs19.unlinkSync(anchor);
3495
+ if (fs18.existsSync(anchor)) {
3496
+ saved = fs18.readFileSync(anchor);
3497
+ fs18.unlinkSync(anchor);
3700
3498
  }
3701
- git(path17.dirname(common), "--git-dir", common, "worktree", "remove", dir);
3499
+ git(path16.dirname(common), "--git-dir", common, "worktree", "remove", dir);
3702
3500
  return { ...safety, removed: true };
3703
3501
  } catch {
3704
- if (saved && fs19.existsSync(dir))
3705
- fs19.writeFileSync(anchor, saved);
3502
+ if (saved && fs18.existsSync(dir))
3503
+ fs18.writeFileSync(anchor, saved);
3706
3504
  return { ...safety, removed: false, reason: "git worktree remove refused" };
3707
3505
  }
3708
3506
  }
@@ -3713,20 +3511,20 @@ var init_worktree_safety = __esm({
3713
3511
  });
3714
3512
 
3715
3513
  // packages/core/dist/trap-start.js
3716
- import * as fs20 from "node:fs";
3717
- import * as path18 from "node:path";
3514
+ import * as fs19 from "node:fs";
3515
+ import * as path17 from "node:path";
3718
3516
  import { createHash as createHash3, randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
3719
3517
  function startPath(trapId) {
3720
- return path18.join(soakingDir(), `${trapId}.starting`);
3518
+ return path17.join(soakingDir(), `${trapId}.starting`);
3721
3519
  }
3722
3520
  function ticketPath(trapId) {
3723
- return path18.join(soakingDir(), `${trapId}.ticket`);
3521
+ return path17.join(soakingDir(), `${trapId}.ticket`);
3724
3522
  }
3725
3523
  function readReservationTicket(trapId) {
3726
3524
  if (!/^[0-9a-f]{8}$/.test(trapId))
3727
3525
  return void 0;
3728
3526
  try {
3729
- const ticket = fs20.readFileSync(ticketPath(trapId), "utf8").trim();
3527
+ const ticket = fs19.readFileSync(ticketPath(trapId), "utf8").trim();
3730
3528
  return TRAP_TICKET_RE.exec(ticket)?.[1] === trapId ? ticket : void 0;
3731
3529
  } catch {
3732
3530
  return void 0;
@@ -3744,17 +3542,17 @@ function trapStartCommands(repoPath, ticket, harness) {
3744
3542
  return harness === "claude" || harness === "codex" ? all.filter((c) => c.harness === harness) : all;
3745
3543
  }
3746
3544
  function writeReservation(r) {
3747
- fs20.mkdirSync(soakingDir(), { recursive: true });
3545
+ fs19.mkdirSync(soakingDir(), { recursive: true });
3748
3546
  const file = startPath(r.trapId);
3749
3547
  const tmp = `${file}.tmp-${process.pid}`;
3750
- fs20.writeFileSync(tmp, JSON.stringify(r, null, 2));
3751
- fs20.renameSync(tmp, file);
3548
+ fs19.writeFileSync(tmp, JSON.stringify(r, null, 2));
3549
+ fs19.renameSync(tmp, file);
3752
3550
  }
3753
3551
  function readReservation(trapId) {
3754
3552
  if (!/^[0-9a-f]{8}$/.test(trapId))
3755
3553
  return void 0;
3756
3554
  try {
3757
- const parsed = JSON.parse(fs20.readFileSync(startPath(trapId), "utf8"));
3555
+ const parsed = JSON.parse(fs19.readFileSync(startPath(trapId), "utf8"));
3758
3556
  return typeof parsed.trapId === "string" && parsed.trapId === trapId ? parsed : void 0;
3759
3557
  } catch {
3760
3558
  return void 0;
@@ -3763,7 +3561,7 @@ function readReservation(trapId) {
3763
3561
  function listReservations() {
3764
3562
  let files;
3765
3563
  try {
3766
- files = fs20.readdirSync(soakingDir()).filter((f) => f.endsWith(".starting"));
3564
+ files = fs19.readdirSync(soakingDir()).filter((f) => f.endsWith(".starting"));
3767
3565
  } catch {
3768
3566
  return [];
3769
3567
  }
@@ -3794,7 +3592,7 @@ function reserveTrap(opts) {
3794
3592
  ...opts.request ? { request: opts.request } : {}
3795
3593
  };
3796
3594
  writeReservation(reservation);
3797
- fs20.writeFileSync(ticketPath(trapId), ticket, { mode: 384 });
3595
+ fs19.writeFileSync(ticketPath(trapId), ticket, { mode: 384 });
3798
3596
  postNotice({
3799
3597
  kind: "trap-starting",
3800
3598
  text: `trap ${trapLabel(reservation)} reserved for repo ${opts.repo} \u2014 starting, sign-on due by ${reservation.deadline}; address work with \`--for ${name}\``,
@@ -3818,8 +3616,8 @@ function reservationForTicket(ticket) {
3818
3616
  function dropReservation(trapId) {
3819
3617
  const r = readReservation(trapId);
3820
3618
  if (r)
3821
- fs20.rmSync(startPath(trapId), { force: true });
3822
- fs20.rmSync(ticketPath(trapId), { force: true });
3619
+ fs19.rmSync(startPath(trapId), { force: true });
3620
+ fs19.rmSync(ticketPath(trapId), { force: true });
3823
3621
  return r;
3824
3622
  }
3825
3623
  function withdrawReservation(trapId, by) {
@@ -3875,27 +3673,27 @@ var init_trap_start = __esm({
3875
3673
  });
3876
3674
 
3877
3675
  // packages/core/dist/soak.js
3878
- import * as fs21 from "node:fs";
3879
- import * as path19 from "node:path";
3676
+ import * as fs20 from "node:fs";
3677
+ import * as path18 from "node:path";
3880
3678
  import { randomBytes as randomBytes4 } from "node:crypto";
3881
3679
  function regPath(trapId) {
3882
- return path19.join(soakingDir(), `${trapId}.json`);
3680
+ return path18.join(soakingDir(), `${trapId}.json`);
3883
3681
  }
3884
- function atomicWrite4(file, content) {
3682
+ function atomicWrite3(file, content) {
3885
3683
  const tmp = `${file}.tmp-${process.pid}`;
3886
- fs21.writeFileSync(tmp, content);
3887
- fs21.renameSync(tmp, file);
3684
+ fs20.writeFileSync(tmp, content);
3685
+ fs20.renameSync(tmp, file);
3888
3686
  }
3889
3687
  function readTrapAnchor(worktree) {
3890
3688
  try {
3891
- const parsed = JSON.parse(fs21.readFileSync(path19.join(worktree, TRAP_FILE), "utf8"));
3689
+ const parsed = JSON.parse(fs20.readFileSync(path18.join(worktree, TRAP_FILE), "utf8"));
3892
3690
  return typeof parsed.trapId === "string" ? parsed : void 0;
3893
3691
  } catch {
3894
3692
  return void 0;
3895
3693
  }
3896
3694
  }
3897
3695
  function writeTrapAnchor(worktree, anchor) {
3898
- atomicWrite4(path19.join(worktree, TRAP_FILE), `${JSON.stringify(anchor, null, 2)}
3696
+ atomicWrite3(path18.join(worktree, TRAP_FILE), `${JSON.stringify(anchor, null, 2)}
3899
3697
  `);
3900
3698
  }
3901
3699
  function trapIdAt(worktree) {
@@ -3915,12 +3713,12 @@ function newTrapId() {
3915
3713
  function findAnchored(root, match) {
3916
3714
  let names;
3917
3715
  try {
3918
- names = fs21.readdirSync(root);
3716
+ names = fs20.readdirSync(root);
3919
3717
  } catch {
3920
3718
  return void 0;
3921
3719
  }
3922
3720
  for (const name of names.sort()) {
3923
- const dir = path19.join(root, name);
3721
+ const dir = path18.join(root, name);
3924
3722
  const anchor = readTrapAnchor(dir);
3925
3723
  if (anchor && match(anchor))
3926
3724
  return dir;
@@ -3935,7 +3733,7 @@ function anchoredWorktree(root, trapId) {
3935
3733
  }
3936
3734
  function readTrap(trapId) {
3937
3735
  try {
3938
- return JSON.parse(fs21.readFileSync(regPath(trapId), "utf8"));
3736
+ return JSON.parse(fs20.readFileSync(regPath(trapId), "utf8"));
3939
3737
  } catch {
3940
3738
  return void 0;
3941
3739
  }
@@ -3976,7 +3774,7 @@ function unknownTrapMessage(address) {
3976
3774
  }
3977
3775
  function listTraps() {
3978
3776
  try {
3979
- return fs21.readdirSync(soakingDir()).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(fs21.readFileSync(path19.join(soakingDir(), f), "utf8"))).filter((r) => typeof r.trapId === "string");
3777
+ 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
3778
  } catch {
3981
3779
  return [];
3982
3780
  }
@@ -4031,12 +3829,12 @@ function signOnTrap(opts) {
4031
3829
  ...sameSession && prior.titleSet ? { titleSet: prior.titleSet, titleSetAt: prior.titleSetAt } : {},
4032
3830
  ...sameSession && prior.titlePending ? { titlePending: prior.titlePending } : {}
4033
3831
  };
4034
- atomicWrite4(regPath(trapId), JSON.stringify(reg, null, 2));
3832
+ atomicWrite3(regPath(trapId), JSON.stringify(reg, null, 2));
4035
3833
  releaseSignedOff(trapId);
4036
3834
  if (!prior) {
4037
3835
  postNotice({
4038
3836
  kind: "trap-signed-on",
4039
- text: `trap ${trapLabel(reg)} signed on (${opts.harness}, ${opts.repo ?? "no repo"}, ${path19.basename(opts.worktree)}) \u2014 address work with \`--for ${name}\``,
3837
+ text: `trap ${trapLabel(reg)} signed on (${opts.harness}, ${opts.repo ?? "no repo"}, ${path18.basename(opts.worktree)}) \u2014 address work with \`--for ${name}\``,
4040
3838
  refId: trapId,
4041
3839
  repo: opts.repo,
4042
3840
  by: opts.sessionId
@@ -4046,14 +3844,14 @@ function signOnTrap(opts) {
4046
3844
  }
4047
3845
  function readSignedOff(trapId) {
4048
3846
  try {
4049
- return JSON.parse(fs21.readFileSync(signedOffPath(trapId), "utf8"));
3847
+ return JSON.parse(fs20.readFileSync(signedOffPath(trapId), "utf8"));
4050
3848
  } catch {
4051
3849
  return void 0;
4052
3850
  }
4053
3851
  }
4054
3852
  function listSignedOff() {
4055
3853
  try {
4056
- return fs21.readdirSync(signedOffDir()).filter((f) => f.endsWith(".json")).flatMap((f) => {
3854
+ return fs20.readdirSync(signedOffDir()).filter((f) => f.endsWith(".json")).flatMap((f) => {
4057
3855
  const s = readSignedOff(f.slice(0, -".json".length));
4058
3856
  return s ? [s] : [];
4059
3857
  });
@@ -4062,7 +3860,7 @@ function listSignedOff() {
4062
3860
  }
4063
3861
  }
4064
3862
  function releaseSignedOff(trapId) {
4065
- fs21.rmSync(signedOffPath(trapId), { force: true });
3863
+ fs20.rmSync(signedOffPath(trapId), { force: true });
4066
3864
  }
4067
3865
  function recentlySignedOff(trapId, graceMs, now2 = Date.now()) {
4068
3866
  const s = readSignedOff(trapId);
@@ -4072,14 +3870,14 @@ function stowTrap(trapId, reason = "signed off", by) {
4072
3870
  const reg = readTrap(trapId);
4073
3871
  if (!reg)
4074
3872
  return void 0;
4075
- fs21.mkdirSync(signedOffDir(), { recursive: true });
4076
- atomicWrite4(signedOffPath(trapId), JSON.stringify({ trapId, name: reg.name, worktree: reg.worktree, at: (/* @__PURE__ */ new Date()).toISOString() }));
4077
- fs21.rmSync(regPath(trapId), { force: true });
4078
- fs21.rmSync(beatPath(trapId), { force: true });
4079
- fs21.rmSync(prProbePath(trapId), { force: true });
3873
+ fs20.mkdirSync(signedOffDir(), { recursive: true });
3874
+ atomicWrite3(signedOffPath(trapId), JSON.stringify({ trapId, name: reg.name, worktree: reg.worktree, at: (/* @__PURE__ */ new Date()).toISOString() }));
3875
+ fs20.rmSync(regPath(trapId), { force: true });
3876
+ fs20.rmSync(beatPath(trapId), { force: true });
3877
+ fs20.rmSync(prProbePath(trapId), { force: true });
4080
3878
  postNotice({
4081
3879
  kind: "trap-stowed",
4082
- text: `trap ${trapLabel(reg)} ${reason} (${path19.basename(reg.worktree)}) \u2014 re-soaking that worktree restores the address`,
3880
+ text: `trap ${trapLabel(reg)} ${reason} (${path18.basename(reg.worktree)}) \u2014 re-soaking that worktree restores the address`,
4083
3881
  refId: trapId,
4084
3882
  repo: reg.repo,
4085
3883
  by
@@ -4095,7 +3893,7 @@ function askTrapTitle(trapId, title) {
4095
3893
  delete next.titlePending;
4096
3894
  else
4097
3895
  next.titlePending = title;
4098
- atomicWrite4(regPath(trapId), JSON.stringify(next, null, 2));
3896
+ atomicWrite3(regPath(trapId), JSON.stringify(next, null, 2));
4099
3897
  return next;
4100
3898
  }
4101
3899
  function countTitleReminder(trapId) {
@@ -4103,7 +3901,7 @@ function countTitleReminder(trapId) {
4103
3901
  if (!reg?.titlePending)
4104
3902
  return void 0;
4105
3903
  const n = (reg.titleReminders ?? 0) + 1;
4106
- atomicWrite4(regPath(trapId), JSON.stringify({ ...reg, titleReminders: n }, null, 2));
3904
+ atomicWrite3(regPath(trapId), JSON.stringify({ ...reg, titleReminders: n }, null, 2));
4107
3905
  return n;
4108
3906
  }
4109
3907
  function confirmTrapTitle(trapId, now2 = Date.now()) {
@@ -4112,7 +3910,7 @@ function confirmTrapTitle(trapId, now2 = Date.now()) {
4112
3910
  return void 0;
4113
3911
  const { titlePending, titleReminders: _reminders, ...rest } = reg;
4114
3912
  const next = { ...rest, titleSet: titlePending ?? reg.titleSet ?? reg.name, titleSetAt: new Date(now2).toISOString() };
4115
- atomicWrite4(regPath(trapId), JSON.stringify(next, null, 2));
3913
+ atomicWrite3(regPath(trapId), JSON.stringify(next, null, 2));
4116
3914
  return next;
4117
3915
  }
4118
3916
  function heartbeatTrap(trapId, opts = {}) {
@@ -4127,7 +3925,7 @@ function heartbeatTrap(trapId, opts = {}) {
4127
3925
  ...opts.parked ? { parkedAt: (/* @__PURE__ */ new Date()).toISOString() } : {},
4128
3926
  claimed: opts.claimed === null ? void 0 : opts.claimed ?? reg.claimed
4129
3927
  };
4130
- atomicWrite4(regPath(trapId), JSON.stringify(next, null, 2));
3928
+ atomicWrite3(regPath(trapId), JSON.stringify(next, null, 2));
4131
3929
  if (firstPark) {
4132
3930
  postNotice({
4133
3931
  kind: "trap-listening",
@@ -4140,11 +3938,11 @@ function heartbeatTrap(trapId, opts = {}) {
4140
3938
  return next;
4141
3939
  }
4142
3940
  function claimPath(id, lane) {
4143
- return path19.join(laneDirs(lane).active, id, "claim.json");
3941
+ return path18.join(laneDirs(lane).active, id, "claim.json");
4144
3942
  }
4145
3943
  function readSessionClaim(id, lane) {
4146
3944
  try {
4147
- return JSON.parse(fs21.readFileSync(claimPath(id, lane), "utf8"));
3945
+ return JSON.parse(fs20.readFileSync(claimPath(id, lane), "utf8"));
4148
3946
  } catch {
4149
3947
  return void 0;
4150
3948
  }
@@ -4156,24 +3954,66 @@ function hasOpenCatch(reg) {
4156
3954
  if (!reg.claimed)
4157
3955
  return false;
4158
3956
  const lane = laneOf(reg.claimed);
4159
- if (!lane || !fs21.existsSync(path19.join(laneDirs(lane).active, reg.claimed)))
3957
+ if (!lane || !fs20.existsSync(path18.join(laneDirs(lane).active, reg.claimed)))
4160
3958
  return false;
4161
3959
  const last = readStatusLog(reg.claimed, lane).at(-1)?.verb;
4162
3960
  return last === void 0 || !TERMINAL_VERBS.includes(last);
4163
3961
  }
3962
+ function unreportedTrapBait(reg) {
3963
+ if (!hasOpenCatch(reg))
3964
+ return void 0;
3965
+ const id = reg.claimed;
3966
+ const lane = laneOf(id);
3967
+ const claim = readSessionClaim(id, lane);
3968
+ if (!claim || claim.by !== `wt:${reg.trapId}`)
3969
+ return void 0;
3970
+ const log = readStatusLog(id, lane);
3971
+ let receipt = -1;
3972
+ for (let i = 0; i < log.length; i++) {
3973
+ const s = log[i];
3974
+ if (s.at === claim.at && s.note === `claimed by ${claim.by}` && !s.reported)
3975
+ receipt = i;
3976
+ }
3977
+ const reports = receipt >= 0 ? log.slice(receipt + 1) : log.filter((s) => Date.parse(s.at) >= Date.parse(claim.at));
3978
+ if (reports.some((s) => s.reported))
3979
+ return void 0;
3980
+ try {
3981
+ const descriptor = JSON.parse(fs20.readFileSync(path18.join(laneDirs(lane).active, id, "descriptor.json"), "utf8"));
3982
+ return { id, lane, descriptor, claim };
3983
+ } catch {
3984
+ return void 0;
3985
+ }
3986
+ }
3987
+ function noticeIdleTrapClaims(now2 = Date.now(), graceMs = DEFAULT_SOAK.claimIdleNoticeSecs * 1e3) {
3988
+ for (const reg of listTraps()) {
3989
+ const bait = unreportedTrapBait(reg);
3990
+ if (!bait || cancelRequested(bait.id, bait.lane))
3991
+ continue;
3992
+ const age2 = now2 - Date.parse(bait.claim.at);
3993
+ if (!Number.isFinite(age2) || age2 < graceMs)
3994
+ continue;
3995
+ postNotice({
3996
+ kind: "trap-claim-idle",
3997
+ refId: bait.id,
3998
+ repo: bait.descriptor.repo,
3999
+ 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.`,
4000
+ dedupeKey: `trap-claim-idle-${bait.id}-${bait.claim.at}`
4001
+ });
4002
+ }
4003
+ }
4164
4004
  function releaseCatch(reg) {
4165
4005
  const id = reg.claimed;
4166
4006
  if (!id)
4167
4007
  return {};
4168
4008
  const lane = laneOf(id);
4169
- if (!lane || !fs21.existsSync(path19.join(laneDirs(lane).active, id)))
4009
+ if (!lane || !fs20.existsSync(path18.join(laneDirs(lane).active, id)))
4170
4010
  return {};
4171
4011
  const last = readStatusLog(id, lane).at(-1)?.verb;
4172
4012
  if (last && TERMINAL_VERBS.includes(last)) {
4173
4013
  complete(id, lane);
4174
4014
  return { finalized: id };
4175
4015
  }
4176
- fs21.rmSync(path19.join(laneDirs(lane).active, id, "claim.json"), { force: true });
4016
+ fs20.rmSync(path18.join(laneDirs(lane).active, id, "claim.json"), { force: true });
4177
4017
  if (cancelRequested(id, lane)) {
4178
4018
  appendStatus(id, lane, "failed", "cancelled by request; claimant gone, work preserved");
4179
4019
  complete(id, lane);
@@ -4195,7 +4035,7 @@ function claimBait(reg) {
4195
4035
  const id = claimNext(lane, (d) => !pass(d));
4196
4036
  if (!id)
4197
4037
  continue;
4198
- const descriptor = JSON.parse(fs21.readFileSync(path19.join(laneDirs(lane).active, id, "descriptor.json"), "utf8"));
4038
+ const descriptor = JSON.parse(fs20.readFileSync(path18.join(laneDirs(lane).active, id, "descriptor.json"), "utf8"));
4199
4039
  const claim = {
4200
4040
  by: mine,
4201
4041
  sessionId: reg.sessionId,
@@ -4203,7 +4043,7 @@ function claimBait(reg) {
4203
4043
  worktree: reg.worktree,
4204
4044
  at: (/* @__PURE__ */ new Date()).toISOString()
4205
4045
  };
4206
- atomicWrite4(claimPath(id, lane), JSON.stringify(claim, null, 2));
4046
+ atomicWrite3(claimPath(id, lane), JSON.stringify(claim, null, 2));
4207
4047
  appendStatus(id, lane, "working", `claimed by ${mine}`, claim.at);
4208
4048
  mergeEvidence(id, lane, { sessionId: reg.sessionId, harness: reg.harness, deliveredTo: mine, deliveredAt: claim.at });
4209
4049
  heartbeatTrap(reg.trapId, { claimed: id, parked: true });
@@ -4258,13 +4098,13 @@ function sweepGhostTraps(ttlMs, now2 = Date.now(), pausedTtlMs = 864e5) {
4258
4098
  }
4259
4099
  }
4260
4100
  const released = releaseCatch(reg);
4261
- const removal = reg.createdWorktree && fs21.existsSync(reg.worktree) ? removeGhostWorktree(reg.worktree) : void 0;
4101
+ const removal = reg.createdWorktree && fs20.existsSync(reg.worktree) ? removeGhostWorktree(reg.worktree) : void 0;
4262
4102
  const catchNote = released.requeued ? ", catch requeued" : released.finalized ? ", catch finalized" : "";
4263
4103
  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
4104
  actions.push({ trapId: reg.trapId, ...released, ...pauseExpired ? { pauseExpired } : {}, ...removal ? { worktree: removal.removed ? "removed" : "kept" } : {} });
4265
- fs21.rmSync(regPath(reg.trapId), { force: true });
4266
- fs21.rmSync(beatPath(reg.trapId), { force: true });
4267
- fs21.rmSync(prProbePath(reg.trapId), { force: true });
4105
+ fs20.rmSync(regPath(reg.trapId), { force: true });
4106
+ fs20.rmSync(beatPath(reg.trapId), { force: true });
4107
+ fs20.rmSync(prProbePath(reg.trapId), { force: true });
4268
4108
  postNotice({
4269
4109
  kind: "trap-ghosted",
4270
4110
  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 +4139,7 @@ function baitBrief(id, d) {
4299
4139
  `You are a lobstah worker session and have been assigned dispatch ${id}.`,
4300
4140
  "",
4301
4141
  "Do the work in THIS worktree on a fresh branch (branch first, never on the checked-out state directly).",
4302
- `Report progress with \`lobstah report ${id} working "<note>"\` at milestones, check \`lobstah inbox ${id}\` at natural checkpoints, and finish with \`lobstah report ${id} done "<note>" [--pr <url>]\` (or \`failed\`).`,
4142
+ `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
4143
  "A needs-decision or blocked report queues your question to the human; the answer arrives in this dispatch's inbox.",
4304
4144
  `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
4145
  "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 +4151,40 @@ function baitBrief(id, d) {
4311
4151
  ].join("\n");
4312
4152
  }
4313
4153
  function beatPath(trapId) {
4314
- return path19.join(soakingDir(), `${trapId}.beat`);
4154
+ return path18.join(soakingDir(), `${trapId}.beat`);
4315
4155
  }
4316
4156
  function readBeat(trapId) {
4317
4157
  try {
4318
- const parsed = JSON.parse(fs21.readFileSync(beatPath(trapId), "utf8"));
4158
+ const parsed = JSON.parse(fs20.readFileSync(beatPath(trapId), "utf8"));
4319
4159
  return typeof parsed.at === "string" ? parsed : void 0;
4320
4160
  } catch {
4321
4161
  return void 0;
4322
4162
  }
4323
4163
  }
4324
4164
  function prProbePath(trapId) {
4325
- return path19.join(soakingDir(), `${trapId}.prprobe`);
4165
+ return path18.join(soakingDir(), `${trapId}.prprobe`);
4326
4166
  }
4327
4167
  function readTrapPrProbe(trapId) {
4328
4168
  try {
4329
- const parsed = JSON.parse(fs21.readFileSync(prProbePath(trapId), "utf8"));
4169
+ const parsed = JSON.parse(fs20.readFileSync(prProbePath(trapId), "utf8"));
4330
4170
  return typeof parsed.dispatch === "string" && typeof parsed.checkedAt === "string" ? parsed : void 0;
4331
4171
  } catch {
4332
4172
  return void 0;
4333
4173
  }
4334
4174
  }
4335
4175
  function writeTrapPrProbe(trapId, probe) {
4336
- atomicWrite4(prProbePath(trapId), JSON.stringify(probe));
4176
+ atomicWrite3(prProbePath(trapId), JSON.stringify(probe));
4337
4177
  }
4338
4178
  function trapLastSeen(reg) {
4339
4179
  return Math.max(msOf(reg.heartbeatAt), msOf(readBeat(reg.trapId)?.at ?? ""));
4340
4180
  }
4341
4181
  function trapIdAbove(dir) {
4342
- let at = path19.resolve(dir);
4182
+ let at = path18.resolve(dir);
4343
4183
  for (; ; ) {
4344
4184
  const id = trapIdAt(at);
4345
4185
  if (id !== void 0)
4346
4186
  return id;
4347
- const up = path19.dirname(at);
4187
+ const up = path18.dirname(at);
4348
4188
  if (up === at)
4349
4189
  return void 0;
4350
4190
  at = up;
@@ -4366,7 +4206,7 @@ function beatTrap(input) {
4366
4206
  if (now2 - last < (input.throttleMs ?? 3e4))
4367
4207
  return { beat: false, reason: "throttled" };
4368
4208
  const at = new Date(now2).toISOString();
4369
- atomicWrite4(beatPath(trapId), JSON.stringify({ at, sessionId: reg.sessionId }));
4209
+ atomicWrite3(beatPath(trapId), JSON.stringify({ at, sessionId: reg.sessionId }));
4370
4210
  if (!hasOpenCatch(reg))
4371
4211
  return { beat: true, trapId };
4372
4212
  const name = input.toolName || "tool";
@@ -4391,16 +4231,229 @@ var init_soak = __esm({
4391
4231
  init_worktrees();
4392
4232
  init_worktree_safety();
4393
4233
  init_trap_start();
4234
+ init_config();
4394
4235
  TRAP_FILE = ".lobstah-trap";
4395
4236
  TRAP_ANCHOR_FILE = TRAP_FILE;
4396
4237
  TRAP_ADDRESS_RE = /\bwt:([a-z0-9][a-z0-9-]*)/g;
4397
- signedOffDir = () => path19.join(path19.dirname(soakingDir()), "signed-off");
4398
- signedOffPath = (trapId) => path19.join(signedOffDir(), `${trapId}.json`);
4238
+ signedOffDir = () => path18.join(path18.dirname(soakingDir()), "signed-off");
4239
+ signedOffPath = (trapId) => path18.join(signedOffDir(), `${trapId}.json`);
4399
4240
  TITLE_REMINDERS = 3;
4400
4241
  msOf = (iso) => Date.parse(iso) || 0;
4401
4242
  }
4402
4243
  });
4403
4244
 
4245
+ // packages/core/dist/helm.js
4246
+ import * as fs21 from "node:fs";
4247
+ import * as path19 from "node:path";
4248
+ function helmDir() {
4249
+ return path19.join(lobstahHome(), "helm");
4250
+ }
4251
+ function relievedDir() {
4252
+ return path19.join(helmDir(), ".relieved");
4253
+ }
4254
+ function helmPath(grounds) {
4255
+ return path19.join(helmDir(), `${grounds}.json`);
4256
+ }
4257
+ function atomicWrite4(file, content) {
4258
+ fs21.mkdirSync(path19.dirname(file), { recursive: true });
4259
+ const tmp = `${file}.tmp-${process.pid}`;
4260
+ fs21.writeFileSync(tmp, content);
4261
+ fs21.renameSync(tmp, file);
4262
+ }
4263
+ function readHelm(grounds) {
4264
+ try {
4265
+ return JSON.parse(fs21.readFileSync(helmPath(grounds), "utf8"));
4266
+ } catch {
4267
+ return void 0;
4268
+ }
4269
+ }
4270
+ function listHelms() {
4271
+ try {
4272
+ return fs21.readdirSync(helmDir()).filter((f) => f.endsWith(".json")).map((f) => JSON.parse(fs21.readFileSync(path19.join(helmDir(), f), "utf8")));
4273
+ } catch {
4274
+ return [];
4275
+ }
4276
+ }
4277
+ function helmOf(sessionId) {
4278
+ return listHelms().find((h) => h.sessionId === sessionId);
4279
+ }
4280
+ function liveHelms(ttlMs, now2 = Date.now()) {
4281
+ return listHelms().filter((h) => now2 - (Date.parse(h.heartbeatAt) || 0) <= ttlMs);
4282
+ }
4283
+ function helmGate(live, sessionId, grounds) {
4284
+ if (grounds !== void 0) {
4285
+ const holder = live.find((h) => h.grounds === grounds);
4286
+ if (holder && holder.sessionId !== sessionId) {
4287
+ 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\`).`;
4288
+ }
4289
+ if (holder)
4290
+ return void 0;
4291
+ }
4292
+ if (live.length === 0)
4293
+ return void 0;
4294
+ if (sessionId !== void 0 && live.some((h) => h.sessionId === sessionId))
4295
+ return void 0;
4296
+ const holders = live.map((h) => `${h.grounds}=${h.sessionId.slice(0, 8)}`).join(", ");
4297
+ 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\`).`;
4298
+ }
4299
+ function helmLabel(h) {
4300
+ if (h.label)
4301
+ return h.label;
4302
+ const where = h.cwd ? path19.basename(h.cwd) : void 0;
4303
+ if (!h.harness && !where)
4304
+ return `session ${h.sessionId.slice(0, 8)}`;
4305
+ return `${h.harness ?? "session"}${where ? ` @ ${where}` : ""}`;
4306
+ }
4307
+ function takeHelm(opts) {
4308
+ const now2 = opts.now ?? Date.now();
4309
+ const existing = readHelm(opts.grounds.name);
4310
+ if (existing && existing.sessionId !== opts.sessionId) {
4311
+ const fresh = now2 - (Date.parse(existing.heartbeatAt) || 0) <= opts.ttlMs;
4312
+ if (fresh && !opts.take)
4313
+ return { held: existing };
4314
+ atomicWrite4(path19.join(relievedDir(), `${existing.sessionId}.json`), JSON.stringify({
4315
+ sessionId: existing.sessionId,
4316
+ grounds: opts.grounds.name,
4317
+ by: opts.sessionId,
4318
+ at: new Date(now2).toISOString()
4319
+ }, null, 2));
4320
+ }
4321
+ const iso = new Date(now2).toISOString();
4322
+ const same = existing?.sessionId === opts.sessionId;
4323
+ const live = existing !== void 0 && now2 - (Date.parse(existing.heartbeatAt) || 0) <= opts.ttlMs;
4324
+ const reg = {
4325
+ sessionId: opts.sessionId,
4326
+ grounds: opts.grounds.name,
4327
+ repos: opts.grounds.repos,
4328
+ signedOnAt: same ? existing.signedOnAt : iso,
4329
+ heartbeatAt: iso,
4330
+ // A new helm on empty grounds starts its cursor now: a notice delivered
4331
+ // to nobody while no helm was signed on is not news. A take from a live
4332
+ // helm, or the same session signing on again, keeps the cursor, so
4333
+ // nothing in flight is dropped.
4334
+ wakesFrom: same || live ? existing.wakesFrom : iso,
4335
+ // Identity refreshes on every sign-on; a re-sign without one keeps what
4336
+ // the registration already knows.
4337
+ harness: opts.identity?.harness ?? (same ? existing.harness : void 0),
4338
+ cwd: opts.identity?.cwd ?? (same ? existing.cwd : void 0),
4339
+ host: opts.identity?.host ?? (same ? existing.host : void 0),
4340
+ label: opts.identity?.label ?? (same ? existing.label : void 0),
4341
+ window: opts.identity?.window ?? (same ? existing.window : void 0),
4342
+ tookFrom: existing && existing.sessionId !== opts.sessionId ? { sessionId: existing.sessionId, at: iso } : existing?.tookFrom
4343
+ };
4344
+ atomicWrite4(helmPath(opts.grounds.name), JSON.stringify(reg, null, 2));
4345
+ return { ok: reg };
4346
+ }
4347
+ function wakeFloorMs(h) {
4348
+ return h?.wakesFrom ? Date.parse(h.wakesFrom) || 0 : 0;
4349
+ }
4350
+ function noticeStands(n) {
4351
+ switch (n.kind) {
4352
+ case "disk-held":
4353
+ return readHold() !== void 0;
4354
+ case "watch-failing":
4355
+ case "watch-held":
4356
+ return listWatches().some((w) => (w.key === n.refId || w.owner === `dispatch:${n.refId}`) && (n.kind === "watch-failing" ? w.failingNoticed === true : true));
4357
+ case "trap-request":
4358
+ return n.refId !== void 0 && isOpenRequest(n.refId);
4359
+ case "trap-claim-idle":
4360
+ return listTraps().some((reg) => {
4361
+ if (reg.claimed !== n.refId)
4362
+ return false;
4363
+ const bait = unreportedTrapBait(reg);
4364
+ return bait !== void 0 && !cancelRequested(bait.id, bait.lane) && Date.parse(bait.claim.at) <= Date.parse(n.at);
4365
+ });
4366
+ default:
4367
+ return false;
4368
+ }
4369
+ }
4370
+ function noticeWakes(h) {
4371
+ const floor = wakeFloorMs(h);
4372
+ if (floor === 0)
4373
+ return void 0;
4374
+ return (n) => (Date.parse(n.at) || 0) >= floor || noticeStands(n);
4375
+ }
4376
+ function relieveHelm(sessionId) {
4377
+ const relieved = [];
4378
+ for (const h of listHelms()) {
4379
+ if (h.sessionId !== sessionId)
4380
+ continue;
4381
+ fs21.rmSync(helmPath(h.grounds), { force: true });
4382
+ relieved.push(h.grounds);
4383
+ }
4384
+ fs21.rmSync(path19.join(relievedDir(), `${sessionId}.json`), { force: true });
4385
+ return relieved;
4386
+ }
4387
+ function heartbeatHelm(sessionId, opts = {}) {
4388
+ const reg = helmOf(sessionId);
4389
+ if (!reg)
4390
+ return void 0;
4391
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
4392
+ const next = { ...reg, heartbeatAt: now2, ...opts.parked ? { parkedAt: now2 } : {} };
4393
+ atomicWrite4(helmPath(reg.grounds), JSON.stringify(next, null, 2));
4394
+ return next;
4395
+ }
4396
+ function consumeRelievedNotice(sessionId) {
4397
+ const file = path19.join(relievedDir(), `${sessionId}.json`);
4398
+ try {
4399
+ const notice = JSON.parse(fs21.readFileSync(file, "utf8"));
4400
+ fs21.rmSync(file, { force: true });
4401
+ return notice;
4402
+ } catch {
4403
+ return void 0;
4404
+ }
4405
+ }
4406
+ function groundsList(cfg) {
4407
+ const entries = Object.entries(cfg.grounds);
4408
+ if (entries.length === 0)
4409
+ return [{ name: "fleet", repos: Object.keys(cfg.repos) }];
4410
+ return entries.map(([name, g]) => ({ name, repos: g.repos }));
4411
+ }
4412
+ function groundsErrors(cfg) {
4413
+ const errors = [];
4414
+ const seen = /* @__PURE__ */ new Map();
4415
+ for (const [name, g] of Object.entries(cfg.grounds)) {
4416
+ for (const repo of g.repos) {
4417
+ if (cfg.repos[repo] === void 0)
4418
+ errors.push(`grounds.${name} names unknown repo "${repo}"`);
4419
+ const other = seen.get(repo);
4420
+ if (other !== void 0)
4421
+ errors.push(`repo "${repo}" is in both grounds.${other} and grounds.${name} \u2014 a repo belongs to at most one grounds`);
4422
+ else
4423
+ seen.set(repo, name);
4424
+ }
4425
+ }
4426
+ return errors;
4427
+ }
4428
+ function resolveGrounds(cfg, name) {
4429
+ const all = groundsList(cfg);
4430
+ if (name === void 0) {
4431
+ if (all.length === 1)
4432
+ return all[0];
4433
+ throw new Error(`multiple grounds configured \u2014 pass --grounds <${all.map((g) => g.name).join("|")}>`);
4434
+ }
4435
+ const found = all.find((g) => g.name === name);
4436
+ if (!found)
4437
+ throw new Error(`unknown grounds "${name}" (configured: ${all.map((g) => g.name).join(", ")})`);
4438
+ return found;
4439
+ }
4440
+ var isOpenRequest;
4441
+ var init_helm = __esm({
4442
+ "packages/core/dist/helm.js"() {
4443
+ "use strict";
4444
+ init_disk();
4445
+ init_paths();
4446
+ init_watch();
4447
+ init_requests();
4448
+ init_queue();
4449
+ init_soak();
4450
+ isOpenRequest = (id) => {
4451
+ const r = readRequest(id);
4452
+ return r !== void 0 && r.closedAt === void 0;
4453
+ };
4454
+ }
4455
+ });
4456
+
4404
4457
  // packages/core/dist/inbox.js
4405
4458
  import * as fs22 from "node:fs";
4406
4459
  import * as path20 from "node:path";
@@ -8693,6 +8746,7 @@ function tick(log = () => {
8693
8746
  for (const t of bounceExpiredSignOffs(signOffGraceMs))
8694
8747
  log(`trap wt:${t} did not re-soak within the sign-off grace \u2014 its messages bounced to the helm`);
8695
8748
  noticeOrphanedBait(Date.now(), signOffGraceMs);
8749
+ noticeIdleTrapClaims(now2, cfg.soak.claimIdleNoticeSecs * 1e3);
8696
8750
  const workSkip = daemonSkip(listTraps(), cfg.soak.deferSecs * 1e3);
8697
8751
  const choreSkip = (d) => d.for !== void 0 && (!d.systemRepair?.trapWaitUntil || Date.now() < Date.parse(d.systemRepair.trapWaitUntil));
8698
8752
  const skipFor = (lane) => lane === "work" ? workSkip : choreSkip;
@@ -17156,8 +17210,10 @@ async function soakPark(trapId, timeout, plain = false) {
17156
17210
  );
17157
17211
  return "woke";
17158
17212
  }
17159
- if (unhandled(id, lane).length > 0) {
17160
- block(`New instruction for your dispatch ${id} \u2014 read it with \`lobstah inbox ${id}\`, act on it, and keep reporting.`);
17213
+ const pending = unreportedTrapBait(reg);
17214
+ 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;
17215
+ if (pending || instruction) {
17216
+ block([instruction, pending ? baitBrief(id, pending.descriptor) : void 0].filter(Boolean).join("\n\n"));
17161
17217
  return "woke";
17162
17218
  }
17163
17219
  } else {