lobstah 0.5.7 → 0.5.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 CHANGED
@@ -175,11 +175,11 @@ import * as fs3 from "node:fs";
175
175
  function isVerb(v) {
176
176
  return VERBS.includes(v);
177
177
  }
178
- function appendStatus(id, lane, verb, note) {
178
+ function appendStatus(id, lane, verb, note, at) {
179
179
  if (!isVerb(verb)) {
180
180
  throw new Error(`invalid status verb "${verb}" \u2014 must be one of: ${VERBS.join(", ")}`);
181
181
  }
182
- const entry = { at: (/* @__PURE__ */ new Date()).toISOString(), verb, ...note ? { note } : {} };
182
+ const entry = { at: at ?? (/* @__PURE__ */ new Date()).toISOString(), verb, ...note ? { note } : {} };
183
183
  fs3.appendFileSync(statusPath(id, lane), `${JSON.stringify(entry)}
184
184
  `);
185
185
  return entry;
@@ -208,6 +208,13 @@ function reconcile({ log, lastEventAt: lastEventAt2, now: now2 = Date.now(), bus
208
208
  return last.verb;
209
209
  return "unknown";
210
210
  }
211
+ function displayState(input) {
212
+ if (input.queued && input.log.length === 0)
213
+ return "queued";
214
+ if (!input.queued && input.claimedAt !== void 0 && input.log.length === 0)
215
+ return "working";
216
+ return reconcile(input);
217
+ }
211
218
  var init_status = __esm({
212
219
  "packages/core/dist/status.js"() {
213
220
  "use strict";
@@ -229,7 +236,11 @@ function enqueue(d, lane = "work") {
229
236
  throw new Error("descriptor requires id, repo, and brief");
230
237
  }
231
238
  const inherited = d.followUp && d.attachments === void 0 ? storedDescriptor(d.followUp, "work")?.attachments ?? storedDescriptor(d.followUp, "chore")?.attachments : void 0;
232
- const descriptor = inherited?.length ? { ...d, attachments: inherited } : d;
239
+ const descriptor = {
240
+ ...d,
241
+ ...inherited?.length ? { attachments: inherited } : {},
242
+ queuedAt: d.queuedAt ?? (/* @__PURE__ */ new Date()).toISOString()
243
+ };
233
244
  atomicWrite(path3.join(laneDirs(lane).queue, `${d.id}.json`), JSON.stringify(descriptor, null, 2));
234
245
  }
235
246
  function pendingIds(lane) {
@@ -243,6 +254,17 @@ function queuedDescriptor(id, lane) {
243
254
  return void 0;
244
255
  }
245
256
  }
257
+ function queuedAt(id, lane) {
258
+ const file = path3.join(laneDirs(lane).queue, `${id}.json`);
259
+ let stat;
260
+ try {
261
+ stat = fs4.statSync(file);
262
+ } catch {
263
+ return void 0;
264
+ }
265
+ const stamped = queuedDescriptor(id, lane)?.queuedAt;
266
+ return stamped && !Number.isNaN(Date.parse(stamped)) ? stamped : stat.mtime.toISOString();
267
+ }
246
268
  function claimNext(lane, skip) {
247
269
  const dirs = laneDirs(lane);
248
270
  for (const id of pendingIds(lane)) {
@@ -1323,6 +1345,7 @@ function loadConfig() {
1323
1345
  limits: { ...DEFAULT_LIMITS, ...raw.limits ?? {} },
1324
1346
  soak: { ...DEFAULT_SOAK, ...raw.soak ?? {} },
1325
1347
  helm: { ...DEFAULT_HELM, ...raw.helm ?? {} },
1348
+ glass: { ...DEFAULT_GLASS, ...raw.glass ?? {} },
1326
1349
  grounds,
1327
1350
  notifyCommand: raw.notifyCommand ? String(raw.notifyCommand) : void 0,
1328
1351
  notifyVerbs: Array.isArray(raw.notifyVerbs) ? raw.notifyVerbs.map(String) : void 0,
@@ -1348,7 +1371,7 @@ function resolveDispatch(d, cfg) {
1348
1371
  flags: d.flags ?? []
1349
1372
  };
1350
1373
  }
1351
- var ATTENTION_KINDS, DEFAULT_ATTENTION_KINDS, DEFAULT_SOAK, DEFAULT_HELM, DEFAULT_LIMITS;
1374
+ var ATTENTION_KINDS, DEFAULT_ATTENTION_KINDS, DEFAULT_SOAK, DEFAULT_HELM, DEFAULT_GLASS, DEFAULT_LIMITS;
1352
1375
  var init_config = __esm({
1353
1376
  "packages/core/dist/config.js"() {
1354
1377
  "use strict";
@@ -1362,14 +1385,17 @@ var init_config = __esm({
1362
1385
  };
1363
1386
  DEFAULT_HELM = {
1364
1387
  ttlSecs: 1800,
1365
- reportSecs: 900
1388
+ reportSecs: 900,
1389
+ armGraceSecs: 5
1366
1390
  };
1391
+ DEFAULT_GLASS = { port: 4949 };
1367
1392
  DEFAULT_LIMITS = {
1368
1393
  maxConcurrent: 2,
1369
1394
  choreConcurrent: 1,
1370
1395
  wedgeThresholdSecs: 600,
1371
1396
  maxRestartAttempts: 2,
1372
1397
  wallClockSecs: 3600,
1398
+ backgroundWaitSecs: 1800,
1373
1399
  choreRetentionDays: 7,
1374
1400
  attachmentMaxBytes: 25 * 1024 * 1024
1375
1401
  };
@@ -1841,6 +1867,7 @@ function claimBait(reg) {
1841
1867
  at: (/* @__PURE__ */ new Date()).toISOString()
1842
1868
  };
1843
1869
  atomicWrite2(claimPath(id, "work"), JSON.stringify(claim, null, 2));
1870
+ appendStatus(id, "work", "working", `claimed by ${mine}`, claim.at);
1844
1871
  mergeEvidence(id, "work", { sessionId: reg.sessionId, harness: reg.harness, deliveredTo: mine, deliveredAt: claim.at });
1845
1872
  heartbeatTrap(reg.trapId, { claimed: id, parked: true });
1846
1873
  return { id, descriptor };
@@ -2188,6 +2215,24 @@ function normalizeChecks(rollup) {
2188
2215
  }
2189
2216
  return out.sort((a, b) => a.name.localeCompare(b.name));
2190
2217
  }
2218
+ function prStandingKinds(pr) {
2219
+ if (pr.state !== "OPEN")
2220
+ return [];
2221
+ const out = [];
2222
+ const { failed, pending, total } = pr.checks;
2223
+ if (pr.draft)
2224
+ out.push("pr:draft");
2225
+ const review = (pr.review?.unresolvedThreads ?? 0) > 0 || pr.review?.changesRequested === true;
2226
+ if (review)
2227
+ out.push("pr:review");
2228
+ if (failed > 0)
2229
+ out.push("pr:checks");
2230
+ if (isConflicting(pr.mergeStateStatus))
2231
+ out.push("pr:conflict");
2232
+ if (!pr.draft && !review && isMergeable(pr.mergeStateStatus) && (pr.reviewDecision === "APPROVED" || total > 0 && failed === 0 && pending === 0))
2233
+ out.push("pr:ready");
2234
+ return out;
2235
+ }
2191
2236
  function prReview(view) {
2192
2237
  const latest = /* @__PURE__ */ new Map();
2193
2238
  let lastReviewAt;
@@ -2422,12 +2467,17 @@ function upsertPr(pr, dispatchId) {
2422
2467
  const dispatches = [...before?.dispatches ?? []];
2423
2468
  if (dispatchId && !dispatches.includes(dispatchId))
2424
2469
  dispatches.push(dispatchId);
2470
+ const merged = { ...before ?? {}, ...pr };
2471
+ const standingSince = {};
2472
+ for (const kind of prStandingKinds(merged)) {
2473
+ standingSince[kind] = before?.standingSince?.[kind] ?? pr.observedAt;
2474
+ }
2425
2475
  const after = {
2426
- ...before ?? {},
2427
- ...pr,
2476
+ ...merged,
2428
2477
  key: ref.key,
2429
2478
  repo: `${ref.owner}/${ref.repo}`,
2430
- dispatches
2479
+ dispatches,
2480
+ standingSince
2431
2481
  };
2432
2482
  fs15.mkdirSync(prsDir(), { recursive: true });
2433
2483
  const file = prRecordFile(ref.key);
@@ -2453,6 +2503,7 @@ var init_prs = __esm({
2453
2503
 
2454
2504
  // packages/core/dist/resume.js
2455
2505
  import * as fs16 from "node:fs";
2506
+ import * as os3 from "node:os";
2456
2507
  import * as path12 from "node:path";
2457
2508
  import { spawnSync as spawnSync3 } from "node:child_process";
2458
2509
  function harnessFromSessionId(sessionId) {
@@ -2544,7 +2595,53 @@ function handoffNote(fromHarness, progress, why) {
2544
2595
 
2545
2596
  ${progress}`;
2546
2597
  }
2547
- var UUID, UNRESUMABLE;
2598
+ function codexHome(env = process.env) {
2599
+ return env.CODEX_HOME ?? path12.join(os3.homedir(), ".codex");
2600
+ }
2601
+ function codexRolloutFile(threadId, home = codexHome()) {
2602
+ const suffix = `-${threadId}.jsonl`;
2603
+ const walk = (dir, depth) => {
2604
+ let entries;
2605
+ try {
2606
+ entries = fs16.readdirSync(dir, { withFileTypes: true });
2607
+ } catch {
2608
+ return void 0;
2609
+ }
2610
+ for (const e of entries) {
2611
+ if (e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(suffix))
2612
+ return path12.join(dir, e.name);
2613
+ }
2614
+ if (depth === 0)
2615
+ return void 0;
2616
+ for (const e of entries.filter((x) => x.isDirectory()).sort((a, b) => b.name.localeCompare(a.name))) {
2617
+ const hit = walk(path12.join(dir, e.name), depth - 1);
2618
+ if (hit)
2619
+ return hit;
2620
+ }
2621
+ return void 0;
2622
+ };
2623
+ return walk(path12.join(home, "sessions"), 3) ?? walk(path12.join(home, "archived_sessions"), 0);
2624
+ }
2625
+ function codexDesktopThread(threadId, home = codexHome()) {
2626
+ const file = codexRolloutFile(threadId, home);
2627
+ if (!file)
2628
+ return void 0;
2629
+ let head = "";
2630
+ try {
2631
+ const fd = fs16.openSync(file, "r");
2632
+ try {
2633
+ const buf = Buffer.alloc(4096);
2634
+ head = buf.subarray(0, fs16.readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
2635
+ } finally {
2636
+ fs16.closeSync(fd);
2637
+ }
2638
+ } catch {
2639
+ return void 0;
2640
+ }
2641
+ const originator = /"originator"\s*:\s*"([^"]*)"/.exec(head)?.[1];
2642
+ return originator && /desktop/i.test(originator) ? originator : void 0;
2643
+ }
2644
+ var UUID, UNRESUMABLE, CODEX_DESKTOP_THREAD;
2548
2645
  var init_resume = __esm({
2549
2646
  "packages/core/dist/resume.js"() {
2550
2647
  "use strict";
@@ -2554,7 +2651,41 @@ var init_resume = __esm({
2554
2651
  init_soak();
2555
2652
  init_status();
2556
2653
  UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-([0-9a-f])[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2557
- UNRESUMABLE = /no conversation found|no rollout found|(session|thread|conversation)( id)?[^.\n]{0,80}\b(not found|does not exist|unknown|invalid)|(could not|cannot|failed to|unable to) (find|load|resume)[^.\n]{0,40}\b(session|thread|conversation)/i;
2654
+ UNRESUMABLE = /no conversation found|no rollout found|thread\/resume failed|(session|thread|conversation)( id)?[^.\n]{0,80}\b(not found|does not exist|unknown|invalid)|(could not|cannot|failed to|unable to) (find|load|resume)[^.\n]{0,40}\b(session|thread|conversation)/i;
2655
+ CODEX_DESKTOP_THREAD = "Codex desktop thread; not resumable from the CLI";
2656
+ }
2657
+ });
2658
+
2659
+ // packages/core/dist/glass.js
2660
+ var init_glass = __esm({
2661
+ "packages/core/dist/glass.js"() {
2662
+ "use strict";
2663
+ }
2664
+ });
2665
+
2666
+ // packages/core/dist/models.js
2667
+ function modelHarness(model) {
2668
+ if (!model)
2669
+ return void 0;
2670
+ return MODEL_OWNERS.find(([re]) => re.test(model))?.[1];
2671
+ }
2672
+ function modelForHarness(harness, model) {
2673
+ const owner = modelHarness(model);
2674
+ if (model && owner && owner !== harness)
2675
+ return { dropped: { model, owner } };
2676
+ return { model };
2677
+ }
2678
+ function droppedModelNote(harness, dropped) {
2679
+ return `model ${dropped.model} is a ${dropped.owner} model \u2014 dropped, using ${harness}'s default`;
2680
+ }
2681
+ var MODEL_OWNERS;
2682
+ var init_models = __esm({
2683
+ "packages/core/dist/models.js"() {
2684
+ "use strict";
2685
+ MODEL_OWNERS = [
2686
+ [/^(anthropic[/.])?(claude|opus|sonnet|haiku|fable)(\b|-|$)|^opusplan$/i, "claude"],
2687
+ [/^(openai\/)?(gpt-|o\d+(\b|-|$)|codex-)/i, "codex"]
2688
+ ];
2558
2689
  }
2559
2690
  });
2560
2691
 
@@ -2583,6 +2714,8 @@ var init_dist2 = __esm({
2583
2714
  init_pr();
2584
2715
  init_prs();
2585
2716
  init_resume();
2717
+ init_glass();
2718
+ init_models();
2586
2719
  }
2587
2720
  });
2588
2721
 
@@ -2654,7 +2787,7 @@ var init_types2 = __esm({
2654
2787
  });
2655
2788
 
2656
2789
  // packages/adapters/dist/claude.js
2657
- import { spawn as spawn4 } from "node:child_process";
2790
+ import { spawn as spawn5 } from "node:child_process";
2658
2791
  function flagsToExtraArgs(flags) {
2659
2792
  const extra = {};
2660
2793
  for (let i = 0; i < flags.length; i++) {
@@ -2677,6 +2810,9 @@ function pumpClaudeMessage(msg, push, onSession) {
2677
2810
  if (msg.session_id)
2678
2811
  onSession(msg.session_id);
2679
2812
  push({ at, type: "session", data: { sessionId: msg.session_id } });
2813
+ } else if (msg.type === "system" && msg.subtype === "background_tasks_changed") {
2814
+ const live = (msg.tasks ?? []).filter((t) => !t.ambient).length;
2815
+ push({ at, type: "background", data: { live } });
2680
2816
  } else if (msg.type === "assistant") {
2681
2817
  for (const block of msg.message?.content ?? []) {
2682
2818
  if (block.type === "tool_use") {
@@ -2697,6 +2833,19 @@ function pumpClaudeMessage(msg, push, onSession) {
2697
2833
  push({ at, type: "turn-end", data: { subtype: msg.subtype, costUsd: msg.total_cost_usd } });
2698
2834
  }
2699
2835
  }
2836
+ function claudeSdkOptions(opts) {
2837
+ return {
2838
+ cwd: opts.cwd,
2839
+ model: opts.model,
2840
+ ...opts.effort ? { effort: opts.effort } : {},
2841
+ maxTurns: opts.limits.maxTurns,
2842
+ resume: opts.resumeSession,
2843
+ forkSession: opts.resumeSession ? true : void 0,
2844
+ permissionMode: "bypassPermissions",
2845
+ env: { ...process.env, ...opts.env },
2846
+ extraArgs: flagsToExtraArgs(opts.flags)
2847
+ };
2848
+ }
2700
2849
  function claudeCliArgs(opts) {
2701
2850
  const args = [
2702
2851
  "-p",
@@ -2710,6 +2859,8 @@ function claudeCliArgs(opts) {
2710
2859
  ];
2711
2860
  if (opts.model)
2712
2861
  args.push("--model", opts.model);
2862
+ if (opts.effort)
2863
+ args.push("--effort", opts.effort);
2713
2864
  if (opts.limits.maxTurns)
2714
2865
  args.push("--max-turns", String(opts.limits.maxTurns));
2715
2866
  if (opts.resumeSession)
@@ -2718,7 +2869,7 @@ function claudeCliArgs(opts) {
2718
2869
  return args;
2719
2870
  }
2720
2871
  function startClaudeCli(invocation, opts) {
2721
- const child = spawn4(invocation.file, [...invocation.argvPrefix, ...claudeCliArgs(opts)], {
2872
+ const child = spawn5(invocation.file, [...invocation.argvPrefix, ...claudeCliArgs(opts)], {
2722
2873
  cwd: opts.cwd,
2723
2874
  env: { ...process.env, ...opts.env },
2724
2875
  stdio: ["pipe", "pipe", "pipe"],
@@ -2817,16 +2968,7 @@ function createClaudeAdapter() {
2817
2968
  }
2818
2969
  const q = sdk.query({
2819
2970
  prompt: userMessages(),
2820
- options: {
2821
- cwd: opts.cwd,
2822
- model: opts.model,
2823
- maxTurns: opts.limits.maxTurns,
2824
- resume: opts.resumeSession,
2825
- forkSession: opts.resumeSession ? true : void 0,
2826
- permissionMode: "bypassPermissions",
2827
- env: { ...process.env, ...opts.env },
2828
- extraArgs: flagsToExtraArgs(opts.flags)
2829
- }
2971
+ options: claudeSdkOptions(opts)
2830
2972
  });
2831
2973
  const events = new AsyncQueue();
2832
2974
  let sessionId;
@@ -2876,25 +3018,25 @@ var init_claude = __esm({
2876
3018
  });
2877
3019
 
2878
3020
  // packages/adapters/dist/codex.js
2879
- import * as fs41 from "node:fs";
2880
- import * as os9 from "node:os";
2881
- import * as path34 from "node:path";
2882
- import { spawn as spawn5 } from "node:child_process";
3021
+ import * as fs42 from "node:fs";
3022
+ import * as os10 from "node:os";
3023
+ import * as path35 from "node:path";
3024
+ import { spawn as spawn6 } from "node:child_process";
2883
3025
  function isolatedCodexHome() {
2884
- const dir = path34.join(lobstahHome(), "codex-home");
2885
- fs41.mkdirSync(dir, { recursive: true });
2886
- const auth = path34.join(dir, "auth.json");
2887
- const realAuth = path34.join(os9.homedir(), ".codex", "auth.json");
2888
- if (!fs41.existsSync(auth) && fs41.existsSync(realAuth)) {
3026
+ const dir = path35.join(lobstahHome(), "codex-home");
3027
+ fs42.mkdirSync(dir, { recursive: true });
3028
+ const auth = path35.join(dir, "auth.json");
3029
+ const realAuth = path35.join(os10.homedir(), ".codex", "auth.json");
3030
+ if (!fs42.existsSync(auth) && fs42.existsSync(realAuth)) {
2889
3031
  try {
2890
- fs41.symlinkSync(realAuth, auth);
3032
+ fs42.symlinkSync(realAuth, auth);
2891
3033
  } catch {
2892
- fs41.copyFileSync(realAuth, auth);
3034
+ fs42.copyFileSync(realAuth, auth);
2893
3035
  }
2894
3036
  }
2895
- const cfg = path34.join(dir, "config.toml");
2896
- if (!fs41.existsSync(cfg)) {
2897
- fs41.writeFileSync(cfg, "# lobstah-managed: auth inherited from ~/.codex, interactive config not\n");
3037
+ const cfg = path35.join(dir, "config.toml");
3038
+ if (!fs42.existsSync(cfg)) {
3039
+ fs42.writeFileSync(cfg, "# lobstah-managed: auth inherited from ~/.codex, interactive config not\n");
2898
3040
  }
2899
3041
  return dir;
2900
3042
  }
@@ -2942,7 +3084,7 @@ function startCodexCli(invoke, opts) {
2942
3084
  function runTurn(prompt, resumeThread) {
2943
3085
  return new Promise((resolve6) => {
2944
3086
  const inv = invoke(codexExecArgs(opts, prompt, resumeThread));
2945
- const child = spawn5(inv.file, inv.argv, {
3087
+ const child = spawn6(inv.file, inv.argv, {
2946
3088
  cwd: opts.cwd,
2947
3089
  env: { ...process.env, ...opts.env, CODEX_HOME: isolatedCodexHome() },
2948
3090
  stdio: ["ignore", "pipe", "pipe"],
@@ -3108,29 +3250,45 @@ var init_dist3 = __esm({
3108
3250
  });
3109
3251
 
3110
3252
  // packages/worktree/dist/index.js
3111
- import * as fs42 from "node:fs";
3112
- import * as path35 from "node:path";
3253
+ import * as fs43 from "node:fs";
3254
+ import * as path36 from "node:path";
3113
3255
  import { exec, execFile } from "node:child_process";
3114
3256
  import { promisify } from "node:util";
3115
3257
  function worktreePath(id) {
3116
- return path35.join(lobstahHome(), "worktrees", id);
3258
+ return path36.join(lobstahHome(), "worktrees", id);
3117
3259
  }
3118
3260
  async function git4(cwd, ...args) {
3119
3261
  const { stdout } = await run3("git", args, { cwd, env: process.env });
3120
3262
  return stdout.trim();
3121
3263
  }
3264
+ function isLockContention(err) {
3265
+ const text = `${err.stderr ?? ""}
3266
+ ${err instanceof Error ? err.message : ""}`;
3267
+ return LOCK_CONTENTION.some((re) => re.test(text));
3268
+ }
3269
+ async function gitShared(cwd, ...args) {
3270
+ for (let attempt = 1; ; attempt++) {
3271
+ try {
3272
+ return await git4(cwd, ...args);
3273
+ } catch (err) {
3274
+ if (attempt >= 6 || !isLockContention(err))
3275
+ throw err;
3276
+ await new Promise((r) => setTimeout(r, 100 * 2 ** (attempt - 1) * (0.5 + Math.random())));
3277
+ }
3278
+ }
3279
+ }
3122
3280
  async function allocate(repo, id) {
3123
3281
  const dir = worktreePath(id);
3124
- if (fs42.existsSync(dir)) {
3282
+ if (fs43.existsSync(dir)) {
3125
3283
  throw new Error(`worktree for ${id} already exists at ${dir} \u2014 never allocate a second`);
3126
3284
  }
3127
- if (!fs42.existsSync(repo.path)) {
3285
+ if (!fs43.existsSync(repo.path)) {
3128
3286
  if (!repo.origin)
3129
3287
  throw new Error(`repo path ${repo.path} missing and no origin configured`);
3130
3288
  await run3("git", ["clone", repo.origin, repo.path], { env: process.env });
3131
3289
  }
3132
- await git4(repo.path, "fetch", "origin", repo.trunk);
3133
- await git4(repo.path, "worktree", "add", dir, "-b", `lobstah/${id}`, `origin/${repo.trunk}`);
3290
+ await gitShared(repo.path, "fetch", "origin", repo.trunk);
3291
+ await gitShared(repo.path, "worktree", "add", "--no-track", dir, "-b", `lobstah/${id}`, `origin/${repo.trunk}`);
3134
3292
  for (const cmd of repo.setup ?? []) {
3135
3293
  await shell(cmd, { cwd: dir, env: { ...process.env, ...repo.env ?? {} } });
3136
3294
  }
@@ -3142,19 +3300,20 @@ async function collectEvidence(repo, id) {
3142
3300
  const log = await git4(dir, "log", "--oneline", `origin/${repo.trunk}..HEAD`);
3143
3301
  return { branch, commits: log ? log.split("\n") : [] };
3144
3302
  }
3145
- var run3, shell;
3303
+ var run3, shell, LOCK_CONTENTION;
3146
3304
  var init_dist4 = __esm({
3147
3305
  "packages/worktree/dist/index.js"() {
3148
3306
  "use strict";
3149
3307
  init_dist2();
3150
3308
  run3 = promisify(execFile);
3151
3309
  shell = promisify(exec);
3310
+ LOCK_CONTENTION = [/cannot lock ref/, /unable to update local ref/, /Unable to create '[^']*\.lock': File exists/];
3152
3311
  }
3153
3312
  });
3154
3313
 
3155
3314
  // packages/runner/dist/contract.js
3156
3315
  function buildPrompt(brief, opts) {
3157
- const reporting = `Report status by running \`lobstah report ${opts.id} <verb> [note]\` (verbs: ${VERBS.join(", ")}). Attach a PR URL to your final report with \`--pr <url>\`. At natural checkpoints, check for operator messages with \`lobstah inbox ${opts.id}\` \u2014 messages also arrive automatically between your turns.`;
3316
+ const reporting = `Report status by running \`lobstah report ${opts.id} <verb> [note]\` (verbs: ${VERBS.join(", ")}). Attach a PR URL to your final report with \`--pr <url>\`. Only your report finishes this dispatch: ending a turn without \`done\` or \`failed\` does not. If you start background work and end your turn to wait for it, you are woken when it finishes. At natural checkpoints, check for operator messages with \`lobstah inbox ${opts.id}\` \u2014 messages also arrive automatically between your turns.`;
3158
3317
  const parts = [
3159
3318
  `You are a dispatched coding agent supervised by lobstah. Dispatch id: ${opts.id}.`,
3160
3319
  `Work only inside the current directory \u2014 it is an isolated git worktree allocated for this dispatch.`,
@@ -3178,6 +3337,9 @@ var init_contract = __esm({
3178
3337
  });
3179
3338
 
3180
3339
  // packages/runner/dist/drive.js
3340
+ function unreportedNudge(id) {
3341
+ return `Your turn ended without a status report, so this dispatch is not finished. If the work is complete, run \`lobstah report ${id} done "<what you did>"\`, adding \`--pr <url>\` if you opened a PR. If no change was needed, report \`done\` with a note saying why. If a human has to answer a question, or you are blocked, report \`needs-decision\` or \`blocked\` with it. If you are waiting on something, run it in the background: you are woken when it finishes.`;
3342
+ }
3181
3343
  function deliver(run4, id, lane) {
3182
3344
  const msgs = unhandled(id, lane);
3183
3345
  for (const m of msgs) {
@@ -3187,7 +3349,7 @@ function deliver(run4, id, lane) {
3187
3349
  return msgs.length;
3188
3350
  }
3189
3351
  async function drive(run4, opts) {
3190
- const { id, lane, pollMs = 3e3, stopped = () => false } = opts;
3352
+ const { id, lane, pollMs = 3e3, stopped = () => false, backgroundWaitMs = 30 * 6e4, settleGraceMs = 12e4 } = opts;
3191
3353
  let cancelled = false;
3192
3354
  let activity = 0;
3193
3355
  let finished = false;
@@ -3213,8 +3375,53 @@ async function drive(run4, opts) {
3213
3375
  await sleep(pollMs);
3214
3376
  }
3215
3377
  };
3378
+ let liveBackground = 0;
3379
+ let nudged = false;
3380
+ let hold;
3381
+ const runnerEvent = (data) => appendEvent(id, lane, { at: (/* @__PURE__ */ new Date()).toISOString(), type: "runner", data });
3382
+ const releaseHold = () => {
3383
+ if (!hold)
3384
+ return;
3385
+ clearInterval(hold.heartbeat);
3386
+ clearTimeout(hold.expiry);
3387
+ hold = void 0;
3388
+ };
3389
+ const unreported = () => {
3390
+ if (!nudged) {
3391
+ nudged = true;
3392
+ runnerEvent({ nudged: "unreported" });
3393
+ run4.send(unreportedNudge(id));
3394
+ return;
3395
+ }
3396
+ appendStatus(id, lane, "failed", "ended without reporting a result, after being asked to");
3397
+ run4.end();
3398
+ };
3399
+ const holdOpen = (ms) => {
3400
+ releaseHold();
3401
+ const heartbeat = setInterval(() => {
3402
+ if (cancelRequested(id, lane)) {
3403
+ releaseHold();
3404
+ cancel();
3405
+ return;
3406
+ }
3407
+ touchEvents(id, lane);
3408
+ }, pollMs);
3409
+ const expiry = setTimeout(() => {
3410
+ releaseHold();
3411
+ runnerEvent({ backgroundWait: "expired", live: liveBackground });
3412
+ unreported();
3413
+ }, ms);
3414
+ hold = { heartbeat, expiry };
3415
+ };
3216
3416
  for await (const ev of run4.events) {
3217
3417
  appendEvent(id, lane, ev);
3418
+ if (ev.type === "background") {
3419
+ liveBackground = Number(ev.data?.live ?? 0);
3420
+ if (hold && liveBackground === 0)
3421
+ holdOpen(settleGraceMs);
3422
+ continue;
3423
+ }
3424
+ releaseHold();
3218
3425
  if (ev.type === "tool-start" || ev.type === "text")
3219
3426
  activity++;
3220
3427
  if (ev.type === "session" && ev.data?.sessionId) {
@@ -3237,8 +3444,23 @@ async function drive(run4, opts) {
3237
3444
  await awaitAnswer(lastVerb);
3238
3445
  continue;
3239
3446
  }
3240
- run4.end();
3447
+ if (lastVerb !== void 0 && TERMINAL_VERBS.includes(lastVerb)) {
3448
+ run4.end();
3449
+ continue;
3450
+ }
3451
+ const subtype = ev.data?.subtype;
3452
+ if (subtype !== void 0 && subtype !== "success") {
3453
+ run4.end();
3454
+ continue;
3455
+ }
3456
+ if (liveBackground > 0) {
3457
+ runnerEvent({ holding: "background", live: liveBackground, forSecs: Math.round(backgroundWaitMs / 1e3) });
3458
+ holdOpen(backgroundWaitMs);
3459
+ continue;
3460
+ }
3461
+ unreported();
3241
3462
  }
3463
+ releaseHold();
3242
3464
  return { cancelled, activity };
3243
3465
  }
3244
3466
  function settle(id, lane, r) {
@@ -3250,7 +3472,7 @@ function settle(id, lane, r) {
3250
3472
  else if (r.error)
3251
3473
  appendStatus(id, lane, "failed", r.error.slice(0, 500));
3252
3474
  else if (!lastVerb || !TERMINAL_VERBS.includes(lastVerb))
3253
- appendStatus(id, lane, "done");
3475
+ appendStatus(id, lane, "failed", "stopped without reporting a result");
3254
3476
  }
3255
3477
  var WAITING_VERBS, sleep;
3256
3478
  var init_drive = __esm({
@@ -3272,6 +3494,13 @@ function planStart(input) {
3272
3494
  if (envResume) {
3273
3495
  const own = resolveSessionHarness(id, cfg, lane);
3274
3496
  const harness = (own.sessionId === envResume ? own.harness : harnessFromSessionId(envResume)) ?? resolvedHarness;
3497
+ if (harness === "codex" && codexDesktopThread(envResume)) {
3498
+ return {
3499
+ harness: resolvedHarness,
3500
+ cold: { why: CODEX_DESKTOP_THREAD, fromHarness: harness },
3501
+ note: `restart: ${CODEX_DESKTOP_THREAD} (${short(envResume)}), starting cold on ${resolvedHarness}`
3502
+ };
3503
+ }
3275
3504
  return {
3276
3505
  harness,
3277
3506
  resume: { sessionId: envResume, own: true },
@@ -3308,7 +3537,7 @@ function planStart(input) {
3308
3537
  };
3309
3538
  }
3310
3539
  const requested = descriptor.harness;
3311
- const swap = requested !== void 0 && requested !== origin.harness && requested !== askedHarness(originId, originLane, cfg);
3540
+ const swap = requested !== void 0 && requested !== origin.harness && (descriptor.harnessExplicit === true || descriptor.harnessExplicit === void 0 && requested !== askedHarness(originId, originLane, cfg));
3312
3541
  if (swap) {
3313
3542
  return {
3314
3543
  harness: requested,
@@ -3316,6 +3545,13 @@ function planStart(input) {
3316
3545
  note: `follow-up of ${short(originId)}: swap \u2014 origin session is ${origin.harness}, ${requested} requested; starting cold on ${requested} with a progress note`
3317
3546
  };
3318
3547
  }
3548
+ if (origin.harness === "codex" && codexDesktopThread(origin.sessionId)) {
3549
+ return {
3550
+ harness: resolvedHarness,
3551
+ cold: { why: CODEX_DESKTOP_THREAD, fromHarness: origin.harness, origin: originRef },
3552
+ note: `follow-up of ${short(originId)}: ${CODEX_DESKTOP_THREAD} (${short(origin.sessionId)}), starting cold on ${resolvedHarness} with a progress note`
3553
+ };
3554
+ }
3319
3555
  const ignored = requested !== void 0 && requested !== origin.harness ? ` (--harness ${requested} ignored for the resume)` : "";
3320
3556
  return {
3321
3557
  harness: origin.harness,
@@ -3333,8 +3569,8 @@ var init_plan = __esm({
3333
3569
  });
3334
3570
 
3335
3571
  // packages/runner/dist/run.js
3336
- import * as fs43 from "node:fs";
3337
- import * as path36 from "node:path";
3572
+ import * as fs44 from "node:fs";
3573
+ import * as path37 from "node:path";
3338
3574
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3339
3575
  function coldNote(cold, cwd, trunk) {
3340
3576
  const parts = [worktreeProgress(cwd, trunk)];
@@ -3343,23 +3579,23 @@ function coldNote(cold, cwd, trunk) {
3343
3579
  return handoffNote(cold.fromHarness ?? "unknown", parts.join("\n\n"), cold.why);
3344
3580
  }
3345
3581
  async function main(activeDir, lane, deps = defaultDeps) {
3346
- const id = path36.basename(activeDir);
3582
+ const id = path37.basename(activeDir);
3347
3583
  const status = (verb, note) => appendStatus(id, lane, verb, note);
3348
3584
  const cfg = loadConfig();
3349
- const descriptor = JSON.parse(fs43.readFileSync(path36.join(activeDir, "descriptor.json"), "utf8"));
3585
+ const descriptor = JSON.parse(fs44.readFileSync(path37.join(activeDir, "descriptor.json"), "utf8"));
3350
3586
  const resolved = resolveDispatch(descriptor, cfg);
3351
3587
  const repo = cfg.repos[descriptor.repo];
3352
- const briefFile = path36.join(activeDir, "brief.md");
3353
- if (!fs43.existsSync(briefFile))
3354
- fs43.writeFileSync(briefFile, descriptor.brief);
3355
- const brief = fs43.readFileSync(briefFile, "utf8");
3588
+ const briefFile = path37.join(activeDir, "brief.md");
3589
+ if (!fs44.existsSync(briefFile))
3590
+ fs44.writeFileSync(briefFile, descriptor.brief);
3591
+ const brief = fs44.readFileSync(briefFile, "utf8");
3356
3592
  const attempts = Number(process.env.LOBSTAH_ATTEMPTS ?? "1");
3357
3593
  const runnerInfo = {
3358
3594
  pid: process.pid,
3359
3595
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3360
3596
  attempts
3361
3597
  };
3362
- fs43.writeFileSync(path36.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
3598
+ fs44.writeFileSync(path37.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
3363
3599
  const plan = planStart({
3364
3600
  id,
3365
3601
  lane,
@@ -3368,7 +3604,12 @@ async function main(activeDir, lane, deps = defaultDeps) {
3368
3604
  resolvedHarness: resolved.harness,
3369
3605
  envResume: process.env.LOBSTAH_RESUME
3370
3606
  });
3371
- const firstNote = [attempts > 1 ? `attempt ${attempts}` : void 0, plan.note].filter(Boolean).join("; ");
3607
+ const firstModel = modelForHarness(plan.harness, resolved.model);
3608
+ const firstNote = [
3609
+ attempts > 1 ? `attempt ${attempts}` : void 0,
3610
+ plan.note,
3611
+ firstModel.dropped ? droppedModelNote(plan.harness, firstModel.dropped) : void 0
3612
+ ].filter(Boolean).join("; ");
3372
3613
  status("working", firstNote || void 0);
3373
3614
  appendEvent(id, lane, {
3374
3615
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3377,36 +3618,35 @@ async function main(activeDir, lane, deps = defaultDeps) {
3377
3618
  });
3378
3619
  const own = readEvidence(id, lane).sessionId;
3379
3620
  mergeEvidence(id, lane, { harness: plan.harness, sessionId: plan.resume?.own ? own : void 0 });
3380
- const wtFile = path36.join(activeDir, "worktree.json");
3621
+ const wtFile = path37.join(activeDir, "worktree.json");
3381
3622
  let cwd;
3382
- if (fs43.existsSync(wtFile)) {
3383
- cwd = JSON.parse(fs43.readFileSync(wtFile, "utf8")).path;
3384
- if (!fs43.existsSync(cwd))
3623
+ if (fs44.existsSync(wtFile)) {
3624
+ cwd = JSON.parse(fs44.readFileSync(wtFile, "utf8")).path;
3625
+ if (!fs44.existsSync(cwd))
3385
3626
  throw new Error(`recorded worktree ${cwd} is gone`);
3386
- } else if (fs43.existsSync(worktreePath(id))) {
3627
+ } else if (fs44.existsSync(worktreePath(id))) {
3387
3628
  throw new Error(`unrecorded worktree already exists for ${id} \u2014 refusing to proceed`);
3388
3629
  } else {
3389
3630
  cwd = await deps.allocate(repo, id);
3390
- fs43.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
3631
+ fs44.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
3391
3632
  }
3392
- const adapter = deps.loadAdapter(plan.harness);
3393
3633
  const envNudge = process.env.LOBSTAH_NUDGE;
3394
3634
  const planNudge = plan.cold && !envNudge ? coldNote(plan.cold, cwd, repo.trunk) : void 0;
3395
3635
  const promptWith = (nudge) => buildPrompt(brief, { id, nudge, attachments: descriptor.attachments });
3396
- const binDir = path36.resolve(path36.dirname(fileURLToPath3(import.meta.url)), "..", "..", "..", "bin");
3397
- const workerPath = fs43.existsSync(path36.join(binDir, "lobstah")) ? `${binDir}${path36.delimiter}${process.env.PATH ?? ""}` : void 0;
3636
+ const binDir = path37.resolve(path37.dirname(fileURLToPath3(import.meta.url)), "..", "..", "..", "bin");
3637
+ const workerPath = fs44.existsSync(path37.join(binDir, "lobstah")) ? `${binDir}${path37.delimiter}${process.env.PATH ?? ""}` : void 0;
3398
3638
  let wallClockHit = false;
3399
3639
  let current;
3400
3640
  const wallTimer = resolved.limits.wallClockSecs ? setTimeout(() => {
3401
3641
  wallClockHit = true;
3402
3642
  current?.kill();
3403
3643
  }, resolved.limits.wallClockSecs * 1e3) : void 0;
3404
- const runOnce = async (prompt, resumeSession) => {
3405
- const run4 = await adapter.start({
3644
+ const runOnce = async (harness, prompt, resumeSession) => {
3645
+ const run4 = await deps.loadAdapter(harness).start({
3406
3646
  id,
3407
3647
  cwd,
3408
3648
  prompt,
3409
- model: resolved.model,
3649
+ model: modelForHarness(harness, resolved.model).model,
3410
3650
  effort: resolved.effort,
3411
3651
  limits: resolved.limits,
3412
3652
  env: {
@@ -3418,22 +3658,33 @@ async function main(activeDir, lane, deps = defaultDeps) {
3418
3658
  resumeSession
3419
3659
  });
3420
3660
  current = run4;
3421
- const { cancelled: cancelled2, activity } = await drive(run4, { id, lane, stopped: () => wallClockHit });
3661
+ const { cancelled: cancelled2, activity } = await drive(run4, {
3662
+ id,
3663
+ lane,
3664
+ stopped: () => wallClockHit,
3665
+ backgroundWaitMs: cfg.limits.backgroundWaitSecs * 1e3
3666
+ });
3422
3667
  const result2 = await run4.done;
3423
3668
  return { cancelled: cancelled2, activity, result: result2 };
3424
3669
  };
3425
- let outcome = await runOnce(promptWith(envNudge ?? planNudge), plan.resume?.sessionId);
3670
+ let outcome = await runOnce(plan.harness, promptWith(envNudge ?? planNudge), plan.resume?.sessionId);
3426
3671
  if (plan.resume && !outcome.cancelled && !wallClockHit && outcome.activity === 0 && isUnresumable(outcome.result.error)) {
3427
- const reason = outcome.result.error.replace(/\s+/g, " ").slice(0, 200);
3428
- status("working", `resume-fallback: ${reason} \u2014 starting cold on ${plan.harness} with a progress note`);
3429
- appendEvent(id, lane, { at: (/* @__PURE__ */ new Date()).toISOString(), type: "runner", data: { resumeFallback: reason } });
3430
- mergeEvidence(id, lane, { resumeFallback: reason, sessionId: void 0 });
3672
+ const reason = outcome.result.error.replace(/\s+/g, " ").slice(0, 200).trim();
3673
+ const coldHarness = resolved.harness;
3674
+ const coldModel = modelForHarness(coldHarness, resolved.model);
3675
+ status("working", `resume-fallback: ${reason} \u2014 starting cold on ${coldHarness}` + (coldModel.dropped ? `; ${droppedModelNote(coldHarness, coldModel.dropped)}` : ""));
3676
+ appendEvent(id, lane, {
3677
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3678
+ type: "runner",
3679
+ data: { resumeFallback: reason, harness: coldHarness }
3680
+ });
3681
+ mergeEvidence(id, lane, { resumeFallback: reason, sessionId: void 0, harness: coldHarness });
3431
3682
  const note = coldNote({
3432
3683
  why: `resume of session ${plan.resume.sessionId} failed`,
3433
3684
  fromHarness: plan.harness,
3434
3685
  ...plan.resume.origin ? { origin: plan.resume.origin } : {}
3435
3686
  }, cwd, repo.trunk);
3436
- outcome = await runOnce(promptWith([envNudge, note].filter(Boolean).join("\n\n")), void 0);
3687
+ outcome = await runOnce(coldHarness, promptWith([envNudge, note].filter(Boolean).join("\n\n")), void 0);
3437
3688
  }
3438
3689
  if (wallTimer)
3439
3690
  clearTimeout(wallTimer);
@@ -3472,15 +3723,15 @@ __export(dist_exports, {
3472
3723
  planStart: () => planStart,
3473
3724
  runRunner: () => runRunner
3474
3725
  });
3475
- import * as fs44 from "node:fs";
3476
- import * as path37 from "node:path";
3726
+ import * as fs45 from "node:fs";
3727
+ import * as path38 from "node:path";
3477
3728
  function runRunner(activeDirArg2, laneArg2) {
3478
3729
  const lane = laneArg2 === "chore" ? "chore" : "work";
3479
3730
  main(activeDirArg2, lane).catch((err) => {
3480
- const id = path37.basename(activeDirArg2);
3731
+ const id = path38.basename(activeDirArg2);
3481
3732
  try {
3482
3733
  appendStatus(id, lane, "failed", String(err instanceof Error ? err.message : err).slice(0, 500));
3483
- fs44.renameSync(activeDirArg2, path37.join(laneDirs(lane).done, id));
3734
+ fs45.renameSync(activeDirArg2, path38.join(laneDirs(lane).done, id));
3484
3735
  } catch {
3485
3736
  }
3486
3737
  process.exitCode = 1;
@@ -3502,9 +3753,9 @@ var init_dist5 = __esm({
3502
3753
 
3503
3754
  // apps/cli/src/main.ts
3504
3755
  init_dist2();
3505
- import * as fs45 from "node:fs";
3506
- import * as os10 from "node:os";
3507
- import * as path38 from "node:path";
3756
+ import * as fs46 from "node:fs";
3757
+ import * as os11 from "node:os";
3758
+ import * as path39 from "node:path";
3508
3759
  import { randomUUID as randomUUID4 } from "node:crypto";
3509
3760
  import { spawnSync as spawnSync9 } from "node:child_process";
3510
3761
  import { fileURLToPath as fileURLToPath4 } from "node:url";
@@ -3570,7 +3821,7 @@ function killGroup(pid, signal = "SIGTERM") {
3570
3821
  // packages/supervisor/dist/daemon.js
3571
3822
  init_dist2();
3572
3823
  import * as fs18 from "node:fs";
3573
- import * as os3 from "node:os";
3824
+ import * as os4 from "node:os";
3574
3825
  import * as path14 from "node:path";
3575
3826
  import { spawn as spawn2 } from "node:child_process";
3576
3827
  import { createRequire as createRequire2 } from "node:module";
@@ -3854,7 +4105,7 @@ function reconcileOne(st, cfg, log) {
3854
4105
  }
3855
4106
  function writeHeartbeat(cfg) {
3856
4107
  const payload = {
3857
- machineId: os3.hostname(),
4108
+ machineId: os4.hostname(),
3858
4109
  repos: Object.keys(cfg.repos),
3859
4110
  harnesses: detectHarnesses(),
3860
4111
  maxConcurrent: cfg.limits.maxConcurrent,
@@ -3989,7 +4240,7 @@ init_dist();
3989
4240
  init_dist2();
3990
4241
  import * as fs19 from "node:fs";
3991
4242
  import { execSync } from "node:child_process";
3992
- import * as os4 from "node:os";
4243
+ import * as os5 from "node:os";
3993
4244
 
3994
4245
  // apps/pick/dist/types.js
3995
4246
  var DEFAULT_MERGE_POLICY = {
@@ -4029,7 +4280,7 @@ function resolveTokenSource(section, label) {
4029
4280
  }
4030
4281
  if (file) {
4031
4282
  return () => {
4032
- const token = fs19.readFileSync(file.startsWith("~/") ? `${os4.homedir()}/${file.slice(2)}` : file, "utf8").trim();
4283
+ const token = fs19.readFileSync(file.startsWith("~/") ? `${os5.homedir()}/${file.slice(2)}` : file, "utf8").trim();
4033
4284
  if (!token)
4034
4285
  throw new Error(`${label}: tokenFile ${file} is empty`);
4035
4286
  return token;
@@ -4231,6 +4482,9 @@ var GithubSource = class {
4231
4482
  this.cfg = cfg;
4232
4483
  this.name = `gh:${cfg.repo}`;
4233
4484
  }
4485
+ owns(key) {
4486
+ return key.startsWith(`${this.name}#`);
4487
+ }
4234
4488
  async api(method, url, body) {
4235
4489
  const res = await fetch(`${API}${url}`, {
4236
4490
  method,
@@ -4463,6 +4717,9 @@ var LinearSource = class {
4463
4717
  constructor(cfg) {
4464
4718
  this.cfg = cfg;
4465
4719
  }
4720
+ owns(key) {
4721
+ return key.startsWith("linear:");
4722
+ }
4466
4723
  async gql(query, variables = {}) {
4467
4724
  const res = await fetch(API2, {
4468
4725
  method: "POST",
@@ -4610,42 +4867,51 @@ function dispatchLane(uuid) {
4610
4867
  async function reportLoop(source, state, log = () => {
4611
4868
  }, notify = () => {
4612
4869
  }) {
4870
+ const failures = [];
4613
4871
  for (const [key, entry] of state.entries()) {
4614
- if (entry.released)
4872
+ if (!source.owns(key))
4615
4873
  continue;
4616
- const lane = dispatchLane(entry.uuid);
4617
- if (!lane)
4874
+ if (entry.released)
4618
4875
  continue;
4619
- const verb = reconcile({
4620
- log: readStatusLog(entry.uuid, lane),
4621
- lastEventAt: lastEventAt(entry.uuid, lane)
4622
- });
4623
- if (verb !== "unknown" && verb !== entry.lastReported) {
4624
- const evidence = readEvidence(entry.uuid, lane);
4625
- await source.report(key, verb, { ...evidence, uuid: entry.uuid });
4626
- state.update(key, { lastReported: verb });
4627
- log(`${key}: reported ${verb}`);
4628
- notify({
4629
- key,
4630
- uuid: entry.uuid,
4631
- verb,
4632
- note: readStatusLog(entry.uuid, lane).at(-1)?.note,
4633
- prUrl: evidence.prUrl
4876
+ try {
4877
+ const lane = dispatchLane(entry.uuid);
4878
+ if (!lane)
4879
+ continue;
4880
+ const verb = reconcile({
4881
+ log: readStatusLog(entry.uuid, lane),
4882
+ lastEventAt: lastEventAt(entry.uuid, lane)
4634
4883
  });
4635
- }
4636
- if (entry.kind === "issue" && verb === "failed" && fs22.existsSync(path17.join(laneDirs(lane).done, entry.uuid))) {
4637
- state.releaseIssue(key);
4638
- log(`${key}: finalized failure released for a bounded retry`);
4639
- continue;
4640
- }
4641
- const msgs = await source.inbound(key, entry.lastInboundAt);
4642
- if (msgs.length > 0) {
4643
- for (const m of msgs)
4644
- sendMessage(entry.uuid, lane, m, `tracker:${source.name.startsWith("gh:") ? "github" : source.name}`);
4645
- state.update(key, { lastInboundAt: (/* @__PURE__ */ new Date()).toISOString() });
4646
- log(`${key}: forwarded ${msgs.length} comment(s) to inbox`);
4884
+ if (verb !== "unknown" && verb !== entry.lastReported) {
4885
+ const evidence = readEvidence(entry.uuid, lane);
4886
+ await source.report(key, verb, { ...evidence, uuid: entry.uuid });
4887
+ state.update(key, { lastReported: verb });
4888
+ log(`${key}: reported ${verb}`);
4889
+ notify({
4890
+ key,
4891
+ uuid: entry.uuid,
4892
+ verb,
4893
+ note: readStatusLog(entry.uuid, lane).at(-1)?.note,
4894
+ prUrl: evidence.prUrl
4895
+ });
4896
+ }
4897
+ if (entry.kind === "issue" && verb === "failed" && fs22.existsSync(path17.join(laneDirs(lane).done, entry.uuid))) {
4898
+ state.releaseIssue(key);
4899
+ log(`${key}: finalized failure released for a bounded retry`);
4900
+ continue;
4901
+ }
4902
+ const msgs = await source.inbound(key, entry.lastInboundAt);
4903
+ if (msgs.length > 0) {
4904
+ for (const m of msgs)
4905
+ sendMessage(entry.uuid, lane, m, `tracker:${source.name.startsWith("gh:") ? "github" : source.name}`);
4906
+ state.update(key, { lastInboundAt: (/* @__PURE__ */ new Date()).toISOString() });
4907
+ log(`${key}: forwarded ${msgs.length} comment(s) to inbox`);
4908
+ }
4909
+ } catch (err) {
4910
+ failures.push(`${key}: ${err instanceof Error ? err.message : String(err)}`);
4647
4911
  }
4648
4912
  }
4913
+ if (failures.length > 0)
4914
+ throw new Error(failures.join("; "));
4649
4915
  }
4650
4916
 
4651
4917
  // apps/pick/dist/loops/dispatch.js
@@ -5117,12 +5383,16 @@ function syncStreams(children, flight, log, notify) {
5117
5383
  }
5118
5384
  async function cycle(sources, merges, state, pollSecs2, log, notify) {
5119
5385
  for (const source of sources) {
5120
- try {
5121
- await dispatchLoop(source, state, log);
5122
- await reportLoop(source, state, log, notify);
5123
- await reconcileLoop(source, state, log);
5124
- } catch (err) {
5125
- log(`${source.name}: ${err instanceof Error ? err.message : String(err)}`);
5386
+ for (const loop of [
5387
+ () => dispatchLoop(source, state, log),
5388
+ () => reportLoop(source, state, log, notify),
5389
+ () => reconcileLoop(source, state, log)
5390
+ ]) {
5391
+ try {
5392
+ await loop();
5393
+ } catch (err) {
5394
+ log(`${source.name}: ${err instanceof Error ? err.message : String(err)}`);
5395
+ }
5126
5396
  }
5127
5397
  }
5128
5398
  for (const { source, policy } of merges) {
@@ -5690,18 +5960,7 @@ function onTheHook(kind, chain, owners) {
5690
5960
  (m) => m.bucket !== "done" && (owners.reviewRounds.has(m.id) || m.id === owners.watchFollowUp)
5691
5961
  )?.id;
5692
5962
  }
5693
- function prKinds(pr) {
5694
- if (pr.state !== "OPEN") return [];
5695
- const out = [];
5696
- const { failed, pending, total } = pr.checks;
5697
- if (pr.draft) out.push("pr:draft");
5698
- const review = (pr.review?.unresolvedThreads ?? 0) > 0 || pr.review?.changesRequested === true;
5699
- if (review) out.push("pr:review");
5700
- if (failed > 0) out.push("pr:checks");
5701
- if (isConflicting(pr.mergeStateStatus)) out.push("pr:conflict");
5702
- if (!pr.draft && !review && isMergeable(pr.mergeStateStatus) && (pr.reviewDecision === "APPROVED" || total > 0 && failed === 0 && pending === 0)) out.push("pr:ready");
5703
- return out;
5704
- }
5963
+ var prKinds = prStandingKinds;
5705
5964
  function readyBlockedByStack(pr, tracked) {
5706
5965
  if (pr.state !== "OPEN" || !pr.baseRefName) return false;
5707
5966
  const ref = parsePrRef(pr.url);
@@ -5778,9 +6037,9 @@ function prAttention(now2, observed = observedPrs()) {
5778
6037
  const ref = parsePrRef(pr.url);
5779
6038
  const watchFollowUp = ref ? readWatch(ref.key)?.lastFollowUpId : void 0;
5780
6039
  const chain = dispatch ? chainOf(id) : [];
5781
- const since = (dispatch ? readStatusLog(id, lane).at(-1)?.at : void 0) ?? pr.observedAt;
5782
6040
  for (const kind of kinds) {
5783
6041
  if (onTheHook(kind, chain, { reviewRounds, watchFollowUp })) continue;
6042
+ const standingSince = pr.standingSince?.[kind] ?? pr.observedAt;
5784
6043
  out.push({
5785
6044
  kind,
5786
6045
  key: ref?.key ?? pr.url,
@@ -5788,8 +6047,9 @@ function prAttention(now2, observed = observedPrs()) {
5788
6047
  id,
5789
6048
  lane,
5790
6049
  verb: kind,
5791
- ageSecs: Math.max(0, Math.round((now2 - Date.parse(since)) / 1e3)),
5792
- at: since,
6050
+ ageSecs: Math.max(0, Math.round((now2 - Date.parse(standingSince)) / 1e3)),
6051
+ at: standingSince,
6052
+ standingSince,
5793
6053
  note: PR_KIND_NOTE[kind](pr),
5794
6054
  repo: dispatch ? repoOf(id, lane) : ref ? `${ref.owner}/${ref.repo}` : void 0,
5795
6055
  prUrl: pr.url,
@@ -5804,8 +6064,7 @@ function prAttention(now2, observed = observedPrs()) {
5804
6064
  });
5805
6065
  }
5806
6066
  }
5807
- const updated = new Map(observed.map(({ pr }) => [pr.url, prSortAt(pr)]));
5808
- return out.sort((a, b) => (updated.get(b.prUrl ?? "") ?? "").localeCompare(updated.get(a.prUrl ?? "") ?? ""));
6067
+ return out;
5809
6068
  }
5810
6069
  function landedAttention(cfg, now2) {
5811
6070
  const out = [];
@@ -5826,6 +6085,7 @@ function landedAttention(cfg, now2) {
5826
6085
  verb: last.verb,
5827
6086
  ageSecs: Math.max(0, Math.round((now2 - at) / 1e3)),
5828
6087
  at: last.at,
6088
+ standingSince: last.at,
5829
6089
  note: last.note,
5830
6090
  repo,
5831
6091
  ...ev.prUrl ? { prUrl: ev.prUrl } : {}
@@ -5892,7 +6152,8 @@ function doneIds(lane) {
5892
6152
  function describeDispatch(id, lane, bucket) {
5893
6153
  const log = readStatusLog(id, lane);
5894
6154
  const last = log.at(-1);
5895
- const state = bucket === "queued" ? "queued" : reconcile({ log, lastEventAt: lastEventAt(id, lane) });
6155
+ const claimedAt = bucket === "active" ? readSessionClaim(id, lane)?.at : void 0;
6156
+ const state = bucket === "queued" ? "queued" : displayState({ log, lastEventAt: lastEventAt(id, lane), queued: false, claimedAt });
5896
6157
  const evidence = readEvidence(id, lane);
5897
6158
  const answered = last && (last.verb === "needs-decision" || last.verb === "blocked") ? answeredAt(id, lane, last.at) : void 0;
5898
6159
  return {
@@ -5901,7 +6162,8 @@ function describeDispatch(id, lane, bucket) {
5901
6162
  bucket,
5902
6163
  state,
5903
6164
  note: last?.note,
5904
- at: last?.at,
6165
+ // Queued work has no log yet; its time is when it entered the queue.
6166
+ at: last?.at ?? (bucket === "queued" ? queuedAt(id, lane) : claimedAt),
5905
6167
  ...answered ? { answeredAt: answered } : {},
5906
6168
  prUrl: evidence.prUrl,
5907
6169
  pr: evidence.pr
@@ -5963,6 +6225,7 @@ function buildTendReport(now2 = Date.now()) {
5963
6225
  verb: last.verb,
5964
6226
  ageSecs: Math.max(0, Math.round((now2 - Date.parse(last.at)) / 1e3)),
5965
6227
  at: last.at,
6228
+ standingSince: last.at,
5966
6229
  note: last.note
5967
6230
  });
5968
6231
  }
@@ -5998,6 +6261,7 @@ function buildTendReport(now2 = Date.now()) {
5998
6261
  verb: "watch",
5999
6262
  ageSecs: oldest?.at ? Math.max(0, Math.round((now2 - Date.parse(oldest.at)) / 1e3)) : 0,
6000
6263
  at: oldest?.at,
6264
+ standingSince: oldest?.at,
6001
6265
  note: pending.at(-1)?.summary
6002
6266
  });
6003
6267
  }
@@ -6074,6 +6338,7 @@ function buildTendReport(now2 = Date.now()) {
6074
6338
  return ack ? { ...a, acked: { at: ack.at, by: ack.by } } : a;
6075
6339
  })
6076
6340
  );
6341
+ attention.sort((a, b) => (a.standingSince ?? a.at ?? "").localeCompare(b.standingSince ?? b.at ?? "") || a.key.localeCompare(b.key));
6077
6342
  const verdict = !daemonUp ? "daemon-down" : stalled ? "stalled" : attention.some((a) => a.kind === "question" || a.kind === "watch") ? "needs-attention" : active.length + queued.length > 0 ? "working" : "idle";
6078
6343
  const traps = listTraps().map((r) => {
6079
6344
  const hbAgeSecs = Math.max(0, Math.round((now2 - (Date.parse(r.heartbeatAt) || 0)) / 1e3));
@@ -6351,7 +6616,7 @@ function charter(g) {
6351
6616
  const repos = g.repos.length > 0 ? g.repos.join(", ") : "(no repos configured)";
6352
6617
  return `the helm charter \u2014 grounds "${g.name}" (${repos})
6353
6618
 
6354
- You hold the helm. You are the lobsterman for these grounds.
6619
+ You hold the helm. You are the lobstah man for these grounds.
6355
6620
 
6356
6621
  Role:
6357
6622
  - Triage incoming work. Dispatch it. Review each catch. Decide requeue or cancel.
@@ -6420,6 +6685,205 @@ function readHookStdin() {
6420
6685
  }
6421
6686
  }
6422
6687
 
6688
+ // apps/cli/src/glass-lifecycle.ts
6689
+ init_dist2();
6690
+ import * as fs33 from "node:fs";
6691
+ import * as http from "node:http";
6692
+ import * as path26 from "node:path";
6693
+ import { spawn as spawn4 } from "node:child_process";
6694
+ var glassUrl = (port) => `http://127.0.0.1:${port}`;
6695
+ var stateFile = () => path26.join(lobstahHome(), "state", "glass.json");
6696
+ function glassPort() {
6697
+ const value = Number(process.env.LOBSTAH_GLASS_PORT ?? loadConfig().glass.port);
6698
+ if (!Number.isInteger(value) || value < 1 || value > 65535) throw new Error("glass port must be an integer from 1 to 65535");
6699
+ return value;
6700
+ }
6701
+ function readGlassState() {
6702
+ const file = stateFile();
6703
+ let state;
6704
+ try {
6705
+ state = JSON.parse(fs33.readFileSync(file, "utf8"));
6706
+ } catch {
6707
+ if (fs33.existsSync(file)) fs33.unlinkSync(file);
6708
+ return void 0;
6709
+ }
6710
+ if (!Number.isInteger(state.pid) || state.pid <= 0 || !Number.isInteger(state.port) || !pidAlive2(state.pid)) {
6711
+ fs33.unlinkSync(file);
6712
+ return void 0;
6713
+ }
6714
+ return state;
6715
+ }
6716
+ function pidAlive2(pid) {
6717
+ try {
6718
+ process.kill(pid, 0);
6719
+ return true;
6720
+ } catch (err) {
6721
+ return err.code === "EPERM";
6722
+ }
6723
+ }
6724
+ function probeVersionEndpoint(port, timeoutMs) {
6725
+ return new Promise((resolve6) => {
6726
+ let settled = false;
6727
+ const finish = (info) => {
6728
+ if (settled) return;
6729
+ settled = true;
6730
+ resolve6(info);
6731
+ };
6732
+ const req = http.get(`${glassUrl(port)}/api/version`, { timeout: timeoutMs }, (res) => {
6733
+ if (res.statusCode !== 200) {
6734
+ res.resume();
6735
+ finish();
6736
+ return;
6737
+ }
6738
+ let body = "";
6739
+ res.setEncoding("utf8");
6740
+ res.on("data", (chunk) => {
6741
+ body += chunk;
6742
+ if (body.length > 1024) {
6743
+ finish();
6744
+ req.destroy();
6745
+ }
6746
+ });
6747
+ res.on("close", () => finish());
6748
+ res.on("end", () => {
6749
+ try {
6750
+ const info = JSON.parse(body);
6751
+ finish(info.service === "lobstah-glass" && Number.isInteger(info.pid) && typeof info.version === "string" ? info : void 0);
6752
+ } catch {
6753
+ finish();
6754
+ }
6755
+ });
6756
+ });
6757
+ req.on("timeout", () => req.destroy());
6758
+ req.on("error", () => finish());
6759
+ });
6760
+ }
6761
+ function probeLegacyGlass(port, timeoutMs) {
6762
+ return new Promise((resolve6) => {
6763
+ let settled = false;
6764
+ const finish = (info) => {
6765
+ if (settled) return;
6766
+ settled = true;
6767
+ resolve6(info);
6768
+ };
6769
+ const req = http.get(`${glassUrl(port)}/data`, { timeout: timeoutMs }, (res) => {
6770
+ if (res.statusCode !== 200) {
6771
+ res.resume();
6772
+ finish();
6773
+ return;
6774
+ }
6775
+ let body = "";
6776
+ res.setEncoding("utf8");
6777
+ res.on("data", (chunk) => {
6778
+ body += chunk;
6779
+ if (body.length > 1e6) {
6780
+ finish();
6781
+ req.destroy();
6782
+ }
6783
+ });
6784
+ res.on("close", () => finish());
6785
+ res.on("end", () => {
6786
+ try {
6787
+ const data = JSON.parse(body);
6788
+ finish(
6789
+ typeof data.version === "string" && Array.isArray(data.dispatches) && Array.isArray(data.helms) ? { service: "lobstah-glass", version: data.version } : void 0
6790
+ );
6791
+ } catch {
6792
+ finish();
6793
+ }
6794
+ });
6795
+ });
6796
+ req.on("timeout", () => req.destroy());
6797
+ req.on("error", () => finish());
6798
+ });
6799
+ }
6800
+ async function probeGlass(port, timeoutMs = 400) {
6801
+ return await probeVersionEndpoint(port, timeoutMs) ?? await probeLegacyGlass(port, timeoutMs);
6802
+ }
6803
+ function glassLines(port, info, already = false) {
6804
+ return {
6805
+ glass: glassUrl(port),
6806
+ ...already ? { already: "running" } : {},
6807
+ ...info.version !== lobstahVersion() ? { stale: `${info.version} (run lobstah glass stop && lobstah glass --detach)` } : {}
6808
+ };
6809
+ }
6810
+ async function startDetachedGlass(port) {
6811
+ const tracked = readGlassState();
6812
+ if (tracked && tracked.port !== port) {
6813
+ const owner = await probeGlass(tracked.port);
6814
+ if (owner?.pid === tracked.pid) {
6815
+ throw new Error(`detached glass is running on ${glassUrl(tracked.port)} \u2014 run lobstah glass stop before changing ports`);
6816
+ }
6817
+ fs33.unlinkSync(stateFile());
6818
+ }
6819
+ const existing = await probeGlass(port);
6820
+ if (existing) return { info: existing, already: true };
6821
+ const home = lobstahHome();
6822
+ const logDir = path26.join(home, "logs");
6823
+ fs33.mkdirSync(logDir, { recursive: true });
6824
+ const fd = fs33.openSync(path26.join(logDir, "glass.log"), "a");
6825
+ let child;
6826
+ try {
6827
+ child = spawn4(process.execPath, [...COMPILED_BINARY ? [] : [fs33.realpathSync(process.argv[1])], "glass", "--port", String(port)], {
6828
+ detached: true,
6829
+ stdio: ["ignore", fd, fd],
6830
+ env: process.env,
6831
+ windowsHide: true
6832
+ });
6833
+ } finally {
6834
+ fs33.closeSync(fd);
6835
+ }
6836
+ let childError;
6837
+ child.on("error", (error) => {
6838
+ childError = error;
6839
+ });
6840
+ child.unref();
6841
+ const deadline = Date.now() + 5e3;
6842
+ while (Date.now() < deadline) {
6843
+ if (childError) throw childError;
6844
+ const info = await probeGlass(port);
6845
+ if (info) {
6846
+ if (info.pid === child.pid) {
6847
+ const state = { pid: info.pid, port, startedAt: (/* @__PURE__ */ new Date()).toISOString(), version: info.version };
6848
+ fs33.mkdirSync(path26.dirname(stateFile()), { recursive: true });
6849
+ fs33.writeFileSync(stateFile(), JSON.stringify(state, null, 2) + "\n");
6850
+ return { info, already: false };
6851
+ }
6852
+ return { info, already: true };
6853
+ }
6854
+ if (child.exitCode !== null) break;
6855
+ await new Promise((resolve6) => setTimeout(resolve6, 100));
6856
+ }
6857
+ throw new Error(`glass did not answer on ${glassUrl(port)} within 5 seconds (see ${path26.join(logDir, "glass.log")})`);
6858
+ }
6859
+ async function stopGlass() {
6860
+ const state = readGlassState();
6861
+ if (!state) return { stopped: false };
6862
+ const info = await probeGlass(state.port);
6863
+ if (!info || info.pid !== state.pid) {
6864
+ fs33.unlinkSync(stateFile());
6865
+ return { stopped: false };
6866
+ }
6867
+ process.kill(state.pid, "SIGTERM");
6868
+ const deadline = Date.now() + 5e3;
6869
+ while (Date.now() < deadline && await probeGlass(state.port)) {
6870
+ await new Promise((resolve6) => setTimeout(resolve6, 100));
6871
+ }
6872
+ if (await probeGlass(state.port)) throw new Error(`glass pid ${state.pid} did not stop`);
6873
+ fs33.unlinkSync(stateFile());
6874
+ return { stopped: true, port: state.port, pid: state.pid };
6875
+ }
6876
+ async function glassStatus(port = glassPort()) {
6877
+ let state = readGlassState();
6878
+ const selectedPort = state?.port ?? port;
6879
+ const info = await probeGlass(selectedPort);
6880
+ if (state && info?.pid !== state.pid) {
6881
+ fs33.unlinkSync(stateFile());
6882
+ state = void 0;
6883
+ }
6884
+ return { port: state || info ? selectedPort : port, info, state };
6885
+ }
6886
+
6423
6887
  // apps/cli/src/brief.ts
6424
6888
  var SOAK_WHERE = "(from a linked worktree, never the primary checkout)";
6425
6889
  function signOnHelp(sessionId, opts = {}) {
@@ -6443,8 +6907,9 @@ function sessionGrounds(cwd) {
6443
6907
  if (owned) return { grounds: owned };
6444
6908
  return { placeholder: `<${all.map((g) => g.name).join("|")}>` };
6445
6909
  }
6446
- function buildBriefContext(sessionId, cwd) {
6910
+ async function buildBriefContext(sessionId, cwd) {
6447
6911
  let fleet = "";
6912
+ let glass = "";
6448
6913
  try {
6449
6914
  const r = buildTendReport();
6450
6915
  const waiting = r.attention.filter((a) => a.kind === "question" || a.kind === "watch").length;
@@ -6452,6 +6917,12 @@ function buildBriefContext(sessionId, cwd) {
6452
6917
  fleet = ` Fleet: ${r.verdict} (${r.counts.queued} queued, ${r.counts.active} active` + (waiting > 0 ? `, ${waiting} awaiting a human` : "") + (toLook > 0 ? `, ${toLook} to look at` : "") + ") \u2014 `lobstah` for the live view.";
6453
6918
  } catch {
6454
6919
  }
6920
+ try {
6921
+ const port = glassPort();
6922
+ if (await probeGlass(port)) glass = ` Glass: ${glassUrl(port)}.`;
6923
+ } catch {
6924
+ }
6925
+ fleet += glass;
6455
6926
  const helmReg = helmOf(sessionId);
6456
6927
  if (helmReg) {
6457
6928
  heartbeatHelm(helmReg.sessionId);
@@ -6478,18 +6949,18 @@ ${help}`;
6478
6949
 
6479
6950
  // apps/cli/src/cull.ts
6480
6951
  init_dist2();
6481
- import * as fs33 from "node:fs";
6482
- import * as path26 from "node:path";
6952
+ import * as fs34 from "node:fs";
6953
+ import * as path27 from "node:path";
6483
6954
  import { execFileSync as execFileSync3 } from "node:child_process";
6484
6955
  var DAY = 864e5;
6485
6956
  function bytesAt(target) {
6486
- const stat = fs33.lstatSync(target);
6487
- if (stat.isDirectory()) return fs33.readdirSync(target).reduce((sum, name) => sum + bytesAt(path26.join(target, name)), 0);
6957
+ const stat = fs34.lstatSync(target);
6958
+ if (stat.isDirectory()) return fs34.readdirSync(target).reduce((sum, name) => sum + bytesAt(path27.join(target, name)), 0);
6488
6959
  return stat.isFile() ? stat.size : 0;
6489
6960
  }
6490
6961
  function idsIn(dir) {
6491
6962
  try {
6492
- return new Set(fs33.readdirSync(dir).filter((f) => !f.startsWith(".")).map((f) => f.replace(/\.json$/, "")));
6963
+ return new Set(fs34.readdirSync(dir).filter((f) => !f.startsWith(".")).map((f) => f.replace(/\.json$/, "")));
6493
6964
  } catch {
6494
6965
  return /* @__PURE__ */ new Set();
6495
6966
  }
@@ -6502,7 +6973,7 @@ function planCull(olderThanDays, now2 = Date.now()) {
6502
6973
  const referencedAttachmentState = /* @__PURE__ */ new Set();
6503
6974
  const retainReferences = (id, lane) => {
6504
6975
  for (const attachment of storedDescriptor(id, lane)?.attachments ?? []) {
6505
- referencedAttachmentState.add(path26.dirname(path26.dirname(attachment.path)));
6976
+ referencedAttachmentState.add(path27.dirname(path27.dirname(attachment.path)));
6506
6977
  }
6507
6978
  };
6508
6979
  for (const lane of ["work", "chore"]) {
@@ -6516,41 +6987,41 @@ function planCull(olderThanDays, now2 = Date.now()) {
6516
6987
  retainReferences(id, lane);
6517
6988
  }
6518
6989
  for (const id of idsIn(d.done)) {
6519
- const p = path26.join(d.done, id);
6520
- const m = fs33.statSync(p).mtimeMs;
6990
+ const p = path27.join(d.done, id);
6991
+ const m = fs34.statSync(p).mtimeMs;
6521
6992
  doneMtimes.set(id, m);
6522
6993
  if (m >= cutoff) retainReferences(id, lane);
6523
6994
  if (m < cutoff) items.push({ kind: "done", id, target: p, ageDays: Math.floor((now2 - m) / DAY), bytes: bytesAt(p) });
6524
6995
  }
6525
6996
  }
6526
- const wtRoot = path26.join(lobstahHome(), "worktrees");
6997
+ const wtRoot = path27.join(lobstahHome(), "worktrees");
6527
6998
  for (const id of idsIn(wtRoot)) {
6528
6999
  if (live.has(id)) continue;
6529
7000
  const doneAt = doneMtimes.get(id);
6530
7001
  if (doneAt !== void 0 && doneAt >= cutoff) continue;
6531
- const p = path26.join(wtRoot, id);
6532
- items.push({ kind: "worktree", id, target: p, ageDays: Math.floor((now2 - fs33.statSync(p).mtimeMs) / DAY), bytes: bytesAt(p) });
7002
+ const p = path27.join(wtRoot, id);
7003
+ items.push({ kind: "worktree", id, target: p, ageDays: Math.floor((now2 - fs34.statSync(p).mtimeMs) / DAY), bytes: bytesAt(p) });
6533
7004
  }
6534
7005
  for (const lane of ["work", "chore"]) {
6535
7006
  const d = laneDirs(lane);
6536
7007
  const groups = /* @__PURE__ */ new Map();
6537
- for (const f of fs33.readdirSync(d.state)) {
6538
- const target = path26.join(d.state, f);
6539
- const directory = fs33.statSync(target).isDirectory();
6540
- if (directory && !fs33.existsSync(path26.join(target, "attachments"))) continue;
7008
+ for (const f of fs34.readdirSync(d.state)) {
7009
+ const target = path27.join(d.state, f);
7010
+ const directory = fs34.statSync(target).isDirectory();
7011
+ if (directory && !fs34.existsSync(path27.join(target, "attachments"))) continue;
6541
7012
  const id = directory ? f : f.replace(/\.(status|events|evidence|attn|notified|runner\.log)$/, "");
6542
7013
  if (id === f && !directory) continue;
6543
- if (live.has(id) || (doneMtimes.get(id) ?? 0) >= cutoff || referencedAttachmentState.has(path26.join(d.state, id))) continue;
7014
+ if (live.has(id) || (doneMtimes.get(id) ?? 0) >= cutoff || referencedAttachmentState.has(path27.join(d.state, id))) continue;
6544
7015
  const previous = groups.get(id);
6545
7016
  groups.set(id, {
6546
- mtime: Math.max(previous?.mtime ?? 0, fs33.statSync(target).mtimeMs),
7017
+ mtime: Math.max(previous?.mtime ?? 0, fs34.statSync(target).mtimeMs),
6547
7018
  bytes: (previous?.bytes ?? 0) + bytesAt(target)
6548
7019
  });
6549
7020
  }
6550
7021
  for (const [id, group] of groups) {
6551
7022
  const ageFrom = doneMtimes.get(id) ?? group.mtime;
6552
7023
  if (ageFrom >= cutoff) continue;
6553
- items.push({ kind: "state", id, target: path26.join(d.state, `${id}.*`), ageDays: Math.floor((now2 - ageFrom) / DAY), bytes: group.bytes });
7024
+ items.push({ kind: "state", id, target: path27.join(d.state, `${id}.*`), ageDays: Math.floor((now2 - ageFrom) / DAY), bytes: group.bytes });
6554
7025
  }
6555
7026
  }
6556
7027
  for (const r of readPrs()) {
@@ -6567,9 +7038,9 @@ function planCull(olderThanDays, now2 = Date.now()) {
6567
7038
  }
6568
7039
  function removeWorktree(id, dir) {
6569
7040
  for (const lane of ["work", "chore"]) {
6570
- const descFile = path26.join(laneDirs(lane).done, id, "descriptor.json");
7041
+ const descFile = path27.join(laneDirs(lane).done, id, "descriptor.json");
6571
7042
  try {
6572
- const desc = JSON.parse(fs33.readFileSync(descFile, "utf8"));
7043
+ const desc = JSON.parse(fs34.readFileSync(descFile, "utf8"));
6573
7044
  const repo = loadConfig().repos[desc.repo];
6574
7045
  if (repo) {
6575
7046
  execFileSync3("git", ["worktree", "remove", "--force", dir], { cwd: repo.path, stdio: "ignore" });
@@ -6578,25 +7049,25 @@ function removeWorktree(id, dir) {
6578
7049
  } catch {
6579
7050
  }
6580
7051
  }
6581
- fs33.rmSync(dir, { recursive: true, force: true });
7052
+ fs34.rmSync(dir, { recursive: true, force: true });
6582
7053
  }
6583
7054
  function applyCull(items) {
6584
7055
  for (const item of items.filter((i) => i.kind === "worktree")) removeWorktree(item.id, item.target);
6585
- for (const item of items.filter((i) => i.kind === "done")) fs33.rmSync(item.target, { recursive: true, force: true });
7056
+ for (const item of items.filter((i) => i.kind === "done")) fs34.rmSync(item.target, { recursive: true, force: true });
6586
7057
  for (const item of items.filter((i) => i.kind === "ack")) removeAck(item.target);
6587
7058
  for (const item of items.filter((i) => i.kind === "pr")) removePr(item.target);
6588
7059
  for (const item of items.filter((i) => i.kind === "state")) {
6589
- const dir = path26.dirname(item.target);
6590
- for (const f of fs33.readdirSync(dir)) {
6591
- if (f === item.id || f.startsWith(`${item.id}.`)) fs33.rmSync(path26.join(dir, f), { recursive: true, force: true });
7060
+ const dir = path27.dirname(item.target);
7061
+ for (const f of fs34.readdirSync(dir)) {
7062
+ if (f === item.id || f.startsWith(`${item.id}.`)) fs34.rmSync(path27.join(dir, f), { recursive: true, force: true });
6592
7063
  }
6593
7064
  }
6594
7065
  }
6595
7066
 
6596
7067
  // apps/cli/src/manual.ts
6597
- var MANUAL = `the lobsterman's manual
7068
+ var MANUAL = `the lobstah man's manual
6598
7069
 
6599
- You are the lobsterman: one interactive session that sets traps (dispatches),
7070
+ You are the lobstah man: one interactive session that sets traps (dispatches),
6600
7071
  reads buoys (status), and hauls when something needs you. The boat (daemon)
6601
7072
  does the supervision \u2014 you never watch a trap work.
6602
7073
 
@@ -6614,11 +7085,14 @@ your working set:
6614
7085
  lobstah man tend [--json] tend the string: fleet verdict,
6615
7086
  unanswered questions, each work
6616
7087
  item's chain + PR + merge gate
6617
- lobstah glass [--port <n>] the spyglass: tend as a live
7088
+ lobstah glass [--port <n>] [--detach] the spyglass: tend as a live
6618
7089
  localhost page \u2014 traps with
6619
7090
  their lifecycle and mail,
6620
7091
  briefs, logs, notices, merge
6621
7092
  view; consumes no cursor
7093
+ lobstah glass stop | status | install | uninstall
7094
+ manage the detached glass or
7095
+ its user service
6622
7096
  lobstah watch add <key> --check <cmd> stand watch on something
6623
7097
  external (a review session, a
6624
7098
  CI run) \u2014 its events wake you
@@ -6649,7 +7123,7 @@ getting woken instead of asking:
6649
7123
  still-waiting, verdict. A man wait timeout carries
6650
7124
  it too, so a wait loop doubles as the periodic
6651
7125
  fleet report
6652
- lobstah man helm take the helm: sign on as the one lobsterman for
7126
+ lobstah man helm take the helm: sign on as the one lobstah man for
6653
7127
  your grounds. The charter prints (and re-injects
6654
7128
  at every session start) and the Stop hook applies.
6655
7129
  \`man relieve\` steps down
@@ -6662,27 +7136,27 @@ paste into your liaison instructions:
6662
7136
  immediately, then \`lobstah send\` the answer. done means brief fulfilled \u2014
6663
7137
  report the catch (\`lobstah catch <id>\`) and never merge anything yourself.
6664
7138
 
6665
- full pattern and trade-offs: docs/lobsterman.md \xB7 config: docs/configuration.md`;
7139
+ full pattern and trade-offs: docs/man.md \xB7 config: docs/configuration.md`;
6666
7140
 
6667
7141
  // apps/cli/src/doctor.ts
6668
7142
  init_dist();
6669
7143
  init_dist2();
6670
- import * as fs35 from "node:fs";
6671
- import * as path28 from "node:path";
6672
- import { spawnSync as spawnSync5 } from "node:child_process";
7144
+ import * as fs37 from "node:fs";
7145
+ import * as path30 from "node:path";
7146
+ import { spawnSync as spawnSync6 } from "node:child_process";
6673
7147
 
6674
7148
  // apps/cli/src/plugin-version.ts
6675
7149
  init_dist();
6676
- import * as fs34 from "node:fs";
6677
- import * as os5 from "node:os";
6678
- import * as path27 from "node:path";
7150
+ import * as fs35 from "node:fs";
7151
+ import * as os6 from "node:os";
7152
+ import * as path28 from "node:path";
6679
7153
  var UPDATE_COMMAND = {
6680
7154
  claude: "/plugin update lobstah@lobstah",
6681
7155
  codex: "codex plugin marketplace upgrade lobstah && codex plugin add lobstah@lobstah"
6682
7156
  };
6683
7157
  var readJson2 = (file) => {
6684
7158
  try {
6685
- return JSON.parse(fs34.readFileSync(file, "utf8"));
7159
+ return JSON.parse(fs35.readFileSync(file, "utf8"));
6686
7160
  } catch {
6687
7161
  return void 0;
6688
7162
  }
@@ -6693,19 +7167,19 @@ var manifestVersion = (file) => {
6693
7167
  };
6694
7168
  function installedClaudePlugin(opts = {}) {
6695
7169
  const env = opts.env ?? process.env;
6696
- const home = opts.home ?? os5.homedir();
7170
+ const home = opts.home ?? os6.homedir();
6697
7171
  const root = env.CLAUDE_PLUGIN_ROOT;
6698
7172
  if (root) {
6699
- const version2 = manifestVersion(path27.join(root, ".claude-plugin", "plugin.json"));
7173
+ const version2 = manifestVersion(path28.join(root, ".claude-plugin", "plugin.json"));
6700
7174
  if (version2) return { harness: "claude", version: version2, root };
6701
7175
  }
6702
- const claudeDir = env.CLAUDE_CONFIG_DIR ?? path27.join(home, ".claude");
7176
+ const claudeDir = env.CLAUDE_CONFIG_DIR ?? path28.join(home, ".claude");
6703
7177
  const installed = readJson2(
6704
- path27.join(claudeDir, "plugins", "installed_plugins.json")
7178
+ path28.join(claudeDir, "plugins", "installed_plugins.json")
6705
7179
  );
6706
7180
  const entry = installed?.plugins?.["lobstah@lobstah"]?.[0];
6707
7181
  if (!entry?.installPath) return void 0;
6708
- const version = manifestVersion(path27.join(entry.installPath, ".claude-plugin", "plugin.json")) ?? entry.version;
7182
+ const version = manifestVersion(path28.join(entry.installPath, ".claude-plugin", "plugin.json")) ?? entry.version;
6709
7183
  return version ? { harness: "claude", version, root: entry.installPath } : void 0;
6710
7184
  }
6711
7185
  var semverKey = (v) => v.split(/[.-]/).slice(0, 3).map((n) => Number(n) || 0);
@@ -6716,25 +7190,25 @@ var newer = (a, b) => {
6716
7190
  };
6717
7191
  function installedCodexPlugin(opts = {}) {
6718
7192
  const env = opts.env ?? process.env;
6719
- const codexDir = env.CODEX_HOME ?? path27.join(opts.home ?? os5.homedir(), ".codex");
7193
+ const codexDir = env.CODEX_HOME ?? path28.join(opts.home ?? os6.homedir(), ".codex");
6720
7194
  let cfg;
6721
7195
  try {
6722
- cfg = parse(fs34.readFileSync(path27.join(codexDir, "config.toml"), "utf8"));
7196
+ cfg = parse(fs35.readFileSync(path28.join(codexDir, "config.toml"), "utf8"));
6723
7197
  } catch {
6724
7198
  return void 0;
6725
7199
  }
6726
7200
  const entry = (cfg.plugins ?? {})["lobstah@lobstah"];
6727
7201
  if (!entry || entry.enabled === false) return void 0;
6728
- const cache = path27.join(codexDir, "plugins", "cache", "lobstah", "lobstah");
7202
+ const cache = path28.join(codexDir, "plugins", "cache", "lobstah", "lobstah");
6729
7203
  let dirs;
6730
7204
  try {
6731
- dirs = fs34.readdirSync(cache);
7205
+ dirs = fs35.readdirSync(cache);
6732
7206
  } catch {
6733
7207
  return void 0;
6734
7208
  }
6735
7209
  for (const d of dirs.sort(newer).reverse()) {
6736
- const root = path27.join(cache, d);
6737
- const version = manifestVersion(path27.join(root, ".codex-plugin", "plugin.json"));
7210
+ const root = path28.join(cache, d);
7211
+ const version = manifestVersion(path28.join(root, ".codex-plugin", "plugin.json"));
6738
7212
  if (version) return { harness: "codex", version, root };
6739
7213
  }
6740
7214
  return void 0;
@@ -6757,10 +7231,125 @@ function pluginBehindLine(cliVersion, opts = {}) {
6757
7231
  }
6758
7232
  }
6759
7233
 
7234
+ // apps/cli/src/service.ts
7235
+ init_dist2();
7236
+ import * as fs36 from "node:fs";
7237
+ import * as os7 from "node:os";
7238
+ import * as path29 from "node:path";
7239
+ import { spawnSync as spawnSync5 } from "node:child_process";
7240
+ function serviceArgs(spec) {
7241
+ return [spec.nodePath, spec.entry, spec.kind, ...spec.kind === "glass" && spec.port ? ["--port", String(spec.port)] : []].filter(Boolean);
7242
+ }
7243
+ function xml(s) {
7244
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
7245
+ }
7246
+ function renderLaunchdPlist(spec) {
7247
+ return `<?xml version="1.0" encoding="UTF-8"?>
7248
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
7249
+ <plist version="1.0">
7250
+ <dict>
7251
+ <key>Label</key><string>lobstah.${spec.kind}</string>
7252
+ <key>ProgramArguments</key>
7253
+ <array>
7254
+ ${serviceArgs(spec).filter(Boolean).map((a) => ` <string>${xml(a)}</string>`).join("\n")}
7255
+ </array>
7256
+ <key>EnvironmentVariables</key>
7257
+ <dict>
7258
+ <key>PATH</key><string>${xml(spec.pathEnv)}</string>
7259
+ <key>LOBSTAH_HOME</key><string>${xml(spec.home)}</string>
7260
+ </dict>
7261
+ <key>RunAtLoad</key><true/>
7262
+ <key>KeepAlive</key><true/>
7263
+ <key>StandardOutPath</key><string>${xml(path29.posix.join(spec.logDir, `${spec.kind}.log`))}</string>
7264
+ <key>StandardErrorPath</key><string>${xml(path29.posix.join(spec.logDir, `${spec.kind}.err`))}</string>
7265
+ </dict>
7266
+ </plist>
7267
+ `;
7268
+ }
7269
+ function renderSystemdUnit(spec) {
7270
+ return `[Unit]
7271
+ Description=lobstah ${spec.kind}
7272
+
7273
+ [Service]
7274
+ ExecStart=${serviceArgs(spec).join(" ")}
7275
+ Restart=always
7276
+ RestartSec=5
7277
+ Environment=PATH=${spec.pathEnv}
7278
+ Environment=LOBSTAH_HOME=${spec.home}
7279
+ StandardOutput=append:${path29.posix.join(spec.logDir, `${spec.kind}.log`)}
7280
+ StandardError=append:${path29.posix.join(spec.logDir, `${spec.kind}.err`)}
7281
+
7282
+ [Install]
7283
+ WantedBy=default.target
7284
+ `;
7285
+ }
7286
+ function servicePathEnv(nodePath, home = os7.homedir()) {
7287
+ const dirs = [
7288
+ path29.posix.dirname(nodePath),
7289
+ path29.posix.join(home, ".local", "bin"),
7290
+ "/opt/homebrew/bin",
7291
+ "/usr/local/bin",
7292
+ "/usr/bin",
7293
+ "/bin"
7294
+ ];
7295
+ return [...new Set(dirs)].join(":");
7296
+ }
7297
+ function serviceFile(kind) {
7298
+ return process.platform === "darwin" ? path29.join(os7.homedir(), "Library", "LaunchAgents", `lobstah.${kind}.plist`) : path29.join(os7.homedir(), ".config", "systemd", "user", `lobstah-${kind}.service`);
7299
+ }
7300
+ function run(cmd, args) {
7301
+ const res = spawnSync5(cmd, args, { encoding: "utf8" });
7302
+ return { ok: res.status === 0, out: `${res.stdout ?? ""}${res.stderr ?? ""}`.trim() };
7303
+ }
7304
+ function installService(kind, port) {
7305
+ if (process.platform === "win32") {
7306
+ throw new Error(`no service manager support on Windows \u2014 run \`lobstah ${kind}\` under your process manager of choice`);
7307
+ }
7308
+ const entry = COMPILED_BINARY ? "" : fs36.realpathSync(process.argv[1]);
7309
+ const spec = {
7310
+ kind,
7311
+ nodePath: process.execPath,
7312
+ entry,
7313
+ pathEnv: servicePathEnv(process.execPath),
7314
+ home: lobstahHome(),
7315
+ logDir: path29.join(lobstahHome(), "logs"),
7316
+ port
7317
+ };
7318
+ if (kind !== "glass" && !onPath("git", spec.pathEnv)) {
7319
+ throw new Error(`git is not reachable on the service PATH (${spec.pathEnv}) \u2014 the daemon cannot allocate worktrees`);
7320
+ }
7321
+ fs36.mkdirSync(spec.logDir, { recursive: true });
7322
+ const file = serviceFile(kind);
7323
+ fs36.mkdirSync(path29.dirname(file), { recursive: true });
7324
+ if (process.platform === "darwin") {
7325
+ if (fs36.existsSync(file)) run("launchctl", ["unload", file]);
7326
+ fs36.writeFileSync(file, renderLaunchdPlist(spec));
7327
+ const load = run("launchctl", ["load", file]);
7328
+ return { file, loaded: load.ok, detail: load.ok ? `label lobstah.${kind}` : load.out };
7329
+ }
7330
+ fs36.writeFileSync(file, renderSystemdUnit(spec));
7331
+ run("systemctl", ["--user", "daemon-reload"]);
7332
+ const enable = run("systemctl", ["--user", "enable", "--now", `lobstah-${kind}.service`]);
7333
+ return { file, loaded: enable.ok, detail: enable.ok ? `unit lobstah-${kind}.service` : enable.out };
7334
+ }
7335
+ function uninstallService(kind) {
7336
+ if (process.platform === "win32") throw new Error(`no service manager support on Windows`);
7337
+ const file = serviceFile(kind);
7338
+ if (process.platform === "darwin") {
7339
+ if (fs36.existsSync(file)) run("launchctl", ["unload", file]);
7340
+ } else {
7341
+ run("systemctl", ["--user", "disable", "--now", `lobstah-${kind}.service`]);
7342
+ }
7343
+ const removed = fs36.existsSync(file);
7344
+ if (removed) fs36.unlinkSync(file);
7345
+ if (process.platform !== "darwin") run("systemctl", ["--user", "daemon-reload"]);
7346
+ return { file, removed };
7347
+ }
7348
+
6760
7349
  // apps/cli/src/doctor.ts
6761
7350
  var HEARTBEAT_STALE_MS2 = 9e4;
6762
7351
  function git2(repoPath, ...args) {
6763
- const res = spawnSync5("git", ["-C", repoPath, ...args], { encoding: "utf8" });
7352
+ const res = spawnSync6("git", ["-C", repoPath, ...args], { encoding: "utf8" });
6764
7353
  return { ok: res.status === 0, out: (res.stdout ?? "").trim() || (res.stderr ?? "").trim() };
6765
7354
  }
6766
7355
  function pluginRows(cliVersion, opts = {}) {
@@ -6791,7 +7380,7 @@ function pluginRows(cliVersion, opts = {}) {
6791
7380
  }
6792
7381
  return rows;
6793
7382
  }
6794
- function runDoctor(now2 = Date.now()) {
7383
+ async function runDoctor(now2 = Date.now()) {
6795
7384
  const rows = [];
6796
7385
  const push = (check, status, detail) => rows.push({ check, status, detail });
6797
7386
  const major = Number(process.versions.node.split(".")[0]);
@@ -6806,14 +7395,22 @@ function runDoctor(now2 = Date.now()) {
6806
7395
  codexPath || codexSdk ? "ok" : "warn",
6807
7396
  codexPath ? "on PATH" : codexSdk ? "vendored SDK (attach uses it too)" : "neither codex nor the SDK \u2014 codex dispatches will fail"
6808
7397
  );
7398
+ try {
7399
+ const port = glassPort();
7400
+ const info = await probeGlass(port);
7401
+ const installed = process.platform === "win32" ? false : fs37.existsSync(serviceFile("glass"));
7402
+ push("glass", info ? "ok" : "warn", `${installed ? "service installed" : "service not installed"}; ${info ? `${glassUrl(port)} answering (v${info.version})` : `${glassUrl(port)} not answering (CLI v${lobstahVersion()})`}`);
7403
+ } catch (err) {
7404
+ push("glass", "warn", err instanceof Error ? err.message : String(err));
7405
+ }
6809
7406
  const cfgFile = configPath();
6810
- if (!fs35.existsSync(cfgFile)) {
7407
+ if (!fs37.existsSync(cfgFile)) {
6811
7408
  push("config", "fail", `${cfgFile} missing \u2014 run \`lobstah init\``);
6812
7409
  return rows;
6813
7410
  }
6814
7411
  let parsed;
6815
7412
  try {
6816
- parsed = parse(fs35.readFileSync(cfgFile, "utf8"));
7413
+ parsed = parse(fs37.readFileSync(cfgFile, "utf8"));
6817
7414
  push("config", "ok", cfgFile);
6818
7415
  } catch (err) {
6819
7416
  push("config", "fail", `${cfgFile}: ${err instanceof Error ? err.message : String(err)}`);
@@ -6824,7 +7421,7 @@ function runDoctor(now2 = Date.now()) {
6824
7421
  if (repoKeys.length === 0) push("repos", "warn", "none configured \u2014 add one with `lobstah repos add <path>`");
6825
7422
  for (const [key, repo] of Object.entries(cfg.repos)) {
6826
7423
  const label = `repo ${key}`;
6827
- if (!fs35.existsSync(repo.path)) {
7424
+ if (!fs37.existsSync(repo.path)) {
6828
7425
  push(label, "fail", `path ${repo.path} does not exist`);
6829
7426
  continue;
6830
7427
  }
@@ -6852,11 +7449,11 @@ function runDoctor(now2 = Date.now()) {
6852
7449
  }
6853
7450
  }
6854
7451
  const hb = executorPath();
6855
- if (!fs35.existsSync(hb)) {
7452
+ if (!fs37.existsSync(hb)) {
6856
7453
  push("daemon", "warn", "no heartbeat \u2014 daemon not running (`lobstah daemon install`)");
6857
7454
  } else {
6858
7455
  try {
6859
- const payload = JSON.parse(fs35.readFileSync(hb, "utf8"));
7456
+ const payload = JSON.parse(fs37.readFileSync(hb, "utf8"));
6860
7457
  const age = now2 - new Date(payload.heartbeat ?? 0).getTime();
6861
7458
  push(
6862
7459
  "daemon",
@@ -6868,10 +7465,10 @@ function runDoctor(now2 = Date.now()) {
6868
7465
  }
6869
7466
  }
6870
7467
  try {
6871
- const soaking = path28.join(lobstahHome(), "soaking");
6872
- const stale = fs35.readdirSync(soaking).filter((f) => f.endsWith(".json")).filter((f) => {
7468
+ const soaking = path30.join(lobstahHome(), "soaking");
7469
+ const stale = fs37.readdirSync(soaking).filter((f) => f.endsWith(".json")).filter((f) => {
6873
7470
  try {
6874
- return typeof JSON.parse(fs35.readFileSync(path28.join(soaking, f), "utf8")).trapId !== "string";
7471
+ return typeof JSON.parse(fs37.readFileSync(path30.join(soaking, f), "utf8")).trapId !== "string";
6875
7472
  } catch {
6876
7473
  return true;
6877
7474
  }
@@ -6892,153 +7489,34 @@ function runDoctor(now2 = Date.now()) {
6892
7489
 
6893
7490
  // apps/cli/src/glass.ts
6894
7491
  init_dist2();
6895
- import * as fs36 from "node:fs";
6896
- import * as os6 from "node:os";
6897
- import * as path29 from "node:path";
6898
- import * as http from "node:http";
7492
+ import * as fs38 from "node:fs";
7493
+ import * as os8 from "node:os";
7494
+ import * as path31 from "node:path";
7495
+ import * as http2 from "node:http";
6899
7496
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6900
7497
 
6901
- // apps/cli/src/glass-lobs.ts
6902
- function lobItems(att, opts) {
6903
- if (!opts.lobs) return [];
6904
- const labels = {
6905
- "pr:draft": "draft",
6906
- "pr:review": "review",
6907
- "pr:checks": "checks",
6908
- "pr:conflict": "conflicts",
6909
- "pr:ready": "ready",
6910
- landed: "landed",
6911
- watch: "watch"
6912
- };
6913
- const hidden = opts.hidden ?? {};
6914
- let items = att.filter((x) => !x.acked && !(x.key !== void 0 && hidden[x.key] === x.stateHash)).map((x) => {
6915
- const label = labels[x.kind ?? ""] ?? "";
6916
- const hide = x.key !== void 0 ? { hideKey: x.key, hideHash: x.stateHash ?? "" } : {};
6917
- return typeof x.kind === "string" && x.kind.startsWith("pr:") && x.prUrl ? { key: x.kind + ":" + (x.key ?? x.prUrl), text: x.note || x.kind, href: x.prUrl, label, ...hide } : {
6918
- key: (x.kind ?? "question") + ":" + (x.key ?? x.lane + ":" + x.id),
6919
- text: x.note || x.verb,
6920
- label,
6921
- click: x.kind === "watch" ? "" : "showModal('dispatch','" + x.lane + ":" + x.id + "')",
6922
- ...hide
6923
- };
6924
- });
6925
- if (!items.length && opts.preview) {
6926
- items = [{ key: "preview", text: "attention questions crawl in here", click: opts.previewClick }];
6927
- }
6928
- const extra = items.length > 4 ? items.length - 4 : 0;
6929
- items = items.slice(0, 4);
6930
- if (extra) items[3] = { ...items[3], text: "\u2026and " + extra + " more \u2014 see attention", label: "" };
6931
- return items;
6932
- }
6933
-
6934
- // apps/cli/src/glass-diff.ts
6935
- var GLASS_DIFF_JS = `
6936
- // On deck's Landed section: the newest LANDED_MAX catches (done or failed)
6937
- // within the last LANDED_WINDOW_MS, whatever the report cursor says.
6938
- const LANDED_MAX=8,LANDED_WINDOW_MS=86400000;
6939
- const STALE_DAEMON_MS=90000,STALE_SEAT_MS=1800000;
6940
- // A PR state badge's class: GitHub's state colors (.pr-merged purple,
6941
- // .pr-open green, .pr-draft grey, .pr-closed red). An open PR whose badge
6942
- // carries news (failed checks, changes requested, pending) keeps that tone;
6943
- // a merge-state badge fills: conflicts in GitHub's red, behind grey.
6944
- function prBadgeClass(b){if(!b)return 'dim';const state=b.state||'open';
6945
- if(state==='open'&&b.merge)return 'pr-'+b.merge;
6946
- return state==='open'&&b.tone&&b.tone!=='ok'?b.tone:'pr-'+state}
6947
- const GLASS_TABS=['deck','dispatches','traps','prs','notices'];
6948
- function tabFromHash(hash){const tab=String(hash||'').replace(/^#/,'');return GLASS_TABS.includes(tab)?tab:'deck'}
6949
- function visibleSections(tab){return ['chips','foot','modal',tabFromHash('#'+tab)]}
6950
- function stableStringify(v){
6951
- if(v===null||typeof v!=='object')return v===undefined?'null':JSON.stringify(v);
6952
- if(Array.isArray(v))return '['+v.map(stableStringify).join(',')+']';
6953
- return '{'+Object.keys(v).sort().filter(k=>v[k]!==undefined).map(k=>JSON.stringify(k)+':'+stableStringify(v[k])).join(',')+'}'}
6954
- function hashStr(s){let h=0x811c9dc5;
6955
- for(let i=0;i<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619)}
6956
- return (h>>>0).toString(36)+':'+s.length}
6957
- function isStale(iso,ms,now){return !!iso&&now-Date.parse(iso)>ms}
6958
- function matches(x,st){
6959
- if(st.lane&&x.lane!==st.lane)return false;
6960
- if(st.repo&&x.repo!==st.repo)return false;
6961
- if(st.verb&&x.verb!==st.verb)return false;
6962
- if(st.q){const q=st.q.toLowerCase();
6963
- if(!((x.id+' '+(x.note||'')+' '+(x.brief||'')+' '+(x.repo||'')+' '+(x.for||'')).toLowerCase().includes(q)))return false}
6964
- return true}
6965
- function modalItem(d,modal){
6966
- if(!modal)return null;
6967
- if(modal.type==='helm')return d.helms.find(v=>v.grounds===modal.key)||null;
6968
- if(modal.type==='dispatch')return d.dispatches.find(v=>v.lane+':'+v.id===modal.key)||null;
6969
- if(modal.type==='pr')return (d.prs||[]).find(v=>v.key===modal.key)||null;
6970
- if(modal.type==='settings')return {attentionKinds:d.attentionKinds||[],attentionError:d.attentionError};
6971
- return d.traps.find(v=>v.trapId===modal.key)||null}
6972
- function sectionInputs(d,ui,now){
6973
- const st=ui.st;
6974
- const query=String(st.q||'').toLowerCase();
6975
- const hasQuery=(...parts)=>!query||parts.join(' ').toLowerCase().includes(query);
6976
- const seat=(x)=>({x,stale:isStale(x.heartbeatAt,STALE_SEAT_MS,now)});
6977
- const item=modalItem(d,ui.modal);
6978
- const recent=(iso,ms)=>!!iso&&now-Date.parse(iso)<=ms;
6979
- const deckTraps=(d.traps||[]).filter(t=>(t.live||(t.notices||[]).some(n=>
6980
- (n.kind==='trap-stowed'||n.kind==='trap-ghosted')&&recent(n.at,3600000)))&&hasQuery(t.trapId,t.repo,t.worktree));
6981
- return {
6982
- chips:{daemon:d.daemon,daemonStale:!!d.daemon&&isStale(d.daemon.heartbeat,STALE_DAEMON_MS,now),helms:d.helms.map(seat)},
6983
- deck:{view:st.view,attention:(d.attention||[]).filter(a=>(a.kind==='question'||a.kind==='landed')&&recent(a.at,86400000)&&hasQuery(a.kind,a.repo,a.note,a.id)).map(({ageSecs,...a})=>a),
6984
- prAttention:(d.attention||[]).filter(a=>a.kind&&a.kind.startsWith('pr:')).map(({ageSecs,...a})=>a),
6985
- landed:(d.landed||[]).filter(a=>recent(a.at,LANDED_WINDOW_MS)&&hasQuery(a.repo,a.note,a.id))
6986
- .sort((a,b)=>Date.parse(b.at)-Date.parse(a.at)).slice(0,LANDED_MAX),
6987
- inflight:d.dispatches.filter(x=>x.bucket!=='done'&&matches(x,{...st,lane:'',repo:'',verb:''})),traps:deckTraps.map(seat),
6988
- stacks:(d.stacks||[]).filter(s=>s.open&&hasQuery(s.repo,s.numbers.join(' '))),
6989
- prs:(d.prs||[]).filter(p=>p.state==='OPEN'),error:d.attentionError},
6990
- dispatches:{view:st.view,chain:st.chain,list:d.dispatches.filter(x=>matches(x,st))},
6991
- traps:{view:st.view,list:d.traps.filter(t=>(!st.repo||t.repo===st.repo)&&hasQuery(t.trapId,t.repo,t.worktree,t.harness)).map(seat)},
6992
- prs:{view:st.view,stacks:(d.stacks||[]).filter(s=>!st.repo||s.repo===st.repo),
6993
- prs:(d.prs||[]).filter(p=>(!st.repo||p.repo===st.repo)&&hasQuery(p.number,p.title,p.url,p.state,p.baseRefName,p.headRefName)),
6994
- watches:(d.watches||[]).filter(w=>!String(w.key).startsWith('pr:'))},
6995
- notices:{list:(d.notices||[]).filter(n=>(!st.repo||!n.repo||n.repo===st.repo)&&(!st.noticeKind||n.kind===st.noticeKind)&&hasQuery(n.kind,n.text,n.repo))},
6996
- foot:{version:d.version,repoUrl:d.repoUrl},
6997
- // The settings modal re-renders when a preference it shows changes.
6998
- modal:{modal:ui.modal,item:item&&seat(item),prefs:ui.modal&&ui.modal.type==='settings'?{view:st.view,lobs:st.lobs}:undefined}}}
6999
- // The PR modal's data: the PR, where it sits in its stack, the dispatch
7000
- // chain (linked to their modals when still on disk), and the watch, whose
7001
- // cursor is shown only here, never in the PRs table.
7002
- function prModalView(d,key){
7003
- const p=(d.prs||[]).find(x=>x.key===key);if(!p)return null;
7004
- const s=(d.stacks||[]).find(x=>x.id===p.stackId);
7005
- const byId=new Map((d.dispatches||[]).map(x=>[x.id,x]));
7006
- const chain=(p.dispatchIds||[]).map(id=>{const x=byId.get(id);
7007
- return x?{id,verb:x.verb,modalKey:x.lane+':'+x.id}:{id,culled:true}});
7008
- const w=p.watch;
7009
- return {pr:p,
7010
- stack:s?{numbers:s.numbers,position:p.position+1,size:s.numbers.length,floor:s.floor,nextNumber:s.nextNumber,
7011
- nextMergeable:!!p.nextMergeable,blockedBy:p.blockedBy}:null,
7012
- chain,
7013
- watch:w?{key:w.key,owner:w.owner||'',lastCheckedAt:w.lastCheckedAt||null,lastError:w.lastError||null,cursor:w.cursor}:null}}
7014
- // What the PRs table says about a watch: a short state, never the cursor.
7015
- function watchState(w){return w?{text:'watching',at:w.lastCheckedAt||null}:{text:'no watch',at:null}}
7016
- function hashInputs(inputs){const out={};
7017
- for(const k of Object.keys(inputs))out[k]=hashStr(stableStringify(inputs[k]));
7018
- return out}
7019
- function sectionHashes(d,ui,now){return hashInputs(sectionInputs(d,ui,now))}
7020
- function dirtySections(prev,next){return Object.keys(next).filter(k=>!prev||prev[k]!==next[k])}
7021
- `;
7498
+ // apps/cli/src/glass-page.generated.ts
7499
+ var GLASS_PAGE = '<!doctype html>\n<html>\n <head>\n <meta charset="utf-8" />\n <meta name="viewport" content="width=device-width,initial-scale=1" />\n <title>spyglass</title>\n <link rel="icon" type="image/png" href="/icon.png" />\n <style>:root{--bg: #0e1116;--card: #161b22;--line: #2b3240;--line-hover: #3a455a;--row-hover: #1c2330;--inset: #10151c;--seg-on: #26436b;--scrim: rgba(4, 7, 11, 0.72);--fg: #dbe2ea;--dim: #8b96a5;--ok: #4fc17c;--warn: #e2b93d;--bad: #e26d5c;--link: #6cb2e2;--on-fill: #fff;--pr-merged: #8957e5;--pr-merged-fg: #ab7df8;--pr-open: #238636;--pr-draft: #6e7681;--pr-closed: #da3633;--font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;--font-size: 13px;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--page-pad-block: 14px;--radius: 6px;--card-radius: 8px;--card-pad: 12px 14px;--card-min: 300px;--deck-card-min: 240px;--badge-radius: 5px;--badge-pad: 1px 7px;--badge-font: 11px;--modal-radius: 10px;--modal-width: 780px}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:var(--font-size)/1.45 var(--font-mono);padding-block:var(--page-pad-block);padding-inline:var(--space-4);display:flex;flex-direction:column;min-height:100vh}h1{font-size:15px;margin:0 0 10px}h1 .dim{color:var(--dim);font-weight:normal}h2{font-size:12px;color:var(--dim);text-transform:uppercase;letter-spacing:0.08em;margin:20px 0 6px}.chips,.controls{display:flex;gap:var(--space-2);flex-wrap:wrap;align-items:center}.controls{margin:0 0 14px}.chips{margin-bottom:8px}.chip{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:3px 9px}.controls select,.controls input[type=search]{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);color:var(--fg);font:inherit;padding:4px 8px}.controls input[type=search]{flex:1 1 140px;min-width:120px;max-width:340px}#chain-control{display:inline-flex;align-items:center;gap:7px;white-space:nowrap}#f-chain{margin:0;width:14px;height:14px;accent-color:var(--link)}.tabs{display:flex;gap:var(--space-1);border-bottom:1px solid var(--line);margin:8px 0 10px}.tabs a{color:var(--dim);padding:6px 11px;border-bottom:2px solid transparent}.tabs a.on{color:var(--fg);border-color:var(--link)}.tabpage{display:none}.tabpage.on{display:block;flex:1 0 auto}.headerline{display:flex;align-items:center;gap:var(--space-2)}.headerline h1{flex:1}#settings-slot{min-width:34px;text-align:right;color:var(--dim)}#settings-slot #gearbtn{margin-left:0}.deckgrid{display:flex;flex-direction:column;gap:var(--space-3)}.deckgrid h2{margin:5px 0}.deckgrid section{width:100%;min-width:0}.deckline{padding:2px 0;border-top:1px solid var(--line);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.deckline.acked,.card.acked,.rowhead.acked,.badge.acked{opacity:0.55}.deckmore{color:var(--dim);font-size:11px;display:inline-block;margin-top:4px}.deckline.click{cursor:pointer}.deckline.click:hover{background:var(--row-hover)}.deckgrid h2 a{color:inherit}.deckgrid h2 a:hover{color:var(--fg)}.deckgrid .cards{grid-template-columns:repeat(auto-fill,minmax(min(var(--deck-card-min),100%),1fr));gap:10px}.deckstack{margin-bottom:9px}.deckstack>.dim{font-size:11px;margin:3px 0 5px}.seg{display:inline-flex;border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}.seg button{background:var(--card);border:none;color:var(--dim);font:inherit;padding:4px 11px;cursor:pointer}.seg button.on{background:var(--seg-on);color:var(--fg)}.wrap{overflow-x:auto}table{border-collapse:collapse;width:100%;background:var(--card);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}th,td{text-align:left;padding:4px 9px;border-top:1px solid var(--line);vertical-align:top;white-space:nowrap}td.grow{white-space:normal;word-break:break-word;min-width:140px}th{color:var(--dim);font-weight:normal;border-top:none;font-size:11px}tr.rowhead{cursor:pointer}tr.rowhead:hover{background:var(--row-hover)}.cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(var(--card-min),100%),1fr));gap:var(--space-3)}.card{background:var(--card);border:1px solid var(--line);border-radius:var(--card-radius);padding:var(--card-pad);cursor:pointer;min-width:0}.card:hover{border-color:var(--line-hover)}.card .top{display:flex;justify-content:space-between;gap:var(--space-3);align-items:center;min-height:22px}.card .top>b{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card .top .badge{flex-shrink:0}.card .meta{color:var(--dim);font-size:12px;margin-top:2px}.card .note{margin-top:6px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.card .foot{margin-top:8px;display:flex;gap:10px;flex-wrap:wrap;color:var(--dim);font-size:12px}.badge{border-radius:var(--badge-radius);padding:var(--badge-pad);font-size:var(--badge-font);line-height:1.5;border:1px solid var(--line);white-space:nowrap;display:inline-block}.badge.pr-merged{background:var(--pr-merged);border-color:var(--pr-merged-fg);color:var(--on-fill)}.badge.pr-open{background:var(--pr-open);border-color:var(--pr-open);color:var(--on-fill)}.badge.pr-draft{background:var(--pr-draft);border-color:var(--pr-draft);color:var(--on-fill)}.badge.pr-closed{background:var(--pr-closed);border-color:var(--pr-closed);color:var(--on-fill)}.badge.pr-conflicts{background:var(--pr-closed);border-color:var(--pr-closed);color:var(--on-fill)}.badge.pr-behind{background:var(--pr-draft);border-color:var(--pr-draft);color:var(--on-fill)}.badge.unreported{font-weight:normal;font-size:10px;padding:0 5px;vertical-align:1px}.v-done{color:var(--ok)}.v-working,.v-queued{color:var(--fg)}.v-needs-decision,.v-blocked{color:var(--bad);font-weight:bold}.v-failed{color:var(--bad)}.v-paused,.v-unknown{color:var(--dim)}.dim{color:var(--dim)}.ok{color:var(--ok)}.warn{color:var(--warn)}.bad{color:var(--bad)}.detail{display:none}.detail.open{display:table-row}.detail td{background:var(--inset);white-space:pre-wrap;word-break:break-word;font-size:12px;color:var(--dim)}.detail b{color:var(--fg)}.empty{color:var(--dim);padding:6px 2px}#stale{display:none;color:var(--bad)}a{color:var(--link);text-decoration:none}.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:middle}.dot.ok{background:var(--ok)}.dot.warn{background:var(--warn)}.dot.bad{background:var(--bad)}#overlay{position:fixed;inset:0;background:var(--scrim);display:none;z-index:10;padding:28px 16px;overflow:auto}#overlay.open{display:block}.modal{max-width:var(--modal-width);margin:0 auto;background:var(--card);border:1px solid var(--line);border-radius:var(--modal-radius);padding:16px 18px;position:relative}.modal .x{position:absolute;top:8px;right:12px;cursor:pointer;color:var(--dim);font-size:18px;line-height:1}.modal .x:hover{color:var(--fg)}.modal h3{margin:0 24px 2px 0;font-size:14px}.modal .sub{color:var(--dim);font-size:12px;margin-bottom:10px}.modal .sec{color:var(--dim);text-transform:uppercase;letter-spacing:0.08em;font-size:11px;margin:14px 0 4px}.modal pre{white-space:pre-wrap;word-break:break-word;margin:0;font:inherit;color:var(--fg)}.msg{border-left:3px solid var(--line);padding:3px 10px;margin:8px 0;white-space:pre-wrap;word-break:break-word}.msg.from-helm{border-color:var(--link)}.msg .hdr{color:var(--dim);font-size:11px}.catch{border:1px solid var(--line);border-radius:var(--radius);padding:6px 10px;margin:8px 0}.catch .hdr{display:flex;gap:10px;flex-wrap:wrap;align-items:baseline}.loglines{color:var(--dim);font-size:12px;white-space:pre-wrap;word-break:break-word;margin-top:4px}.cmd{display:flex;gap:var(--space-2);align-items:center;margin:5px 0;flex-wrap:wrap}.cmd code{background:var(--inset);border:1px solid var(--line);border-radius:5px;padding:3px 8px;word-break:break-all;cursor:pointer}.cmd code:hover{border-color:var(--line-hover)}.cmd code.copied{border-color:var(--ok)}.cmd button{background:var(--card);border:1px solid var(--line);border-radius:5px;color:var(--dim);font:inherit;font-size:11px;padding:2px 8px;cursor:pointer}.cmd button:hover{color:var(--fg)}.chip.click{cursor:pointer}.chip.click:hover{border-color:var(--line-hover)}footer{margin-top:26px;padding-top:10px;border-top:1px solid var(--line);color:var(--dim);font-size:12px;display:flex;gap:var(--space-2);flex-wrap:wrap}.lob{position:fixed;bottom:6px;left:0;z-index:5;cursor:pointer;font-size:34px;line-height:1;user-select:none;animation:crawl 18s linear infinite}a.lob{color:inherit}.lob .bub .badge{display:inline-block;margin:0 0 3px}.lob .sprite{width:72px;height:56px;background:url(/lob-sprite.png) 0 0 no-repeat;background-size:400% 100%;image-rendering:pixelated;animation:step 0.5s steps(4) infinite}.lob .fallback{display:inline-block;animation:waddle 0.45s ease-in-out infinite alternate}.lob .bub{display:none;position:absolute;bottom:58px;right:-8px;z-index:2;background:var(--card);border:1px solid var(--line);border-radius:9px;padding:6px 9px 5px;font-size:11px;line-height:1.35;color:var(--fg);width:max-content;max-width:150px;box-shadow:0 2px 8px rgba(0,0,0,0.4)}.lob:hover .bub{display:block}.lob .bub span{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;white-space:normal;word-break:break-word}.lob .star{position:absolute;left:56px;top:-9px;height:14px;image-rendering:pixelated;z-index:3}.lob:hover{animation-play-state:paused}@keyframes crawl{0%{transform:translateX(-90px)}100%{transform:translateX(100vw)}}@keyframes step{to{background-position-x:-288px}}@keyframes waddle{from{transform:rotate(-8deg) translateY(0)}to{transform:rotate(8deg) translateY(-3px)}}#gearbtn{background:none;border:1px solid var(--line);border-radius:var(--radius);color:var(--dim);font:inherit;font-size:13px;padding:1px 7px;margin-left:8px;cursor:pointer;vertical-align:1px}#gearbtn:hover,#gearbtn.on{color:var(--fg);border-color:var(--line-hover)}.settings .row{display:flex;justify-content:space-between;align-items:center;gap:14px;margin:12px 0}.settings .lbl{color:var(--fg);font-size:12px}.settings .hint{display:block;color:var(--dim);font-size:11px;margin-top:2px}</style>\n </head>\n <body>\n <!-- The page is rendered by Preact (src/components/app.ts): header, tabs, controls, the five tab pages, footer, lobs, modal overlay. -->\n <script>"use strict";(()=>{var ce,v,Ve,ps,E,Oe,Ke,ze,be,ne,q,Je,ye,ke,$e,us,ae={},ie=[],fs=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,de=Array.isArray;function A(e,t){for(var s in t)e[s]=t[s];return e}function we(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function V(e,t,s){var n,a,r,o={};for(r in t)r=="key"?n=t[r]:r=="ref"?a=t[r]:o[r]=t[r];if(arguments.length>2&&(o.children=arguments.length>3?ce.call(arguments,2):s),typeof e=="function"&&e.defaultProps!=null)for(r in e.defaultProps)o[r]===void 0&&(o[r]=e.defaultProps[r]);return re(e,o,n,a,null)}function re(e,t,s,n,a){var r={type:e,props:t,key:s,ref:n,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:a??++Ve,__i:-1,__u:0};return a==null&&v.vnode!=null&&v.vnode(r),r}function pe(e){return e.children}function oe(e,t){this.props=e,this.context=t}function R(e,t){if(t==null)return e.__?R(e.__,e.__i+1):null;for(var s;t<e.__k.length;t++)if((s=e.__k[t])!=null&&s.__e!=null)return s.__e;return typeof e.type=="function"?R(e):null}function _s(e){if(e.__P&&e.__d){var t=e.__v,s=t.__e,n=[],a=[],r=A({},t);r.__v=t.__v+1,v.vnode&&v.vnode(r),Se(e.__P,r,t,e.__n,e.__P.namespaceURI,32&t.__u?[s]:null,n,s??R(t),!!(32&t.__u),a),r.__v=t.__v,r.__.__k[r.__i]=r,et(n,r,a),t.__e=t.__=null,r.__e!=s&&Xe(r)}}function Xe(e){if((e=e.__)!=null&&e.__c!=null)return e.__e=e.__c.base=null,e.__k.some(function(t){if(t!=null&&t.__e!=null)return e.__e=e.__c.base=t.__e}),Xe(e)}function Fe(e){(!e.__d&&(e.__d=!0)&&E.push(e)&&!le.__r++||Oe!=v.debounceRendering)&&((Oe=v.debounceRendering)||Ke)(le)}function le(){try{for(var e,t=1;E.length;)E.length>t&&E.sort(ze),e=E.shift(),t=E.length,_s(e)}finally{E.length=le.__r=0}}function Qe(e,t,s,n,a,r,o,c,u,p,f){var d,l,_,m,S,w,$=n&&n.__k||ie,h=t.length;for(u=hs(s,t,$,u,h),d=0;d<h;d++)(_=s.__k[d])!=null&&(l=_.__i!=-1&&$[_.__i]||ae,_.__i=d,w=Se(e,_,l,a,r,o,c,u,p,f),m=_.__e,_.ref&&l.ref!=_.ref&&(l.ref&&Ge(l.ref,null,_),f.push(_.ref,_.__c||m,_)),S==null&&m!=null&&(S=m),4&_.__u?(u=Ye(_,u,e),l.__e&&(l.__e=null)):typeof _.type=="function"&&w!==void 0?u=w:m&&(u=m.nextSibling),_.__u&=-7);return s.__e=S,u}function hs(e,t,s,n,a){var r,o,c,u,p,f=s.length,d=f,l=0;for(e.__k=new Array(a),r=0;r<a;r++)(o=t[r])!=null&&typeof o!="boolean"&&typeof o!="function"?(typeof o=="string"||typeof o=="number"||typeof o=="bigint"||o.constructor==String?o=e.__k[r]=re(null,o,null,null,null):de(o)?o=e.__k[r]=re(pe,{children:o},null,null,null):o.constructor===void 0&&o.__b>0?o=e.__k[r]=re(o.type,o.props,o.key,o.ref?o.ref:null,o.__v):e.__k[r]=o,u=r+l,o.__=e,o.__b=e.__b+1,c=null,(p=o.__i=ms(o,s,u,d))!=-1&&(d--,(c=s[p])&&(c.__u|=2)),c==null||c.__v==null?(p==-1&&(a>f?l--:a<f&&l++),typeof o.type!="function"&&(o.__u|=4)):p!=u&&(p==u-1?l--:p==u+1?l++:(p>u?l--:l++,o.__u|=4))):e.__k[r]=null;if(d)for(r=0;r<f;r++)(c=s[r])!=null&&(2&c.__u)==0&&(c.__e==n&&(n=R(c)),st(c,c));return n}function Ye(e,t,s){var n,a;if(typeof e.type=="function"){for(n=e.__k,a=0;n&&a<n.length;a++)n[a]&&(n[a].__=e,t=Ye(n[a],t,s));return t}e.__e!=t&&(t&&e.type&&!t.parentNode&&(t=R(e)),t=s.insertBefore(e.__e,t||null));do t=t&&t.nextSibling;while(t!=null&&t.nodeType==8);return t}function ms(e,t,s,n){var a,r,o,c=e.key,u=e.type,p=t[s],f=p!=null&&(2&p.__u)==0;if(p===null&&c==null||f&&c==p.key&&u==p.type)return s;if(n>(f?1:0)){for(a=s-1,r=s+1;a>=0||r<t.length;)if((p=t[o=a>=0?a--:r++])!=null&&(2&p.__u)==0&&c==p.key&&u==p.type)return o}return-1}function We(e,t,s){t[0]=="-"?e.setProperty(t,s??""):e[t]=s==null?"":typeof s!="number"||fs.test(t)?s:s+"px"}function se(e,t,s,n,a){var r,o;e:if(t=="style")if(typeof s=="string")e.style.cssText=s;else{if(typeof n=="string"&&(e.style.cssText=n=""),n)for(t in n)s&&t in s||We(e.style,t,"");if(s)for(t in s)n&&s[t]==n[t]||We(e.style,t,s[t])}else if(t[0]=="o"&&t[1]=="n")r=t!=(t=t.replace(Je,"$1")),o=t.toLowerCase(),t=o in e||t=="onFocusOut"||t=="onFocusIn"?o.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+r]=s,s?n?s[q]=n[q]:(s[q]=ye,e.addEventListener(t,r?$e:ke,r)):e.removeEventListener(t,r?$e:ke,r);else{if(a=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=s??"";break e}catch{}typeof s=="function"||(s==null||s===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&s==1?"":s))}}function qe(e){return function(t){if(this.l){var s=this.l[t.type+e];if(t[ne]==null)t[ne]=ye++;else if(t[ne]<s[q])return;return s(v.event?v.event(t):t)}}}function Se(e,t,s,n,a,r,o,c,u,p){var f,d,l,_,m,S,w,$,h,C,F,x,W,je,te,ge,T=t.type;if(t.constructor!==void 0)return null;128&s.__u&&(u=!!(32&s.__u),r=[c=t.__e=s.__e]),(f=v.__b)&&f(t);e:if(typeof T=="function"){d=o.length;try{if(h=t.props,C=T.prototype&&T.prototype.render,F=(f=T.contextType)&&n[f.__c],x=f?F?F.props.value:f.__:n,s.__c?$=(l=t.__c=s.__c).__=l.__E:(C?t.__c=l=new T(h,x):(t.__c=l=new oe(h,x),l.constructor=T,l.render=gs),F&&F.sub(l),l.state||(l.state={}),l.__n=n,_=l.__d=!0,l.__h=[],l._sb=[]),C&&l.__s==null&&(l.__s=l.state),C&&T.getDerivedStateFromProps!=null&&(l.__s==l.state&&(l.__s=A({},l.__s)),A(l.__s,T.getDerivedStateFromProps(h,l.__s))),m=l.props,S=l.state,l.__v=t,_)C&&T.getDerivedStateFromProps==null&&l.componentWillMount!=null&&l.componentWillMount(),C&&l.componentDidMount!=null&&l.__h.push(l.componentDidMount);else{if(C&&T.getDerivedStateFromProps==null&&h!==m&&l.componentWillReceiveProps!=null&&l.componentWillReceiveProps(h,x),t.__v==s.__v||!l.__e&&l.shouldComponentUpdate!=null&&l.shouldComponentUpdate(h,l.__s,x)===!1){t.__v!=s.__v&&(l.props=h,l.state=l.__s,l.__d=!1),t.__e=s.__e,t.__k=s.__k,t.__k.some(function(B){B&&(B.__=t)}),ie.push.apply(l.__h,l._sb),l._sb=[],l.__h.length&&o.push(l),c=R(s);break e}l.componentWillUpdate!=null&&l.componentWillUpdate(h,l.__s,x),C&&l.componentDidUpdate!=null&&l.__h.push(function(){l.componentDidUpdate(m,S,w)})}if(l.context=x,l.props=h,l.__P=e,l.__e=!1,W=v.__r,je=0,C)l.state=l.__s,l.__d=!1,W&&W(t),f=l.render(l.props,l.state,l.context),ie.push.apply(l.__h,l._sb),l._sb=[];else do l.__d=!1,W&&W(t),f=l.render(l.props,l.state,l.context),l.state=l.__s;while(l.__d&&++je<25);l.state=l.__s,l.getChildContext!=null&&(n=A(A({},n),l.getChildContext())),C&&!_&&l.getSnapshotBeforeUpdate!=null&&(w=l.getSnapshotBeforeUpdate(m,S)),te=f!=null&&f.type===pe&&f.key==null?tt(f.props.children):f,c=Qe(e,de(te)?te:[te],t,s,n,a,r,o,c,u,p),l.base=t.__e,t.__u&=-161,l.__h.length&&o.push(l),$&&(l.__E=l.__=null)}catch(B){if(o.length=d,t.__v=null,u||r!=null){if(B.then){for(t.__u|=u?160:128;c&&c.nodeType==8&&c.nextSibling;)c=c.nextSibling;r!=null&&(r[r.indexOf(c)]=null),t.__e=c}else if(r!=null)for(ge=r.length;ge--;)we(r[ge])}else t.__e=s.__e;t.__k==null&&(t.__k=s.__k||[]),B.then||Ze(t),v.__e(B,t,s)}}else r==null&&t.__v==s.__v?(t.__k=s.__k,t.__e=s.__e):c=t.__e=vs(s.__e,t,s,n,a,r,o,u,p);return(f=v.diffed)&&f(t),128&t.__u?void 0:c}function Ze(e){e&&(e.__c&&(e.__c.__e=!0),e.__k&&e.__k.some(Ze))}function et(e,t,s){for(var n=0;n<s.length;n++)Ge(s[n],s[++n],s[++n]);v.__c&&v.__c(t,e),e.some(function(a){try{e=a.__h,a.__h=[],e.some(function(r){r.call(a)})}catch(r){v.__e(r,a.__v)}})}function tt(e){return typeof e!="object"||e==null||e.__b>0?e:de(e)?e.map(tt):e.constructor!==void 0?null:A({},e)}function vs(e,t,s,n,a,r,o,c,u){var p,f,d,l,_,m,S,w=s.props||ae,$=t.props,h=t.type;if(h=="svg"?a="http://www.w3.org/2000/svg":h=="math"?a="http://www.w3.org/1998/Math/MathML":a||(a="http://www.w3.org/1999/xhtml"),r!=null){for(p=0;p<r.length;p++)if((_=r[p])&&"setAttribute"in _==!!h&&(h?_.localName==h:_.nodeType==3)){e=_,r[p]=null;break}}if(e==null){if(h==null)return document.createTextNode($);e=document.createElementNS(a,h,$.is&&$),c&&(v.__m&&v.__m(t,r),c=!1),r=null}if(h==null)w===$||c&&e.data==$||(e.data=$);else{if(r=h=="textarea"&&$.defaultValue!=null?null:r&&ce.call(e.childNodes),!c&&r!=null)for(w={},p=0;p<e.attributes.length;p++)w[(_=e.attributes[p]).name]=_.value;for(p in w)_=w[p],p=="dangerouslySetInnerHTML"?d=_:p=="children"||p in $||p=="value"&&"defaultValue"in $||p=="checked"&&"defaultChecked"in $||se(e,p,null,_,a);for(p in $)_=$[p],p=="children"?l=_:p=="dangerouslySetInnerHTML"?f=_:p=="value"?m=_:p=="checked"?S=_:c&&typeof _!="function"||w[p]===_||se(e,p,_,w[p],a);if(f)c||d&&(f.__html==d.__html||f.__html==e.innerHTML)||(e.innerHTML=f.__html),t.__k=[];else if(d&&(e.innerHTML=""),Qe(t.type=="template"?e.content:e,de(l)?l:[l],t,s,n,h=="foreignObject"?"http://www.w3.org/1999/xhtml":a,r,o,r?r[0]:s.__k&&R(s,0),c,u),r!=null)for(p=r.length;p--;)we(r[p]);c&&h!="textarea"||(p="value",h=="progress"&&m==null?e.removeAttribute("value"):m!=null&&(m!==e[p]||h=="progress"&&!m||h=="option"&&m!=w[p])&&se(e,p,m,w[p],a),p="checked",S!=null&&S!=e[p]&&se(e,p,S,w[p],a))}return e}function Ge(e,t,s){try{if(typeof e=="function"){var n=typeof e.__u=="function";n&&e.__u(),n&&t==null||(e.__u=e(t))}else e.current=t}catch(a){v.__e(a,s)}}function st(e,t,s){var n,a;if(v.unmount&&v.unmount(e),(n=e.ref)&&(n.current&&n.current!=e.__e||Ge(n,null,t)),(n=e.__c)!=null){if(n.componentWillUnmount)try{n.componentWillUnmount()}catch(r){v.__e(r,t)}n.base=n.__P=n.__n=null}if(n=e.__k)for(a=0;a<n.length;a++)n[a]&&st(n[a],t,s||typeof e.type!="function");s||we(e.__e),e.__c=e.__=e.__e=void 0}function gs(e,t,s){return this.constructor(e,s)}function nt(e,t,s){var n,a,r,o;t==document&&(t=document.documentElement),v.__&&v.__(e,t),a=(n=typeof s=="function")?null:s&&s.__k||t.__k,r=[],o=[],Se(t,e=(!n&&s||t).__k=V(pe,null,[e]),a||ae,ae,t.namespaceURI,!n&&s?[s]:a?null:t.firstChild?ce.call(t.childNodes):null,r,!n&&s?s:a?a.__e:t.firstChild,n,o),et(r,e,o),e.props.children=null}ce=ie.slice,v={__e:function(e,t,s,n){for(var a,r,o;t=t.__;)if((a=t.__c)&&!a.__)try{if((r=a.constructor)&&r.getDerivedStateFromError!=null&&(a.setState(r.getDerivedStateFromError(e)),o=a.__d),a.componentDidCatch!=null&&(a.componentDidCatch(e,n||{}),o=a.__d),o)return a.__E=a}catch(c){e=c}throw e}},Ve=0,ps=function(e){return e!=null&&e.constructor===void 0},oe.prototype.setState=function(e,t){var s;s=this.__s!=null&&this.__s!=this.state?this.__s:this.__s=A({},this.state),typeof e=="function"&&(e=e(A({},s),this.props)),e&&A(s,e),e!=null&&this.__v&&(t&&this._sb.push(t),Fe(this))},oe.prototype.forceUpdate=function(e){this.__v&&(this.__e=!0,e&&this.__h.push(e),Fe(this))},oe.prototype.render=pe,E=[],Ke=typeof Promise=="function"?Promise.prototype.then.bind(Promise.resolve()):setTimeout,ze=function(e,t){return e.__v.__b-t.__v.__b},le.__r=0,be=Math.random().toString(8),ne="__d"+be,q="__a"+be,Je=/(PointerCapture)$|Capture$/i,ye=0,ke=qe(!1),$e=qe(!0),us=0;function I(e){if(!e)return"dim";let t=e.state||"open";return t==="open"&&e.merge?"pr-"+e.merge:t==="open"&&e.tone&&e.tone!=="ok"?e.tone:"pr-"+t}var K=["deck","dispatches","traps","prs","notices"];function at(e){let t=String(e||"").replace(/^#/,"");return K.includes(t)?t:"deck"}function rt(e,t,s){return!!e&&s-Date.parse(e)>t}function ot(e,t){if(t.lane&&e.lane!==t.lane||t.repo&&e.repo!==t.repo||t.verb&&e.verb!==t.verb)return!1;if(t.q){let s=t.q.toLowerCase();if(!(e.id+" "+(e.note||"")+" "+(e.brief||"")+" "+(e.repo||"")+" "+(e.for||"")).toLowerCase().includes(s))return!1}return!0}function U(e,t){return t?t.type==="helm"?e.helms.find(s=>s.grounds===t.key)||null:t.type==="dispatch"?e.dispatches.find(s=>s.lane+":"+s.id===t.key)||null:t.type==="pr"?(e.prs||[]).find(s=>s.key===t.key)||null:t.type==="settings"?{attentionKinds:e.attentionKinds||[],attentionError:e.attentionError}:e.traps.find(s=>s.trapId===t.key)||null:null}function it(e,t,s){let n=t.st,a=String(n.q||"").toLowerCase(),r=(...d)=>!a||d.join(" ").toLowerCase().includes(a),o=d=>({x:d,stale:rt(d.heartbeatAt,18e5,s)}),c=U(e,t.modal),u=(d,l)=>!!d&&s-Date.parse(d)<=l,p=(e.traps||[]).filter(d=>(d.live||(d.notices||[]).some(l=>(l.kind==="trap-stowed"||l.kind==="trap-ghosted")&&u(l.at,36e5)))&&r(d.trapId,d.repo,d.worktree)),f=({ageSecs:d,...l})=>l;return{chips:{daemon:e.daemon,daemonStale:!!e.daemon&&rt(e.daemon.heartbeat,9e4,s),helms:e.helms.map(o)},deck:{view:n.view,attention:(e.attention||[]).filter(d=>(d.kind==="question"||d.kind==="landed")&&u(d.at,864e5)&&r(d.kind,d.repo,d.note,d.id)).map(f),prAttention:(e.attention||[]).filter(d=>d.kind&&d.kind.startsWith("pr:")).map(f),landed:(e.landed||[]).filter(d=>u(d.at,864e5)&&r(d.repo,d.note,d.id)).sort((d,l)=>Date.parse(l.at)-Date.parse(d.at)).slice(0,8),inflight:e.dispatches.filter(d=>d.bucket!=="done"&&ot(d,{...n,lane:"",repo:"",verb:""})),traps:p.map(o),stacks:(e.stacks||[]).filter(d=>d.open&&r(d.repo,d.numbers.join(" "))),prs:(e.prs||[]).filter(d=>d.state==="OPEN"),error:e.attentionError},dispatches:{view:n.view,chain:n.chain,list:e.dispatches.filter(d=>ot(d,n))},traps:{view:n.view,list:e.traps.filter(d=>(!n.repo||d.repo===n.repo)&&r(d.trapId,d.repo,d.worktree,d.harness)).map(o)},prs:{view:n.view,stacks:(e.stacks||[]).filter(d=>!n.repo||d.repo===n.repo),prs:(e.prs||[]).filter(d=>(!n.repo||d.repo===n.repo)&&r(d.number,d.title,d.url,d.state,d.baseRefName,d.headRefName)),watches:(e.watches||[]).filter(d=>!String(d.key).startsWith("pr:"))},notices:{list:(e.notices||[]).filter(d=>(!n.repo||!d.repo||d.repo===n.repo)&&(!n.noticeKind||d.kind===n.noticeKind)&&r(d.kind,d.text,d.repo))},foot:{version:e.version,repoUrl:e.repoUrl},modal:{modal:t.modal,item:c&&o(c),prefs:t.modal&&t.modal.type==="settings"?{view:n.view,lobs:n.lobs}:void 0}}}function lt(e,t){let s=(e.prs||[]).find(c=>c.key===t);if(!s)return null;let n=(e.stacks||[]).find(c=>c.id===s.stackId),a=new Map((e.dispatches||[]).map(c=>[c.id,c])),r=(s.dispatchIds||[]).map(c=>{let u=a.get(c);return u?{id:c,verb:u.verb,modalKey:u.lane+":"+u.id}:{id:c,culled:!0}}),o=s.watch;return{pr:s,stack:n?{numbers:n.numbers,position:s.position+1,size:n.numbers.length,floor:n.floor,nextNumber:n.nextNumber,nextMergeable:!!s.nextMergeable,blockedBy:s.blockedBy}:null,chain:r,watch:o?{key:o.key,owner:o.owner||"",lastCheckedAt:o.lastCheckedAt||null,lastError:o.lastError||null,cursor:o.cursor}:null}}function ct(e){return e?{text:"watching",at:e.lastCheckedAt||null}:{text:"no watch",at:null}}function dt(){let e={view:"table",lane:"",repo:"",verb:"",q:"",lobs:!0,chain:!1,noticeKind:""};try{Object.assign(e,JSON.parse(localStorage.getItem("spyglass")||"{}"))}catch{}return e.view!=="table"&&e.view!=="cards"&&(e.view="table"),e.lobs=e.lobs!==!1,e}function pt(e){try{localStorage.setItem("spyglass",JSON.stringify(e))}catch{}}function ut(){try{return JSON.parse(localStorage.getItem("spyglass-lob-hidden")||"{}")||{}}catch{return{}}}function ft(e){try{localStorage.setItem("spyglass-lob-hidden",JSON.stringify(e))}catch{}}var z,_t=new Set,N=()=>z;function ht(e){z=e}function G(e){z={...z,...e};for(let t of _t)t(z)}var mt=e=>{_t.add(e)};function vt(e){let{modal:t}=N();G({snapshot:e,stale:!1,modal:t&&U(e,t)?t:null})}var gt=()=>G({stale:!0});function L(e,t){let{snapshot:s}=N(),n={type:e,key:t};G({modal:!s||U(s,n)?n:null})}var j=()=>G({modal:null});function M(e){let t={...N().prefs,...e};pt(t),G({prefs:t})}function bt(e){e!=="table"&&e!=="cards"||M({view:e})}var kt=e=>M({lobs:e==="on"});function $t(e,t){let s={...N().lobHidden,[e]:t};ft(s),setTimeout(()=>G({lobHidden:s}),0)}async function yt(e){try{return await navigator.clipboard.writeText(e),!0}catch{return!1}}var St=function(e,t,s,n){var a;t[0]=0;for(var r=1;r<t.length;r++){var o=t[r++],c=t[r]?(t[0]|=o?1:2,s[t[r++]]):t[++r];o===3?n[0]=c:o===4?n[1]=Object.assign(n[1]||{},c):o===5?(n[1]=n[1]||{})[t[++r]]=c:o===6?n[1][t[++r]]+=c+"":o?(a=e.apply(c,St(e,c,s,["",null])),n.push(a),c[0]?t[0]|=2:(t[r-2]=0,t[r]=a)):n.push(c)}return n},wt=new Map;function Gt(e){var t=wt.get(this);return t||(t=new Map,wt.set(this,t)),(t=St(this,t.get(e)||(t.set(e,t=(function(s){for(var n,a,r=1,o="",c="",u=[0],p=function(l){r===1&&(l||(o=o.replace(/^\\s*\\n\\s*|\\s*\\n\\s*$/g,"")))?u.push(0,l,o):r===3&&(l||o)?(u.push(3,l,o),r=2):r===2&&o==="..."&&l?u.push(4,l,0):r===2&&o&&!l?u.push(5,0,!0,o):r>=5&&((o||!l&&r===5)&&(u.push(r,0,o,a),r=6),l&&(u.push(r,l,0,a),r=6)),o=""},f=0;f<s.length;f++){f&&(r===1&&p(),p(f));for(var d=0;d<s[f].length;d++)n=s[f][d],r===1?n==="<"?(p(),u=[u],r=3):o+=n:r===4?o==="--"&&n===">"?(r=1,o=""):o=n+o[0]:c?n===c?c="":o+=n:n===\'"\'||n==="\'"?c=n:n===">"?(p(),r=1):r&&(n==="="?(r=5,a=o,o=""):n==="/"&&(r<5||s[f][d+1]===">")?(p(),r===3&&(u=u[0]),r=u,(u=u[0]).push(2,0,r),r=0):n===" "||n===" "||n===`\n`||n==="\\r"?(p(),r=2):o+=n),r===3&&o==="!--"&&(r=4,u=u[0])}return p(),u})(e)),t),arguments,[])).length>1?t:t[0]}var Pt=e=>Array.isArray(e)?e.map(Pt):e===""?null:e,bs=(e,t,...s)=>V(e,t,...s.map(Pt)),i=Gt.bind(bs);var fe,b,Pe,Ct,Ce=0,xt=[],k=v,Tt=k.__b,At=k.__r,It=k.diffed,Mt=k.__c,Dt=k.unmount,Et=k.__;function Rt(e,t){k.__h&&k.__h(b,e,Ce||t),Ce=0;var s=b.__H||(b.__H={__:[],__h:[]});return e>=s.__.length&&s.__.push({}),s.__[e]}function Nt(e){return Ce=1,ks(Bt,e)}function ks(e,t,s){var n=Rt(fe++,2);if(n.t=e,!n.__c&&(n.__=[s?s(t):Bt(void 0,t),function(c){var u=n.__N?n.__N[0]:n.__[0],p=n.t(u,c);u!==p&&(n.__N=[p,n.__[1]],n.__c.setState({}))}],n.__c=b,!b.__f)){var a=function(c,u,p){if(!n.__c.__H)return!0;var f=!1,d=n.__c.props!==c;if(n.__c.__H.__.some(function(_){if(_.__N){f=!0;var m=_.__[0];_.__=_.__N,_.__N=void 0,m!==_.__[0]&&(d=!0)}}),r){var l=r.call(this,c,u,p);return f?l||d:l}return!f||d};b.__f=!0;var r=b.shouldComponentUpdate,o=b.componentWillUpdate;b.componentWillUpdate=function(c,u,p){if(this.__e){var f=r;r=void 0,a(c,u,p),r=f}o&&o.call(this,c,u,p)},b.shouldComponentUpdate=a}return n.__N||n.__}function Ht(e,t){var s=Rt(fe++,7);return ws(s.__H,t)&&(s.__=e(),s.__H=t,s.__h=e),s.__}function $s(){for(var e;e=xt.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(ue),t.__h.some(Te),t.__h=[]}catch(s){t.__h=[],k.__e(s,e.__v)}}}k.__b=function(e){b=null,Tt&&Tt(e)},k.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),Et&&Et(e,t)},k.__r=function(e){At&&At(e),fe=0;var t=(b=e.__c).__H;t&&(Pe===b?(t.__h=[],b.__h=[],t.__.some(function(s){s.__N&&(s.__=s.__N),s.u=s.__N=void 0})):(t.__h.some(ue),t.__h.some(Te),t.__h=[],fe=0)),Pe=b},k.diffed=function(e){It&&It(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(xt.push(t)!==1&&Ct===k.requestAnimationFrame||((Ct=k.requestAnimationFrame)||ys)($s)),t.__H.__.some(function(s){s.u&&(s.__H=s.u,s.u=void 0)})),Pe=b=null},k.__c=function(e,t){t.some(function(s){try{s.__h.some(ue),s.__h=s.__h.filter(function(n){return!n.__||Te(n)})}catch(n){t.some(function(a){a.__h&&(a.__h=[])}),t=[],k.__e(n,s.__v)}}),Mt&&Mt(e,t)},k.unmount=function(e){Dt&&Dt(e);var t,s=e.__c;s&&s.__H&&(s.__H.__.some(function(n){try{ue(n)}catch(a){t=a}}),s.__H=void 0,t&&k.__e(t,s.__v))};var Lt=typeof requestAnimationFrame=="function";function ys(e){var t,s=function(){clearTimeout(n),Lt&&cancelAnimationFrame(t),setTimeout(e)},n=setTimeout(s,35);Lt&&(t=requestAnimationFrame(s))}function ue(e){var t=b,s=e.__c;typeof s=="function"&&(e.__c=void 0,s()),b=t}function Te(e){var t=b;e.__c=e.__(),b=t}function ws(e,t){return!e||e.length!==t.length||t.some(function(s,n){return s!==e[n]})}function Bt(e,t){return typeof t=="function"?t(e):t}var Ss=e=>{if(!e)return"";let t=Math.max(0,(Date.now()-Date.parse(e))/1e3);return t<90?Math.round(t)+"s":t<5400?Math.round(t/60)+"m":t<172800?(t/3600).toFixed(1)+"h":Math.round(t/86400)+"d"},g=e=>i`<span data-age=${e??""}>${Ss(e)}</span>`,y=(e,t)=>()=>L(e,t),Ut=e=>e.stopPropagation();function P(e,t,s){return t.length?i`<div class="wrap"><table><tbody><tr>${e.map(n=>i`<th>${n}</th>`)}</tr>${t}</tbody></table></div>`:i`<div class="empty">${s}</div>`}function Gs({text:e}){let[t,s]=Nt(null),n=a=>async()=>{await yt(e)&&(s(a),setTimeout(()=>s(null),1e3))};return i`<div class="cmd"><code class=${t==="code"?"copied":void 0} title="click to copy" onClick=${n("code")}>${e}</code><button title="copy" onClick=${n("button")}>${t==="button"?"\\u2713":"\\u29C9"}</button></div>`}var D=(e,t)=>i`<${Gs} key=${t} text=${e} />`,_e=e=>e.map(t=>[i`<div class="sub">${t.name} \xB7 ${t.type} \xB7 ${t.bytes} bytes</div>`,D(t.path)]),Ae=e=>e.log.map(t=>t.at+" "+t.verb+(t.note?" "+t.note:"")).join(`\n`);function jt(e){return i`<div class="sec">brief</div><pre>${e.brief}</pre>${e.attachments.length>0&&[i`<div class="sec">attachments (${e.attachments.length})</div>`,_e(e.attachments)]}${e.messageAttachments.length>0&&[i`<div class="sec">message attachments (${e.messageAttachments.length})</div>`,_e(e.messageAttachments)]}${e.followUp&&[i`<div class="sec">forks</div>`,i`<pre>${e.followUp}</pre>`]}<div class="sec">log</div><div class="loglines">${e.log.length?Ae(e):"no entries yet"}</div>${e.inbox.length>0&&[i`<div class="sec">inbox</div>`,i`<div class="loglines">${e.inbox.join(`\n---\n`)}</div>`]}${e.evidence&&[i`<div class="sec">evidence</div>`,i`<div class="loglines">${JSON.stringify(e.evidence)}</div>`]}`}function J(e){return e.for?[e.for,e.evidence&&e.evidence.deliveredTo?[" ",i`<span class="ok">\u2713delivered</span>`]:[" ",i`<span class="warn">waiting</span>`]]:e.claimedBy?e.claimedBy:""}function O(e){let t=e.evidence&&(e.evidence.prUrl||e.evidence.pr&&e.evidence.pr.url);if(!t)return"";let s=e.prBadge;return[i`<a href=${t} target="_blank" rel="noopener" onClick=${Ut}>PR</a>`,s&&[" ",i`<span class=${"badge "+I(s)} title=${"observed "+s.observedAt}>${s.text}</span>`],e.prGate&&[" ",i`<span class="badge dim" title="merge gate (pick)">${e.prGate}</span>`]]}var Ps={"pr:draft":"draft","pr:review":"review","pr:checks":"checks","pr:conflict":"conflicts","pr:ready":"ready",landed:"landed",watch:"watch"},he={"pr:review":"bad","pr:checks":"bad","pr:conflict":"pr-conflicts","pr:ready":"ok","pr:draft":"dim",watch:"warn"},Ie=e=>Ps[e]||"";function Ot(e){if(e.kind==="question")return i`<span class=${"v-"+e.verb}>${e.verb}</span>`;let t=e.kind==="landed"?e.verb==="failed"?"bad":"ok":he[e.kind]||"dim";return[i`<span class=${"badge "+t}>${Ie(e.kind)||e.kind}</span>`,e.kind==="landed"&&[" ",i`<span class="dim">${e.verb}</span>`]]}var me=e=>i`<a href=${e.url} target="_blank" rel="noopener" onClick=${Ut}>#${e.number}</a>`;function X(e){let t=e.checks;return t.passed+"/"+t.total+" passed"+(t.failed?" \\xB7 "+t.failed+" failed":"")+(t.pending?" \\xB7 "+t.pending+" pending":"")}function ve(e){let t=e.review||{};return(e.reviewDecision||"")+(t.unresolvedThreads?" \\xB7 "+t.unresolvedThreads+" unresolved":"")+(t.changesRequested?" \\xB7 changes requested":"")}function Q(e){return[e.mergeStateStatus,e.nextMergeable?[" \\xB7 ",i`<span class="ok">next mergeable</span>`]:e.blockedBy?" \\xB7 blocked by #"+e.blockedBy:""]}function Me(e){let t=ct(e);return e?[t.text," \\xB7 ",t.at?[g(t.at)," ago"]:"never checked"]:i`<span class="dim">${t.text}</span>`}function H(e){if(!e.live)return{stale:!1,listen:[i`<span class="dot"></span>`,i`<span class="dim">signed off</span>`],hb:i`<span class="dim">\u2014</span>`};let t=Date.now()-Date.parse(e.heartbeatAt)>18e5;return{stale:t,listen:e.firstParkedAt?[i`<span class="dot ok"></span>`,"listening"]:[i`<span class="dot warn"></span>`,"never parked"],hb:i`<span class=${t?"bad":"ok"}>${t&&"stale "}${g(e.heartbeatAt)} ago</span>`}}function De(e){let t=e.messages.filter(s=>s.state==="pending").length;return e.messages.length?["\\u2709 "+e.messages.length,t>0&&[" ",i`<span class="warn">(${t} pending)</span>`]]:null}function Ts(e,t){let s=e.badge&&i`<span class=${"badge "+(e.badge.tone||"dim")}>${e.badge.text}</span>`;return t==="cards"?i`<div key=${e.key} class=${"card"+(e.acked?" acked":"")} onClick=${e.open} style=${e.open?void 0:"cursor:default"}><div class="top"><b>${e.title}</b>${s}</div>${e.meta&&i`<div class="meta">${e.meta}</div>`}</div>`:i`<div key=${e.key} class=${"deckline"+(e.open?" click":"")+(e.acked?" acked":"")} onClick=${e.open}>${s&&[s," "]}<b>${e.title}</b>${e.meta&&[" ",i`<span class="dim">\xB7 ${e.meta}</span>`]}</div>`}var Le=(e,t)=>e>0&&i`<a class="deckmore" href=${"#"+t}>+${e} more \u2192</a>`;function Ee(e,t,s,n,a){let r=t.slice(0,n),o=r.map(u=>Ts(u,a)),c=r.length?a==="cards"?i`<div class="cards">${o}</div>`:o:i`<div class="empty">none</div>`;return i`<section><h2><a href=${"#"+s}>${e} \u2192</a></h2>${c}${Le(t.length-r.length,s)}</section>`}function As(e){let t=e.slice(0,4),s=t.map(n=>i`<tr key=${n.kind+":"+n.key} class=${"rowhead"+(n.acked?" acked":"")} onClick=${y("dispatch",n.lane+":"+n.id)}><td>${Ot(n)}</td><td class="grow">${n.note||n.verb}</td><td>${n.repo||""}</td><td>${g(n.at)}</td></tr>`);return i`<section><h2><a href="#notices">attention \u2192</a></h2>${P(["kind","note","repo","age"],s,"none")}${Le(e.length-t.length,"notices")}</section>`}function Is(e,t,s,n){let a=t.find(p=>p.number===e.nextNumber)||t[0],r=e.numbers.map(p=>"#"+p).join(" \\u2192 "),o=a?"next #"+a.number:"nothing mergeable";if(n==="cards"){let p=f=>{let d=s.get(f.key)||[],l=d.length>0&&d.every(m=>m.acked),_=d.length?d.map(m=>i`<span class=${"badge "+(he[m.kind]||"dim")}>${Ie(m.kind)}</span>`):i`<span class=${"badge "+I(f.badge)}>${f.badge.text}</span>`;return i`<div key=${f.key} class=${"card"+(l?" acked":"")} onClick=${y("pr",f.key)}><div class="top"><b>#${f.number} ${f.title||""}</b>${_}</div><div class="meta">${f.repo} \xB7 ${f.badge.text}${l&&" \\xB7 acked"}</div></div>`};return i`<div key=${e.id} class="deckstack"><div class="dim">${r} \xB7 ${o}</div><div class="cards">${t.map(p)}</div></div>`}let c=t.flatMap(p=>(s.get(p.key)||[]).map(f=>i`<span class=${"badge "+(he[f.kind]||I(p.badge))+(f.acked?" acked":"")}>#${p.number} ${p.badge.text}</span>`)),u=c.flatMap((p,f)=>f?[" ",p]:[p]);return i`<div key=${e.id} class=${"deckline"+(a?" click":"")} onClick=${a&&y("pr",a.key)}><b>${r}</b> <span class="dim">\xB7 ${o}</span>${c.length>0&&[" \\xB7 ",u]}</div>`}function Ms(e,t){let s=e.stacks.slice(0,3),n=new Map;for(let r of e.prAttention){let o=n.get(r.key)||[];o.push(r),n.set(r.key,o)}let a=s.length?s.map(r=>Is(r,e.prs.filter(o=>o.stackId===r.id).sort((o,c)=>o.position-c.position),n,t)):i`<div class="empty">none</div>`;return i`<section><h2><a href="#prs">PRs \u2192</a></h2>${a}${Le(e.stacks.length-s.length,"prs")}</section>`}function Ft({inp:e}){let t=e.view,s=e.inflight.map(r=>({key:r.lane+":"+r.id,title:[r.id.slice(0,8)," ",r.repo||""],badge:{text:r.verb,tone:r.verb==="needs-decision"||r.verb==="blocked"?"bad":"dim"},meta:[(r.note||"").slice(0,90),r.verbAt&&[" \\xB7 ",g(r.verbAt)," ago"],(r.for||r.claimedBy)&&" \\xB7 "+(r.for||r.claimedBy)],open:y("dispatch",r.lane+":"+r.id)})),n=e.landed.map(r=>({key:r.key,title:[r.id.slice(0,8)," ",r.repo||"",r.unreported&&[" ",i`<span class="badge warn unreported">unreported</span>`]],badge:{text:r.verb,tone:r.verb==="failed"?"bad":"ok"},meta:[(r.note||"").slice(0,90)," \\xB7 ",g(r.at)," ago"],open:y("dispatch",r.lane+":"+r.id)})),a=e.traps.map(({x:r})=>({key:r.trapId,title:"\\u{1FAA4} wt:"+r.trapId,badge:{text:r.live?r.harness||"live":"signed off",tone:r.live?"ok":"dim"},meta:[r.repo||""," \\xB7 ",r.live?H(r).listen:"stowed / ghosted"],open:y("trap",r.trapId)}));return i`<div class="deckgrid">${As(e.attention)}${Ee("in flight",s,"dispatches",4,t)}${Ee("Landed \\xB7 24h",n,"dispatches",8,t)}${Ee("traps",a,"traps",3,t)}${Ms(e,t)}</div>`}function Ds(e,t){if(!t)return e;let s=new Map(e.map(r=>[r.id,r])),n=r=>{let o=r,c=new Set;for(;o.followUp&&s.has(o.followUp)&&!c.has(o.id);)c.add(o.id),o=s.get(o.followUp);return o.id},a=new Map;for(let r of e){let o=n(r),c=a.get(o)||[];c.push(r),a.set(o,c)}return[...a.values()].sort((r,o)=>o[0].sort-r[0].sort).flatMap(r=>r.sort((o,c)=>o.id===n(o)?-1:c.id===n(c)?1:o.sort-c.sort))}function Es(e,t){let s=e.lane+":"+e.id;return i`<tr key=${s} class="rowhead" onClick=${y("dispatch",s)}><td>${t&&e.followUp&&i`<span class="dim">\u21B3 </span>`}${e.id.slice(0,8)}</td><td>${e.lane} / ${e.bucket}</td><td>${e.repo}</td><td class=${"v-"+e.verb}>${e.verb}</td><td class="grow">${(e.note??"").slice(0,90)}</td><td>${g(e.verbAt)}</td><td>${J(e)}</td><td>${O(e)}</td></tr>`}function Ls(e){let t=e.lane+":"+e.id;return i`<div key=${t} class="card" onClick=${y("dispatch",t)}><div class="top"><b>${e.id.slice(0,8)}</b><span class=${"badge v-"+e.verb}>${e.verb}</span></div><div class="meta">${e.repo} \xB7 ${e.lane} ${e.bucket}${e.verbAt&&[" \\xB7 ",g(e.verbAt)]}</div>${e.note&&i`<div class="note">${e.note}</div>`}<div class="foot">${J(e)} ${O(e)}</div></div>`}function Wt({inp:e}){let t=e.list;return e.view==="cards"?t.length?i`<div class="cards">${t.map(Ls)}</div>`:i`<div class="empty">no dispatches match</div>`:P(["id","lane / bucket","repo","verb","note","age","addressed","pr"],Ds(t,e.chain).map(s=>Es(s,e.chain)),"no dispatches match")}var xs={deck:"On deck",dispatches:"Dispatches",traps:"Traps",prs:"PRs",notices:"Notices"},Rs=["working","needs-decision","blocked","paused","done","failed","unknown"];function Ns(e,t){let s=t.daemonStale,n=e.daemon?[i`<span class=${s?"bad":"ok"}>${s&&"stale "}${g(e.daemon.heartbeat)} ago</span>`," ",i`<span class="dim">v${e.daemon.version}</span>`]:i`<span class="bad">down</span>`;return[i`<span class="chip">daemon ${n}</span>`,t.helms.map(({x:a,stale:r})=>i`<span key=${a.grounds} class="chip click" onClick=${y("helm",a.grounds)}>\u26F5 <b>${a.man}</b> <span class="dim">helm ${a.grounds}</span> <span class=${r?"warn":"ok"}>${r&&"stale "}${g(a.heartbeatAt)} ago</span></span>`)]}var xe=[];function Hs(e){let t=[...new Set([...e.dispatches.map(s=>s.repo),...e.notices.map(s=>s.repo),...e.prs.map(s=>s.repo)].filter(Boolean))].sort();return t.length!==xe.length&&(xe=t),xe}var Re=[];function Bs(e){let t=[...new Set(e.notices.map(s=>s.kind))].sort();return t.length!==Re.length&&(Re=t),Re}var Y=e=>e.currentTarget.value;function Us(e,t,s){let n=o=>t===o?"":"display:none",a=e?Hs(e):[],r=e?Bs(e):[];return i`<div class="controls">\n <select id="f-lane" style=${e&&n("dispatches")} value=${s.lane} onChange=${o=>M({lane:Y(o)})}>\n <option value="">all lanes</option>\n <option value="work">work</option>\n <option value="chore">chore</option>\n </select>\n <select id="f-repo" value=${s.repo} onChange=${o=>M({repo:Y(o)})}>\n <option value="">all repos</option>\n ${a.map(o=>i`<option key=${o}>${o}</option>`)}\n </select>\n <select id="f-verb" style=${e&&n("dispatches")} value=${s.verb} onChange=${o=>M({verb:Y(o)})}>\n <option value="">all verbs</option>\n ${Rs.map(o=>i`<option key=${o}>${o}</option>`)}\n </select>\n <label id="chain-control" style=${e&&n("dispatches")}><input id="f-chain" type="checkbox" checked=${!!s.chain} onChange=${o=>M({chain:o.currentTarget.checked})} /> group by chain</label>\n <select id="f-kind" style=${e&&n("notices")} value=${s.noticeKind} onChange=${o=>M({noticeKind:Y(o)})}>\n <option value="">all notice kinds</option>\n ${r.map(o=>i`<option key=${o}>${o}</option>`)}\n </select>\n <input id="f-q" type="search" placeholder="search id \xB7 note \xB7 brief" value=${s.q} onInput=${o=>M({q:Y(o)})} />\n </div>`}function qt({d:e,inp:t,tab:s,st:n,stale:a}){return[i`<div class="headerline"><h1>\u{1F99E}\u2728 spyglass<span id="stale" style=${e||a?a?"display:inline":"display:none":void 0}> \xB7 STALE FEED</span></h1><span id="settings-slot"><button id="gearbtn" title="settings" aria-label="settings" onClick=${()=>L("settings","browser")}>\u2699</button></span></div>`,i`<div class="chips"><span id="chips" style="display:contents">${e&&t&&Ns(e,t.chips)}</span><span class="chip dim" id="clock">${e&&new Date(e.now).toLocaleTimeString("en-GB")}</span></div>`,i`<nav class="tabs" id="tabs" aria-label="Spyglass views">${K.map(r=>i`<a href=${"#"+r} data-tab=${r} class=${e?r===s?"on":"":void 0}>${xs[r]}</a>`)}</nav>`,Us(e,s,n)]}var Vt=({d:e})=>e&&["\\u{1F99E}\\u2728 lobstah v"+e.version+" \\xB7 ",i`<a href=${e.repoUrl} target="_blank">${e.repoUrl.replace("https://github.com/","")}</a>`];function Kt(e,t){if(!t.lobs)return[];let s={"pr:draft":"draft","pr:review":"review","pr:checks":"checks","pr:conflict":"conflicts","pr:ready":"ready",landed:"landed",watch:"watch"},n=t.hidden??{},a=e.filter(o=>!o.acked&&!(o.key!==void 0&&n[o.key]===o.stateHash)).map(o=>{let c=s[o.kind??""]??"",u=o.key!==void 0?{hideKey:o.key,hideHash:o.stateHash??""}:{};return typeof o.kind=="string"&&o.kind.startsWith("pr:")&&o.prUrl?{key:o.kind+":"+(o.key??o.prUrl),text:o.note||o.kind,href:o.prUrl,label:c,...u}:{key:(o.kind??"question")+":"+(o.key??o.lane+":"+o.id),text:o.note||o.verb,label:c,...o.kind==="watch"?{}:{open:{type:"dispatch",key:o.lane+":"+o.id}},...u}});!a.length&&t.preview&&(a=[{key:"preview",text:"attention questions crawl in here",...t.previewHelm?{open:{type:"helm",key:t.previewHelm}}:{}}]);let r=a.length>4?a.length-4:0;return a=a.slice(0,4),r&&(a[3]={...a[3],text:"\\u2026and "+r+" more \\u2014 see attention",label:""}),a}var zt=!0;function js(e,t,s){let n="animation-duration:"+((innerWidth+180)/(100+t*12)).toFixed(1)+"s;animation-delay:-"+t*9%14+"s",a=()=>{e.hideKey&&$t(e.hideKey,e.hideHash??"")},r=[i`<div class="bub">${e.label&&[i`<span class="badge dim">${e.label}</span>`," "]}<span>${e.text.length>48?e.text.slice(0,47)+"\\u2026":e.text}</span></div>`,s===!1?i`<span class="fallback">\u{1F99E}</span>`:i`<div class="sprite"></div>`,zt&&i`<img\n class="star"\n src="/star.png"\n alt=""\n onError=${c=>{zt=!1,c.currentTarget.remove()}}\n />`],o=e.open;return e.href?i`<a key=${e.key} class="lob" style=${n} title="open the PR" href=${e.href} target="_blank" rel="noopener" onClick=${a}>${r}</a>`:i`<div\n key=${e.key}\n class="lob"\n style=${n}\n title="click to open"\n onClick=${()=>{a(),o&&L(o.type,o.key)}}\n >\n ${r}\n </div>`}function Jt({state:e}){let t=e.snapshot,s=Kt(t?.attention||[],{lobs:e.prefs.lobs,hidden:e.lobHidden,preview:e.preview,previewHelm:t&&t.helms.length?t.helms[0].grounds:void 0}),n=s.map(a=>a.key).join("|")+(e.spriteOk===null?"?":e.spriteOk?"s":"e");return Ht(()=>s.map((a,r)=>js(a,r,e.spriteOk)),[n])}var Z=i`<span class="x" onClick=${j}>\xD7</span>`,Xt=(e,t,s)=>i`<span class="seg">${t.map(([n,a])=>i`<button class=${n===s?"on":""} onClick=${()=>e(n)}>${a}</button>`)}</span>`;function Os(e,t){let s=Xt(bt,[["table","table"],["cards","cards"]],t.view),n=Xt(kt,[["on","on"],["off","off"]],t.lobs?"on":"off"),a=e.attentionError?e.attentionError:(e.attentionKinds||[]).join(" \\xB7 ");return[Z,i`<h3>\u2699 settings <span class="dim" style="font-weight:normal">\xB7 this browser only</span></h3>`,i`<div class="settings"><div class="row"><span class="lbl">view<span class="hint">cards or table (notices stay a table)</span></span>${s}</div><div class="row"><span class="lbl">lobs<span class="hint">crawling lobsters in this page</span></span>${n}</div><div class="row"><span class="lbl">attention<span class="hint">kinds shown \u2014 attentionKinds in config.toml (read-only here)</span></span><span class="dim" style="font-size:11px;text-align:right;max-width:260px">${a}</span></div></div>`]}function Fs(e,t){let s=t.numbers.flatMap((a,r)=>[r?" \\u2192 ":"",a===e.number?i`<b>#${a}</b>`:"#"+a]),n=t.nextMergeable?[" \\xB7 ",i`<span class="ok">next mergeable</span>`]:t.blockedBy?" \\xB7 blocked by #"+t.blockedBy:t.nextNumber?" \\xB7 next is #"+t.nextNumber:"";return[s," \\xB7 ",t.position," of ",t.size," \\xB7 floor ",t.floor,n]}var Ws=e=>t=>{t.preventDefault(),L("dispatch",e)};function qs(e,t){let s=lt(e,t),n=s.pr,a=s.stack,r=s.watch,o=ve(n),c=s.chain.length?s.chain.flatMap((p,f)=>[f?" \\u2192 ":"",p.culled?i`<span class="dim">${p.id.slice(0,8)} (culled)</span>`:[i`<a href="#" onClick=${Ws(p.modalKey)}>${p.id.slice(0,8)}</a>`," ",i`<span class="dim">${p.verb}</span>`]]):i`<span class="dim">none</span>`,u=r?[i`<div>${r.key} \xB7 owner ${r.owner||"?"} \xB7 ${r.lastCheckedAt?["checked ",g(r.lastCheckedAt)," ago"]:"never checked"}</div>`,r.lastError&&i`<div class="bad">${r.lastError}</div>`,i`<div class="dim" style="font-size:11px;margin-top:4px">cursor</div>`,D(r.cursor)]:i`<div class="dim">no watch \u2014 <code>lobstah watch add ${n.url}</code> registers one</div>`;return[Z,i`<h3>${me(n)} ${n.title||""} <span class=${"badge "+I(n.badge)}>${n.badge.text}</span>${n.draft&&n.badge.text!=="draft"&&[" ",i`<span class="badge pr-draft">draft</span>`]}</h3>`,i`<div class="sub">${n.repo} \xB7 ${n.state} \xB7 observed ${g(n.observedAt)} ago${n.gate&&" \\xB7 gate "+n.gate}</div>`,i`<div class="sec">checks</div><div>${X(n)}</div><div class="sec">review</div><div>${o||i`<span class="dim">no review yet</span>`}</div><div class="sec">merge</div><div>${Q(n)}</div><div class="sec">refs</div><div>${n.headRefName||"?"} \u2192 ${n.baseRefName||"?"}</div><div class="sec">stack</div><div>${a?Fs(n,a):i`<span class="dim">not stacked</span>`}</div><div class="sec">dispatch chain</div><div>${c}</div><div class="sec">watch</div>`,u]}var Qt=(e,t)=>(e==="codex"?"codex resume ":"claude --resume ")+t;function Vs(e){let t=Date.now()-Date.parse(e.heartbeatAt)>18e5;return[Z,i`<h3>\u26F5 ${e.man}</h3><div class="sub">helm of <b>${e.grounds}</b> (${(e.repos||[]).join(", ")})</div><div class="sub">${e.harness??"?"} \xB7 ${e.cwd??"?"}${e.host&&" \\xB7 "+e.host}</div><div class="sub">session ${e.sessionId??""} \xB7 signed on ${g(e.signedOnAt)} ago \xB7 heartbeat <span class=${t?"warn":"ok"}>${g(e.heartbeatAt)} ago</span></div>`,e.sessionId&&[i`<div class="sec">open this session</div>`,D(Qt(e.harness,e.sessionId))],e.transcript&&[i`<div class="sec">transcript</div>`,D(e.transcript)]]}function Ks(e){let t=e.claimedBy&&e.claimedBy.startsWith("wt:")?[i`<div class="sec">worked by trap</div>`,D(e.claimedBy),i`<div class="dim" style="font-size:11px">an opted-in interactive session mans this seat \u2014 attach would resume someone\'s live thread. Message it instead: lobstah send ${e.claimedBy} "\u2026"</div>`]:[i`<div class="sec">open this session</div>`,D("lobstah attach "+e.id)];return[Z,i`<h3>${e.id.slice(0,8)} <span class=${"badge v-"+e.verb}>${e.verb}</span></h3>`,i`<div class="sub">${e.repo} \xB7 ${e.lane} ${e.bucket} \xB7 ${g(e.verbAt)}${e.for&&[" \\xB7 ",J(e)]} ${O(e)}</div>`,t,e.transcript&&[i`<div class="sec">transcript</div>`,D(e.transcript)],jt(e)]}function zs(e){let t=H(e),s=e.live?[e.repo??"addressed bait only"," \\xB7 session ",e.sessionId??""," \\xB7 signed on ",g(e.signedOnAt)," ago \\xB7 ",t.listen," \\xB7 heartbeat ",t.hb]:"signed off \\u2014 registration gone; the lifecycle, messages, and catches are the surviving record. Re-soaking the same worktree restores this address.",n=e.notices.length?e.notices.map(o=>i`<div key=${o.seq} class="loglines">${g(o.at)} ago \xB7 <b>${o.kind}</b> \u2014 ${o.text}</div>`):i`<div class="empty">none recorded</div>`,a=e.messages.length?e.messages.map(o=>i`<div key=${o.file} class=${"msg"+(o.from==="helm"?" from-helm":"")}><div class="hdr">from ${o.from} \xB7 ${o.at&&[g(o.at)," ago"]} \xB7 ${o.state==="pending"?i`<span class="warn">pending</span>`:i`<span class="ok">delivered</span>`}</div>${o.text}${o.attachments?.length?_e(o.attachments):""}</div>`):i`<div class="empty">none</div>`,r=e.catches.length?e.catches.map(o=>i`<div key=${o.lane+":"+o.id} class="catch"><div class="hdr"><b>${o.id.slice(0,8)}</b><span class=${"badge v-"+o.verb}>${o.verb}</span><span class="dim">${g(o.verbAt)}</span>${O(o)}</div><div class="loglines">${Ae(o)}</div></div>`):i`<div class="empty">none yet</div>`;return[Z,i`<h3>\u{1FAA4} wt:${e.trapId} <span class="badge">${e.harness??"signed off"}</span></h3>`,e.worktree&&i`<div class="sub">${e.worktree}</div>`,i`<div class="sub">${s}</div>`,e.live&&e.sessionId&&[i`<div class="sec">open this session</div>`,D(Qt(e.harness,e.sessionId)),i`<div class="dim" style="font-size:11px">as registered at sign-on \u2014 a hookless enlistment may hold a made-up id</div>`],i`<div class="sec">lifecycle (${e.notices.length})</div>`,n,i`<div class="sec">messages (${e.messages.length})</div>`,a,i`<div class="sec">catches (${e.catches.length})</div>`,r]}function Yt({snapshot:e,modal:t,prefs:s}){if(!e||!t)return null;let n=U(e,t);return n?t.type==="settings"?Os(n,s):t.type==="pr"?qs(e,t.key):t.type==="helm"?Vs(n):t.type==="dispatch"?Ks(n):zs(n):null}var Js=e=>i`<tr key=${e.seq}><td class="dim">${g(e.at)}</td><td>${e.kind}</td><td class="grow">${e.text}</td><td class="dim">${e.repo??""}</td></tr>`,Zt=({inp:e})=>P(["at","kind","text","repo"],e.list.map(Js),"no notices");function es(e){let t=new Map;for(let s of e.prs){let n=t.get(s.stackId)||[];n.push(s),t.set(s.stackId,n)}return e.stacks.map(s=>({s,prs:t.get(s.id)||[]})).filter(s=>s.prs.length)}var ts=e=>e.numbers.map(t=>"#"+t).join(" \\u2192 "),ss=e=>e.state+(e.draft?" \\xB7 draft":"")+(e.state==="MERGED"&&e.mergedAt?" \\xB7 "+e.mergedAt:"");function ns(e){let t=n=>{let a=String(n??"");return a.length>24?a.slice(0,23)+"\\u2026":a},s=e.watches.map(n=>i`<tr key=${n.key}><td>${n.key}</td><td>${n.owner}</td><td title=${n.cursor}>${t(n.cursor)}</td><td>${n.lastCheckedAt&&g(n.lastCheckedAt)}</td><td>${n.lastError||""}</td></tr>`);return[i`<h2>other watches</h2>`,P(["key","owner","cursor","last check","error"],s,"no other watches")]}function Xs(e){let t=es(e).flatMap(({s,prs:n})=>[i`<tr key=${"stack:"+s.id}><th colspan="8">${ts(s)} \xB7 floor ${s.floor}${s.open?" \\xB7 open":" \\xB7 history"}</th></tr>`,...n.map(a=>i`<tr key=${a.key} class="rowhead" onClick=${y("pr",a.key)}><td>${me(a)}</td><td class="grow">${a.title||""}</td><td>${ss(a)}</td><td>${X(a)}</td><td>${ve(a)}</td><td>${Q(a)}</td><td>${Me(a.watch)}</td><td>${a.gate||""}</td></tr>`)]);return[P(["PR","title","state","checks","review","merge","watch","gate"],t,"no PR evidence"),ns(e)]}function Qs(e){let t=es(e),s=a=>i`<div key=${a.key} class="card" onClick=${y("pr",a.key)}><div class="top"><b>#${a.number} ${a.title||""}</b><span class=${"badge "+I(a.badge)}>${a.badge.text}</span></div><div class="meta">${a.repo} \xB7 ${ss(a)} \xB7 ${Q(a)}</div><div class="foot"><span>${X(a)}</span><span>${Me(a.watch)}</span>${a.gate&&i`<span>gate ${a.gate}</span>`}</div></div>`;return[t.length?t.map(({s:a,prs:r})=>[i`<h2>${ts(a)} \xB7 floor ${a.floor}${a.open?"":" \\xB7 history"}</h2>`,i`<div class="cards">${r.map(s)}</div>`]):i`<div class="empty">no PR evidence</div>`,ns(e)]}var rs=({inp:e})=>e.view==="cards"?Qs(e):Xs(e);function Ys(e){let t=H(e);return i`<tr key=${e.trapId} class=${"rowhead"+(e.live?"":" dim")} onClick=${y("trap",e.trapId)}><td><b>wt:${e.trapId}</b></td><td>${e.repo??"\\u2014"}</td><td class="grow">${e.worktree??""}</td><td>${e.harness??""}</td><td>${(e.sessionId??"").slice(0,8)}</td><td>${t.listen}</td><td>${t.hb}</td><td>${De(e)}</td><td>${e.catches.length}</td></tr>`}function Zs(e){let t=H(e),s=De(e);return i`<div key=${e.trapId} class=${"card"+(e.live?"":" dim")} onClick=${y("trap",e.trapId)}><div class="top"><b>\u{1FAA4} wt:${e.trapId}</b><span class="badge">${e.harness??(e.live?"":"signed off")}</span></div><div class="meta">${e.repo??(e.live?"addressed bait only":"history")}${e.sessionId&&" \\xB7 session "+e.sessionId.slice(0,8)}</div>${e.worktree&&i`<div class="note">${e.worktree}</div>`}<div class="foot">${t.listen}${e.live&&[" \\xB7 heartbeat ",t.hb]} \xB7 ${e.catches.length} catch${e.catches.length===1?"":"es"}${s&&[" \\xB7 ",s]}</div></div>`}function os({inp:e}){let t=e.list.map(s=>s.x);return e.view==="cards"?t.length?i`<div class="cards">${t.map(Zs)}</div>`:i`<div class="empty">no traps soaking</div>`:P(["address","repo","worktree","harness","session","listening","heartbeat","mail","catches"],t.map(Ys),"no traps soaking")}function as({state:e}){let t=e.snapshot,s=e.route,n=t&&it(t,{st:e.prefs,modal:e.modal},Date.now()),a=o=>!n||o!==s?null:o==="deck"?i`<${Ft} inp=${n.deck} />`:o==="dispatches"?i`<${Wt} inp=${n.dispatches} />`:o==="traps"?i`<${os} inp=${n.traps} />`:o==="prs"?i`<${rs} inp=${n.prs} />`:i`<${Zt} inp=${n.notices} />`,r=o=>{o.target===o.currentTarget&&j()};return[i`<${qt} d=${t} inp=${n} tab=${s} st=${e.prefs} stale=${e.stale} />`,K.map(o=>i`<main key=${o} id=${"page-"+o} class=${t&&o===s?"tabpage on":"tabpage"}><div id=${o}>${a(o)}</div></main>`),i`<footer id="foot"><${Vt} d=${t} /></footer>`,i`<div id="lobs"><${Jt} state=${e} /></div>`,i`<div id="overlay" class=${e.modal&&t?"open":t?"":void 0} onClick=${r}><div class="modal" id="modalbox"><${Yt} snapshot=${t} modal=${e.modal} prefs=${e.prefs} /></div></div>`]}var Ne=!1,ee=null;async function He(){if(!Ne){Ne=!0;try{let e=await fetch("/data");vt(await e.json())}catch{gt()}finally{Ne=!1}}}var is=()=>{ee||(ee=setInterval(He,2e3))},en=()=>{ee&&clearInterval(ee),ee=null};function ls(){document.addEventListener("visibilitychange",()=>{document.hidden?en():(He(),is())}),He(),document.hidden||is()}var Be=()=>at(location.hash),cs=e=>window.addEventListener("hashchange",()=>e(Be()));ht({snapshot:void 0,route:Be(),prefs:dt(),modal:null,stale:!1,lobHidden:ut(),spriteOk:null,preview:new URLSearchParams(location.search).has("lob")});var ds=()=>nt(V(as,{state:N()}),document.body);mt(ds);ds();var Ue=new Image;Ue.onload=()=>G({spriteOk:!0});Ue.onerror=()=>G({spriteOk:!1});Ue.src="/lob-sprite.png";cs(e=>G({route:e}));document.addEventListener("keydown",e=>{e.key==="Escape"&&j()});ls();})();</script>\n </body>\n</html>\n';
7022
7500
 
7023
7501
  // apps/cli/src/glass.ts
7024
7502
  var REPO_URL = "https://github.com/aequitas-labs/lobstah";
7025
7503
  var readJson3 = (f) => {
7026
7504
  try {
7027
- return JSON.parse(fs36.readFileSync(f, "utf8"));
7505
+ return JSON.parse(fs38.readFileSync(f, "utf8"));
7028
7506
  } catch {
7029
7507
  return void 0;
7030
7508
  }
7031
7509
  };
7032
7510
  var listDir = (d) => {
7033
7511
  try {
7034
- return fs36.readdirSync(d);
7512
+ return fs38.readdirSync(d);
7035
7513
  } catch {
7036
7514
  return [];
7037
7515
  }
7038
7516
  };
7039
7517
  var mtime = (f) => {
7040
7518
  try {
7041
- return fs36.statSync(f).mtimeMs;
7519
+ return fs38.statSync(f).mtimeMs;
7042
7520
  } catch {
7043
7521
  return 0;
7044
7522
  }
@@ -7047,7 +7525,7 @@ function assetPath(name) {
7047
7525
  for (const rel of [`../docs/assets/${name}`, `../../../docs/assets/${name}`]) {
7048
7526
  try {
7049
7527
  const p = fileURLToPath2(new URL(rel, import.meta.url));
7050
- if (fs36.existsSync(p)) return p;
7528
+ if (fs38.existsSync(p)) return p;
7051
7529
  } catch {
7052
7530
  }
7053
7531
  }
@@ -7055,34 +7533,34 @@ function assetPath(name) {
7055
7533
  }
7056
7534
  function transcriptPath(harness, cwd, sessionId) {
7057
7535
  if (harness !== "claude" || !cwd || !sessionId) return void 0;
7058
- const p = path29.join(os6.homedir(), ".claude", "projects", cwd.replace(/[^A-Za-z0-9-]/g, "-"), `${sessionId}.jsonl`);
7059
- return fs36.existsSync(p) ? p : void 0;
7536
+ const p = path31.join(os8.homedir(), ".claude", "projects", cwd.replace(/[^A-Za-z0-9-]/g, "-"), `${sessionId}.jsonl`);
7537
+ return fs38.existsSync(p) ? p : void 0;
7060
7538
  }
7061
7539
  function messageAttachments(dir) {
7062
- return [dir, path29.join(dir, "handled")].flatMap(
7063
- (folder) => listDir(folder).filter((file) => file.endsWith(".meta.json")).flatMap((file) => readJson3(path29.join(folder, file))?.attachments ?? [])
7540
+ return [dir, path31.join(dir, "handled")].flatMap(
7541
+ (folder) => listDir(folder).filter((file) => file.endsWith(".meta.json")).flatMap((file) => readJson3(path31.join(folder, file))?.attachments ?? [])
7064
7542
  );
7065
7543
  }
7066
7544
  function trapMessages(trapId) {
7067
- const dir = path29.join(laneDirs("work").inbox, `trap-${trapId}`);
7545
+ const dir = path31.join(laneDirs("work").inbox, `trap-${trapId}`);
7068
7546
  const parse2 = (file, state) => {
7069
7547
  let raw;
7070
7548
  try {
7071
- raw = fs36.readFileSync(file, "utf8");
7549
+ raw = fs38.readFileSync(file, "utf8");
7072
7550
  } catch {
7073
7551
  return void 0;
7074
7552
  }
7075
7553
  const attachments = readJson3(file.replace(/\.msg$/, ".meta.json"))?.attachments;
7076
7554
  try {
7077
7555
  const p = JSON.parse(raw);
7078
- return { file: path29.basename(file), state, from: p.from ?? "unknown", at: p.at ?? "", text: p.text ?? "", attachments };
7556
+ return { file: path31.basename(file), state, from: p.from ?? "unknown", at: p.at ?? "", text: p.text ?? "", attachments };
7079
7557
  } catch {
7080
- return { file: path29.basename(file), state, from: "unknown", at: "", text: raw, attachments };
7558
+ return { file: path31.basename(file), state, from: "unknown", at: "", text: raw, attachments };
7081
7559
  }
7082
7560
  };
7083
7561
  const rows = [
7084
- ...listDir(dir).filter((f) => f.endsWith(".msg")).map((f) => parse2(path29.join(dir, f), "pending")),
7085
- ...listDir(path29.join(dir, "handled")).filter((f) => f.endsWith(".msg")).map((f) => parse2(path29.join(dir, "handled", f), "delivered"))
7562
+ ...listDir(dir).filter((f) => f.endsWith(".msg")).map((f) => parse2(path31.join(dir, f), "pending")),
7563
+ ...listDir(path31.join(dir, "handled")).filter((f) => f.endsWith(".msg")).map((f) => parse2(path31.join(dir, "handled", f), "delivered"))
7086
7564
  ];
7087
7565
  return rows.filter((m) => m !== void 0).sort((a, b) => a.file.localeCompare(b.file));
7088
7566
  }
@@ -7091,16 +7569,16 @@ function dispatchRows() {
7091
7569
  for (const lane of ["work", "chore"]) {
7092
7570
  const dirs = laneDirs(lane);
7093
7571
  for (const id of pendingIds(lane)) {
7094
- const d = readJson3(path29.join(dirs.queue, `${id}.json`));
7095
- if (d) rows.push({ lane, bucket: "queued", d, sort: mtime(path29.join(dirs.queue, `${id}.json`)) });
7572
+ const d = readJson3(path31.join(dirs.queue, `${id}.json`));
7573
+ if (d) rows.push({ lane, bucket: "queued", d, sort: mtime(path31.join(dirs.queue, `${id}.json`)) });
7096
7574
  }
7097
7575
  for (const id of activeIds(lane)) {
7098
- const d = readJson3(path29.join(dirs.active, id, "descriptor.json"));
7099
- if (d) rows.push({ lane, bucket: "active", d, sort: mtime(path29.join(dirs.active, id)) });
7576
+ const d = readJson3(path31.join(dirs.active, id, "descriptor.json"));
7577
+ if (d) rows.push({ lane, bucket: "active", d, sort: mtime(path31.join(dirs.active, id)) });
7100
7578
  }
7101
- const done = listDir(dirs.done).filter((f) => !f.startsWith(".")).map((id) => ({ id, at: mtime(path29.join(dirs.done, id)) })).sort((a, b) => b.at - a.at);
7579
+ const done = listDir(dirs.done).filter((f) => !f.startsWith(".")).map((id) => ({ id, at: mtime(path31.join(dirs.done, id)) })).sort((a, b) => b.at - a.at);
7102
7580
  for (const { id, at } of done) {
7103
- const d = readJson3(path29.join(dirs.done, id, "descriptor.json"));
7581
+ const d = readJson3(path31.join(dirs.done, id, "descriptor.json"));
7104
7582
  if (d) rows.push({ lane, bucket: "done", d, sort: at });
7105
7583
  }
7106
7584
  }
@@ -7110,7 +7588,7 @@ function dispatchRows() {
7110
7588
  const last = log.at(-1);
7111
7589
  const claim = r.bucket === "active" ? readSessionClaim(id, r.lane) : void 0;
7112
7590
  const evidence = readEvidence(id, r.lane);
7113
- const inboxDir2 = path29.join(laneDirs(r.lane).inbox, id);
7591
+ const inboxDir2 = path31.join(laneDirs(r.lane).inbox, id);
7114
7592
  return {
7115
7593
  id,
7116
7594
  lane: r.lane,
@@ -7121,12 +7599,16 @@ function dispatchRows() {
7121
7599
  brief: r.d.brief,
7122
7600
  attachments: r.d.attachments ?? [],
7123
7601
  messageAttachments: messageAttachments(inboxDir2),
7124
- verb: last?.verb ?? "unknown",
7602
+ // A queued descriptor with no log is waiting, not unknown; its time
7603
+ // is the queue time.
7604
+ // An active dispatch a trap claimed, with no log, is working since
7605
+ // the claim.
7606
+ verb: r.bucket === "queued" && log.length === 0 ? "queued" : last?.verb ?? (claim ? "working" : "unknown"),
7125
7607
  note: last?.note,
7126
- verbAt: last?.at,
7608
+ verbAt: last?.at ?? (r.bucket === "queued" ? queuedAt(id, r.lane) : claim?.at),
7127
7609
  claimedBy: claim?.by,
7128
7610
  log,
7129
- inbox: listDir(inboxDir2).filter((f) => f.endsWith(".msg")).sort().map((f) => fs36.readFileSync(path29.join(inboxDir2, f), "utf8").trim()),
7611
+ inbox: listDir(inboxDir2).filter((f) => f.endsWith(".msg")).sort().map((f) => fs38.readFileSync(path31.join(inboxDir2, f), "utf8").trim()),
7130
7612
  evidence: Object.keys(evidence).length > 0 ? evidence : void 0,
7131
7613
  transcript: transcriptPath(claim?.harness, claim?.worktree, claim?.sessionId),
7132
7614
  sort: r.sort
@@ -7211,500 +7693,33 @@ function buildGlassSnapshot() {
7211
7693
  mergeView
7212
7694
  };
7213
7695
  }
7214
- var PAGE = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
7215
- <title>spyglass</title>
7216
- <link rel="icon" type="image/png" href="/icon.png">
7217
- <style>
7218
- :root{--bg:#0e1116;--card:#161b22;--line:#2b3240;--fg:#dbe2ea;--dim:#8b96a5;--ok:#4fc17c;--warn:#e2b93d;--bad:#e26d5c;--link:#6cb2e2;
7219
- /* GitHub's dark-mode PR state colors (light: merged #8250df, open #1f883d, draft #6e7781, closed #cf222e) */
7220
- --pr-merged:#8957e5;--pr-merged-fg:#ab7df8;--pr-open:#238636;--pr-draft:#6e7681;--pr-closed:#da3633}
7221
- *{box-sizing:border-box}
7222
- body{margin:0;background:var(--bg);color:var(--fg);font:13px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;padding-block:14px;padding-inline:16px;
7223
- /* sticky footer: a full-height column whose active tab grows, so the footer sits at the viewport bottom on short pages */
7224
- display:flex;flex-direction:column;min-height:100vh}
7225
- h1{font-size:15px;margin:0 0 10px}h1 .dim{color:var(--dim);font-weight:normal}
7226
- h2{font-size:12px;color:var(--dim);text-transform:uppercase;letter-spacing:.08em;margin:20px 0 6px}
7227
- .chips,.controls{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
7228
- .controls{margin:0 0 14px}
7229
- .chips{margin-bottom:8px}
7230
- .chip{background:var(--card);border:1px solid var(--line);border-radius:6px;padding:3px 9px}
7231
- .controls select,.controls input[type=search]{background:var(--card);border:1px solid var(--line);border-radius:6px;color:var(--fg);font:inherit;padding:4px 8px}
7232
- .controls input[type=search]{flex:1 1 140px;min-width:120px;max-width:340px}
7233
- #chain-control{display:inline-flex;align-items:center;gap:7px;white-space:nowrap}
7234
- #f-chain{margin:0;width:14px;height:14px;accent-color:var(--link)}
7235
- .tabs{display:flex;gap:4px;border-bottom:1px solid var(--line);margin:8px 0 10px}
7236
- .tabs a{color:var(--dim);padding:6px 11px;border-bottom:2px solid transparent}
7237
- .tabs a.on{color:var(--fg);border-color:var(--link)}
7238
- .tabpage{display:none}.tabpage.on{display:block;flex:1 0 auto}
7239
- .headerline{display:flex;align-items:center;gap:8px}.headerline h1{flex:1}
7240
- #settings-slot{min-width:34px;text-align:right;color:var(--dim)}
7241
- #settings-slot #gearbtn{margin-left:0}
7242
- .deckgrid{display:flex;flex-direction:column;gap:12px}
7243
- .deckgrid h2{margin:5px 0}.deckgrid section{width:100%;min-width:0}
7244
- .deckline{padding:2px 0;border-top:1px solid var(--line);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
7245
- .deckline.acked,.card.acked,.rowhead.acked,.badge.acked{opacity:.55}.deckmore{color:var(--dim);font-size:11px;display:inline-block;margin-top:4px}
7246
- .deckline.click{cursor:pointer}.deckline.click:hover{background:#1c2330}
7247
- .deckgrid h2 a{color:inherit}.deckgrid h2 a:hover{color:var(--fg)}
7248
- .deckgrid .cards{grid-template-columns:repeat(auto-fill,minmax(min(240px,100%),1fr));gap:10px}
7249
- .deckstack{margin-bottom:9px}.deckstack>.dim{font-size:11px;margin:3px 0 5px}
7250
- .seg{display:inline-flex;border:1px solid var(--line);border-radius:6px;overflow:hidden}
7251
- .seg button{background:var(--card);border:none;color:var(--dim);font:inherit;padding:4px 11px;cursor:pointer}
7252
- .seg button.on{background:#26436b;color:var(--fg)}
7253
- .wrap{overflow-x:auto}
7254
- table{border-collapse:collapse;width:100%;background:var(--card);border:1px solid var(--line);border-radius:6px;overflow:hidden}
7255
- th,td{text-align:left;padding:4px 9px;border-top:1px solid var(--line);vertical-align:top;white-space:nowrap}
7256
- td.grow{white-space:normal;word-break:break-word;min-width:140px}
7257
- th{color:var(--dim);font-weight:normal;border-top:none;font-size:11px}
7258
- tr.rowhead{cursor:pointer}tr.rowhead:hover{background:#1c2330}
7259
- .cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(300px,100%),1fr));gap:12px}
7260
- /* One card style for every tab: even inner padding, and a top row whose badge never touches the id or the edge. */
7261
- .card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:12px 14px;cursor:pointer;min-width:0}
7262
- .card:hover{border-color:#3a455a}
7263
- .card .top{display:flex;justify-content:space-between;gap:12px;align-items:center;min-height:22px}
7264
- .card .top>b{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
7265
- .card .top .badge{flex-shrink:0}
7266
- .card .meta{color:var(--dim);font-size:12px;margin-top:2px}
7267
- .card .note{margin-top:6px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
7268
- .card .foot{margin-top:8px;display:flex;gap:10px;flex-wrap:wrap;color:var(--dim);font-size:12px}
7269
- .badge{border-radius:5px;padding:1px 7px;font-size:11px;line-height:1.5;border:1px solid var(--line);white-space:nowrap;display:inline-block}
7270
- .badge.pr-merged{background:var(--pr-merged);border-color:var(--pr-merged-fg);color:#fff}.badge.pr-open{background:var(--pr-open);border-color:var(--pr-open);color:#fff}
7271
- .badge.pr-draft{background:var(--pr-draft);border-color:var(--pr-draft);color:#fff}.badge.pr-closed{background:var(--pr-closed);border-color:var(--pr-closed);color:#fff}
7272
- .badge.pr-conflicts{background:var(--pr-closed);border-color:var(--pr-closed);color:#fff}.badge.pr-behind{background:var(--pr-draft);border-color:var(--pr-draft);color:#fff}
7273
- .badge.unreported{font-weight:normal;font-size:10px;padding:0 5px;vertical-align:1px}
7274
- .v-done{color:var(--ok)}.v-working{color:var(--fg)}.v-needs-decision,.v-blocked{color:var(--bad);font-weight:bold}
7275
- .v-failed{color:var(--bad)}.v-paused,.v-unknown{color:var(--dim)}
7276
- .dim{color:var(--dim)}.ok{color:var(--ok)}.warn{color:var(--warn)}.bad{color:var(--bad)}
7277
- .detail{display:none}.detail.open{display:table-row}.detail td{background:#10151c;white-space:pre-wrap;word-break:break-word;font-size:12px;color:var(--dim)}
7278
- .detail b{color:var(--fg)}
7279
- .empty{color:var(--dim);padding:6px 2px}
7280
- #stale{display:none;color:var(--bad)}
7281
- a{color:var(--link);text-decoration:none}
7282
- .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:middle}
7283
- .dot.ok{background:var(--ok)}.dot.warn{background:var(--warn)}.dot.bad{background:var(--bad)}
7284
- #overlay{position:fixed;inset:0;background:rgba(4,7,11,.72);display:none;z-index:10;padding:28px 16px;overflow:auto}
7285
- #overlay.open{display:block}
7286
- .modal{max-width:780px;margin:0 auto;background:var(--card);border:1px solid var(--line);border-radius:10px;padding:16px 18px;position:relative}
7287
- .modal .x{position:absolute;top:8px;right:12px;cursor:pointer;color:var(--dim);font-size:18px;line-height:1}
7288
- .modal .x:hover{color:var(--fg)}
7289
- .modal h3{margin:0 24px 2px 0;font-size:14px}
7290
- .modal .sub{color:var(--dim);font-size:12px;margin-bottom:10px}
7291
- .modal .sec{color:var(--dim);text-transform:uppercase;letter-spacing:.08em;font-size:11px;margin:14px 0 4px}
7292
- .modal pre{white-space:pre-wrap;word-break:break-word;margin:0;font:inherit;color:var(--fg)}
7293
- .msg{border-left:3px solid var(--line);padding:3px 10px;margin:8px 0;white-space:pre-wrap;word-break:break-word}
7294
- .msg.from-helm{border-color:var(--link)}
7295
- .msg .hdr{color:var(--dim);font-size:11px}
7296
- .catch{border:1px solid var(--line);border-radius:6px;padding:6px 10px;margin:8px 0}
7297
- .catch .hdr{display:flex;gap:10px;flex-wrap:wrap;align-items:baseline}
7298
- .loglines{color:var(--dim);font-size:12px;white-space:pre-wrap;word-break:break-word;margin-top:4px}
7299
- .cmd{display:flex;gap:8px;align-items:center;margin:5px 0;flex-wrap:wrap}
7300
- .cmd code{background:#10151c;border:1px solid var(--line);border-radius:5px;padding:3px 8px;word-break:break-all;cursor:pointer}
7301
- .cmd code:hover{border-color:#3a455a}
7302
- .cmd code.copied{border-color:var(--ok)}
7303
- .cmd button{background:var(--card);border:1px solid var(--line);border-radius:5px;color:var(--dim);font:inherit;font-size:11px;padding:2px 8px;cursor:pointer}
7304
- .cmd button:hover{color:var(--fg)}
7305
- .chip.click{cursor:pointer}.chip.click:hover{border-color:#3a455a}
7306
- footer{margin-top:26px;padding-top:10px;border-top:1px solid var(--line);color:var(--dim);font-size:12px;display:flex;gap:8px;flex-wrap:wrap}
7307
- .lob{position:fixed;bottom:6px;left:0;z-index:5;cursor:pointer;font-size:34px;line-height:1;user-select:none;animation:crawl 18s linear infinite}
7308
- a.lob{color:inherit}
7309
- .lob .bub .badge{display:inline-block;margin:0 0 3px}
7310
- .lob .sprite{width:72px;height:56px;background:url(/lob-sprite.png) 0 0 no-repeat;background-size:400% 100%;image-rendering:pixelated;animation:step .5s steps(4) infinite}
7311
- .lob .fallback{display:inline-block;animation:waddle .45s ease-in-out infinite alternate}
7312
- .lob .bub{display:none;position:absolute;bottom:58px;right:-8px;z-index:2;background:var(--card);border:1px solid var(--line);border-radius:9px;padding:6px 9px 5px;font-size:11px;line-height:1.35;color:var(--fg);width:max-content;max-width:150px;box-shadow:0 2px 8px rgba(0,0,0,.4)}
7313
- .lob:hover .bub{display:block}
7314
- .lob .bub span{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;white-space:normal;word-break:break-word}
7315
- .lob .star{position:absolute;left:56px;top:-9px;height:14px;image-rendering:pixelated;z-index:3}
7316
- .lob:hover{animation-play-state:paused}
7317
- @keyframes crawl{0%{transform:translateX(-90px)}100%{transform:translateX(100vw)}}
7318
- @keyframes step{to{background-position-x:-288px}}
7319
- @keyframes waddle{from{transform:rotate(-8deg) translateY(0)}to{transform:rotate(8deg) translateY(-3px)}}
7320
- #gearbtn{background:none;border:1px solid var(--line);border-radius:6px;color:var(--dim);font:inherit;font-size:13px;padding:1px 7px;margin-left:8px;cursor:pointer;vertical-align:1px}
7321
- #gearbtn:hover,#gearbtn.on{color:var(--fg);border-color:#3a455a}
7322
- .settings .row{display:flex;justify-content:space-between;align-items:center;gap:14px;margin:12px 0}
7323
- .settings .lbl{color:var(--fg);font-size:12px}
7324
- .settings .hint{display:block;color:var(--dim);font-size:11px;margin-top:2px}
7325
- </style></head><body>
7326
- <div class="headerline"><h1>\u{1F99E}\u2728 spyglass<span id="stale"> \xB7 STALE FEED</span></h1><span id="settings-slot"><button id="gearbtn" title="settings" aria-label="settings">\u2699</button></span></div>
7327
- <div class="chips"><span id="chips" style="display:contents"></span><span class="chip dim" id="clock"></span></div>
7328
- <nav class="tabs" id="tabs" aria-label="Spyglass views"><a href="#deck" data-tab="deck">On deck</a><a href="#dispatches" data-tab="dispatches">Dispatches</a><a href="#traps" data-tab="traps">Traps</a><a href="#prs" data-tab="prs">PRs</a><a href="#notices" data-tab="notices">Notices</a></nav>
7329
- <div class="controls">
7330
- <select id="f-lane"><option value="">all lanes</option><option value="work">work</option><option value="chore">chore</option></select>
7331
- <select id="f-repo"><option value="">all repos</option></select>
7332
- <select id="f-verb"><option value="">all verbs</option><option>working</option><option>needs-decision</option><option>blocked</option><option>paused</option><option>done</option><option>failed</option><option>unknown</option></select>
7333
- <label id="chain-control"><input id="f-chain" type="checkbox"> group by chain</label>
7334
- <select id="f-kind"><option value="">all notice kinds</option></select>
7335
- <input id="f-q" type="search" placeholder="search id \xB7 note \xB7 brief">
7336
- </div>
7337
- <main id="page-deck" class="tabpage"><div id="deck"></div></main>
7338
- <main id="page-dispatches" class="tabpage"><div id="dispatches"></div></main>
7339
- <main id="page-traps" class="tabpage"><div id="traps"></div></main>
7340
- <main id="page-prs" class="tabpage"><div id="prs"></div></main>
7341
- <main id="page-notices" class="tabpage"><div id="notices"></div></main>
7342
- <footer id="foot"></footer>
7343
- <div id="lobs"></div>
7344
- <div id="overlay"><div class="modal" id="modalbox"></div></div>
7345
- <script>
7346
- const esc=(s)=>String(s??'').replace(/[&<>"]/g,(c)=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
7347
- const age=(iso)=>{if(!iso)return '';const s=Math.max(0,(Date.now()-Date.parse(iso))/1000);
7348
- if(s<90)return Math.round(s)+'s';if(s<5400)return Math.round(s/60)+'m';if(s<172800)return (s/3600).toFixed(1)+'h';return Math.round(s/86400)+'d'};
7349
- // Ages tick in place (refreshAges) so a quiet section never needs a rewrite.
7350
- const ageEl=(iso)=>'<span data-age="'+esc(iso)+'">'+age(iso)+'</span>';
7351
- function refreshAges(){for(const el of document.querySelectorAll('[data-age]')){const t=age(el.dataset.age);if(el.textContent!==t)el.textContent=t}}
7352
- ${GLASS_DIFF_JS}const open=new Set();
7353
- let modal=null;
7354
- let st={view:'table',lane:'',repo:'',verb:'',q:'',lobs:true,chain:false,noticeKind:''};
7355
- try{Object.assign(st,JSON.parse(localStorage.getItem('spyglass')||'{}'))}catch(e){}
7356
- // A stored value this page doesn't understand falls back to the default.
7357
- if(st.view!=='table'&&st.view!=='cards')st.view='table';
7358
- st.lobs=st.lobs!==false;
7359
- const save=()=>{try{localStorage.setItem('spyglass',JSON.stringify(st))}catch(e){}};
7360
- function table(headers,rows,empty){if(!rows.length)return '<div class="empty">'+empty+'</div>';
7361
- return '<div class="wrap"><table><tr>'+headers.map(h=>'<th>'+h+'</th>').join('')+'</tr>'+rows.join('')+'</table></div>'}
7362
- window.copyCmd=async(el,text)=>{try{await navigator.clipboard.writeText(text)}catch(e){return}
7363
- if(el.tagName==='BUTTON'){el.textContent='\u2713';setTimeout(()=>{el.textContent='\u29C9'},1000)}
7364
- else{el.classList.add('copied');setTimeout(()=>el.classList.remove('copied'),1000)}};
7365
- function cmdRow(text){const j=JSON.stringify(text).replace(/"/g,'&quot;');
7366
- return '<div class="cmd"><code title="click to copy" onclick="copyCmd(this,'+j+')">'+esc(text)+'</code><button title="copy" onclick="copyCmd(this,'+j+')">\u29C9</button></div>'}
7367
- function attachmentRows(items){return items.map(a=>'<div class="sub">'+esc(a.name)+' \xB7 '+esc(a.type)+' \xB7 '+esc(a.bytes)+' bytes</div>'+cmdRow(a.path)).join('')}
7368
- function detailBody(x){return '<div class="sec">brief</div><pre>'+esc(x.brief)+'</pre>'
7369
- +(x.attachments.length?'<div class="sec">attachments ('+x.attachments.length+')</div>'+attachmentRows(x.attachments):'')
7370
- +(x.messageAttachments.length?'<div class="sec">message attachments ('+x.messageAttachments.length+')</div>'+attachmentRows(x.messageAttachments):'')
7371
- +(x.followUp?'<div class="sec">forks</div><pre>'+esc(x.followUp)+'</pre>':'')
7372
- +'<div class="sec">log</div><div class="loglines">'+(x.log.length?x.log.map(e=>esc(e.at)+' '+esc(e.verb)+(e.note?' '+esc(e.note):'')).join('\\n'):'no entries yet')+'</div>'
7373
- +(x.inbox.length?'<div class="sec">inbox</div><div class="loglines">'+x.inbox.map(esc).join('\\n---\\n')+'</div>':'')
7374
- +(x.evidence?'<div class="sec">evidence</div><div class="loglines">'+esc(JSON.stringify(x.evidence))+'</div>':'')}
7375
- function tableDetail(x){return '<b>brief</b>\\n'+esc(x.brief)
7376
- +(x.attachments.length?'<div class="sec">attachments ('+x.attachments.length+')</div>'+attachmentRows(x.attachments):'')
7377
- +(x.messageAttachments.length?'<div class="sec">message attachments ('+x.messageAttachments.length+')</div>'+attachmentRows(x.messageAttachments):'')
7378
- +(x.followUp?'\\n<b>forks</b> '+esc(x.followUp):'')
7379
- +'\\n<b>log</b>\\n'+x.log.map(e=>esc(e.at)+' '+esc(e.verb)+(e.note?' '+esc(e.note):'')).join('\\n')
7380
- +(x.inbox.length?'\\n<b>inbox</b>\\n'+x.inbox.map(esc).join('\\n---\\n'):'')
7381
- +(x.evidence?'\\n<b>evidence</b> '+esc(JSON.stringify(x.evidence)):'')}
7382
- function addrCell(x){return x.for?esc(x.for)+(x.evidence&&x.evidence.deliveredTo?' <span class="ok">\u2713delivered</span>':' <span class="warn">waiting</span>'):(x.claimedBy?esc(x.claimedBy):'')}
7383
- function prCell(x){const url=x.evidence&&(x.evidence.prUrl||x.evidence.pr&&x.evidence.pr.url);if(!url)return '';
7384
- const b=x.prBadge;
7385
- return '<a href="'+esc(url)+'" target="_blank" rel="noopener" onclick="event.stopPropagation()">PR</a>'
7386
- +(b?' <span class="badge '+esc(prBadgeClass(b))+'" title="observed '+esc(b.observedAt)+'">'+esc(b.text)+'</span>':'')
7387
- +(x.prGate?' <span class="badge dim" title="merge gate (pick)">'+esc(x.prGate)+'</span>':'')}
7388
- function chainRows(list){if(!st.chain)return list;
7389
- const byId=new Map(list.map(x=>[x.id,x]));
7390
- const root=(x)=>{let at=x,seen=new Set();while(at.followUp&&byId.has(at.followUp)&&!seen.has(at.id)){seen.add(at.id);at=byId.get(at.followUp)}return at.id};
7391
- const groups=new Map();for(const x of list){const k=root(x),g=groups.get(k)||[];g.push(x);groups.set(k,g)}
7392
- return [...groups.values()].sort((a,b)=>b[0].sort-a[0].sort).flatMap(g=>g.sort((a,b)=>a.id===root(a)?-1:b.id===root(b)?1:a.sort-b.sort))}
7393
- function dispatchTable(list){return table(['id','lane / bucket','repo','verb','note','age','addressed','pr'],
7394
- chainRows(list).map(x=>{const k=x.lane+':'+x.id;
7395
- return '<tr class="rowhead" onclick="showModal(\\'dispatch\\',\\''+k+'\\')"><td>'+(st.chain&&x.followUp?'<span class="dim">\u21B3 </span>':'')+esc(x.id.slice(0,8))+'</td><td>'+x.lane+' / '+x.bucket+'</td><td>'+esc(x.repo)+'</td><td class="v-'+x.verb+'">'+x.verb+'</td><td class="grow">'+esc((x.note??'').slice(0,90))+'</td><td>'+ageEl(x.verbAt)+'</td><td>'+addrCell(x)+'</td><td>'+prCell(x)+'</td></tr>'}),
7396
- 'no dispatches match')}
7397
- function dispatchCards(list){if(!list.length)return '<div class="empty">no dispatches match</div>';
7398
- return '<div class="cards">'+list.map(x=>{const k=x.lane+':'+x.id;
7399
- return '<div class="card" onclick="showModal(\\'dispatch\\',\\''+k+'\\')"><div class="top"><b>'+esc(x.id.slice(0,8))+'</b><span class="badge v-'+x.verb+'">'+x.verb+'</span></div>'
7400
- +'<div class="meta">'+esc(x.repo)+' \xB7 '+x.lane+' '+x.bucket+' \xB7 '+ageEl(x.verbAt)+'</div>'
7401
- +(x.note?'<div class="note">'+esc(x.note)+'</div>':'')
7402
- +'<div class="foot">'+addrCell(x)+' '+prCell(x)+'</div></div>'}).join('')+'</div>'}
7403
- // Attention kinds (tend's contract): the short label shown in lobs, the pet, and the table.
7404
- const KIND_LABEL={'pr:draft':'draft','pr:review':'review','pr:checks':'checks','pr:conflict':'conflicts','pr:ready':'ready',landed:'landed',watch:'watch'};
7405
- const KIND_TONE={'pr:review':'bad','pr:checks':'bad','pr:conflict':'pr-conflicts','pr:ready':'ok','pr:draft':'dim',watch:'warn'};
7406
- const kindLabel=(k)=>KIND_LABEL[k]||'';
7407
- function kindCell(x){
7408
- if(x.kind==='question')return '<span class="v-'+x.verb+'">'+x.verb+'</span>';
7409
- const tone=x.kind==='landed'?(x.verb==='failed'?'bad':'ok'):(KIND_TONE[x.kind]||'dim');
7410
- return '<span class="badge '+tone+'">'+esc(kindLabel(x.kind)||x.kind)+'</span>'+(x.kind==='landed'?' <span class="dim">'+esc(x.verb)+'</span>':'')}
7411
- // On deck's dispatch, trap, and landed items follow the site-wide view.
7412
- // Attention is always a notices table; PRs have their own stack presentation.
7413
- function deckItem(it,view){
7414
- const badge=it.badge?'<span class="badge '+esc(it.badge.tone||'dim')+'">'+esc(it.badge.text)+'</span>':'';
7415
- const click=it.open?' onclick="'+it.open+'"':'';
7416
- if(view==='cards')return '<div class="card'+(it.acked?' acked':'')+'"'+click+(it.open?'':' style="cursor:default"')+'><div class="top"><b>'+it.title+'</b>'+badge+'</div>'
7417
- +(it.meta?'<div class="meta">'+it.meta+'</div>':'')+'</div>';
7418
- return '<div class="deckline'+(it.open?' click':'')+(it.acked?' acked':'')+'"'+click+'>'+(badge?badge+' ':'')+'<b>'+it.title+'</b>'+(it.meta?' <span class="dim">\xB7 '+it.meta+'</span>':'')+'</div>'}
7419
- function deckBlock(title,items,tab,max,view){const shown=items.slice(0,max),more=items.length-shown.length;
7420
- const body=shown.length?(view==='cards'?'<div class="cards">'+shown.map(i=>deckItem(i,view)).join('')+'</div>':shown.map(i=>deckItem(i,view)).join('')):'<div class="empty">none</div>';
7421
- return '<section><h2><a href="#'+tab+'">'+title+' \u2192</a></h2>'+body+(more?'<a class="deckmore" href="#'+tab+'">+'+more+' more \u2192</a>':'')+'</section>'}
7422
- const openDispatch=(lane,id)=>"showModal(\\'dispatch\\',\\'"+esc(lane+':'+id)+"\\')";
7423
- function deckNotices(list){const shown=list.slice(0,4),more=list.length-shown.length;
7424
- const rows=shown.map(x=>'<tr class="rowhead'+(x.acked?' acked':'')+'" onclick="'+openDispatch(x.lane,x.id)+'"><td>'+kindCell(x)+'</td><td class="grow">'+esc(x.note||x.verb)+'</td><td>'+esc(x.repo||'')+'</td><td>'+ageEl(x.at)+'</td></tr>');
7425
- return '<section><h2><a href="#notices">attention \u2192</a></h2>'+table(['kind','note','repo','age'],rows,'none')
7426
- +(more?'<a class="deckmore" href="#notices">+'+more+' more \u2192</a>':'')+'</section>'}
7427
- function deckPrs(inp,view){const shown=inp.stacks.slice(0,3),more=inp.stacks.length-shown.length;
7428
- const standing=new Map();for(const a of inp.prAttention){const kinds=standing.get(a.key)||[];kinds.push(a);standing.set(a.key,kinds)}
7429
- const groups=shown.map(s=>({s,members:inp.prs.filter(p=>p.stackId===s.id).sort((a,b)=>a.position-b.position)}));
7430
- const body=groups.length?groups.map(({s,members})=>{const next=members.find(p=>p.number===s.nextNumber)||members[0];
7431
- const chain=esc(s.numbers.map(n=>'#'+n).join(' \u2192 '));
7432
- if(view==='cards')return '<div class="deckstack"><div class="dim">'+chain+' \xB7 '+(next?'next #'+next.number:'nothing mergeable')+'</div><div class="cards">'
7433
- +members.map(p=>{const kinds=standing.get(p.key)||[];const acked=kinds.length>0&&kinds.every(a=>a.acked);
7434
- const badges=kinds.length?kinds.map(a=>'<span class="badge '+esc(KIND_TONE[a.kind]||'dim')+'">'+esc(kindLabel(a.kind))+'</span>').join('')
7435
- :'<span class="badge '+esc(prBadgeClass(p.badge))+'">'+esc(p.badge.text)+'</span>';
7436
- return '<div class="card'+(acked?' acked':'')+'" onclick="'+prOpen(p)+'"><div class="top"><b>#'+p.number+' '+esc(p.title||'')+'</b>'+badges+'</div>'
7437
- +'<div class="meta">'+esc(p.repo)+' \xB7 '+esc(p.badge.text)+(acked?' \xB7 acked':'')+'</div></div>'}).join('')+'</div></div>';
7438
- const badges=members.flatMap(p=>(standing.get(p.key)||[]).map(a=>'<span class="badge '+esc(KIND_TONE[a.kind]||prBadgeClass(p.badge))+(a.acked?' acked':'')+'">#'+p.number+' '+esc(p.badge.text)+'</span>')).join(' ');
7439
- return '<div class="deckline'+(next?' click':'')+'"'+(next?' onclick="'+prOpen(next)+'"':'')+'><b>'+chain+'</b> <span class="dim">\xB7 '+(next?'next #'+next.number:'nothing mergeable')+'</span>'+(badges?' \xB7 '+badges:'')+'</div>'}).join(''):'<div class="empty">none</div>';
7440
- return '<section><h2><a href="#prs">PRs \u2192</a></h2>'+body+(more?'<a class="deckmore" href="#prs">+'+more+' more \u2192</a>':'')+'</section>'}
7441
- function renderDeck(d,inp){const view=inp.view;
7442
- const flight=inp.inflight.map(x=>({title:esc(x.id.slice(0,8))+' '+esc(x.repo||''),badge:{text:x.verb,tone:x.verb==='needs-decision'||x.verb==='blocked'?'bad':'dim'},
7443
- meta:esc((x.note||'').slice(0,90))+' \xB7 '+ageEl(x.verbAt)+' ago'+(x.for||x.claimedBy?' \xB7 '+esc(x.for||x.claimedBy):''),open:openDispatch(x.lane,x.id)}));
7444
- const landed=inp.landed.map(x=>({title:esc(x.id.slice(0,8))+' '+esc(x.repo||'')+(x.unreported?' <span class="badge warn unreported">unreported</span>':''),
7445
- badge:{text:x.verb,tone:x.verb==='failed'?'bad':'ok'},
7446
- meta:esc((x.note||'').slice(0,90))+' \xB7 '+ageEl(x.at)+' ago',open:openDispatch(x.lane,x.id)}));
7447
- const traps=inp.traps.map(({x:t})=>({title:'\u{1FAA4} wt:'+esc(t.trapId),badge:{text:t.live?(t.harness||'live'):'signed off',tone:t.live?'ok':'dim'},
7448
- meta:esc(t.repo||'')+' \xB7 '+(t.live?trapRow(t).listen:'stowed / ghosted'),open:"showModal(\\'trap\\',\\'"+esc(t.trapId)+"\\')"}));
7449
- return '<div class="deckgrid">'+deckNotices(inp.attention)+deckBlock('in flight',flight,'dispatches',4,view)
7450
- +deckBlock('Landed \xB7 24h',landed,'dispatches',LANDED_MAX,view)+deckBlock('traps',traps,'traps',3,view)+deckPrs(inp,view)+'</div>'}
7451
- const watchCell=(w)=>{const ws=watchState(w);return w?ws.text+' \xB7 '+(ws.at?ageEl(ws.at)+' ago':'never checked'):'<span class="dim">'+ws.text+'</span>'};
7452
- const prOpen=(p)=>"showModal(\\'pr\\',\\'"+esc(p.key)+"\\')";
7453
- const prLink=(p)=>'<a href="'+esc(p.url)+'" target="_blank" rel="noopener" onclick="event.stopPropagation()">#'+p.number+'</a>';
7454
- function prChecks(p){return esc(p.checks.passed)+'/'+esc(p.checks.total)+' passed'+(p.checks.failed?' \xB7 '+p.checks.failed+' failed':'')+(p.checks.pending?' \xB7 '+p.checks.pending+' pending':'')}
7455
- function prReview(p){const r=p.review||{};return esc(p.reviewDecision||'')+(r.unresolvedThreads?' \xB7 '+r.unresolvedThreads+' unresolved':'')+(r.changesRequested?' \xB7 changes requested':'')}
7456
- function prMerge(p){return esc(p.mergeStateStatus)+(p.nextMergeable?' \xB7 <span class="ok">next mergeable</span>':p.blockedBy?' \xB7 blocked by #'+p.blockedBy:'')}
7457
- function prGroups(inp){const byStack=new Map();for(const p of inp.prs){const a=byStack.get(p.stackId)||[];a.push(p);byStack.set(p.stackId,a)}
7458
- return inp.stacks.map(s=>({s,prs:byStack.get(s.id)||[]})).filter(g=>g.prs.length)}
7459
- function prTable(d,inp){const rows=[];
7460
- for(const {s,prs} of prGroups(inp)){
7461
- rows.push('<tr><th colspan="8">'+esc(s.numbers.map(n=>'#'+n).join(' \u2192 '))+' \xB7 floor '+esc(s.floor)+(s.open?' \xB7 open':' \xB7 history')+'</th></tr>');
7462
- for(const p of prs)rows.push('<tr class="rowhead" onclick="'+prOpen(p)+'"><td>'+prLink(p)+'</td><td class="grow">'+esc(p.title||'')+'</td>'
7463
- +'<td>'+esc(p.state)+(p.draft?' \xB7 draft':'')+(p.state==='MERGED'&&p.mergedAt?' \xB7 '+esc(p.mergedAt):'')+'</td><td>'+prChecks(p)+'</td><td>'+prReview(p)+'</td><td>'+prMerge(p)+'</td>'
7464
- +'<td>'+watchCell(p.watch)+'</td><td>'+esc(p.gate||'')+'</td></tr>')}
7465
- return table(['PR','title','state','checks','review','merge','watch','gate'],rows,'no PR evidence')+otherWatches(inp)}
7466
- function prCards(d,inp){const groups=prGroups(inp);
7467
- const body=groups.length?groups.map(({s,prs})=>'<h2>'+esc(s.numbers.map(n=>'#'+n).join(' \u2192 '))+' \xB7 floor '+esc(s.floor)+(s.open?'':' \xB7 history')+'</h2><div class="cards">'
7468
- +prs.map(p=>'<div class="card" onclick="'+prOpen(p)+'"><div class="top"><b>#'+p.number+' '+esc(p.title||'')+'</b><span class="badge '+esc(prBadgeClass(p.badge))+'">'+esc(p.badge.text)+'</span></div>'
7469
- +'<div class="meta">'+esc(p.repo)+' \xB7 '+esc(p.state)+(p.draft?' \xB7 draft':'')+(p.state==='MERGED'&&p.mergedAt?' \xB7 '+esc(p.mergedAt):'')+' \xB7 '+prMerge(p)+'</div>'
7470
- +'<div class="foot"><span>'+prChecks(p)+'</span><span>'+watchCell(p.watch)+'</span>'+(p.gate?'<span>gate '+esc(p.gate)+'</span>':'')+'</div></div>').join('')+'</div>').join('')
7471
- :'<div class="empty">no PR evidence</div>';
7472
- return body+otherWatches(inp)}
7473
- function otherWatches(inp){const short=(c)=>{c=String(c??'');return c.length>24?c.slice(0,23)+'\u2026':c};
7474
- return '<h2>other watches</h2>'+table(['key','owner','cursor','last check','error'],inp.watches.map(w=>'<tr><td>'+esc(w.key)+'</td><td>'+esc(w.owner)+'</td><td title="'+esc(w.cursor)+'">'+esc(short(w.cursor))+'</td><td>'+(w.lastCheckedAt?ageEl(w.lastCheckedAt):'')+'</td><td>'+esc(w.lastError||'')+'</td></tr>'),'no other watches')}
7475
- function noticeTable(list){return table(['at','kind','text','repo'],
7476
- list.map(n=>'<tr><td class="dim">'+ageEl(n.at)+'</td><td>'+esc(n.kind)+'</td><td class="grow">'+esc(n.text)+'</td><td class="dim">'+esc(n.repo??'')+'</td></tr>'),
7477
- 'no notices')}
7478
- function trapRow(t){
7479
- if(!t.live)return {stale:false,listen:'<span class="dot"></span><span class="dim">signed off</span>',hb:'<span class="dim">\u2014</span>'};
7480
- const stale=Date.now()-Date.parse(t.heartbeatAt)>1800000;
7481
- return {stale,listen:t.firstParkedAt?'<span class="dot ok"></span>listening':'<span class="dot warn"></span>never parked',
7482
- hb:'<span class="'+(stale?'bad':'ok')+'">'+(stale?'stale ':'')+ageEl(t.heartbeatAt)+' ago</span>'}}
7483
- function mailCell(t){const p=t.messages.filter(m=>m.state==='pending').length;
7484
- return t.messages.length?('\u2709 '+t.messages.length+(p?' <span class="warn">('+p+' pending)</span>':'')):''}
7485
- function trapTable(list){return table(['address','repo','worktree','harness','session','listening','heartbeat','mail','catches'],
7486
- list.map(t=>{const r=trapRow(t);
7487
- return '<tr class="rowhead'+(t.live?'':' dim')+'" onclick="showModal(\\'trap\\',\\''+esc(t.trapId)+'\\')"><td><b>wt:'+esc(t.trapId)+'</b></td><td>'+esc(t.repo??'\u2014')+'</td><td class="grow">'+esc(t.worktree??'')+'</td><td>'+esc(t.harness??'')+'</td><td>'+esc((t.sessionId??'').slice(0,8))+'</td><td>'+r.listen+'</td><td>'+r.hb+'</td><td>'+mailCell(t)+'</td><td>'+t.catches.length+'</td></tr>'}),
7488
- 'no traps soaking')}
7489
- function trapCards(list){if(!list.length)return '<div class="empty">no traps soaking</div>';
7490
- return '<div class="cards">'+list.map(t=>{const r=trapRow(t);
7491
- return '<div class="card'+(t.live?'':' dim')+'" onclick="showModal(\\'trap\\',\\''+esc(t.trapId)+'\\')"><div class="top"><b>\u{1FAA4} wt:'+esc(t.trapId)+'</b><span class="badge">'+esc(t.harness??(t.live?'':'signed off'))+'</span></div>'
7492
- +'<div class="meta">'+esc(t.repo??(t.live?'addressed bait only':'history'))+(t.sessionId?' \xB7 session '+esc(t.sessionId.slice(0,8)):'')+'</div>'
7493
- +(t.worktree?'<div class="note">'+esc(t.worktree)+'</div>':'')
7494
- +'<div class="foot">'+r.listen+(t.live?' \xB7 heartbeat '+r.hb:'')+' \xB7 '+t.catches.length+' catch'+(t.catches.length===1?'':'es')+(mailCell(t)?' \xB7 '+mailCell(t):'')+'</div></div>'}).join('')+'</div>'}
7495
- function renderModal(d){
7496
- const box=document.getElementById('modalbox');
7497
- const ov=document.getElementById('overlay');
7498
- if(!modal){ov.classList.remove('open');return}
7499
- const item=modalItem(d,modal);
7500
- if(!item){modal=null;ov.classList.remove('open');return}
7501
- let html='';
7502
- if(modal.type==='settings'){
7503
- // Per-browser preferences (localStorage, save()); the kinds line is read-only config.
7504
- const seg=(name,opts,cur)=>'<span class="seg">'+opts.map(([v,l])=>'<button class="'+(v===cur?'on':'')+'" onclick="'+name+'(\\''+v+'\\')">'+l+'</button>').join('')+'</span>';
7505
- html='<span class="x" onclick="closeModal()">\xD7</span><h3>\u2699 settings <span class="dim" style="font-weight:normal">\xB7 this browser only</span></h3>'
7506
- +'<div class="settings">'
7507
- +'<div class="row"><span class="lbl">view<span class="hint">cards or table (notices stay a table)</span></span>'+seg('setView',[['table','table'],['cards','cards']],st.view)+'</div>'
7508
- +'<div class="row"><span class="lbl">lobs<span class="hint">crawling lobsters in this page</span></span>'+seg('setLobs',[['on','on'],['off','off']],st.lobs?'on':'off')+'</div>'
7509
- +'<div class="row"><span class="lbl">attention<span class="hint">kinds shown \u2014 attentionKinds in config.toml (read-only here)</span></span><span class="dim" style="font-size:11px;text-align:right;max-width:260px">'
7510
- +esc(item.attentionError?item.attentionError:(item.attentionKinds||[]).join(' \xB7 '))+'</span></div></div>';
7511
- }else if(modal.type==='pr'){
7512
- const v=prModalView(d,modal.key),p=v.pr,s=v.stack,w=v.watch;
7513
- html='<span class="x" onclick="closeModal()">\xD7</span><h3>'+prLink(p)+' '+esc(p.title||'')+' <span class="badge '+esc(prBadgeClass(p.badge))+'">'+esc(p.badge.text)+'</span>'
7514
- +(p.draft&&p.badge.text!=='draft'?' <span class="badge pr-draft">draft</span>':'')+'</h3>'
7515
- +'<div class="sub">'+esc(p.repo)+' \xB7 '+esc(p.state)+' \xB7 observed '+ageEl(p.observedAt)+' ago'+(p.gate?' \xB7 gate '+esc(p.gate):'')+'</div>'
7516
- +'<div class="sec">checks</div><div>'+prChecks(p)+'</div>'
7517
- +'<div class="sec">review</div><div>'+(prReview(p)||'<span class="dim">no review yet</span>')+'</div>'
7518
- +'<div class="sec">merge</div><div>'+prMerge(p)+'</div>'
7519
- +'<div class="sec">refs</div><div>'+esc(p.headRefName||'?')+' \u2192 '+esc(p.baseRefName||'?')+'</div>'
7520
- +'<div class="sec">stack</div><div>'+(s?s.numbers.map(n=>n===p.number?'<b>#'+n+'</b>':'#'+n).join(' \u2192 ')+' \xB7 '+s.position+' of '+s.size+' \xB7 floor '+esc(s.floor)
7521
- +(s.nextMergeable?' \xB7 <span class="ok">next mergeable</span>':s.blockedBy?' \xB7 blocked by #'+s.blockedBy:s.nextNumber?' \xB7 next is #'+s.nextNumber:''):'<span class="dim">not stacked</span>')+'</div>'
7522
- +'<div class="sec">dispatch chain</div><div>'+(v.chain.length?v.chain.map(c=>c.culled?'<span class="dim">'+esc(c.id.slice(0,8))+' (culled)</span>'
7523
- :'<a href="#" onclick="event.preventDefault();showModal(\\'dispatch\\',\\''+esc(c.modalKey)+'\\')">'+esc(c.id.slice(0,8))+'</a> <span class="dim">'+esc(c.verb)+'</span>').join(' \u2192 '):'<span class="dim">none</span>')+'</div>'
7524
- +'<div class="sec">watch</div>'+(w?'<div>'+esc(w.key)+' \xB7 owner '+esc(w.owner||'?')+' \xB7 '+(w.lastCheckedAt?'checked '+ageEl(w.lastCheckedAt)+' ago':'never checked')+'</div>'
7525
- +(w.lastError?'<div class="bad">'+esc(w.lastError)+'</div>':'')+'<div class="dim" style="font-size:11px;margin-top:4px">cursor</div>'+cmdRow(w.cursor)
7526
- :'<div class="dim">no watch \u2014 <code>lobstah watch add '+esc(p.url)+'</code> registers one</div>');
7527
- }else if(modal.type==='helm'){
7528
- const h=item;
7529
- const stale=Date.now()-Date.parse(h.heartbeatAt)>1800000;
7530
- html='<span class="x" onclick="closeModal()">\xD7</span><h3>\u26F5 '+esc(h.man)+'</h3>'
7531
- +'<div class="sub">helm of <b>'+esc(h.grounds)+'</b> ('+(h.repos||[]).map(esc).join(', ')+')</div>'
7532
- +'<div class="sub">'+esc(h.harness??'?')+' \xB7 '+esc(h.cwd??'?')+(h.host?' \xB7 '+esc(h.host):'')+'</div>'
7533
- +'<div class="sub">session '+esc(h.sessionId??'')+' \xB7 signed on '+ageEl(h.signedOnAt)+' ago \xB7 heartbeat <span class="'+(stale?'warn':'ok')+'">'+ageEl(h.heartbeatAt)+' ago</span></div>'
7534
- +(h.sessionId?'<div class="sec">open this session</div>'+cmdRow((h.harness==='codex'?'codex resume ':'claude --resume ')+h.sessionId):'')
7535
- +(h.transcript?'<div class="sec">transcript</div>'+cmdRow(h.transcript):'');
7536
- }else if(modal.type==='dispatch'){
7537
- const x=item;
7538
- html='<span class="x" onclick="closeModal()">\xD7</span><h3>'+esc(x.id.slice(0,8))+' <span class="badge v-'+x.verb+'">'+x.verb+'</span></h3>'
7539
- +'<div class="sub">'+esc(x.repo)+' \xB7 '+x.lane+' '+x.bucket+' \xB7 '+ageEl(x.verbAt)+(x.for?' \xB7 '+addrCell(x):'')+' '+prCell(x)+'</div>'
7540
- +(x.claimedBy&&x.claimedBy.startsWith('wt:')
7541
- ?'<div class="sec">worked by trap</div>'+cmdRow(x.claimedBy)
7542
- +'<div class="dim" style="font-size:11px">an opted-in interactive session mans this seat \u2014 attach would resume someone\\'s live thread. Message it instead: lobstah send '+esc(x.claimedBy)+' "\u2026"</div>'
7543
- :'<div class="sec">open this session</div>'+cmdRow('lobstah attach '+x.id))
7544
- +(x.transcript?'<div class="sec">transcript</div>'+cmdRow(x.transcript):'')
7545
- +detailBody(x);
7546
- }else{
7547
- const t=item;
7548
- const r=trapRow(t);
7549
- html='<span class="x" onclick="closeModal()">\xD7</span><h3>\u{1FAA4} wt:'+esc(t.trapId)+' <span class="badge">'+esc(t.harness??'signed off')+'</span></h3>'
7550
- +(t.worktree?'<div class="sub">'+esc(t.worktree)+'</div>':'')
7551
- +'<div class="sub">'+(t.live
7552
- ?esc(t.repo??'addressed bait only')+' \xB7 session '+esc(t.sessionId??'')+' \xB7 signed on '+ageEl(t.signedOnAt)+' ago \xB7 '+r.listen+' \xB7 heartbeat '+r.hb
7553
- :'signed off \u2014 registration gone; the lifecycle, messages, and catches are the surviving record. Re-soaking the same worktree restores this address.')+'</div>'
7554
- +(t.live&&t.sessionId?'<div class="sec">open this session</div>'
7555
- +cmdRow((t.harness==='codex'?'codex resume ':'claude --resume ')+t.sessionId)
7556
- +'<div class="dim" style="font-size:11px">as registered at sign-on \u2014 a hookless enlistment may hold a made-up id</div>':'')
7557
- +'<div class="sec">lifecycle ('+t.notices.length+')</div>'
7558
- +(t.notices.length?t.notices.map(n=>'<div class="loglines">'+ageEl(n.at)+' ago \xB7 <b>'+esc(n.kind)+'</b> \u2014 '+esc(n.text)+'</div>').join(''):'<div class="empty">none recorded</div>')
7559
- +'<div class="sec">messages ('+t.messages.length+')</div>'
7560
- +(t.messages.length?t.messages.map(m=>'<div class="msg'+(m.from==='helm'?' from-helm':'')+'"><div class="hdr">from '+esc(m.from)+' \xB7 '+(m.at?ageEl(m.at)+' ago':'')+' \xB7 '+(m.state==='pending'?'<span class="warn">pending</span>':'<span class="ok">delivered</span>')+'</div>'+esc(m.text)+(m.attachments?.length?attachmentRows(m.attachments):'')+'</div>').join(''):'<div class="empty">none</div>')
7561
- +'<div class="sec">catches ('+t.catches.length+')</div>'
7562
- +(t.catches.length?t.catches.map(c=>'<div class="catch"><div class="hdr"><b>'+esc(c.id.slice(0,8))+'</b><span class="badge v-'+c.verb+'">'+c.verb+'</span><span class="dim">'+ageEl(c.verbAt)+'</span>'+prCell(c)+'</div>'
7563
- +'<div class="loglines">'+c.log.map(e=>esc(e.at)+' '+esc(e.verb)+(e.note?' '+esc(e.note):'')).join('\\n')+'</div></div>').join(''):'<div class="empty">none yet</div>');
7564
- }
7565
- // The overlay is the scroll container; keep the reader's place across a rebuild.
7566
- const top=ov.scrollTop,boxTop=box.scrollTop;
7567
- box.innerHTML=html;ov.classList.add('open');
7568
- ov.scrollTop=top;box.scrollTop=boxTop;
7569
- }
7570
- // Rewrite one section, keeping any horizontal/vertical scroll inside it.
7571
- function setHTML(id,html){const el=document.getElementById(id);
7572
- const sc=[...el.querySelectorAll('.wrap')].map(w=>[w.scrollLeft,w.scrollTop]);
7573
- el.innerHTML=html;
7574
- el.querySelectorAll('.wrap').forEach((w,i)=>{if(sc[i]){w.scrollLeft=sc[i][0];w.scrollTop=sc[i][1]}})}
7575
- const setText=(el,t)=>{if(el.textContent!==t)el.textContent=t};
7576
- const TABS=GLASS_TABS;
7577
- const activeTab=()=>tabFromHash(location.hash);
7578
- let hashes={};
7579
- // Render only what changed: each section is keyed on a hash of its inputs,
7580
- // so a quiet tick touches nothing but ticking ages and the clock.
7581
- function render(d){
7582
- const inp=sectionInputs(d,{st,open,modal},Date.now());
7583
- const next=hashInputs(inp);
7584
- const dirty=new Set(dirtySections(hashes,next));
7585
- const tab=activeTab();
7586
- const visible=new Set(visibleSections(tab));
7587
- for(const k of visible)hashes[k]=next[k];
7588
- for(const name of TABS)document.getElementById('page-'+name).classList.toggle('on',name===tab);
7589
- for(const a of document.querySelectorAll('#tabs a'))a.classList.toggle('on',a.dataset.tab===tab);
7590
- document.getElementById('f-lane').style.display=tab==='dispatches'?'':'none';
7591
- document.getElementById('f-verb').style.display=tab==='dispatches'?'':'none';
7592
- document.getElementById('chain-control').style.display=tab==='dispatches'?'':'none';
7593
- document.getElementById('f-kind').style.display=tab==='notices'?'':'none';
7594
- setText(document.getElementById('clock'),new Date(d.now).toLocaleTimeString('en-GB'));
7595
- const repos=[...new Set([...d.dispatches.map(x=>x.repo),...d.notices.map(x=>x.repo),...d.prs.map(x=>x.repo)].filter(Boolean))].sort();
7596
- const rsel=document.getElementById('f-repo');
7597
- if(rsel.options.length!==repos.length+1){const cur=st.repo;
7598
- rsel.innerHTML='<option value="">all repos</option>'+repos.map(r=>'<option'+(r===cur?' selected':'')+'>'+esc(r)+'</option>').join('')}
7599
- const kinds=[...new Set(d.notices.map(n=>n.kind))].sort(),ksel=document.getElementById('f-kind');
7600
- if(ksel.options.length!==kinds.length+1)ksel.innerHTML='<option value="">all notice kinds</option>'+kinds.map(k=>'<option'+(k===st.noticeKind?' selected':'')+'>'+esc(k)+'</option>').join('');
7601
- if(dirty.has('chips')){
7602
- const hbOld=inp.chips.daemonStale;
7603
- setHTML('chips',
7604
- '<span class="chip">daemon '+(d.daemon?('<span class="'+(hbOld?'bad':'ok')+'">'+(hbOld?'stale ':'')+ageEl(d.daemon.heartbeat)+' ago</span> <span class="dim">v'+esc(d.daemon.version)+'</span>'):'<span class="bad">down</span>')+'</span>'
7605
- +inp.chips.helms.map(({x:h,stale})=>
7606
- '<span class="chip click" onclick="showModal(\\'helm\\',\\''+esc(h.grounds)+'\\')">\u26F5 <b>'+esc(h.man)+'</b> <span class="dim">helm '+esc(h.grounds)+'</span> <span class="'+(stale?'warn':'ok')+'">'+(stale?'stale ':'')+ageEl(h.heartbeatAt)+' ago</span></span>').join(''))}
7607
- if(tab==='deck'&&dirty.has('deck'))setHTML('deck',renderDeck(d,inp.deck));
7608
- if(tab==='dispatches'&&dirty.has('dispatches')){const list=inp.dispatches.list;
7609
- setHTML('dispatches',st.view==='cards'?dispatchCards(list):dispatchTable(list))}
7610
- if(tab==='traps'&&dirty.has('traps')){const traps=inp.traps.list.map(t=>t.x);
7611
- setHTML('traps',st.view==='cards'?trapCards(traps):trapTable(traps))}
7612
- if(tab==='notices'&&dirty.has('notices'))setHTML('notices',noticeTable(inp.notices.list));
7613
- if(tab==='prs'&&dirty.has('prs'))setHTML('prs',st.view==='cards'?prCards(d,inp.prs):prTable(d,inp.prs));
7614
- if(dirty.has('foot'))setHTML('foot',
7615
- '\u{1F99E}\u2728 lobstah v'+esc(d.version)+' \xB7 <a href="'+esc(d.repoUrl)+'" target="_blank">'+esc(d.repoUrl.replace('https://github.com/',''))+'</a>');
7616
- renderLobs(d.attention||[]);
7617
- // The open modal is rebuilt only when its own item (or which one) changed.
7618
- if(dirty.has('modal'))renderModal(d);
7619
- refreshAges();
7620
- }
7621
- let spriteOk=null;
7622
- (()=>{const i=new Image();
7623
- i.onload=()=>{spriteOk=true;lobKey='';tick(true)};
7624
- i.onerror=()=>{spriteOk=false;lobKey='';tick(true)};
7625
- i.src='/lob-sprite.png'})();
7626
- let lobKey='';
7627
- ${lobItems.toString()}
7628
- // Per-browser lob hides: {itemKey: stateHash}. localStorage only, guarded like st.
7629
- let lobHidden={};
7630
- try{lobHidden=JSON.parse(localStorage.getItem('spyglass-lob-hidden')||'{}')||{}}catch(e){lobHidden={}}
7631
- window.hideLob=(key,hash)=>{lobHidden[key]=hash;try{localStorage.setItem('spyglass-lob-hidden',JSON.stringify(lobHidden))}catch(e){}lobKey='';setTimeout(()=>tick(true),0)};
7632
- const hideCall=(it)=>it.hideKey?"hideLob("+esc(JSON.stringify(it.hideKey)).replace(/'/g,'&#39;')+","+esc(JSON.stringify(it.hideHash))+");":'';
7633
- function renderLobs(att){
7634
- // st.lobs gates the lobs; acked items (the pet's shared ack) and lobs this
7635
- // browser already clicked (hidden by item key + state hash) don't walk; a
7636
- // new state re-shows them. lobItems (glass-lobs.ts) makes that decision;
7637
- // the glass writes nothing to lobstah \u2014 the hide is localStorage.
7638
- const items=lobItems(att,{lobs:st.lobs,hidden:lobHidden,preview:new URLSearchParams(location.search).has('lob'),
7639
- previewClick:last&&last.helms.length?"showModal('helm','"+last.helms[0].grounds+"')":''});
7640
- const key=items.map(i=>i.key).join('|')+(spriteOk===null?'?':spriteOk?'s':'e');
7641
- if(key===lobKey)return;
7642
- lobKey=key;
7643
- // A PR lob is a plain link out (read-only: the glass opens, never acts);
7644
- // a question lob opens its dispatch modal.
7645
- document.getElementById('lobs').innerHTML=items.map((it,i)=>{
7646
- const style='animation-duration:'+(((innerWidth+180)/(100+i*12)).toFixed(1))+'s;animation-delay:-'+((i*9)%14)+'s';
7647
- const body='<div class="bub">'+(it.label?'<span class="badge dim">'+esc(it.label)+'</span> ':'')+'<span>'+esc(it.text.length>48?it.text.slice(0,47)+'\u2026':it.text)+'</span></div>'
7648
- +(spriteOk===false?'<span class="fallback">\u{1F99E}</span>':'<div class="sprite"></div>')
7649
- +'<img class="star" src="/star.png" alt="" onerror="this.remove()">';
7650
- return it.href
7651
- ?'<a class="lob" style="'+style+'" title="open the PR" href="'+esc(it.href)+'" target="_blank" rel="noopener" onclick="'+hideCall(it)+'">'+body+'</a>'
7652
- :'<div class="lob" style="'+style+'" title="click to open" onclick="'+hideCall(it)+(it.click||'')+'">'+body+'</div>'}).join('');
7653
- }
7654
- window.tog=(k)=>{open.has(k)?open.delete(k):open.add(k);tick(true)};
7655
- window.showModal=(type,key)=>{modal={type,key};tick(true)};
7656
- window.closeModal=()=>{modal=null;if(hashes)hashes.modal=null;document.getElementById('overlay').classList.remove('open')};
7657
- document.getElementById('overlay').addEventListener('click',(e)=>{if(e.target.id==='overlay')closeModal()});
7658
- document.addEventListener('keydown',(e)=>{if(e.key==='Escape')closeModal()});
7659
- // \u2699 opens the settings modal \u2014 the same overlay as every other modal (Escape
7660
- // and click-outside close it). Its two preferences are this browser's own:
7661
- // localStorage via save(); view applies to every tab.
7662
- document.getElementById('gearbtn').addEventListener('click',()=>showModal('settings','browser'));
7663
- window.setView=(v)=>{if(v!=='table'&&v!=='cards')return;st.view=v;save();tick(true)};
7664
- window.setLobs=(v)=>{st.lobs=v==='on';save();lobKey='';tick(true)};
7665
- for(const[id,key]of[['f-lane','lane'],['f-repo','repo'],['f-verb','verb'],['f-kind','noticeKind']]){
7666
- const el=document.getElementById(id);el.value=st[key];
7667
- el.addEventListener('change',()=>{st[key]=el.value;save();tick(true)})}
7668
- const chain=document.getElementById('f-chain');chain.checked=!!st.chain;
7669
- chain.addEventListener('change',()=>{st.chain=chain.checked;save();tick(true)});
7670
- const q=document.getElementById('f-q');q.value=st.q;
7671
- q.addEventListener('input',()=>{st.q=q.value;save();tick(true)});
7672
- window.addEventListener('hashchange',()=>tick(true));
7673
- let last,inflight=false,timer=null;
7674
- const setStale=(on)=>{const el=document.getElementById('stale');const v=on?'inline':'none';if(el.style.display!==v)el.style.display=v};
7675
- // User-driven ticks render synchronously from last; polls fetch one at a time.
7676
- async function tick(rerender){
7677
- if(rerender&&last){render(last);return}
7678
- if(inflight)return;
7679
- inflight=true;
7680
- try{const r=await fetch('/data');last=await r.json();render(last);setStale(false)}
7681
- catch(e){setStale(true)}
7682
- finally{inflight=false}}
7683
- const startPoll=()=>{if(!timer)timer=setInterval(()=>tick(false),2000)};
7684
- const stopPoll=()=>{clearInterval(timer);timer=null};
7685
- document.addEventListener('visibilitychange',()=>{if(document.hidden)stopPoll();else{tick(false);startPoll()}});
7686
- tick();if(!document.hidden)startPoll();
7687
- </script></body></html>`;
7696
+ var PAGE = GLASS_PAGE;
7688
7697
  function serveGlass(port) {
7689
7698
  const icon = assetPath("favicon.png") ?? assetPath("lob-star.png");
7690
7699
  const lob = assetPath("lob.png");
7691
7700
  const sprite = assetPath("lob-sprite.png");
7692
7701
  const star = assetPath("star.png");
7693
7702
  const fallbackIcon = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>\u{1F99E}</text></svg>`;
7694
- const server = http.createServer((req, res) => {
7703
+ const server = http2.createServer((req, res) => {
7704
+ res.setHeader("Server", `lobstah-glass/${lobstahVersion()}`);
7705
+ if (req.url === "/api/version" && req.method === "GET") {
7706
+ res.writeHead(200, { "content-type": "application/json" });
7707
+ res.end(JSON.stringify({ service: "lobstah-glass", version: lobstahVersion(), pid: process.pid }));
7708
+ return;
7709
+ }
7695
7710
  if (req.url === "/lob.png" && lob) {
7696
7711
  res.writeHead(200, { "content-type": "image/png", "cache-control": "no-cache" });
7697
- res.end(fs36.readFileSync(lob));
7712
+ res.end(fs38.readFileSync(lob));
7698
7713
  } else if (req.url === "/star.png" && star) {
7699
7714
  res.writeHead(200, { "content-type": "image/png", "cache-control": "no-cache" });
7700
- res.end(fs36.readFileSync(star));
7715
+ res.end(fs38.readFileSync(star));
7701
7716
  } else if (req.url === "/lob-sprite.png" && sprite) {
7702
7717
  res.writeHead(200, { "content-type": "image/png", "cache-control": "no-cache" });
7703
- res.end(fs36.readFileSync(sprite));
7718
+ res.end(fs38.readFileSync(sprite));
7704
7719
  } else if (req.url === "/icon.png") {
7705
7720
  if (icon) {
7706
7721
  res.writeHead(200, { "content-type": "image/png", "cache-control": "no-cache" });
7707
- res.end(fs36.readFileSync(icon));
7722
+ res.end(fs38.readFileSync(icon));
7708
7723
  } else {
7709
7724
  res.writeHead(200, { "content-type": "image/svg+xml", "cache-control": "no-cache" });
7710
7725
  res.end(fallbackIcon);
@@ -7723,127 +7738,15 @@ function serveGlass(port) {
7723
7738
 
7724
7739
  // apps/cli/src/pet.ts
7725
7740
  init_dist2();
7726
- import * as fs38 from "node:fs";
7727
- import * as os8 from "node:os";
7728
- import * as path31 from "node:path";
7741
+ import * as fs39 from "node:fs";
7742
+ import * as os9 from "node:os";
7743
+ import * as path32 from "node:path";
7729
7744
  import { spawnSync as spawnSync7 } from "node:child_process";
7730
-
7731
- // apps/cli/src/service.ts
7732
- init_dist2();
7733
- import * as fs37 from "node:fs";
7734
- import * as os7 from "node:os";
7735
- import * as path30 from "node:path";
7736
- import { spawnSync as spawnSync6 } from "node:child_process";
7737
- function xml(s) {
7738
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
7739
- }
7740
- function renderLaunchdPlist(spec) {
7741
- return `<?xml version="1.0" encoding="UTF-8"?>
7742
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
7743
- <plist version="1.0">
7744
- <dict>
7745
- <key>Label</key><string>lobstah.${spec.kind}</string>
7746
- <key>ProgramArguments</key>
7747
- <array>
7748
- ${[spec.nodePath, spec.entry, spec.kind].filter(Boolean).map((a) => ` <string>${xml(a)}</string>`).join("\n")}
7749
- </array>
7750
- <key>EnvironmentVariables</key>
7751
- <dict>
7752
- <key>PATH</key><string>${xml(spec.pathEnv)}</string>
7753
- <key>LOBSTAH_HOME</key><string>${xml(spec.home)}</string>
7754
- </dict>
7755
- <key>RunAtLoad</key><true/>
7756
- <key>KeepAlive</key><true/>
7757
- <key>StandardOutPath</key><string>${xml(path30.posix.join(spec.logDir, `${spec.kind}.log`))}</string>
7758
- <key>StandardErrorPath</key><string>${xml(path30.posix.join(spec.logDir, `${spec.kind}.err`))}</string>
7759
- </dict>
7760
- </plist>
7761
- `;
7762
- }
7763
- function renderSystemdUnit(spec) {
7764
- return `[Unit]
7765
- Description=lobstah ${spec.kind}
7766
-
7767
- [Service]
7768
- ExecStart=${[spec.nodePath, spec.entry, spec.kind].filter(Boolean).join(" ")}
7769
- Restart=always
7770
- RestartSec=5
7771
- Environment=PATH=${spec.pathEnv}
7772
- Environment=LOBSTAH_HOME=${spec.home}
7773
- StandardOutput=append:${path30.posix.join(spec.logDir, `${spec.kind}.log`)}
7774
- StandardError=append:${path30.posix.join(spec.logDir, `${spec.kind}.err`)}
7775
-
7776
- [Install]
7777
- WantedBy=default.target
7778
- `;
7779
- }
7780
- function servicePathEnv(nodePath, home = os7.homedir()) {
7781
- const dirs = [
7782
- path30.posix.dirname(nodePath),
7783
- path30.posix.join(home, ".local", "bin"),
7784
- "/opt/homebrew/bin",
7785
- "/usr/local/bin",
7786
- "/usr/bin",
7787
- "/bin"
7788
- ];
7789
- return [...new Set(dirs)].join(":");
7790
- }
7791
- function serviceFile(kind) {
7792
- return process.platform === "darwin" ? path30.join(os7.homedir(), "Library", "LaunchAgents", `lobstah.${kind}.plist`) : path30.join(os7.homedir(), ".config", "systemd", "user", `lobstah-${kind}.service`);
7793
- }
7794
- function run(cmd, args) {
7795
- const res = spawnSync6(cmd, args, { encoding: "utf8" });
7796
- return { ok: res.status === 0, out: `${res.stdout ?? ""}${res.stderr ?? ""}`.trim() };
7797
- }
7798
- function installService(kind) {
7799
- if (process.platform === "win32") {
7800
- throw new Error(`no service manager support on Windows \u2014 run \`lobstah ${kind}\` under your process manager of choice`);
7801
- }
7802
- const entry = COMPILED_BINARY ? "" : fs37.realpathSync(process.argv[1]);
7803
- const spec = {
7804
- kind,
7805
- nodePath: process.execPath,
7806
- entry,
7807
- pathEnv: servicePathEnv(process.execPath),
7808
- home: lobstahHome(),
7809
- logDir: path30.join(lobstahHome(), "logs")
7810
- };
7811
- if (!onPath("git", spec.pathEnv)) {
7812
- throw new Error(`git is not reachable on the service PATH (${spec.pathEnv}) \u2014 the daemon cannot allocate worktrees`);
7813
- }
7814
- fs37.mkdirSync(spec.logDir, { recursive: true });
7815
- const file = serviceFile(kind);
7816
- fs37.mkdirSync(path30.dirname(file), { recursive: true });
7817
- if (process.platform === "darwin") {
7818
- if (fs37.existsSync(file)) run("launchctl", ["unload", file]);
7819
- fs37.writeFileSync(file, renderLaunchdPlist(spec));
7820
- const load = run("launchctl", ["load", file]);
7821
- return { file, loaded: load.ok, detail: load.ok ? `label lobstah.${kind}` : load.out };
7822
- }
7823
- fs37.writeFileSync(file, renderSystemdUnit(spec));
7824
- run("systemctl", ["--user", "daemon-reload"]);
7825
- const enable = run("systemctl", ["--user", "enable", "--now", `lobstah-${kind}.service`]);
7826
- return { file, loaded: enable.ok, detail: enable.ok ? `unit lobstah-${kind}.service` : enable.out };
7827
- }
7828
- function uninstallService(kind) {
7829
- const file = serviceFile(kind);
7830
- if (process.platform === "darwin") {
7831
- if (fs37.existsSync(file)) run("launchctl", ["unload", file]);
7832
- } else {
7833
- run("systemctl", ["--user", "disable", "--now", `lobstah-${kind}.service`]);
7834
- }
7835
- const removed = fs37.existsSync(file);
7836
- if (removed) fs37.unlinkSync(file);
7837
- if (process.platform !== "darwin") run("systemctl", ["--user", "daemon-reload"]);
7838
- return { file, removed };
7839
- }
7840
-
7841
- // apps/cli/src/pet.ts
7842
7745
  function petBinaryHome() {
7843
- return path31.join(lobstahHome(), "bin", "LobstahPet");
7746
+ return path32.join(lobstahHome(), "bin", "LobstahPet");
7844
7747
  }
7845
7748
  function petPlistFile() {
7846
- return path31.join(os8.homedir(), "Library", "LaunchAgents", "lobstah.pet.plist");
7749
+ return path32.join(os9.homedir(), "Library", "LaunchAgents", "lobstah.pet.plist");
7847
7750
  }
7848
7751
  function xml2(s) {
7849
7752
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -7865,8 +7768,8 @@ function renderPetPlist(binary, pathEnv, home, logDir) {
7865
7768
  </dict>
7866
7769
  <key>RunAtLoad</key><true/>
7867
7770
  <key>KeepAlive</key><false/>
7868
- <key>StandardOutPath</key><string>${xml2(path31.posix.join(logDir, "pet.log"))}</string>
7869
- <key>StandardErrorPath</key><string>${xml2(path31.posix.join(logDir, "pet.err"))}</string>
7771
+ <key>StandardOutPath</key><string>${xml2(path32.posix.join(logDir, "pet.log"))}</string>
7772
+ <key>StandardErrorPath</key><string>${xml2(path32.posix.join(logDir, "pet.err"))}</string>
7870
7773
  </dict>
7871
7774
  </plist>
7872
7775
  `;
@@ -7881,13 +7784,13 @@ function installPet(binaryArg) {
7881
7784
  }
7882
7785
  const candidates = [
7883
7786
  binaryArg,
7884
- path31.join(process.cwd(), "apps", "pet", ".build", "release", "LobstahPet"),
7885
- path31.join(process.cwd(), ".build", "release", "LobstahPet"),
7886
- path31.join(process.cwd(), "apps", "pet", ".build", "debug", "LobstahPet"),
7887
- path31.join(process.cwd(), ".build", "debug", "LobstahPet"),
7787
+ path32.join(process.cwd(), "apps", "pet", ".build", "release", "LobstahPet"),
7788
+ path32.join(process.cwd(), ".build", "release", "LobstahPet"),
7789
+ path32.join(process.cwd(), "apps", "pet", ".build", "debug", "LobstahPet"),
7790
+ path32.join(process.cwd(), ".build", "debug", "LobstahPet"),
7888
7791
  petBinaryHome()
7889
7792
  ].filter((c) => c !== void 0);
7890
- const source = candidates.find((c) => fs38.existsSync(c));
7793
+ const source = candidates.find((c) => fs39.existsSync(c));
7891
7794
  if (!source) {
7892
7795
  throw new Error(
7893
7796
  "no pet binary found \u2014 build it first (`cd apps/pet && swift build -c release`) and run install from the repo root, or pass --binary <path>"
@@ -7895,33 +7798,33 @@ function installPet(binaryArg) {
7895
7798
  }
7896
7799
  const binary = petBinaryHome();
7897
7800
  const file0 = petPlistFile();
7898
- if (fs38.existsSync(file0)) run2("launchctl", ["unload", file0]);
7801
+ if (fs39.existsSync(file0)) run2("launchctl", ["unload", file0]);
7899
7802
  run2("pkill", ["-f", "LobstahPet"]);
7900
- if (path31.resolve(source) !== path31.resolve(binary)) {
7901
- fs38.mkdirSync(path31.dirname(binary), { recursive: true });
7902
- fs38.rmSync(binary, { force: true });
7903
- fs38.copyFileSync(source, binary);
7904
- fs38.chmodSync(binary, 493);
7905
- const bundle = path31.join(path31.dirname(source), "LobstahPet_LobstahPet.bundle");
7906
- if (fs38.existsSync(bundle)) {
7907
- const dest = path31.join(path31.dirname(binary), "LobstahPet_LobstahPet.bundle");
7908
- fs38.rmSync(dest, { recursive: true, force: true });
7909
- fs38.cpSync(bundle, dest, { recursive: true });
7910
- }
7911
- }
7912
- const logDir = path31.join(lobstahHome(), "logs");
7913
- fs38.mkdirSync(logDir, { recursive: true });
7803
+ if (path32.resolve(source) !== path32.resolve(binary)) {
7804
+ fs39.mkdirSync(path32.dirname(binary), { recursive: true });
7805
+ fs39.rmSync(binary, { force: true });
7806
+ fs39.copyFileSync(source, binary);
7807
+ fs39.chmodSync(binary, 493);
7808
+ const bundle = path32.join(path32.dirname(source), "LobstahPet_LobstahPet.bundle");
7809
+ if (fs39.existsSync(bundle)) {
7810
+ const dest = path32.join(path32.dirname(binary), "LobstahPet_LobstahPet.bundle");
7811
+ fs39.rmSync(dest, { recursive: true, force: true });
7812
+ fs39.cpSync(bundle, dest, { recursive: true });
7813
+ }
7814
+ }
7815
+ const logDir = path32.join(lobstahHome(), "logs");
7816
+ fs39.mkdirSync(logDir, { recursive: true });
7914
7817
  const file = petPlistFile();
7915
- fs38.mkdirSync(path31.dirname(file), { recursive: true });
7916
- fs38.writeFileSync(file, renderPetPlist(binary, servicePathEnv(process.execPath), lobstahHome(), logDir));
7818
+ fs39.mkdirSync(path32.dirname(file), { recursive: true });
7819
+ fs39.writeFileSync(file, renderPetPlist(binary, servicePathEnv(process.execPath), lobstahHome(), logDir));
7917
7820
  const load = run2("launchctl", ["load", file]);
7918
7821
  return { file, binary, loaded: load.ok, detail: load.ok ? "label lobstah.pet \u2014 starts at login" : load.out };
7919
7822
  }
7920
7823
  function uninstallPet() {
7921
7824
  const file = petPlistFile();
7922
- if (process.platform === "darwin" && fs38.existsSync(file)) run2("launchctl", ["unload", file]);
7923
- const removed = fs38.existsSync(file);
7924
- if (removed) fs38.unlinkSync(file);
7825
+ if (process.platform === "darwin" && fs39.existsSync(file)) run2("launchctl", ["unload", file]);
7826
+ const removed = fs39.existsSync(file);
7827
+ if (removed) fs39.unlinkSync(file);
7925
7828
  run2("pkill", ["-f", "LobstahPet"]);
7926
7829
  return { file, removed };
7927
7830
  }
@@ -7929,8 +7832,8 @@ function uninstallPet() {
7929
7832
  // apps/cli/src/repos.ts
7930
7833
  init_dist();
7931
7834
  init_dist2();
7932
- import * as fs39 from "node:fs";
7933
- import * as path32 from "node:path";
7835
+ import * as fs40 from "node:fs";
7836
+ import * as path33 from "node:path";
7934
7837
  import { spawnSync as spawnSync8 } from "node:child_process";
7935
7838
  function git3(dir, ...args) {
7936
7839
  const res = spawnSync8("git", ["-C", dir, ...args], { encoding: "utf8" });
@@ -7946,41 +7849,41 @@ var LOCKFILE_SETUP = [
7946
7849
  ["poetry.lock", ["poetry install"]]
7947
7850
  ];
7948
7851
  function detectRepo(dir) {
7949
- const abs = path32.resolve(dir);
7852
+ const abs = path33.resolve(dir);
7950
7853
  if (git3(abs, "rev-parse", "--is-inside-work-tree") !== "true") return void 0;
7951
7854
  const top = git3(abs, "rev-parse", "--show-toplevel");
7952
7855
  const canon2 = (p) => {
7953
- const real = path32.resolve(fs39.realpathSync.native(p));
7856
+ const real = path33.resolve(fs40.realpathSync.native(p));
7954
7857
  return process.platform === "win32" ? real.toLowerCase() : real;
7955
7858
  };
7956
7859
  if (!top || canon2(top) !== canon2(abs)) return void 0;
7957
7860
  const origin = git3(abs, "remote", "get-url", "origin");
7958
7861
  const headRef = git3(abs, "symbolic-ref", "--short", "refs/remotes/origin/HEAD");
7959
7862
  const trunk = headRef?.replace(/^origin\//, "") ?? git3(abs, "branch", "--show-current") ?? "main";
7960
- const setup = LOCKFILE_SETUP.find(([f]) => fs39.existsSync(path32.join(abs, f)))?.[1];
7961
- return { key: path32.basename(abs), path: abs, trunk, origin, setup };
7863
+ const setup = LOCKFILE_SETUP.find(([f]) => fs40.existsSync(path33.join(abs, f)))?.[1];
7864
+ return { key: path33.basename(abs), path: abs, trunk, origin, setup };
7962
7865
  }
7963
7866
  function scanForRepos(roots) {
7964
7867
  const found = [];
7965
7868
  for (const root of roots) {
7966
- const abs = path32.resolve(root);
7967
- if (!fs39.existsSync(abs)) continue;
7869
+ const abs = path33.resolve(root);
7870
+ if (!fs40.existsSync(abs)) continue;
7968
7871
  const self = detectRepo(abs);
7969
7872
  if (self) {
7970
7873
  found.push(self);
7971
7874
  continue;
7972
7875
  }
7973
- for (const entry of fs39.readdirSync(abs, { withFileTypes: true })) {
7876
+ for (const entry of fs40.readdirSync(abs, { withFileTypes: true })) {
7974
7877
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
7975
- const child = detectRepo(path32.join(abs, entry.name));
7878
+ const child = detectRepo(path33.join(abs, entry.name));
7976
7879
  if (child) found.push(child);
7977
7880
  }
7978
7881
  }
7979
7882
  return found;
7980
7883
  }
7981
7884
  function configuredRepoKeys(file = configPath()) {
7982
- if (!fs39.existsSync(file)) return /* @__PURE__ */ new Set();
7983
- const raw = parse(fs39.readFileSync(file, "utf8"));
7885
+ if (!fs40.existsSync(file)) return /* @__PURE__ */ new Set();
7886
+ const raw = parse(fs40.readFileSync(file, "utf8"));
7984
7887
  return new Set(Object.keys(raw.repos ?? {}));
7985
7888
  }
7986
7889
  function appendRepoBlock(repo, opts = {}, file = configPath()) {
@@ -7988,7 +7891,7 @@ function appendRepoBlock(repo, opts = {}, file = configPath()) {
7988
7891
  if (repo.origin) lines.push(`origin = ${JSON.stringify(repo.origin)}`);
7989
7892
  if (repo.setup) lines.push(`setup = [${repo.setup.map((s) => JSON.stringify(s)).join(", ")}]`);
7990
7893
  if (opts.pickup) lines.push(`pickup = true`);
7991
- fs39.appendFileSync(file, `${lines.join("\n")}
7894
+ fs40.appendFileSync(file, `${lines.join("\n")}
7992
7895
  `);
7993
7896
  }
7994
7897
 
@@ -8077,7 +7980,7 @@ var COMMANDS = {
8077
7980
  daemon: { subverbs: ["install", "uninstall"], flags: { "--interval": { value: "<ms>" } } },
8078
7981
  pick: { subverbs: ["once", "install", "uninstall"], flags: {} },
8079
7982
  doctor: { flags: {} },
8080
- glass: { flags: { "--port": { value: "<n>" } } },
7983
+ glass: { subverbs: ["stop", "status", "install", "uninstall"], flags: { "--port": { value: "<n>" }, "--detach": {} } },
8081
7984
  pet: { subverbs: ["install", "uninstall"], flags: { "--binary": { value: "<path>" } } },
8082
7985
  repos: { subverbs: ["add"], flags: { "--pickup": {}, "--key": { value: "<k>" } }, positionals: "[<path>]" },
8083
7986
  init: { flags: { "--scan": {}, "--pickup": {} }, positionals: "[<dir>...]" },
@@ -8179,12 +8082,13 @@ Quitting the pet sticks until next login; uninstall removes the agent.`,
8179
8082
  glass: `The spyglass: tend as a live localhost web page \u2014 attention, dispatches,
8180
8083
  traps with lifecycle and mail, notices, merge view; filters and a
8181
8084
  table/cards toggle. Read-only and binds 127.0.0.1 only: looking through it
8182
- consumes no cursor and steers nothing.`,
8085
+ consumes no cursor and steers nothing. --detach starts it in the background;
8086
+ stop and status manage that process. install and uninstall manage a user service.`,
8183
8087
  repos: `List configured repos, or detect one and append its [repos.*] block.`,
8184
8088
  init: `Create ~/.lobstah + config; --scan appends a [repos.*] block per repo found
8185
8089
  under the given directories.`,
8186
8090
  version: `The installed lobstah version.`,
8187
- "man:manual": `The lobsterman's manual.`,
8091
+ "man:manual": `The lobstah man's manual.`,
8188
8092
  "man:tend": `The whole-fleet pass: verdict, unanswered questions, each work item's chain,
8189
8093
  PR, and merge gate. Pure disk read.`,
8190
8094
  "man:report": `The delta since the last report: landed, arisen, still-waiting, verdict.
@@ -8192,7 +8096,7 @@ Advances the cursor unless --peek \u2014 the acknowledgment man wait's timeout
8192
8096
  digest defers to. --grounds scopes digest and cursor to one helm's territory
8193
8097
  (that helm's alone). Reserved for the claimed helm; identify with --session
8194
8098
  (defaults --grounds to its own).`,
8195
- "man:helm": `Take the helm: sign this session on as the one lobsterman for its grounds.
8099
+ "man:helm": `Take the helm: sign this session on as the one lobstah man for its grounds.
8196
8100
  Prints the charter, enables the Stop hook, and gates the digest.
8197
8101
  Sign-on records who the man is (harness, directory, host; --label names it).
8198
8102
  A live foreign holder refuses without --take; a stale one is claimable.
@@ -8307,15 +8211,15 @@ function detectHarness(opts) {
8307
8211
 
8308
8212
  // apps/cli/src/watchers.ts
8309
8213
  init_dist2();
8310
- import * as fs40 from "node:fs";
8311
- import * as path33 from "node:path";
8214
+ import * as fs41 from "node:fs";
8215
+ import * as path34 from "node:path";
8312
8216
  var STALE_MS = 5e3;
8313
8217
  var HEARTBEAT_MS = 1500;
8314
- var watcherDir = () => path33.join(lobstahHome(), "watchers");
8315
- var watcherFile = (sessionId) => path33.join(watcherDir(), `${encodeURIComponent(sessionId)}.json`);
8218
+ var watcherDir = () => path34.join(lobstahHome(), "watchers");
8219
+ var watcherFile = (sessionId) => path34.join(watcherDir(), `${encodeURIComponent(sessionId)}.json`);
8316
8220
  function liveRegistration(sessionId, now2 = Date.now()) {
8317
8221
  try {
8318
- const w = JSON.parse(fs40.readFileSync(watcherFile(sessionId), "utf8"));
8222
+ const w = JSON.parse(fs41.readFileSync(watcherFile(sessionId), "utf8"));
8319
8223
  const beat = Date.parse(w.heartbeatAt);
8320
8224
  if (w.sessionId !== sessionId || !Number.isInteger(w.pid) || w.pid <= 0 || !Number.isFinite(beat) || now2 - beat > STALE_MS) return void 0;
8321
8225
  return w;
@@ -8327,14 +8231,22 @@ function liveWatcher(sessionId, kind, trapId, now2 = Date.now()) {
8327
8231
  const w = liveRegistration(sessionId, now2);
8328
8232
  return w?.kind === kind && (!trapId || w.trapId === trapId) ? w : void 0;
8329
8233
  }
8234
+ async function awaitWatcher(sessionId, kind, graceMs, trapId, pollMs = 100) {
8235
+ const deadline = Date.now() + Math.max(0, graceMs);
8236
+ for (; ; ) {
8237
+ const w = liveWatcher(sessionId, kind, trapId);
8238
+ if (w || Date.now() >= deadline) return w;
8239
+ await new Promise((resolve6) => setTimeout(resolve6, Math.min(pollMs, Math.max(1, deadline - Date.now()))));
8240
+ }
8241
+ }
8330
8242
  function armWatcher(sessionId, kind, trapId) {
8331
- fs40.mkdirSync(watcherDir(), { recursive: true });
8243
+ fs41.mkdirSync(watcherDir(), { recursive: true });
8332
8244
  const file = watcherFile(sessionId);
8333
8245
  const own = () => ({ sessionId, kind, pid: process.pid, heartbeatAt: (/* @__PURE__ */ new Date()).toISOString(), ...trapId ? { trapId } : {} });
8334
8246
  let claimed = false;
8335
8247
  for (let attempt = 0; attempt < 3; attempt++) {
8336
8248
  try {
8337
- fs40.writeFileSync(file, `${JSON.stringify(own())}
8249
+ fs41.writeFileSync(file, `${JSON.stringify(own())}
8338
8250
  `, { flag: "wx" });
8339
8251
  claimed = true;
8340
8252
  break;
@@ -8343,7 +8255,7 @@ function armWatcher(sessionId, kind, trapId) {
8343
8255
  const live = liveRegistration(sessionId);
8344
8256
  if (live) throw new Error(`watcher already armed for session ${sessionId} (pid ${live.pid})`);
8345
8257
  try {
8346
- fs40.rmSync(file);
8258
+ fs41.rmSync(file);
8347
8259
  } catch {
8348
8260
  }
8349
8261
  }
@@ -8351,7 +8263,7 @@ function armWatcher(sessionId, kind, trapId) {
8351
8263
  if (!claimed) throw new Error(`could not arm watcher for session ${sessionId}`);
8352
8264
  const ownsFile = () => {
8353
8265
  try {
8354
- return JSON.parse(fs40.readFileSync(file, "utf8")).pid === process.pid;
8266
+ return JSON.parse(fs41.readFileSync(file, "utf8")).pid === process.pid;
8355
8267
  } catch {
8356
8268
  return false;
8357
8269
  }
@@ -8360,12 +8272,12 @@ function armWatcher(sessionId, kind, trapId) {
8360
8272
  if (!ownsFile()) return;
8361
8273
  const tmp = `${file}.tmp-${process.pid}`;
8362
8274
  try {
8363
- fs40.writeFileSync(tmp, `${JSON.stringify(own())}
8275
+ fs41.writeFileSync(tmp, `${JSON.stringify(own())}
8364
8276
  `);
8365
- fs40.renameSync(tmp, file);
8277
+ fs41.renameSync(tmp, file);
8366
8278
  } catch {
8367
8279
  try {
8368
- fs40.rmSync(tmp, { force: true });
8280
+ fs41.rmSync(tmp, { force: true });
8369
8281
  } catch {
8370
8282
  }
8371
8283
  }
@@ -8377,7 +8289,7 @@ function armWatcher(sessionId, kind, trapId) {
8377
8289
  if (stopped) return;
8378
8290
  stopped = true;
8379
8291
  clearInterval(timer);
8380
- if (ownsFile()) fs40.rmSync(file, { force: true });
8292
+ if (ownsFile()) fs41.rmSync(file, { force: true });
8381
8293
  process.off("exit", stop);
8382
8294
  process.off("SIGINT", interrupt);
8383
8295
  process.off("SIGTERM", terminate);
@@ -8467,18 +8379,19 @@ host processes:
8467
8379
  pet install|uninstall the desktop pet (macOS): a login LaunchAgent
8468
8380
  walks attention questions across the screen
8469
8381
 
8470
- lobsterman (orchestrator sessions \u2014 bare \`lobstah man\` prints the manual):
8382
+ lobstah man (orchestrator sessions \u2014 bare \`lobstah man\` prints the manual):
8471
8383
  man tend [--json] tend the whole string: fleet verdict (daemon
8472
8384
  up, stalled vs idle), unanswered questions
8473
8385
  with ages, each work item's dispatch chain,
8474
8386
  PR, and merge-gate status from pick's last
8475
8387
  observation. Pure disk read \u2014 no forge calls.
8476
- glass [--port <n>] the spyglass: tend as a live localhost web
8388
+ glass [--port <n>] [--detach] the spyglass: tend as a live localhost web
8477
8389
  page \u2014 attention, dispatches, traps with
8478
8390
  their lifecycle and mail, notices, merge
8479
8391
  view. Read-only; consumes no cursor.
8392
+ stop | status | install | uninstall manage it.
8480
8393
  Port: --port, else $LOBSTAH_GLASS_PORT,
8481
- else 4949 (the pet reads the same var).
8394
+ else [glass].port (default 4949).
8482
8395
  man wait [--timeout <secs>] [--peek]
8483
8396
  block until a dispatch or watched source
8484
8397
  needs attention, then print the event and
@@ -8515,7 +8428,7 @@ lobsterman (orchestrator sessions \u2014 bare \`lobstah man\` prints the manual)
8515
8428
  helm registration, LOBSTAH_MAN=1, or a
8516
8429
  .lobstah-man file. Hookless sessions use the
8517
8430
  foreground verbs: soak --wait (worker),
8518
- man wait (lobsterman).
8431
+ man wait (lobstah man).
8519
8432
 
8520
8433
  workers (dispatched agents; injected into every brief):
8521
8434
  report <uuid> <verb> [--pr <url>] [--no-watch] [--] [note]
@@ -8565,6 +8478,9 @@ function gateHelm(who, grounds) {
8565
8478
  const refusal = helmGate(liveHelms(loadConfig().helm.ttlSecs * 1e3), who?.id, grounds);
8566
8479
  if (refusal) throw new Error(explainRefusal(refusal, who));
8567
8480
  }
8481
+ function armGraceNote(graceSecs) {
8482
+ return `(No watcher registered within ${graceSecs}s of this stop; one started in the last few seconds would have been accepted.)`;
8483
+ }
8568
8484
  function hookParkMode(configured, harness) {
8569
8485
  return configured ?? (harness === "claude" || !harness && !!process.env.CLAUDE_CODE_SESSION_ID ? "arm" : "block");
8570
8486
  }
@@ -8668,7 +8584,7 @@ function emitWatchAttention(attns, sessionId) {
8668
8584
  function findLane(id) {
8669
8585
  for (const lane of ["work", "chore"]) {
8670
8586
  const d = laneDirs(lane);
8671
- if (fs45.existsSync(path38.join(d.active, id)) || fs45.existsSync(path38.join(d.queue, `${id}.json`)) || fs45.existsSync(path38.join(d.done, id)) || fs45.existsSync(path38.join(d.state, `${id}.status`))) {
8587
+ if (fs46.existsSync(path39.join(d.active, id)) || fs46.existsSync(path39.join(d.queue, `${id}.json`)) || fs46.existsSync(path39.join(d.done, id)) || fs46.existsSync(path39.join(d.state, `${id}.status`))) {
8672
8588
  return lane;
8673
8589
  }
8674
8590
  }
@@ -8677,7 +8593,7 @@ function findLane(id) {
8677
8593
  function anythingInFlight() {
8678
8594
  const dispatches = ["work", "chore"].some((lane) => {
8679
8595
  const dirs = laneDirs(lane);
8680
- return [dirs.queue, dirs.active].some((dir) => fs45.readdirSync(dir).some((file) => !file.startsWith(".")));
8596
+ return [dirs.queue, dirs.active].some((dir) => fs46.readdirSync(dir).some((file) => !file.startsWith(".")));
8681
8597
  });
8682
8598
  return dispatches || listWatches().some((watch2) => watch2.owner === "man");
8683
8599
  }
@@ -8691,12 +8607,16 @@ function inheritedAttachments(id) {
8691
8607
  }
8692
8608
  function rowsFor(lane, bucket) {
8693
8609
  const dir = laneDirs(lane)[bucket];
8694
- const entries = fs45.readdirSync(dir).filter((f) => !f.startsWith(".")).map((f) => ({ f, m: fs45.statSync(path38.join(dir, f)).mtimeMs })).sort((a, b) => b.m - a.m);
8610
+ const entries = fs46.readdirSync(dir).filter((f) => !f.startsWith(".")).map((f) => ({ f, m: fs46.statSync(path39.join(dir, f)).mtimeMs })).sort((a, b) => b.m - a.m);
8695
8611
  const sliced = bucket === "done" ? entries.slice(0, 10) : entries;
8696
8612
  return sliced.map(({ f, m }) => {
8697
8613
  const id = f.replace(/\.json$/, "");
8698
- const state = reconcile({ log: readStatusLog(id, lane), lastEventAt: lastEventAt(id, lane) });
8699
- return { id, lane, bucket, state, updated: new Date(m).toISOString() };
8614
+ const queued = bucket === "queue";
8615
+ const log = readStatusLog(id, lane);
8616
+ const claimedAt = bucket === "active" ? readSessionClaim(id, lane)?.at : void 0;
8617
+ const state = displayState({ log, lastEventAt: lastEventAt(id, lane), queued, claimedAt });
8618
+ const updated = (queued && state === "queued" ? queuedAt(id, lane) : void 0) ?? (log.length === 0 ? claimedAt : void 0) ?? new Date(m).toISOString();
8619
+ return { id, lane, bucket, state, updated };
8700
8620
  });
8701
8621
  }
8702
8622
  async function mainCli() {
@@ -8783,21 +8703,24 @@ ${usageFor(cmd)}`);
8783
8703
  const d = {
8784
8704
  id: opt("--id") ?? randomUUID4(),
8785
8705
  repo,
8786
- brief: briefText ?? fs45.readFileSync(briefFile, "utf8"),
8706
+ brief: briefText ?? fs46.readFileSync(briefFile, "utf8"),
8787
8707
  harness: opt("--harness"),
8788
8708
  model: opt("--model"),
8789
8709
  effort: opt("--effort"),
8790
8710
  followUp: opt("--follow-up"),
8791
8711
  for: address
8792
8712
  };
8713
+ if (d.harness) d.harnessExplicit = true;
8714
+ if (d.model) d.modelExplicit = true;
8793
8715
  const lane = has("--chore") ? "chore" : "work";
8794
8716
  const attachments = [
8795
8717
  ...inheritedAttachments(d.followUp) ?? [],
8796
8718
  ...copyFiles(values("--attach"), dispatchAttachmentsDir(d.id, lane)) ?? []
8797
8719
  ];
8798
8720
  if (attachments.length > 0) d.attachments = attachments;
8721
+ d.queuedAt = (/* @__PURE__ */ new Date()).toISOString();
8799
8722
  enqueue(d, lane);
8800
- console.log(toonKV({ id: d.id, repo, lane, ...address ? { for: address } : {}, queued: (/* @__PURE__ */ new Date()).toISOString() }));
8723
+ console.log(toonKV({ id: d.id, repo, lane, ...address ? { for: address } : {}, queued: d.queuedAt }));
8801
8724
  for (const w of warnings) console.log(toonKV({ warning: w }));
8802
8725
  console.log(
8803
8726
  toonHelp([
@@ -8826,8 +8749,10 @@ ${usageFor(cmd)}`);
8826
8749
  }
8827
8750
  const lane = findLane(id);
8828
8751
  const log = readStatusLog(id, lane);
8829
- const state = reconcile({ log, lastEventAt: lastEventAt(id, lane) });
8830
- console.log(toonKV({ id, lane, state, lastNote: log.at(-1)?.note, entries: log.length, attachments: storedDescriptor(id, lane)?.attachments?.length ?? 0 }));
8752
+ const since = queuedAt(id, lane);
8753
+ const claimedAt = readSessionClaim(id, lane)?.at;
8754
+ const state = displayState({ log, lastEventAt: lastEventAt(id, lane), queued: since !== void 0, claimedAt });
8755
+ console.log(toonKV({ id, lane, state, ...state === "queued" ? { queued: since } : {}, lastNote: log.at(-1)?.note, entries: log.length, attachments: storedDescriptor(id, lane)?.attachments?.length ?? 0 }));
8831
8756
  console.log(
8832
8757
  toonHelp(
8833
8758
  state === "needs-decision" || state === "blocked" ? [`lobstah send ${id} "<answer>"`, `lobstah logs ${id} --follow`] : state === "done" || state === "failed" ? [`lobstah catch ${id} (branch, commits, PR)`] : [`lobstah logs ${id} --follow`, `lobstah send ${id} "<instruction>"`]
@@ -8840,8 +8765,8 @@ ${usageFor(cmd)}`);
8840
8765
  if (!id) throw new Error("logs requires a dispatch id");
8841
8766
  const lane = findLane(id);
8842
8767
  const file = eventsPath(id, lane);
8843
- if (fs45.existsSync(file)) {
8844
- const raw = fs45.readFileSync(file, "utf8");
8768
+ if (fs46.existsSync(file)) {
8769
+ const raw = fs46.readFileSync(file, "utf8");
8845
8770
  const lines = raw.split("\n").filter((l) => l.length > 0);
8846
8771
  const LIMIT = 50;
8847
8772
  if (!has("--full") && lines.length > LIMIT) {
@@ -8854,15 +8779,15 @@ ${usageFor(cmd)}`);
8854
8779
  }
8855
8780
  }
8856
8781
  if (has("--follow")) {
8857
- let size = fs45.existsSync(file) ? fs45.statSync(file).size : 0;
8782
+ let size = fs46.existsSync(file) ? fs46.statSync(file).size : 0;
8858
8783
  setInterval(() => {
8859
- if (!fs45.existsSync(file)) return;
8860
- const now2 = fs45.statSync(file).size;
8784
+ if (!fs46.existsSync(file)) return;
8785
+ const now2 = fs46.statSync(file).size;
8861
8786
  if (now2 > size) {
8862
- const fd = fs45.openSync(file, "r");
8787
+ const fd = fs46.openSync(file, "r");
8863
8788
  const buf = Buffer.alloc(now2 - size);
8864
- fs45.readSync(fd, buf, 0, buf.length, size);
8865
- fs45.closeSync(fd);
8789
+ fs46.readSync(fd, buf, 0, buf.length, size);
8790
+ fs46.closeSync(fd);
8866
8791
  process.stdout.write(buf.toString("utf8"));
8867
8792
  size = now2;
8868
8793
  }
@@ -8961,8 +8886,13 @@ ${[text, block].filter(Boolean).join("\n\n")}`, from, attachments);
8961
8886
  }
8962
8887
  const { harness = "claude", sessionId } = resolveSessionHarness(id, loadConfig(), lane);
8963
8888
  if (!sessionId) throw new Error(`${id} has no recorded harness session to attach to`);
8964
- const worktree = path38.join(lobstahHome(), "worktrees", id);
8965
- const cwd = fs45.existsSync(worktree) ? worktree : process.cwd();
8889
+ if (harness === "codex" && codexDesktopThread(sessionId)) {
8890
+ throw new Error(
8891
+ `${id}: ${CODEX_DESKTOP_THREAD} (session ${sessionId}) \u2014 open it in the Codex desktop app, or \`lobstah dispatch --follow-up ${id}\` to start cold with a progress note`
8892
+ );
8893
+ }
8894
+ const worktree = path39.join(lobstahHome(), "worktrees", id);
8895
+ const cwd = fs46.existsSync(worktree) ? worktree : process.cwd();
8966
8896
  const invocation = harness === "codex" ? codexInvocation(["resume", sessionId]) : { file: "claude", argv: ["--resume", sessionId] };
8967
8897
  if (!invocation) throw new Error("no codex CLI found \u2014 neither on PATH nor vendored by @openai/codex-sdk");
8968
8898
  if (has("--print")) {
@@ -8980,23 +8910,25 @@ ${[text, block].filter(Boolean).join("\n\n")}`, from, attachments);
8980
8910
  gateHelm(callerSession(opt("--session")));
8981
8911
  }
8982
8912
  const lane = findLane(id);
8983
- const activeDir = path38.join(laneDirs(lane).active, id);
8984
- if (!fs45.existsSync(activeDir)) throw new Error(`${id} is not active \u2014 swap only applies to in-flight dispatches`);
8985
- const descFile = path38.join(activeDir, "descriptor.json");
8986
- const descriptor = JSON.parse(fs45.readFileSync(descFile, "utf8"));
8913
+ const activeDir = path39.join(laneDirs(lane).active, id);
8914
+ if (!fs46.existsSync(activeDir)) throw new Error(`${id} is not active \u2014 swap only applies to in-flight dispatches`);
8915
+ const descFile = path39.join(activeDir, "descriptor.json");
8916
+ const descriptor = JSON.parse(fs46.readFileSync(descFile, "utf8"));
8987
8917
  const fromHarness = resolveSessionHarness(id, loadConfig(), lane).harness ?? descriptor.harness ?? "default";
8988
8918
  for (const key of ["harness", "model", "effort"]) {
8989
8919
  const v = opt(`--${key}`);
8990
8920
  if (v) descriptor[key] = v;
8991
8921
  }
8992
- fs45.writeFileSync(descFile, JSON.stringify(descriptor, null, 2));
8993
- const worktree = path38.join(lobstahHome(), "worktrees", id);
8994
- fs45.writeFileSync(path38.join(activeDir, "handoff"), handoffNote(fromHarness, worktreeProgress(worktree)));
8995
- const runnerFile = path38.join(activeDir, "runner.json");
8996
- if (fs45.existsSync(runnerFile)) {
8997
- const runner = JSON.parse(fs45.readFileSync(runnerFile, "utf8"));
8922
+ if (opt("--harness")) descriptor.harnessExplicit = true;
8923
+ if (opt("--model")) descriptor.modelExplicit = true;
8924
+ fs46.writeFileSync(descFile, JSON.stringify(descriptor, null, 2));
8925
+ const worktree = path39.join(lobstahHome(), "worktrees", id);
8926
+ fs46.writeFileSync(path39.join(activeDir, "handoff"), handoffNote(fromHarness, worktreeProgress(worktree)));
8927
+ const runnerFile = path39.join(activeDir, "runner.json");
8928
+ if (fs46.existsSync(runnerFile)) {
8929
+ const runner = JSON.parse(fs46.readFileSync(runnerFile, "utf8"));
8998
8930
  if (pidAlive(runner.pid, runner.processStartTime)) killGroup(runner.pid, "SIGKILL");
8999
- fs45.unlinkSync(runnerFile);
8931
+ fs46.unlinkSync(runnerFile);
9000
8932
  }
9001
8933
  appendStatus(id, lane, "working", `swapped to ${descriptor.harness ?? "default"} \u2014 awaiting respawn`);
9002
8934
  console.log(toonKV({ id, harness: descriptor.harness ?? "default", model: descriptor.model, swapped: true }));
@@ -9012,7 +8944,12 @@ ${[text, block].filter(Boolean).join("\n\n")}`, from, attachments);
9012
8944
  console.log(
9013
8945
  toonKV({
9014
8946
  id,
9015
- state: reconcile({ log, lastEventAt: lastEventAt(id, lane) }),
8947
+ state: displayState({
8948
+ log,
8949
+ lastEventAt: lastEventAt(id, lane),
8950
+ queued: queuedAt(id, lane) !== void 0,
8951
+ claimedAt: readSessionClaim(id, lane)?.at
8952
+ }),
9016
8953
  branch: ev.branch,
9017
8954
  prUrl: ev.prUrl,
9018
8955
  sessionId: ev.sessionId,
@@ -9166,7 +9103,7 @@ ${usageFor("attention")}`);
9166
9103
  ${errs.map((e) => `- ${e}`).join("\n")}`);
9167
9104
  const grounds = resolveGrounds(cfg, opt("--grounds"));
9168
9105
  const { harness } = detectHarness({ flag: opt("--harness"), sessionId });
9169
- const identity = { harness, cwd: process.cwd(), host: os10.hostname(), label: opt("--label"), window: captureWindow() };
9106
+ const identity = { harness, cwd: process.cwd(), host: os11.hostname(), label: opt("--label"), window: captureWindow() };
9170
9107
  const res = takeHelm({ sessionId, grounds, ttlMs: cfg.helm.ttlSecs * 1e3, take: has("--take"), identity });
9171
9108
  if ("held" in res) {
9172
9109
  const ageSecs = Math.max(0, Math.round((Date.now() - (Date.parse(res.held.heartbeatAt) || 0)) / 1e3));
@@ -9202,7 +9139,7 @@ ${errs.map((e) => `- ${e}`).join("\n")}`);
9202
9139
  gateHelm(callerSession(opt("--session")));
9203
9140
  }
9204
9141
  const lane = findLane(id);
9205
- if (fs45.existsSync(path38.join(laneDirs(lane).active, id))) {
9142
+ if (fs46.existsSync(path39.join(laneDirs(lane).active, id))) {
9206
9143
  requestCancel(id, lane);
9207
9144
  console.log(toonKV({ id, cancel: "requested", note: "the claimant (daemon or trap) winds it down at its next check" }));
9208
9145
  break;
@@ -9211,7 +9148,7 @@ ${errs.map((e) => `- ${e}`).join("\n")}`);
9211
9148
  console.log(toonKV({ id, cancel: "finalized", note: "cancelled before claim \u2014 recorded as failed in done/" }));
9212
9149
  break;
9213
9150
  }
9214
- if (fs45.existsSync(path38.join(laneDirs(lane).active, id))) {
9151
+ if (fs46.existsSync(path39.join(laneDirs(lane).active, id))) {
9215
9152
  requestCancel(id, lane);
9216
9153
  console.log(toonKV({ id, cancel: "requested", note: "claimed while cancelling \u2014 the claimant winds it down" }));
9217
9154
  break;
@@ -9226,6 +9163,7 @@ ${usageFor("man:wait")}`);
9226
9163
  }
9227
9164
  const caller = callerSession(opt("--session"));
9228
9165
  const sid = caller?.id;
9166
+ const waiter = !has("--peek") && sid ? armWatcher(sid, "man") : void 0;
9229
9167
  const cfgWait = loadConfig();
9230
9168
  let groundsName = opt("--grounds");
9231
9169
  gateHelm(caller, groundsName);
@@ -9234,7 +9172,6 @@ ${usageFor("man:wait")}`);
9234
9172
  heartbeatHelm(callerHelm.sessionId);
9235
9173
  groundsName ??= callerHelm.grounds;
9236
9174
  }
9237
- const waiter = !has("--peek") && sid ? armWatcher(sid, "man") : void 0;
9238
9175
  try {
9239
9176
  const timeoutSecs = Number(opt("--timeout") ?? "0");
9240
9177
  const deadline = timeoutSecs > 0 ? Date.now() + timeoutSecs * 1e3 : Number.POSITIVE_INFINITY;
@@ -9314,20 +9251,20 @@ ${usageFor("man:wait")}`);
9314
9251
  }
9315
9252
  }
9316
9253
  case "man:init": {
9317
- const file = has("--global") ? path38.join(os10.homedir(), ".claude", "settings.json") : path38.join(".claude", has("--shared") ? "settings.json" : "settings.local.json");
9254
+ const file = has("--global") ? path39.join(os11.homedir(), ".claude", "settings.json") : path39.join(".claude", has("--shared") ? "settings.json" : "settings.local.json");
9318
9255
  let existing;
9319
9256
  try {
9320
- existing = JSON.parse(fs45.readFileSync(file, "utf8"));
9257
+ existing = JSON.parse(fs46.readFileSync(file, "utf8"));
9321
9258
  } catch {
9322
9259
  existing = void 0;
9323
9260
  }
9324
9261
  const { settings, changed } = mergeHaulHook(existing);
9325
9262
  if (changed) {
9326
- fs45.mkdirSync(path38.dirname(file), { recursive: true });
9327
- fs45.writeFileSync(file, `${JSON.stringify(settings, null, 2)}
9263
+ fs46.mkdirSync(path39.dirname(file), { recursive: true });
9264
+ fs46.writeFileSync(file, `${JSON.stringify(settings, null, 2)}
9328
9265
  `);
9329
9266
  }
9330
- if (has("--marker")) fs45.writeFileSync(".lobstah-man", "");
9267
+ if (has("--marker")) fs46.writeFileSync(".lobstah-man", "");
9331
9268
  console.log(
9332
9269
  toonKV({
9333
9270
  hook: "lobstah man haul",
@@ -9346,8 +9283,10 @@ ${usageFor("man:wait")}`);
9346
9283
  const cfg = loadConfig();
9347
9284
  if (!has("--park") && hookParkMode(cfg.helm.park, trapReg.harness) === "arm") {
9348
9285
  if (await soakPark(trapReg.trapId, "0")) break;
9349
- if (!anythingInFlight() || liveWatcher(trapReg.sessionId, "trap", trapReg.trapId)) break;
9350
- console.log(JSON.stringify({ decision: "block", reason: `Arm the watcher: run \`lobstah soak --wait --timeout 900\` as a background task (it wakes this session when the fleet needs you), then end your turn.` }));
9286
+ if (!anythingInFlight()) break;
9287
+ const graceSecs = cfg.helm.armGraceSecs;
9288
+ if (await awaitWatcher(trapReg.sessionId, "trap", graceSecs * 1e3, trapReg.trapId)) break;
9289
+ console.log(JSON.stringify({ decision: "block", reason: `Arm the watcher: run \`lobstah soak --wait --timeout 900\` as a background task (it wakes this session when the fleet needs you), then end your turn. ` + armGraceNote(graceSecs) }));
9351
9290
  break;
9352
9291
  }
9353
9292
  await soakPark(trapReg.trapId, opt("--timeout"));
@@ -9365,7 +9304,7 @@ ${usageFor("man:wait")}`);
9365
9304
  if (helm) heartbeatHelm(helm.sessionId);
9366
9305
  const cfgHaul = loadConfig();
9367
9306
  if (!helm && liveHelms(cfgHaul.helm.ttlSecs * 1e3).length > 0) break;
9368
- if (!helm && process.env.LOBSTAH_MAN !== "1" && !fs45.existsSync(".lobstah-man")) break;
9307
+ if (!helm && process.env.LOBSTAH_MAN !== "1" && !fs46.existsSync(".lobstah-man")) break;
9369
9308
  const dueDigest = () => helm ? dueHelmDigest(helm, cfgHaul.helm.reportSecs) : void 0;
9370
9309
  const blockDigest = (d) => {
9371
9310
  advanceCursor(helm.grounds, d.now);
@@ -9415,8 +9354,9 @@ Handle anything actionable; the Stop hook checks again at turn end.`
9415
9354
  ].join("\n"));
9416
9355
  break;
9417
9356
  }
9418
- if (liveWatcher(hook.session_id, "man")) break;
9419
- emit(`Arm the watcher: run \`lobstah man wait --session ${hook.session_id} --timeout 900\` as a background task (it wakes this session when the fleet needs you), then end your turn.`);
9357
+ const graceSecs = cfgHaul.helm.armGraceSecs;
9358
+ if (await awaitWatcher(hook.session_id, "man", graceSecs * 1e3)) break;
9359
+ emit(`Arm the watcher: run \`lobstah man wait --session ${hook.session_id} --timeout 900\` as a background task (it wakes this session when the fleet needs you), then end your turn. ` + armGraceNote(graceSecs));
9420
9360
  break;
9421
9361
  }
9422
9362
  const timeoutSecs = Number(opt("--timeout") ?? "14000");
@@ -9469,7 +9409,7 @@ Handle anything actionable; the Stop hook checks again at turn end.`
9469
9409
  const hook = readHookStdin();
9470
9410
  if (!hook?.session_id) break;
9471
9411
  const behind = pluginBehindLine(lobstahVersion());
9472
- const context = buildBriefContext(hook.session_id, hook.cwd ?? process.cwd()) + (behind ? `
9412
+ const context = await buildBriefContext(hook.session_id, hook.cwd ?? process.cwd()) + (behind ? `
9473
9413
  ${behind}` : "");
9474
9414
  console.log(
9475
9415
  JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: context } })
@@ -9612,11 +9552,50 @@ ${usageFor("soak")}`);
9612
9552
  ${usageFor("pet")}`);
9613
9553
  }
9614
9554
  case "glass": {
9615
- const port = Number(opt("--port") ?? process.env.LOBSTAH_GLASS_PORT ?? "4949");
9616
- serveGlass(port);
9555
+ readGlassState();
9556
+ const requestedPort = opt("--port") === void 0 ? void 0 : Number(opt("--port"));
9557
+ if (requestedPort !== void 0 && (!Number.isInteger(requestedPort) || requestedPort < 1 || requestedPort > 65535)) {
9558
+ throw new UsageError("glass --port must be an integer from 1 to 65535");
9559
+ }
9560
+ if (pos[0] === "stop") {
9561
+ const stopped = await stopGlass();
9562
+ console.log(toonKV({ glass: stopped.stopped ? "stopped" : "not running", ...stopped }));
9563
+ break;
9564
+ }
9565
+ if (pos[0] === "status") {
9566
+ const status = await glassStatus(requestedPort ?? glassPort());
9567
+ console.log(toonKV({ glass: status.info ? "running" : "stopped", port: status.port, ...status.info ? { ...status.info.pid ? { pid: status.info.pid } : {}, version: status.info.version } : {} }));
9568
+ break;
9569
+ }
9570
+ const port = requestedPort ?? glassPort();
9571
+ if (pos[0] === "install") {
9572
+ const res = installService("glass", port);
9573
+ console.log(toonKV({ service: "glass", ...res }));
9574
+ break;
9575
+ }
9576
+ if (pos[0] === "uninstall") {
9577
+ const res = uninstallService("glass");
9578
+ console.log(toonKV({ service: "glass", ...res }));
9579
+ break;
9580
+ }
9581
+ if (has("--detach")) {
9582
+ const { info, already } = await startDetachedGlass(port);
9583
+ console.log(toonKV(glassLines(port, info, already)));
9584
+ break;
9585
+ }
9586
+ const existing = await probeGlass(port);
9587
+ if (existing) {
9588
+ console.log(toonKV(glassLines(port, existing, true)));
9589
+ break;
9590
+ }
9591
+ const server = serveGlass(port);
9592
+ await new Promise((resolve6, reject) => {
9593
+ server.once("listening", () => resolve6());
9594
+ server.once("error", reject);
9595
+ });
9617
9596
  console.log(
9618
9597
  toonKV({
9619
- glass: `http://127.0.0.1:${port}`,
9598
+ glass: glassUrl(port),
9620
9599
  mode: "read-only \u2014 looking consumes nothing",
9621
9600
  stop: "ctrl-c"
9622
9601
  })
@@ -9624,7 +9603,7 @@ ${usageFor("pet")}`);
9624
9603
  return;
9625
9604
  }
9626
9605
  case "doctor": {
9627
- const rows = runDoctor();
9606
+ const rows = await runDoctor();
9628
9607
  console.log(toonTable("doctor", rows, ["check", "status", "detail"]));
9629
9608
  if (rows.some((r) => r.status === "fail")) process.exitCode = 1;
9630
9609
  break;
@@ -9647,7 +9626,7 @@ ${usageFor("pet")}`);
9647
9626
  path: r.path,
9648
9627
  trunk: r.trunk,
9649
9628
  pickup: r.pickup ?? false,
9650
- exists: fs45.existsSync(r.path)
9629
+ exists: fs46.existsSync(r.path)
9651
9630
  }));
9652
9631
  console.log(toonTable("repos", rows, ["key", "path", "trunk", "pickup", "exists"]));
9653
9632
  break;
@@ -9715,7 +9694,7 @@ ${usageFor("pet")}`);
9715
9694
  break;
9716
9695
  }
9717
9696
  case "init": {
9718
- if (!fs45.existsSync(configPath())) {
9697
+ if (!fs46.existsSync(configPath())) {
9719
9698
  const exampleRepo = has("--scan") ? "" : `[repos.example]
9720
9699
  path = "~/src/example"
9721
9700
  trunk = "main"
@@ -9724,7 +9703,7 @@ trunk = "main"
9724
9703
  # pickup = true # opt into [pickup.github] multi-repo tracker pickup
9725
9704
 
9726
9705
  `;
9727
- fs45.writeFileSync(
9706
+ fs46.writeFileSync(
9728
9707
  configPath(),
9729
9708
  `# lobstah workspace definitions \u2014 the descriptor's repo key resolves here.
9730
9709
  # Top-level keys must come before any [section].
@@ -9761,12 +9740,12 @@ wallClockSecs = 3600
9761
9740
  let reference = "https://github.com/aequitas-labs/lobstah/blob/main/docs/configuration.md";
9762
9741
  try {
9763
9742
  const local = fileURLToPath4(new URL("../docs/configuration.md", import.meta.url));
9764
- if (fs45.existsSync(local)) reference = local;
9743
+ if (fs46.existsSync(local)) reference = local;
9765
9744
  } catch {
9766
9745
  }
9767
9746
  console.log(
9768
9747
  toonKV({
9769
- home: path38.dirname(configPath()),
9748
+ home: path39.dirname(configPath()),
9770
9749
  config: configPath(),
9771
9750
  ...added.length > 0 ? { added: added.join(", ") } : {},
9772
9751
  ...unmarked.length > 0 ? { note: `none marked pickable \u2014 set pickup = true per [repos.*] (or rerun with --pickup)` } : {},