lobstah 0.5.7 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -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,8 +1385,10 @@ 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,
@@ -1841,6 +1866,7 @@ function claimBait(reg) {
1841
1866
  at: (/* @__PURE__ */ new Date()).toISOString()
1842
1867
  };
1843
1868
  atomicWrite2(claimPath(id, "work"), JSON.stringify(claim, null, 2));
1869
+ appendStatus(id, "work", "working", `claimed by ${mine}`, claim.at);
1844
1870
  mergeEvidence(id, "work", { sessionId: reg.sessionId, harness: reg.harness, deliveredTo: mine, deliveredAt: claim.at });
1845
1871
  heartbeatTrap(reg.trapId, { claimed: id, parked: true });
1846
1872
  return { id, descriptor };
@@ -2188,6 +2214,24 @@ function normalizeChecks(rollup) {
2188
2214
  }
2189
2215
  return out.sort((a, b) => a.name.localeCompare(b.name));
2190
2216
  }
2217
+ function prStandingKinds(pr) {
2218
+ if (pr.state !== "OPEN")
2219
+ return [];
2220
+ const out = [];
2221
+ const { failed, pending, total } = pr.checks;
2222
+ if (pr.draft)
2223
+ out.push("pr:draft");
2224
+ const review = (pr.review?.unresolvedThreads ?? 0) > 0 || pr.review?.changesRequested === true;
2225
+ if (review)
2226
+ out.push("pr:review");
2227
+ if (failed > 0)
2228
+ out.push("pr:checks");
2229
+ if (isConflicting(pr.mergeStateStatus))
2230
+ out.push("pr:conflict");
2231
+ if (!pr.draft && !review && isMergeable(pr.mergeStateStatus) && (pr.reviewDecision === "APPROVED" || total > 0 && failed === 0 && pending === 0))
2232
+ out.push("pr:ready");
2233
+ return out;
2234
+ }
2191
2235
  function prReview(view) {
2192
2236
  const latest = /* @__PURE__ */ new Map();
2193
2237
  let lastReviewAt;
@@ -2422,12 +2466,17 @@ function upsertPr(pr, dispatchId) {
2422
2466
  const dispatches = [...before?.dispatches ?? []];
2423
2467
  if (dispatchId && !dispatches.includes(dispatchId))
2424
2468
  dispatches.push(dispatchId);
2469
+ const merged = { ...before ?? {}, ...pr };
2470
+ const standingSince = {};
2471
+ for (const kind of prStandingKinds(merged)) {
2472
+ standingSince[kind] = before?.standingSince?.[kind] ?? pr.observedAt;
2473
+ }
2425
2474
  const after = {
2426
- ...before ?? {},
2427
- ...pr,
2475
+ ...merged,
2428
2476
  key: ref.key,
2429
2477
  repo: `${ref.owner}/${ref.repo}`,
2430
- dispatches
2478
+ dispatches,
2479
+ standingSince
2431
2480
  };
2432
2481
  fs15.mkdirSync(prsDir(), { recursive: true });
2433
2482
  const file = prRecordFile(ref.key);
@@ -2453,6 +2502,7 @@ var init_prs = __esm({
2453
2502
 
2454
2503
  // packages/core/dist/resume.js
2455
2504
  import * as fs16 from "node:fs";
2505
+ import * as os3 from "node:os";
2456
2506
  import * as path12 from "node:path";
2457
2507
  import { spawnSync as spawnSync3 } from "node:child_process";
2458
2508
  function harnessFromSessionId(sessionId) {
@@ -2544,7 +2594,53 @@ function handoffNote(fromHarness, progress, why) {
2544
2594
 
2545
2595
  ${progress}`;
2546
2596
  }
2547
- var UUID, UNRESUMABLE;
2597
+ function codexHome(env = process.env) {
2598
+ return env.CODEX_HOME ?? path12.join(os3.homedir(), ".codex");
2599
+ }
2600
+ function codexRolloutFile(threadId, home = codexHome()) {
2601
+ const suffix = `-${threadId}.jsonl`;
2602
+ const walk = (dir, depth) => {
2603
+ let entries;
2604
+ try {
2605
+ entries = fs16.readdirSync(dir, { withFileTypes: true });
2606
+ } catch {
2607
+ return void 0;
2608
+ }
2609
+ for (const e of entries) {
2610
+ if (e.isFile() && e.name.startsWith("rollout-") && e.name.endsWith(suffix))
2611
+ return path12.join(dir, e.name);
2612
+ }
2613
+ if (depth === 0)
2614
+ return void 0;
2615
+ for (const e of entries.filter((x) => x.isDirectory()).sort((a, b) => b.name.localeCompare(a.name))) {
2616
+ const hit = walk(path12.join(dir, e.name), depth - 1);
2617
+ if (hit)
2618
+ return hit;
2619
+ }
2620
+ return void 0;
2621
+ };
2622
+ return walk(path12.join(home, "sessions"), 3) ?? walk(path12.join(home, "archived_sessions"), 0);
2623
+ }
2624
+ function codexDesktopThread(threadId, home = codexHome()) {
2625
+ const file = codexRolloutFile(threadId, home);
2626
+ if (!file)
2627
+ return void 0;
2628
+ let head = "";
2629
+ try {
2630
+ const fd = fs16.openSync(file, "r");
2631
+ try {
2632
+ const buf = Buffer.alloc(4096);
2633
+ head = buf.subarray(0, fs16.readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
2634
+ } finally {
2635
+ fs16.closeSync(fd);
2636
+ }
2637
+ } catch {
2638
+ return void 0;
2639
+ }
2640
+ const originator = /"originator"\s*:\s*"([^"]*)"/.exec(head)?.[1];
2641
+ return originator && /desktop/i.test(originator) ? originator : void 0;
2642
+ }
2643
+ var UUID, UNRESUMABLE, CODEX_DESKTOP_THREAD;
2548
2644
  var init_resume = __esm({
2549
2645
  "packages/core/dist/resume.js"() {
2550
2646
  "use strict";
@@ -2554,7 +2650,41 @@ var init_resume = __esm({
2554
2650
  init_soak();
2555
2651
  init_status();
2556
2652
  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;
2653
+ 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;
2654
+ CODEX_DESKTOP_THREAD = "Codex desktop thread; not resumable from the CLI";
2655
+ }
2656
+ });
2657
+
2658
+ // packages/core/dist/glass.js
2659
+ var init_glass = __esm({
2660
+ "packages/core/dist/glass.js"() {
2661
+ "use strict";
2662
+ }
2663
+ });
2664
+
2665
+ // packages/core/dist/models.js
2666
+ function modelHarness(model) {
2667
+ if (!model)
2668
+ return void 0;
2669
+ return MODEL_OWNERS.find(([re]) => re.test(model))?.[1];
2670
+ }
2671
+ function modelForHarness(harness, model) {
2672
+ const owner = modelHarness(model);
2673
+ if (model && owner && owner !== harness)
2674
+ return { dropped: { model, owner } };
2675
+ return { model };
2676
+ }
2677
+ function droppedModelNote(harness, dropped) {
2678
+ return `model ${dropped.model} is a ${dropped.owner} model \u2014 dropped, using ${harness}'s default`;
2679
+ }
2680
+ var MODEL_OWNERS;
2681
+ var init_models = __esm({
2682
+ "packages/core/dist/models.js"() {
2683
+ "use strict";
2684
+ MODEL_OWNERS = [
2685
+ [/^(anthropic[/.])?(claude|opus|sonnet|haiku|fable)(\b|-|$)|^opusplan$/i, "claude"],
2686
+ [/^(openai\/)?(gpt-|o\d+(\b|-|$)|codex-)/i, "codex"]
2687
+ ];
2558
2688
  }
2559
2689
  });
2560
2690
 
@@ -2583,6 +2713,8 @@ var init_dist2 = __esm({
2583
2713
  init_pr();
2584
2714
  init_prs();
2585
2715
  init_resume();
2716
+ init_glass();
2717
+ init_models();
2586
2718
  }
2587
2719
  });
2588
2720
 
@@ -2654,7 +2786,7 @@ var init_types2 = __esm({
2654
2786
  });
2655
2787
 
2656
2788
  // packages/adapters/dist/claude.js
2657
- import { spawn as spawn4 } from "node:child_process";
2789
+ import { spawn as spawn5 } from "node:child_process";
2658
2790
  function flagsToExtraArgs(flags) {
2659
2791
  const extra = {};
2660
2792
  for (let i = 0; i < flags.length; i++) {
@@ -2718,7 +2850,7 @@ function claudeCliArgs(opts) {
2718
2850
  return args;
2719
2851
  }
2720
2852
  function startClaudeCli(invocation, opts) {
2721
- const child = spawn4(invocation.file, [...invocation.argvPrefix, ...claudeCliArgs(opts)], {
2853
+ const child = spawn5(invocation.file, [...invocation.argvPrefix, ...claudeCliArgs(opts)], {
2722
2854
  cwd: opts.cwd,
2723
2855
  env: { ...process.env, ...opts.env },
2724
2856
  stdio: ["pipe", "pipe", "pipe"],
@@ -2876,25 +3008,25 @@ var init_claude = __esm({
2876
3008
  });
2877
3009
 
2878
3010
  // 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";
3011
+ import * as fs42 from "node:fs";
3012
+ import * as os10 from "node:os";
3013
+ import * as path35 from "node:path";
3014
+ import { spawn as spawn6 } from "node:child_process";
2883
3015
  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)) {
3016
+ const dir = path35.join(lobstahHome(), "codex-home");
3017
+ fs42.mkdirSync(dir, { recursive: true });
3018
+ const auth = path35.join(dir, "auth.json");
3019
+ const realAuth = path35.join(os10.homedir(), ".codex", "auth.json");
3020
+ if (!fs42.existsSync(auth) && fs42.existsSync(realAuth)) {
2889
3021
  try {
2890
- fs41.symlinkSync(realAuth, auth);
3022
+ fs42.symlinkSync(realAuth, auth);
2891
3023
  } catch {
2892
- fs41.copyFileSync(realAuth, auth);
3024
+ fs42.copyFileSync(realAuth, auth);
2893
3025
  }
2894
3026
  }
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");
3027
+ const cfg = path35.join(dir, "config.toml");
3028
+ if (!fs42.existsSync(cfg)) {
3029
+ fs42.writeFileSync(cfg, "# lobstah-managed: auth inherited from ~/.codex, interactive config not\n");
2898
3030
  }
2899
3031
  return dir;
2900
3032
  }
@@ -2942,7 +3074,7 @@ function startCodexCli(invoke, opts) {
2942
3074
  function runTurn(prompt, resumeThread) {
2943
3075
  return new Promise((resolve6) => {
2944
3076
  const inv = invoke(codexExecArgs(opts, prompt, resumeThread));
2945
- const child = spawn5(inv.file, inv.argv, {
3077
+ const child = spawn6(inv.file, inv.argv, {
2946
3078
  cwd: opts.cwd,
2947
3079
  env: { ...process.env, ...opts.env, CODEX_HOME: isolatedCodexHome() },
2948
3080
  stdio: ["ignore", "pipe", "pipe"],
@@ -3108,12 +3240,12 @@ var init_dist3 = __esm({
3108
3240
  });
3109
3241
 
3110
3242
  // packages/worktree/dist/index.js
3111
- import * as fs42 from "node:fs";
3112
- import * as path35 from "node:path";
3243
+ import * as fs43 from "node:fs";
3244
+ import * as path36 from "node:path";
3113
3245
  import { exec, execFile } from "node:child_process";
3114
3246
  import { promisify } from "node:util";
3115
3247
  function worktreePath(id) {
3116
- return path35.join(lobstahHome(), "worktrees", id);
3248
+ return path36.join(lobstahHome(), "worktrees", id);
3117
3249
  }
3118
3250
  async function git4(cwd, ...args) {
3119
3251
  const { stdout } = await run3("git", args, { cwd, env: process.env });
@@ -3121,10 +3253,10 @@ async function git4(cwd, ...args) {
3121
3253
  }
3122
3254
  async function allocate(repo, id) {
3123
3255
  const dir = worktreePath(id);
3124
- if (fs42.existsSync(dir)) {
3256
+ if (fs43.existsSync(dir)) {
3125
3257
  throw new Error(`worktree for ${id} already exists at ${dir} \u2014 never allocate a second`);
3126
3258
  }
3127
- if (!fs42.existsSync(repo.path)) {
3259
+ if (!fs43.existsSync(repo.path)) {
3128
3260
  if (!repo.origin)
3129
3261
  throw new Error(`repo path ${repo.path} missing and no origin configured`);
3130
3262
  await run3("git", ["clone", repo.origin, repo.path], { env: process.env });
@@ -3272,6 +3404,13 @@ function planStart(input) {
3272
3404
  if (envResume) {
3273
3405
  const own = resolveSessionHarness(id, cfg, lane);
3274
3406
  const harness = (own.sessionId === envResume ? own.harness : harnessFromSessionId(envResume)) ?? resolvedHarness;
3407
+ if (harness === "codex" && codexDesktopThread(envResume)) {
3408
+ return {
3409
+ harness: resolvedHarness,
3410
+ cold: { why: CODEX_DESKTOP_THREAD, fromHarness: harness },
3411
+ note: `restart: ${CODEX_DESKTOP_THREAD} (${short(envResume)}), starting cold on ${resolvedHarness}`
3412
+ };
3413
+ }
3275
3414
  return {
3276
3415
  harness,
3277
3416
  resume: { sessionId: envResume, own: true },
@@ -3308,7 +3447,7 @@ function planStart(input) {
3308
3447
  };
3309
3448
  }
3310
3449
  const requested = descriptor.harness;
3311
- const swap = requested !== void 0 && requested !== origin.harness && requested !== askedHarness(originId, originLane, cfg);
3450
+ const swap = requested !== void 0 && requested !== origin.harness && (descriptor.harnessExplicit === true || descriptor.harnessExplicit === void 0 && requested !== askedHarness(originId, originLane, cfg));
3312
3451
  if (swap) {
3313
3452
  return {
3314
3453
  harness: requested,
@@ -3316,6 +3455,13 @@ function planStart(input) {
3316
3455
  note: `follow-up of ${short(originId)}: swap \u2014 origin session is ${origin.harness}, ${requested} requested; starting cold on ${requested} with a progress note`
3317
3456
  };
3318
3457
  }
3458
+ if (origin.harness === "codex" && codexDesktopThread(origin.sessionId)) {
3459
+ return {
3460
+ harness: resolvedHarness,
3461
+ cold: { why: CODEX_DESKTOP_THREAD, fromHarness: origin.harness, origin: originRef },
3462
+ note: `follow-up of ${short(originId)}: ${CODEX_DESKTOP_THREAD} (${short(origin.sessionId)}), starting cold on ${resolvedHarness} with a progress note`
3463
+ };
3464
+ }
3319
3465
  const ignored = requested !== void 0 && requested !== origin.harness ? ` (--harness ${requested} ignored for the resume)` : "";
3320
3466
  return {
3321
3467
  harness: origin.harness,
@@ -3333,8 +3479,8 @@ var init_plan = __esm({
3333
3479
  });
3334
3480
 
3335
3481
  // packages/runner/dist/run.js
3336
- import * as fs43 from "node:fs";
3337
- import * as path36 from "node:path";
3482
+ import * as fs44 from "node:fs";
3483
+ import * as path37 from "node:path";
3338
3484
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3339
3485
  function coldNote(cold, cwd, trunk) {
3340
3486
  const parts = [worktreeProgress(cwd, trunk)];
@@ -3343,23 +3489,23 @@ function coldNote(cold, cwd, trunk) {
3343
3489
  return handoffNote(cold.fromHarness ?? "unknown", parts.join("\n\n"), cold.why);
3344
3490
  }
3345
3491
  async function main(activeDir, lane, deps = defaultDeps) {
3346
- const id = path36.basename(activeDir);
3492
+ const id = path37.basename(activeDir);
3347
3493
  const status = (verb, note) => appendStatus(id, lane, verb, note);
3348
3494
  const cfg = loadConfig();
3349
- const descriptor = JSON.parse(fs43.readFileSync(path36.join(activeDir, "descriptor.json"), "utf8"));
3495
+ const descriptor = JSON.parse(fs44.readFileSync(path37.join(activeDir, "descriptor.json"), "utf8"));
3350
3496
  const resolved = resolveDispatch(descriptor, cfg);
3351
3497
  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");
3498
+ const briefFile = path37.join(activeDir, "brief.md");
3499
+ if (!fs44.existsSync(briefFile))
3500
+ fs44.writeFileSync(briefFile, descriptor.brief);
3501
+ const brief = fs44.readFileSync(briefFile, "utf8");
3356
3502
  const attempts = Number(process.env.LOBSTAH_ATTEMPTS ?? "1");
3357
3503
  const runnerInfo = {
3358
3504
  pid: process.pid,
3359
3505
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3360
3506
  attempts
3361
3507
  };
3362
- fs43.writeFileSync(path36.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
3508
+ fs44.writeFileSync(path37.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
3363
3509
  const plan = planStart({
3364
3510
  id,
3365
3511
  lane,
@@ -3368,7 +3514,12 @@ async function main(activeDir, lane, deps = defaultDeps) {
3368
3514
  resolvedHarness: resolved.harness,
3369
3515
  envResume: process.env.LOBSTAH_RESUME
3370
3516
  });
3371
- const firstNote = [attempts > 1 ? `attempt ${attempts}` : void 0, plan.note].filter(Boolean).join("; ");
3517
+ const firstModel = modelForHarness(plan.harness, resolved.model);
3518
+ const firstNote = [
3519
+ attempts > 1 ? `attempt ${attempts}` : void 0,
3520
+ plan.note,
3521
+ firstModel.dropped ? droppedModelNote(plan.harness, firstModel.dropped) : void 0
3522
+ ].filter(Boolean).join("; ");
3372
3523
  status("working", firstNote || void 0);
3373
3524
  appendEvent(id, lane, {
3374
3525
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3377,36 +3528,35 @@ async function main(activeDir, lane, deps = defaultDeps) {
3377
3528
  });
3378
3529
  const own = readEvidence(id, lane).sessionId;
3379
3530
  mergeEvidence(id, lane, { harness: plan.harness, sessionId: plan.resume?.own ? own : void 0 });
3380
- const wtFile = path36.join(activeDir, "worktree.json");
3531
+ const wtFile = path37.join(activeDir, "worktree.json");
3381
3532
  let cwd;
3382
- if (fs43.existsSync(wtFile)) {
3383
- cwd = JSON.parse(fs43.readFileSync(wtFile, "utf8")).path;
3384
- if (!fs43.existsSync(cwd))
3533
+ if (fs44.existsSync(wtFile)) {
3534
+ cwd = JSON.parse(fs44.readFileSync(wtFile, "utf8")).path;
3535
+ if (!fs44.existsSync(cwd))
3385
3536
  throw new Error(`recorded worktree ${cwd} is gone`);
3386
- } else if (fs43.existsSync(worktreePath(id))) {
3537
+ } else if (fs44.existsSync(worktreePath(id))) {
3387
3538
  throw new Error(`unrecorded worktree already exists for ${id} \u2014 refusing to proceed`);
3388
3539
  } else {
3389
3540
  cwd = await deps.allocate(repo, id);
3390
- fs43.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
3541
+ fs44.writeFileSync(wtFile, JSON.stringify({ path: cwd }, null, 2));
3391
3542
  }
3392
- const adapter = deps.loadAdapter(plan.harness);
3393
3543
  const envNudge = process.env.LOBSTAH_NUDGE;
3394
3544
  const planNudge = plan.cold && !envNudge ? coldNote(plan.cold, cwd, repo.trunk) : void 0;
3395
3545
  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;
3546
+ const binDir = path37.resolve(path37.dirname(fileURLToPath3(import.meta.url)), "..", "..", "..", "bin");
3547
+ const workerPath = fs44.existsSync(path37.join(binDir, "lobstah")) ? `${binDir}${path37.delimiter}${process.env.PATH ?? ""}` : void 0;
3398
3548
  let wallClockHit = false;
3399
3549
  let current;
3400
3550
  const wallTimer = resolved.limits.wallClockSecs ? setTimeout(() => {
3401
3551
  wallClockHit = true;
3402
3552
  current?.kill();
3403
3553
  }, resolved.limits.wallClockSecs * 1e3) : void 0;
3404
- const runOnce = async (prompt, resumeSession) => {
3405
- const run4 = await adapter.start({
3554
+ const runOnce = async (harness, prompt, resumeSession) => {
3555
+ const run4 = await deps.loadAdapter(harness).start({
3406
3556
  id,
3407
3557
  cwd,
3408
3558
  prompt,
3409
- model: resolved.model,
3559
+ model: modelForHarness(harness, resolved.model).model,
3410
3560
  effort: resolved.effort,
3411
3561
  limits: resolved.limits,
3412
3562
  env: {
@@ -3422,18 +3572,24 @@ async function main(activeDir, lane, deps = defaultDeps) {
3422
3572
  const result2 = await run4.done;
3423
3573
  return { cancelled: cancelled2, activity, result: result2 };
3424
3574
  };
3425
- let outcome = await runOnce(promptWith(envNudge ?? planNudge), plan.resume?.sessionId);
3575
+ let outcome = await runOnce(plan.harness, promptWith(envNudge ?? planNudge), plan.resume?.sessionId);
3426
3576
  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 });
3577
+ const reason = outcome.result.error.replace(/\s+/g, " ").slice(0, 200).trim();
3578
+ const coldHarness = resolved.harness;
3579
+ const coldModel = modelForHarness(coldHarness, resolved.model);
3580
+ status("working", `resume-fallback: ${reason} \u2014 starting cold on ${coldHarness}` + (coldModel.dropped ? `; ${droppedModelNote(coldHarness, coldModel.dropped)}` : ""));
3581
+ appendEvent(id, lane, {
3582
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3583
+ type: "runner",
3584
+ data: { resumeFallback: reason, harness: coldHarness }
3585
+ });
3586
+ mergeEvidence(id, lane, { resumeFallback: reason, sessionId: void 0, harness: coldHarness });
3431
3587
  const note = coldNote({
3432
3588
  why: `resume of session ${plan.resume.sessionId} failed`,
3433
3589
  fromHarness: plan.harness,
3434
3590
  ...plan.resume.origin ? { origin: plan.resume.origin } : {}
3435
3591
  }, cwd, repo.trunk);
3436
- outcome = await runOnce(promptWith([envNudge, note].filter(Boolean).join("\n\n")), void 0);
3592
+ outcome = await runOnce(coldHarness, promptWith([envNudge, note].filter(Boolean).join("\n\n")), void 0);
3437
3593
  }
3438
3594
  if (wallTimer)
3439
3595
  clearTimeout(wallTimer);
@@ -3472,15 +3628,15 @@ __export(dist_exports, {
3472
3628
  planStart: () => planStart,
3473
3629
  runRunner: () => runRunner
3474
3630
  });
3475
- import * as fs44 from "node:fs";
3476
- import * as path37 from "node:path";
3631
+ import * as fs45 from "node:fs";
3632
+ import * as path38 from "node:path";
3477
3633
  function runRunner(activeDirArg2, laneArg2) {
3478
3634
  const lane = laneArg2 === "chore" ? "chore" : "work";
3479
3635
  main(activeDirArg2, lane).catch((err) => {
3480
- const id = path37.basename(activeDirArg2);
3636
+ const id = path38.basename(activeDirArg2);
3481
3637
  try {
3482
3638
  appendStatus(id, lane, "failed", String(err instanceof Error ? err.message : err).slice(0, 500));
3483
- fs44.renameSync(activeDirArg2, path37.join(laneDirs(lane).done, id));
3639
+ fs45.renameSync(activeDirArg2, path38.join(laneDirs(lane).done, id));
3484
3640
  } catch {
3485
3641
  }
3486
3642
  process.exitCode = 1;
@@ -3502,9 +3658,9 @@ var init_dist5 = __esm({
3502
3658
 
3503
3659
  // apps/cli/src/main.ts
3504
3660
  init_dist2();
3505
- import * as fs45 from "node:fs";
3506
- import * as os10 from "node:os";
3507
- import * as path38 from "node:path";
3661
+ import * as fs46 from "node:fs";
3662
+ import * as os11 from "node:os";
3663
+ import * as path39 from "node:path";
3508
3664
  import { randomUUID as randomUUID4 } from "node:crypto";
3509
3665
  import { spawnSync as spawnSync9 } from "node:child_process";
3510
3666
  import { fileURLToPath as fileURLToPath4 } from "node:url";
@@ -3570,7 +3726,7 @@ function killGroup(pid, signal = "SIGTERM") {
3570
3726
  // packages/supervisor/dist/daemon.js
3571
3727
  init_dist2();
3572
3728
  import * as fs18 from "node:fs";
3573
- import * as os3 from "node:os";
3729
+ import * as os4 from "node:os";
3574
3730
  import * as path14 from "node:path";
3575
3731
  import { spawn as spawn2 } from "node:child_process";
3576
3732
  import { createRequire as createRequire2 } from "node:module";
@@ -3854,7 +4010,7 @@ function reconcileOne(st, cfg, log) {
3854
4010
  }
3855
4011
  function writeHeartbeat(cfg) {
3856
4012
  const payload = {
3857
- machineId: os3.hostname(),
4013
+ machineId: os4.hostname(),
3858
4014
  repos: Object.keys(cfg.repos),
3859
4015
  harnesses: detectHarnesses(),
3860
4016
  maxConcurrent: cfg.limits.maxConcurrent,
@@ -3989,7 +4145,7 @@ init_dist();
3989
4145
  init_dist2();
3990
4146
  import * as fs19 from "node:fs";
3991
4147
  import { execSync } from "node:child_process";
3992
- import * as os4 from "node:os";
4148
+ import * as os5 from "node:os";
3993
4149
 
3994
4150
  // apps/pick/dist/types.js
3995
4151
  var DEFAULT_MERGE_POLICY = {
@@ -4029,7 +4185,7 @@ function resolveTokenSource(section, label) {
4029
4185
  }
4030
4186
  if (file) {
4031
4187
  return () => {
4032
- const token = fs19.readFileSync(file.startsWith("~/") ? `${os4.homedir()}/${file.slice(2)}` : file, "utf8").trim();
4188
+ const token = fs19.readFileSync(file.startsWith("~/") ? `${os5.homedir()}/${file.slice(2)}` : file, "utf8").trim();
4033
4189
  if (!token)
4034
4190
  throw new Error(`${label}: tokenFile ${file} is empty`);
4035
4191
  return token;
@@ -5690,18 +5846,7 @@ function onTheHook(kind, chain, owners) {
5690
5846
  (m) => m.bucket !== "done" && (owners.reviewRounds.has(m.id) || m.id === owners.watchFollowUp)
5691
5847
  )?.id;
5692
5848
  }
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
- }
5849
+ var prKinds = prStandingKinds;
5705
5850
  function readyBlockedByStack(pr, tracked) {
5706
5851
  if (pr.state !== "OPEN" || !pr.baseRefName) return false;
5707
5852
  const ref = parsePrRef(pr.url);
@@ -5778,9 +5923,9 @@ function prAttention(now2, observed = observedPrs()) {
5778
5923
  const ref = parsePrRef(pr.url);
5779
5924
  const watchFollowUp = ref ? readWatch(ref.key)?.lastFollowUpId : void 0;
5780
5925
  const chain = dispatch ? chainOf(id) : [];
5781
- const since = (dispatch ? readStatusLog(id, lane).at(-1)?.at : void 0) ?? pr.observedAt;
5782
5926
  for (const kind of kinds) {
5783
5927
  if (onTheHook(kind, chain, { reviewRounds, watchFollowUp })) continue;
5928
+ const standingSince = pr.standingSince?.[kind] ?? pr.observedAt;
5784
5929
  out.push({
5785
5930
  kind,
5786
5931
  key: ref?.key ?? pr.url,
@@ -5788,8 +5933,9 @@ function prAttention(now2, observed = observedPrs()) {
5788
5933
  id,
5789
5934
  lane,
5790
5935
  verb: kind,
5791
- ageSecs: Math.max(0, Math.round((now2 - Date.parse(since)) / 1e3)),
5792
- at: since,
5936
+ ageSecs: Math.max(0, Math.round((now2 - Date.parse(standingSince)) / 1e3)),
5937
+ at: standingSince,
5938
+ standingSince,
5793
5939
  note: PR_KIND_NOTE[kind](pr),
5794
5940
  repo: dispatch ? repoOf(id, lane) : ref ? `${ref.owner}/${ref.repo}` : void 0,
5795
5941
  prUrl: pr.url,
@@ -5804,8 +5950,7 @@ function prAttention(now2, observed = observedPrs()) {
5804
5950
  });
5805
5951
  }
5806
5952
  }
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 ?? "") ?? ""));
5953
+ return out;
5809
5954
  }
5810
5955
  function landedAttention(cfg, now2) {
5811
5956
  const out = [];
@@ -5826,6 +5971,7 @@ function landedAttention(cfg, now2) {
5826
5971
  verb: last.verb,
5827
5972
  ageSecs: Math.max(0, Math.round((now2 - at) / 1e3)),
5828
5973
  at: last.at,
5974
+ standingSince: last.at,
5829
5975
  note: last.note,
5830
5976
  repo,
5831
5977
  ...ev.prUrl ? { prUrl: ev.prUrl } : {}
@@ -5892,7 +6038,8 @@ function doneIds(lane) {
5892
6038
  function describeDispatch(id, lane, bucket) {
5893
6039
  const log = readStatusLog(id, lane);
5894
6040
  const last = log.at(-1);
5895
- const state = bucket === "queued" ? "queued" : reconcile({ log, lastEventAt: lastEventAt(id, lane) });
6041
+ const claimedAt = bucket === "active" ? readSessionClaim(id, lane)?.at : void 0;
6042
+ const state = bucket === "queued" ? "queued" : displayState({ log, lastEventAt: lastEventAt(id, lane), queued: false, claimedAt });
5896
6043
  const evidence = readEvidence(id, lane);
5897
6044
  const answered = last && (last.verb === "needs-decision" || last.verb === "blocked") ? answeredAt(id, lane, last.at) : void 0;
5898
6045
  return {
@@ -5901,7 +6048,8 @@ function describeDispatch(id, lane, bucket) {
5901
6048
  bucket,
5902
6049
  state,
5903
6050
  note: last?.note,
5904
- at: last?.at,
6051
+ // Queued work has no log yet; its time is when it entered the queue.
6052
+ at: last?.at ?? (bucket === "queued" ? queuedAt(id, lane) : claimedAt),
5905
6053
  ...answered ? { answeredAt: answered } : {},
5906
6054
  prUrl: evidence.prUrl,
5907
6055
  pr: evidence.pr
@@ -5963,6 +6111,7 @@ function buildTendReport(now2 = Date.now()) {
5963
6111
  verb: last.verb,
5964
6112
  ageSecs: Math.max(0, Math.round((now2 - Date.parse(last.at)) / 1e3)),
5965
6113
  at: last.at,
6114
+ standingSince: last.at,
5966
6115
  note: last.note
5967
6116
  });
5968
6117
  }
@@ -5998,6 +6147,7 @@ function buildTendReport(now2 = Date.now()) {
5998
6147
  verb: "watch",
5999
6148
  ageSecs: oldest?.at ? Math.max(0, Math.round((now2 - Date.parse(oldest.at)) / 1e3)) : 0,
6000
6149
  at: oldest?.at,
6150
+ standingSince: oldest?.at,
6001
6151
  note: pending.at(-1)?.summary
6002
6152
  });
6003
6153
  }
@@ -6074,6 +6224,7 @@ function buildTendReport(now2 = Date.now()) {
6074
6224
  return ack ? { ...a, acked: { at: ack.at, by: ack.by } } : a;
6075
6225
  })
6076
6226
  );
6227
+ attention.sort((a, b) => (a.standingSince ?? a.at ?? "").localeCompare(b.standingSince ?? b.at ?? "") || a.key.localeCompare(b.key));
6077
6228
  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
6229
  const traps = listTraps().map((r) => {
6079
6230
  const hbAgeSecs = Math.max(0, Math.round((now2 - (Date.parse(r.heartbeatAt) || 0)) / 1e3));
@@ -6351,7 +6502,7 @@ function charter(g) {
6351
6502
  const repos = g.repos.length > 0 ? g.repos.join(", ") : "(no repos configured)";
6352
6503
  return `the helm charter \u2014 grounds "${g.name}" (${repos})
6353
6504
 
6354
- You hold the helm. You are the lobsterman for these grounds.
6505
+ You hold the helm. You are the lobstah man for these grounds.
6355
6506
 
6356
6507
  Role:
6357
6508
  - Triage incoming work. Dispatch it. Review each catch. Decide requeue or cancel.
@@ -6420,6 +6571,205 @@ function readHookStdin() {
6420
6571
  }
6421
6572
  }
6422
6573
 
6574
+ // apps/cli/src/glass-lifecycle.ts
6575
+ init_dist2();
6576
+ import * as fs33 from "node:fs";
6577
+ import * as http from "node:http";
6578
+ import * as path26 from "node:path";
6579
+ import { spawn as spawn4 } from "node:child_process";
6580
+ var glassUrl = (port) => `http://127.0.0.1:${port}`;
6581
+ var stateFile = () => path26.join(lobstahHome(), "state", "glass.json");
6582
+ function glassPort() {
6583
+ const value = Number(process.env.LOBSTAH_GLASS_PORT ?? loadConfig().glass.port);
6584
+ if (!Number.isInteger(value) || value < 1 || value > 65535) throw new Error("glass port must be an integer from 1 to 65535");
6585
+ return value;
6586
+ }
6587
+ function readGlassState() {
6588
+ const file = stateFile();
6589
+ let state;
6590
+ try {
6591
+ state = JSON.parse(fs33.readFileSync(file, "utf8"));
6592
+ } catch {
6593
+ if (fs33.existsSync(file)) fs33.unlinkSync(file);
6594
+ return void 0;
6595
+ }
6596
+ if (!Number.isInteger(state.pid) || state.pid <= 0 || !Number.isInteger(state.port) || !pidAlive2(state.pid)) {
6597
+ fs33.unlinkSync(file);
6598
+ return void 0;
6599
+ }
6600
+ return state;
6601
+ }
6602
+ function pidAlive2(pid) {
6603
+ try {
6604
+ process.kill(pid, 0);
6605
+ return true;
6606
+ } catch (err) {
6607
+ return err.code === "EPERM";
6608
+ }
6609
+ }
6610
+ function probeVersionEndpoint(port, timeoutMs) {
6611
+ return new Promise((resolve6) => {
6612
+ let settled = false;
6613
+ const finish = (info) => {
6614
+ if (settled) return;
6615
+ settled = true;
6616
+ resolve6(info);
6617
+ };
6618
+ const req = http.get(`${glassUrl(port)}/api/version`, { timeout: timeoutMs }, (res) => {
6619
+ if (res.statusCode !== 200) {
6620
+ res.resume();
6621
+ finish();
6622
+ return;
6623
+ }
6624
+ let body = "";
6625
+ res.setEncoding("utf8");
6626
+ res.on("data", (chunk) => {
6627
+ body += chunk;
6628
+ if (body.length > 1024) {
6629
+ finish();
6630
+ req.destroy();
6631
+ }
6632
+ });
6633
+ res.on("close", () => finish());
6634
+ res.on("end", () => {
6635
+ try {
6636
+ const info = JSON.parse(body);
6637
+ finish(info.service === "lobstah-glass" && Number.isInteger(info.pid) && typeof info.version === "string" ? info : void 0);
6638
+ } catch {
6639
+ finish();
6640
+ }
6641
+ });
6642
+ });
6643
+ req.on("timeout", () => req.destroy());
6644
+ req.on("error", () => finish());
6645
+ });
6646
+ }
6647
+ function probeLegacyGlass(port, timeoutMs) {
6648
+ return new Promise((resolve6) => {
6649
+ let settled = false;
6650
+ const finish = (info) => {
6651
+ if (settled) return;
6652
+ settled = true;
6653
+ resolve6(info);
6654
+ };
6655
+ const req = http.get(`${glassUrl(port)}/data`, { timeout: timeoutMs }, (res) => {
6656
+ if (res.statusCode !== 200) {
6657
+ res.resume();
6658
+ finish();
6659
+ return;
6660
+ }
6661
+ let body = "";
6662
+ res.setEncoding("utf8");
6663
+ res.on("data", (chunk) => {
6664
+ body += chunk;
6665
+ if (body.length > 1e6) {
6666
+ finish();
6667
+ req.destroy();
6668
+ }
6669
+ });
6670
+ res.on("close", () => finish());
6671
+ res.on("end", () => {
6672
+ try {
6673
+ const data = JSON.parse(body);
6674
+ finish(
6675
+ typeof data.version === "string" && Array.isArray(data.dispatches) && Array.isArray(data.helms) ? { service: "lobstah-glass", version: data.version } : void 0
6676
+ );
6677
+ } catch {
6678
+ finish();
6679
+ }
6680
+ });
6681
+ });
6682
+ req.on("timeout", () => req.destroy());
6683
+ req.on("error", () => finish());
6684
+ });
6685
+ }
6686
+ async function probeGlass(port, timeoutMs = 400) {
6687
+ return await probeVersionEndpoint(port, timeoutMs) ?? await probeLegacyGlass(port, timeoutMs);
6688
+ }
6689
+ function glassLines(port, info, already = false) {
6690
+ return {
6691
+ glass: glassUrl(port),
6692
+ ...already ? { already: "running" } : {},
6693
+ ...info.version !== lobstahVersion() ? { stale: `${info.version} (run lobstah glass stop && lobstah glass --detach)` } : {}
6694
+ };
6695
+ }
6696
+ async function startDetachedGlass(port) {
6697
+ const tracked = readGlassState();
6698
+ if (tracked && tracked.port !== port) {
6699
+ const owner = await probeGlass(tracked.port);
6700
+ if (owner?.pid === tracked.pid) {
6701
+ throw new Error(`detached glass is running on ${glassUrl(tracked.port)} \u2014 run lobstah glass stop before changing ports`);
6702
+ }
6703
+ fs33.unlinkSync(stateFile());
6704
+ }
6705
+ const existing = await probeGlass(port);
6706
+ if (existing) return { info: existing, already: true };
6707
+ const home = lobstahHome();
6708
+ const logDir = path26.join(home, "logs");
6709
+ fs33.mkdirSync(logDir, { recursive: true });
6710
+ const fd = fs33.openSync(path26.join(logDir, "glass.log"), "a");
6711
+ let child;
6712
+ try {
6713
+ child = spawn4(process.execPath, [...COMPILED_BINARY ? [] : [fs33.realpathSync(process.argv[1])], "glass", "--port", String(port)], {
6714
+ detached: true,
6715
+ stdio: ["ignore", fd, fd],
6716
+ env: process.env,
6717
+ windowsHide: true
6718
+ });
6719
+ } finally {
6720
+ fs33.closeSync(fd);
6721
+ }
6722
+ let childError;
6723
+ child.on("error", (error) => {
6724
+ childError = error;
6725
+ });
6726
+ child.unref();
6727
+ const deadline = Date.now() + 5e3;
6728
+ while (Date.now() < deadline) {
6729
+ if (childError) throw childError;
6730
+ const info = await probeGlass(port);
6731
+ if (info) {
6732
+ if (info.pid === child.pid) {
6733
+ const state = { pid: info.pid, port, startedAt: (/* @__PURE__ */ new Date()).toISOString(), version: info.version };
6734
+ fs33.mkdirSync(path26.dirname(stateFile()), { recursive: true });
6735
+ fs33.writeFileSync(stateFile(), JSON.stringify(state, null, 2) + "\n");
6736
+ return { info, already: false };
6737
+ }
6738
+ return { info, already: true };
6739
+ }
6740
+ if (child.exitCode !== null) break;
6741
+ await new Promise((resolve6) => setTimeout(resolve6, 100));
6742
+ }
6743
+ throw new Error(`glass did not answer on ${glassUrl(port)} within 5 seconds (see ${path26.join(logDir, "glass.log")})`);
6744
+ }
6745
+ async function stopGlass() {
6746
+ const state = readGlassState();
6747
+ if (!state) return { stopped: false };
6748
+ const info = await probeGlass(state.port);
6749
+ if (!info || info.pid !== state.pid) {
6750
+ fs33.unlinkSync(stateFile());
6751
+ return { stopped: false };
6752
+ }
6753
+ process.kill(state.pid, "SIGTERM");
6754
+ const deadline = Date.now() + 5e3;
6755
+ while (Date.now() < deadline && await probeGlass(state.port)) {
6756
+ await new Promise((resolve6) => setTimeout(resolve6, 100));
6757
+ }
6758
+ if (await probeGlass(state.port)) throw new Error(`glass pid ${state.pid} did not stop`);
6759
+ fs33.unlinkSync(stateFile());
6760
+ return { stopped: true, port: state.port, pid: state.pid };
6761
+ }
6762
+ async function glassStatus(port = glassPort()) {
6763
+ let state = readGlassState();
6764
+ const selectedPort = state?.port ?? port;
6765
+ const info = await probeGlass(selectedPort);
6766
+ if (state && info?.pid !== state.pid) {
6767
+ fs33.unlinkSync(stateFile());
6768
+ state = void 0;
6769
+ }
6770
+ return { port: state || info ? selectedPort : port, info, state };
6771
+ }
6772
+
6423
6773
  // apps/cli/src/brief.ts
6424
6774
  var SOAK_WHERE = "(from a linked worktree, never the primary checkout)";
6425
6775
  function signOnHelp(sessionId, opts = {}) {
@@ -6443,8 +6793,9 @@ function sessionGrounds(cwd) {
6443
6793
  if (owned) return { grounds: owned };
6444
6794
  return { placeholder: `<${all.map((g) => g.name).join("|")}>` };
6445
6795
  }
6446
- function buildBriefContext(sessionId, cwd) {
6796
+ async function buildBriefContext(sessionId, cwd) {
6447
6797
  let fleet = "";
6798
+ let glass = "";
6448
6799
  try {
6449
6800
  const r = buildTendReport();
6450
6801
  const waiting = r.attention.filter((a) => a.kind === "question" || a.kind === "watch").length;
@@ -6452,6 +6803,12 @@ function buildBriefContext(sessionId, cwd) {
6452
6803
  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
6804
  } catch {
6454
6805
  }
6806
+ try {
6807
+ const port = glassPort();
6808
+ if (await probeGlass(port)) glass = ` Glass: ${glassUrl(port)}.`;
6809
+ } catch {
6810
+ }
6811
+ fleet += glass;
6455
6812
  const helmReg = helmOf(sessionId);
6456
6813
  if (helmReg) {
6457
6814
  heartbeatHelm(helmReg.sessionId);
@@ -6478,18 +6835,18 @@ ${help}`;
6478
6835
 
6479
6836
  // apps/cli/src/cull.ts
6480
6837
  init_dist2();
6481
- import * as fs33 from "node:fs";
6482
- import * as path26 from "node:path";
6838
+ import * as fs34 from "node:fs";
6839
+ import * as path27 from "node:path";
6483
6840
  import { execFileSync as execFileSync3 } from "node:child_process";
6484
6841
  var DAY = 864e5;
6485
6842
  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);
6843
+ const stat = fs34.lstatSync(target);
6844
+ if (stat.isDirectory()) return fs34.readdirSync(target).reduce((sum, name) => sum + bytesAt(path27.join(target, name)), 0);
6488
6845
  return stat.isFile() ? stat.size : 0;
6489
6846
  }
6490
6847
  function idsIn(dir) {
6491
6848
  try {
6492
- return new Set(fs33.readdirSync(dir).filter((f) => !f.startsWith(".")).map((f) => f.replace(/\.json$/, "")));
6849
+ return new Set(fs34.readdirSync(dir).filter((f) => !f.startsWith(".")).map((f) => f.replace(/\.json$/, "")));
6493
6850
  } catch {
6494
6851
  return /* @__PURE__ */ new Set();
6495
6852
  }
@@ -6502,7 +6859,7 @@ function planCull(olderThanDays, now2 = Date.now()) {
6502
6859
  const referencedAttachmentState = /* @__PURE__ */ new Set();
6503
6860
  const retainReferences = (id, lane) => {
6504
6861
  for (const attachment of storedDescriptor(id, lane)?.attachments ?? []) {
6505
- referencedAttachmentState.add(path26.dirname(path26.dirname(attachment.path)));
6862
+ referencedAttachmentState.add(path27.dirname(path27.dirname(attachment.path)));
6506
6863
  }
6507
6864
  };
6508
6865
  for (const lane of ["work", "chore"]) {
@@ -6516,41 +6873,41 @@ function planCull(olderThanDays, now2 = Date.now()) {
6516
6873
  retainReferences(id, lane);
6517
6874
  }
6518
6875
  for (const id of idsIn(d.done)) {
6519
- const p = path26.join(d.done, id);
6520
- const m = fs33.statSync(p).mtimeMs;
6876
+ const p = path27.join(d.done, id);
6877
+ const m = fs34.statSync(p).mtimeMs;
6521
6878
  doneMtimes.set(id, m);
6522
6879
  if (m >= cutoff) retainReferences(id, lane);
6523
6880
  if (m < cutoff) items.push({ kind: "done", id, target: p, ageDays: Math.floor((now2 - m) / DAY), bytes: bytesAt(p) });
6524
6881
  }
6525
6882
  }
6526
- const wtRoot = path26.join(lobstahHome(), "worktrees");
6883
+ const wtRoot = path27.join(lobstahHome(), "worktrees");
6527
6884
  for (const id of idsIn(wtRoot)) {
6528
6885
  if (live.has(id)) continue;
6529
6886
  const doneAt = doneMtimes.get(id);
6530
6887
  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) });
6888
+ const p = path27.join(wtRoot, id);
6889
+ items.push({ kind: "worktree", id, target: p, ageDays: Math.floor((now2 - fs34.statSync(p).mtimeMs) / DAY), bytes: bytesAt(p) });
6533
6890
  }
6534
6891
  for (const lane of ["work", "chore"]) {
6535
6892
  const d = laneDirs(lane);
6536
6893
  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;
6894
+ for (const f of fs34.readdirSync(d.state)) {
6895
+ const target = path27.join(d.state, f);
6896
+ const directory = fs34.statSync(target).isDirectory();
6897
+ if (directory && !fs34.existsSync(path27.join(target, "attachments"))) continue;
6541
6898
  const id = directory ? f : f.replace(/\.(status|events|evidence|attn|notified|runner\.log)$/, "");
6542
6899
  if (id === f && !directory) continue;
6543
- if (live.has(id) || (doneMtimes.get(id) ?? 0) >= cutoff || referencedAttachmentState.has(path26.join(d.state, id))) continue;
6900
+ if (live.has(id) || (doneMtimes.get(id) ?? 0) >= cutoff || referencedAttachmentState.has(path27.join(d.state, id))) continue;
6544
6901
  const previous = groups.get(id);
6545
6902
  groups.set(id, {
6546
- mtime: Math.max(previous?.mtime ?? 0, fs33.statSync(target).mtimeMs),
6903
+ mtime: Math.max(previous?.mtime ?? 0, fs34.statSync(target).mtimeMs),
6547
6904
  bytes: (previous?.bytes ?? 0) + bytesAt(target)
6548
6905
  });
6549
6906
  }
6550
6907
  for (const [id, group] of groups) {
6551
6908
  const ageFrom = doneMtimes.get(id) ?? group.mtime;
6552
6909
  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 });
6910
+ items.push({ kind: "state", id, target: path27.join(d.state, `${id}.*`), ageDays: Math.floor((now2 - ageFrom) / DAY), bytes: group.bytes });
6554
6911
  }
6555
6912
  }
6556
6913
  for (const r of readPrs()) {
@@ -6567,9 +6924,9 @@ function planCull(olderThanDays, now2 = Date.now()) {
6567
6924
  }
6568
6925
  function removeWorktree(id, dir) {
6569
6926
  for (const lane of ["work", "chore"]) {
6570
- const descFile = path26.join(laneDirs(lane).done, id, "descriptor.json");
6927
+ const descFile = path27.join(laneDirs(lane).done, id, "descriptor.json");
6571
6928
  try {
6572
- const desc = JSON.parse(fs33.readFileSync(descFile, "utf8"));
6929
+ const desc = JSON.parse(fs34.readFileSync(descFile, "utf8"));
6573
6930
  const repo = loadConfig().repos[desc.repo];
6574
6931
  if (repo) {
6575
6932
  execFileSync3("git", ["worktree", "remove", "--force", dir], { cwd: repo.path, stdio: "ignore" });
@@ -6578,25 +6935,25 @@ function removeWorktree(id, dir) {
6578
6935
  } catch {
6579
6936
  }
6580
6937
  }
6581
- fs33.rmSync(dir, { recursive: true, force: true });
6938
+ fs34.rmSync(dir, { recursive: true, force: true });
6582
6939
  }
6583
6940
  function applyCull(items) {
6584
6941
  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 });
6942
+ for (const item of items.filter((i) => i.kind === "done")) fs34.rmSync(item.target, { recursive: true, force: true });
6586
6943
  for (const item of items.filter((i) => i.kind === "ack")) removeAck(item.target);
6587
6944
  for (const item of items.filter((i) => i.kind === "pr")) removePr(item.target);
6588
6945
  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 });
6946
+ const dir = path27.dirname(item.target);
6947
+ for (const f of fs34.readdirSync(dir)) {
6948
+ if (f === item.id || f.startsWith(`${item.id}.`)) fs34.rmSync(path27.join(dir, f), { recursive: true, force: true });
6592
6949
  }
6593
6950
  }
6594
6951
  }
6595
6952
 
6596
6953
  // apps/cli/src/manual.ts
6597
- var MANUAL = `the lobsterman's manual
6954
+ var MANUAL = `the lobstah man's manual
6598
6955
 
6599
- You are the lobsterman: one interactive session that sets traps (dispatches),
6956
+ You are the lobstah man: one interactive session that sets traps (dispatches),
6600
6957
  reads buoys (status), and hauls when something needs you. The boat (daemon)
6601
6958
  does the supervision \u2014 you never watch a trap work.
6602
6959
 
@@ -6614,11 +6971,14 @@ your working set:
6614
6971
  lobstah man tend [--json] tend the string: fleet verdict,
6615
6972
  unanswered questions, each work
6616
6973
  item's chain + PR + merge gate
6617
- lobstah glass [--port <n>] the spyglass: tend as a live
6974
+ lobstah glass [--port <n>] [--detach] the spyglass: tend as a live
6618
6975
  localhost page \u2014 traps with
6619
6976
  their lifecycle and mail,
6620
6977
  briefs, logs, notices, merge
6621
6978
  view; consumes no cursor
6979
+ lobstah glass stop | status | install | uninstall
6980
+ manage the detached glass or
6981
+ its user service
6622
6982
  lobstah watch add <key> --check <cmd> stand watch on something
6623
6983
  external (a review session, a
6624
6984
  CI run) \u2014 its events wake you
@@ -6649,7 +7009,7 @@ getting woken instead of asking:
6649
7009
  still-waiting, verdict. A man wait timeout carries
6650
7010
  it too, so a wait loop doubles as the periodic
6651
7011
  fleet report
6652
- lobstah man helm take the helm: sign on as the one lobsterman for
7012
+ lobstah man helm take the helm: sign on as the one lobstah man for
6653
7013
  your grounds. The charter prints (and re-injects
6654
7014
  at every session start) and the Stop hook applies.
6655
7015
  \`man relieve\` steps down
@@ -6662,27 +7022,27 @@ paste into your liaison instructions:
6662
7022
  immediately, then \`lobstah send\` the answer. done means brief fulfilled \u2014
6663
7023
  report the catch (\`lobstah catch <id>\`) and never merge anything yourself.
6664
7024
 
6665
- full pattern and trade-offs: docs/lobsterman.md \xB7 config: docs/configuration.md`;
7025
+ full pattern and trade-offs: docs/man.md \xB7 config: docs/configuration.md`;
6666
7026
 
6667
7027
  // apps/cli/src/doctor.ts
6668
7028
  init_dist();
6669
7029
  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";
7030
+ import * as fs37 from "node:fs";
7031
+ import * as path30 from "node:path";
7032
+ import { spawnSync as spawnSync6 } from "node:child_process";
6673
7033
 
6674
7034
  // apps/cli/src/plugin-version.ts
6675
7035
  init_dist();
6676
- import * as fs34 from "node:fs";
6677
- import * as os5 from "node:os";
6678
- import * as path27 from "node:path";
7036
+ import * as fs35 from "node:fs";
7037
+ import * as os6 from "node:os";
7038
+ import * as path28 from "node:path";
6679
7039
  var UPDATE_COMMAND = {
6680
7040
  claude: "/plugin update lobstah@lobstah",
6681
7041
  codex: "codex plugin marketplace upgrade lobstah && codex plugin add lobstah@lobstah"
6682
7042
  };
6683
7043
  var readJson2 = (file) => {
6684
7044
  try {
6685
- return JSON.parse(fs34.readFileSync(file, "utf8"));
7045
+ return JSON.parse(fs35.readFileSync(file, "utf8"));
6686
7046
  } catch {
6687
7047
  return void 0;
6688
7048
  }
@@ -6693,19 +7053,19 @@ var manifestVersion = (file) => {
6693
7053
  };
6694
7054
  function installedClaudePlugin(opts = {}) {
6695
7055
  const env = opts.env ?? process.env;
6696
- const home = opts.home ?? os5.homedir();
7056
+ const home = opts.home ?? os6.homedir();
6697
7057
  const root = env.CLAUDE_PLUGIN_ROOT;
6698
7058
  if (root) {
6699
- const version2 = manifestVersion(path27.join(root, ".claude-plugin", "plugin.json"));
7059
+ const version2 = manifestVersion(path28.join(root, ".claude-plugin", "plugin.json"));
6700
7060
  if (version2) return { harness: "claude", version: version2, root };
6701
7061
  }
6702
- const claudeDir = env.CLAUDE_CONFIG_DIR ?? path27.join(home, ".claude");
7062
+ const claudeDir = env.CLAUDE_CONFIG_DIR ?? path28.join(home, ".claude");
6703
7063
  const installed = readJson2(
6704
- path27.join(claudeDir, "plugins", "installed_plugins.json")
7064
+ path28.join(claudeDir, "plugins", "installed_plugins.json")
6705
7065
  );
6706
7066
  const entry = installed?.plugins?.["lobstah@lobstah"]?.[0];
6707
7067
  if (!entry?.installPath) return void 0;
6708
- const version = manifestVersion(path27.join(entry.installPath, ".claude-plugin", "plugin.json")) ?? entry.version;
7068
+ const version = manifestVersion(path28.join(entry.installPath, ".claude-plugin", "plugin.json")) ?? entry.version;
6709
7069
  return version ? { harness: "claude", version, root: entry.installPath } : void 0;
6710
7070
  }
6711
7071
  var semverKey = (v) => v.split(/[.-]/).slice(0, 3).map((n) => Number(n) || 0);
@@ -6716,25 +7076,25 @@ var newer = (a, b) => {
6716
7076
  };
6717
7077
  function installedCodexPlugin(opts = {}) {
6718
7078
  const env = opts.env ?? process.env;
6719
- const codexDir = env.CODEX_HOME ?? path27.join(opts.home ?? os5.homedir(), ".codex");
7079
+ const codexDir = env.CODEX_HOME ?? path28.join(opts.home ?? os6.homedir(), ".codex");
6720
7080
  let cfg;
6721
7081
  try {
6722
- cfg = parse(fs34.readFileSync(path27.join(codexDir, "config.toml"), "utf8"));
7082
+ cfg = parse(fs35.readFileSync(path28.join(codexDir, "config.toml"), "utf8"));
6723
7083
  } catch {
6724
7084
  return void 0;
6725
7085
  }
6726
7086
  const entry = (cfg.plugins ?? {})["lobstah@lobstah"];
6727
7087
  if (!entry || entry.enabled === false) return void 0;
6728
- const cache = path27.join(codexDir, "plugins", "cache", "lobstah", "lobstah");
7088
+ const cache = path28.join(codexDir, "plugins", "cache", "lobstah", "lobstah");
6729
7089
  let dirs;
6730
7090
  try {
6731
- dirs = fs34.readdirSync(cache);
7091
+ dirs = fs35.readdirSync(cache);
6732
7092
  } catch {
6733
7093
  return void 0;
6734
7094
  }
6735
7095
  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"));
7096
+ const root = path28.join(cache, d);
7097
+ const version = manifestVersion(path28.join(root, ".codex-plugin", "plugin.json"));
6738
7098
  if (version) return { harness: "codex", version, root };
6739
7099
  }
6740
7100
  return void 0;
@@ -6757,10 +7117,125 @@ function pluginBehindLine(cliVersion, opts = {}) {
6757
7117
  }
6758
7118
  }
6759
7119
 
7120
+ // apps/cli/src/service.ts
7121
+ init_dist2();
7122
+ import * as fs36 from "node:fs";
7123
+ import * as os7 from "node:os";
7124
+ import * as path29 from "node:path";
7125
+ import { spawnSync as spawnSync5 } from "node:child_process";
7126
+ function serviceArgs(spec) {
7127
+ return [spec.nodePath, spec.entry, spec.kind, ...spec.kind === "glass" && spec.port ? ["--port", String(spec.port)] : []].filter(Boolean);
7128
+ }
7129
+ function xml(s) {
7130
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
7131
+ }
7132
+ function renderLaunchdPlist(spec) {
7133
+ return `<?xml version="1.0" encoding="UTF-8"?>
7134
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
7135
+ <plist version="1.0">
7136
+ <dict>
7137
+ <key>Label</key><string>lobstah.${spec.kind}</string>
7138
+ <key>ProgramArguments</key>
7139
+ <array>
7140
+ ${serviceArgs(spec).filter(Boolean).map((a) => ` <string>${xml(a)}</string>`).join("\n")}
7141
+ </array>
7142
+ <key>EnvironmentVariables</key>
7143
+ <dict>
7144
+ <key>PATH</key><string>${xml(spec.pathEnv)}</string>
7145
+ <key>LOBSTAH_HOME</key><string>${xml(spec.home)}</string>
7146
+ </dict>
7147
+ <key>RunAtLoad</key><true/>
7148
+ <key>KeepAlive</key><true/>
7149
+ <key>StandardOutPath</key><string>${xml(path29.posix.join(spec.logDir, `${spec.kind}.log`))}</string>
7150
+ <key>StandardErrorPath</key><string>${xml(path29.posix.join(spec.logDir, `${spec.kind}.err`))}</string>
7151
+ </dict>
7152
+ </plist>
7153
+ `;
7154
+ }
7155
+ function renderSystemdUnit(spec) {
7156
+ return `[Unit]
7157
+ Description=lobstah ${spec.kind}
7158
+
7159
+ [Service]
7160
+ ExecStart=${serviceArgs(spec).join(" ")}
7161
+ Restart=always
7162
+ RestartSec=5
7163
+ Environment=PATH=${spec.pathEnv}
7164
+ Environment=LOBSTAH_HOME=${spec.home}
7165
+ StandardOutput=append:${path29.posix.join(spec.logDir, `${spec.kind}.log`)}
7166
+ StandardError=append:${path29.posix.join(spec.logDir, `${spec.kind}.err`)}
7167
+
7168
+ [Install]
7169
+ WantedBy=default.target
7170
+ `;
7171
+ }
7172
+ function servicePathEnv(nodePath, home = os7.homedir()) {
7173
+ const dirs = [
7174
+ path29.posix.dirname(nodePath),
7175
+ path29.posix.join(home, ".local", "bin"),
7176
+ "/opt/homebrew/bin",
7177
+ "/usr/local/bin",
7178
+ "/usr/bin",
7179
+ "/bin"
7180
+ ];
7181
+ return [...new Set(dirs)].join(":");
7182
+ }
7183
+ function serviceFile(kind) {
7184
+ return process.platform === "darwin" ? path29.join(os7.homedir(), "Library", "LaunchAgents", `lobstah.${kind}.plist`) : path29.join(os7.homedir(), ".config", "systemd", "user", `lobstah-${kind}.service`);
7185
+ }
7186
+ function run(cmd, args) {
7187
+ const res = spawnSync5(cmd, args, { encoding: "utf8" });
7188
+ return { ok: res.status === 0, out: `${res.stdout ?? ""}${res.stderr ?? ""}`.trim() };
7189
+ }
7190
+ function installService(kind, port) {
7191
+ if (process.platform === "win32") {
7192
+ throw new Error(`no service manager support on Windows \u2014 run \`lobstah ${kind}\` under your process manager of choice`);
7193
+ }
7194
+ const entry = COMPILED_BINARY ? "" : fs36.realpathSync(process.argv[1]);
7195
+ const spec = {
7196
+ kind,
7197
+ nodePath: process.execPath,
7198
+ entry,
7199
+ pathEnv: servicePathEnv(process.execPath),
7200
+ home: lobstahHome(),
7201
+ logDir: path29.join(lobstahHome(), "logs"),
7202
+ port
7203
+ };
7204
+ if (kind !== "glass" && !onPath("git", spec.pathEnv)) {
7205
+ throw new Error(`git is not reachable on the service PATH (${spec.pathEnv}) \u2014 the daemon cannot allocate worktrees`);
7206
+ }
7207
+ fs36.mkdirSync(spec.logDir, { recursive: true });
7208
+ const file = serviceFile(kind);
7209
+ fs36.mkdirSync(path29.dirname(file), { recursive: true });
7210
+ if (process.platform === "darwin") {
7211
+ if (fs36.existsSync(file)) run("launchctl", ["unload", file]);
7212
+ fs36.writeFileSync(file, renderLaunchdPlist(spec));
7213
+ const load = run("launchctl", ["load", file]);
7214
+ return { file, loaded: load.ok, detail: load.ok ? `label lobstah.${kind}` : load.out };
7215
+ }
7216
+ fs36.writeFileSync(file, renderSystemdUnit(spec));
7217
+ run("systemctl", ["--user", "daemon-reload"]);
7218
+ const enable = run("systemctl", ["--user", "enable", "--now", `lobstah-${kind}.service`]);
7219
+ return { file, loaded: enable.ok, detail: enable.ok ? `unit lobstah-${kind}.service` : enable.out };
7220
+ }
7221
+ function uninstallService(kind) {
7222
+ if (process.platform === "win32") throw new Error(`no service manager support on Windows`);
7223
+ const file = serviceFile(kind);
7224
+ if (process.platform === "darwin") {
7225
+ if (fs36.existsSync(file)) run("launchctl", ["unload", file]);
7226
+ } else {
7227
+ run("systemctl", ["--user", "disable", "--now", `lobstah-${kind}.service`]);
7228
+ }
7229
+ const removed = fs36.existsSync(file);
7230
+ if (removed) fs36.unlinkSync(file);
7231
+ if (process.platform !== "darwin") run("systemctl", ["--user", "daemon-reload"]);
7232
+ return { file, removed };
7233
+ }
7234
+
6760
7235
  // apps/cli/src/doctor.ts
6761
7236
  var HEARTBEAT_STALE_MS2 = 9e4;
6762
7237
  function git2(repoPath, ...args) {
6763
- const res = spawnSync5("git", ["-C", repoPath, ...args], { encoding: "utf8" });
7238
+ const res = spawnSync6("git", ["-C", repoPath, ...args], { encoding: "utf8" });
6764
7239
  return { ok: res.status === 0, out: (res.stdout ?? "").trim() || (res.stderr ?? "").trim() };
6765
7240
  }
6766
7241
  function pluginRows(cliVersion, opts = {}) {
@@ -6791,7 +7266,7 @@ function pluginRows(cliVersion, opts = {}) {
6791
7266
  }
6792
7267
  return rows;
6793
7268
  }
6794
- function runDoctor(now2 = Date.now()) {
7269
+ async function runDoctor(now2 = Date.now()) {
6795
7270
  const rows = [];
6796
7271
  const push = (check, status, detail) => rows.push({ check, status, detail });
6797
7272
  const major = Number(process.versions.node.split(".")[0]);
@@ -6806,14 +7281,22 @@ function runDoctor(now2 = Date.now()) {
6806
7281
  codexPath || codexSdk ? "ok" : "warn",
6807
7282
  codexPath ? "on PATH" : codexSdk ? "vendored SDK (attach uses it too)" : "neither codex nor the SDK \u2014 codex dispatches will fail"
6808
7283
  );
7284
+ try {
7285
+ const port = glassPort();
7286
+ const info = await probeGlass(port);
7287
+ const installed = process.platform === "win32" ? false : fs37.existsSync(serviceFile("glass"));
7288
+ 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()})`}`);
7289
+ } catch (err) {
7290
+ push("glass", "warn", err instanceof Error ? err.message : String(err));
7291
+ }
6809
7292
  const cfgFile = configPath();
6810
- if (!fs35.existsSync(cfgFile)) {
7293
+ if (!fs37.existsSync(cfgFile)) {
6811
7294
  push("config", "fail", `${cfgFile} missing \u2014 run \`lobstah init\``);
6812
7295
  return rows;
6813
7296
  }
6814
7297
  let parsed;
6815
7298
  try {
6816
- parsed = parse(fs35.readFileSync(cfgFile, "utf8"));
7299
+ parsed = parse(fs37.readFileSync(cfgFile, "utf8"));
6817
7300
  push("config", "ok", cfgFile);
6818
7301
  } catch (err) {
6819
7302
  push("config", "fail", `${cfgFile}: ${err instanceof Error ? err.message : String(err)}`);
@@ -6824,7 +7307,7 @@ function runDoctor(now2 = Date.now()) {
6824
7307
  if (repoKeys.length === 0) push("repos", "warn", "none configured \u2014 add one with `lobstah repos add <path>`");
6825
7308
  for (const [key, repo] of Object.entries(cfg.repos)) {
6826
7309
  const label = `repo ${key}`;
6827
- if (!fs35.existsSync(repo.path)) {
7310
+ if (!fs37.existsSync(repo.path)) {
6828
7311
  push(label, "fail", `path ${repo.path} does not exist`);
6829
7312
  continue;
6830
7313
  }
@@ -6852,11 +7335,11 @@ function runDoctor(now2 = Date.now()) {
6852
7335
  }
6853
7336
  }
6854
7337
  const hb = executorPath();
6855
- if (!fs35.existsSync(hb)) {
7338
+ if (!fs37.existsSync(hb)) {
6856
7339
  push("daemon", "warn", "no heartbeat \u2014 daemon not running (`lobstah daemon install`)");
6857
7340
  } else {
6858
7341
  try {
6859
- const payload = JSON.parse(fs35.readFileSync(hb, "utf8"));
7342
+ const payload = JSON.parse(fs37.readFileSync(hb, "utf8"));
6860
7343
  const age = now2 - new Date(payload.heartbeat ?? 0).getTime();
6861
7344
  push(
6862
7345
  "daemon",
@@ -6868,10 +7351,10 @@ function runDoctor(now2 = Date.now()) {
6868
7351
  }
6869
7352
  }
6870
7353
  try {
6871
- const soaking = path28.join(lobstahHome(), "soaking");
6872
- const stale = fs35.readdirSync(soaking).filter((f) => f.endsWith(".json")).filter((f) => {
7354
+ const soaking = path30.join(lobstahHome(), "soaking");
7355
+ const stale = fs37.readdirSync(soaking).filter((f) => f.endsWith(".json")).filter((f) => {
6873
7356
  try {
6874
- return typeof JSON.parse(fs35.readFileSync(path28.join(soaking, f), "utf8")).trapId !== "string";
7357
+ return typeof JSON.parse(fs37.readFileSync(path30.join(soaking, f), "utf8")).trapId !== "string";
6875
7358
  } catch {
6876
7359
  return true;
6877
7360
  }
@@ -6892,153 +7375,34 @@ function runDoctor(now2 = Date.now()) {
6892
7375
 
6893
7376
  // apps/cli/src/glass.ts
6894
7377
  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";
7378
+ import * as fs38 from "node:fs";
7379
+ import * as os8 from "node:os";
7380
+ import * as path31 from "node:path";
7381
+ import * as http2 from "node:http";
6899
7382
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6900
7383
 
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
- `;
7384
+ // apps/cli/src/glass-page.generated.ts
7385
+ 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
7386
 
7023
7387
  // apps/cli/src/glass.ts
7024
7388
  var REPO_URL = "https://github.com/aequitas-labs/lobstah";
7025
7389
  var readJson3 = (f) => {
7026
7390
  try {
7027
- return JSON.parse(fs36.readFileSync(f, "utf8"));
7391
+ return JSON.parse(fs38.readFileSync(f, "utf8"));
7028
7392
  } catch {
7029
7393
  return void 0;
7030
7394
  }
7031
7395
  };
7032
7396
  var listDir = (d) => {
7033
7397
  try {
7034
- return fs36.readdirSync(d);
7398
+ return fs38.readdirSync(d);
7035
7399
  } catch {
7036
7400
  return [];
7037
7401
  }
7038
7402
  };
7039
7403
  var mtime = (f) => {
7040
7404
  try {
7041
- return fs36.statSync(f).mtimeMs;
7405
+ return fs38.statSync(f).mtimeMs;
7042
7406
  } catch {
7043
7407
  return 0;
7044
7408
  }
@@ -7047,7 +7411,7 @@ function assetPath(name) {
7047
7411
  for (const rel of [`../docs/assets/${name}`, `../../../docs/assets/${name}`]) {
7048
7412
  try {
7049
7413
  const p = fileURLToPath2(new URL(rel, import.meta.url));
7050
- if (fs36.existsSync(p)) return p;
7414
+ if (fs38.existsSync(p)) return p;
7051
7415
  } catch {
7052
7416
  }
7053
7417
  }
@@ -7055,34 +7419,34 @@ function assetPath(name) {
7055
7419
  }
7056
7420
  function transcriptPath(harness, cwd, sessionId) {
7057
7421
  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;
7422
+ const p = path31.join(os8.homedir(), ".claude", "projects", cwd.replace(/[^A-Za-z0-9-]/g, "-"), `${sessionId}.jsonl`);
7423
+ return fs38.existsSync(p) ? p : void 0;
7060
7424
  }
7061
7425
  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 ?? [])
7426
+ return [dir, path31.join(dir, "handled")].flatMap(
7427
+ (folder) => listDir(folder).filter((file) => file.endsWith(".meta.json")).flatMap((file) => readJson3(path31.join(folder, file))?.attachments ?? [])
7064
7428
  );
7065
7429
  }
7066
7430
  function trapMessages(trapId) {
7067
- const dir = path29.join(laneDirs("work").inbox, `trap-${trapId}`);
7431
+ const dir = path31.join(laneDirs("work").inbox, `trap-${trapId}`);
7068
7432
  const parse2 = (file, state) => {
7069
7433
  let raw;
7070
7434
  try {
7071
- raw = fs36.readFileSync(file, "utf8");
7435
+ raw = fs38.readFileSync(file, "utf8");
7072
7436
  } catch {
7073
7437
  return void 0;
7074
7438
  }
7075
7439
  const attachments = readJson3(file.replace(/\.msg$/, ".meta.json"))?.attachments;
7076
7440
  try {
7077
7441
  const p = JSON.parse(raw);
7078
- return { file: path29.basename(file), state, from: p.from ?? "unknown", at: p.at ?? "", text: p.text ?? "", attachments };
7442
+ return { file: path31.basename(file), state, from: p.from ?? "unknown", at: p.at ?? "", text: p.text ?? "", attachments };
7079
7443
  } catch {
7080
- return { file: path29.basename(file), state, from: "unknown", at: "", text: raw, attachments };
7444
+ return { file: path31.basename(file), state, from: "unknown", at: "", text: raw, attachments };
7081
7445
  }
7082
7446
  };
7083
7447
  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"))
7448
+ ...listDir(dir).filter((f) => f.endsWith(".msg")).map((f) => parse2(path31.join(dir, f), "pending")),
7449
+ ...listDir(path31.join(dir, "handled")).filter((f) => f.endsWith(".msg")).map((f) => parse2(path31.join(dir, "handled", f), "delivered"))
7086
7450
  ];
7087
7451
  return rows.filter((m) => m !== void 0).sort((a, b) => a.file.localeCompare(b.file));
7088
7452
  }
@@ -7091,16 +7455,16 @@ function dispatchRows() {
7091
7455
  for (const lane of ["work", "chore"]) {
7092
7456
  const dirs = laneDirs(lane);
7093
7457
  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`)) });
7458
+ const d = readJson3(path31.join(dirs.queue, `${id}.json`));
7459
+ if (d) rows.push({ lane, bucket: "queued", d, sort: mtime(path31.join(dirs.queue, `${id}.json`)) });
7096
7460
  }
7097
7461
  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)) });
7462
+ const d = readJson3(path31.join(dirs.active, id, "descriptor.json"));
7463
+ if (d) rows.push({ lane, bucket: "active", d, sort: mtime(path31.join(dirs.active, id)) });
7100
7464
  }
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);
7465
+ 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
7466
  for (const { id, at } of done) {
7103
- const d = readJson3(path29.join(dirs.done, id, "descriptor.json"));
7467
+ const d = readJson3(path31.join(dirs.done, id, "descriptor.json"));
7104
7468
  if (d) rows.push({ lane, bucket: "done", d, sort: at });
7105
7469
  }
7106
7470
  }
@@ -7110,7 +7474,7 @@ function dispatchRows() {
7110
7474
  const last = log.at(-1);
7111
7475
  const claim = r.bucket === "active" ? readSessionClaim(id, r.lane) : void 0;
7112
7476
  const evidence = readEvidence(id, r.lane);
7113
- const inboxDir2 = path29.join(laneDirs(r.lane).inbox, id);
7477
+ const inboxDir2 = path31.join(laneDirs(r.lane).inbox, id);
7114
7478
  return {
7115
7479
  id,
7116
7480
  lane: r.lane,
@@ -7121,12 +7485,16 @@ function dispatchRows() {
7121
7485
  brief: r.d.brief,
7122
7486
  attachments: r.d.attachments ?? [],
7123
7487
  messageAttachments: messageAttachments(inboxDir2),
7124
- verb: last?.verb ?? "unknown",
7488
+ // A queued descriptor with no log is waiting, not unknown; its time
7489
+ // is the queue time.
7490
+ // An active dispatch a trap claimed, with no log, is working since
7491
+ // the claim.
7492
+ verb: r.bucket === "queued" && log.length === 0 ? "queued" : last?.verb ?? (claim ? "working" : "unknown"),
7125
7493
  note: last?.note,
7126
- verbAt: last?.at,
7494
+ verbAt: last?.at ?? (r.bucket === "queued" ? queuedAt(id, r.lane) : claim?.at),
7127
7495
  claimedBy: claim?.by,
7128
7496
  log,
7129
- inbox: listDir(inboxDir2).filter((f) => f.endsWith(".msg")).sort().map((f) => fs36.readFileSync(path29.join(inboxDir2, f), "utf8").trim()),
7497
+ inbox: listDir(inboxDir2).filter((f) => f.endsWith(".msg")).sort().map((f) => fs38.readFileSync(path31.join(inboxDir2, f), "utf8").trim()),
7130
7498
  evidence: Object.keys(evidence).length > 0 ? evidence : void 0,
7131
7499
  transcript: transcriptPath(claim?.harness, claim?.worktree, claim?.sessionId),
7132
7500
  sort: r.sort
@@ -7211,500 +7579,33 @@ function buildGlassSnapshot() {
7211
7579
  mergeView
7212
7580
  };
7213
7581
  }
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>`;
7582
+ var PAGE = GLASS_PAGE;
7688
7583
  function serveGlass(port) {
7689
7584
  const icon = assetPath("favicon.png") ?? assetPath("lob-star.png");
7690
7585
  const lob = assetPath("lob.png");
7691
7586
  const sprite = assetPath("lob-sprite.png");
7692
7587
  const star = assetPath("star.png");
7693
7588
  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) => {
7589
+ const server = http2.createServer((req, res) => {
7590
+ res.setHeader("Server", `lobstah-glass/${lobstahVersion()}`);
7591
+ if (req.url === "/api/version" && req.method === "GET") {
7592
+ res.writeHead(200, { "content-type": "application/json" });
7593
+ res.end(JSON.stringify({ service: "lobstah-glass", version: lobstahVersion(), pid: process.pid }));
7594
+ return;
7595
+ }
7695
7596
  if (req.url === "/lob.png" && lob) {
7696
7597
  res.writeHead(200, { "content-type": "image/png", "cache-control": "no-cache" });
7697
- res.end(fs36.readFileSync(lob));
7598
+ res.end(fs38.readFileSync(lob));
7698
7599
  } else if (req.url === "/star.png" && star) {
7699
7600
  res.writeHead(200, { "content-type": "image/png", "cache-control": "no-cache" });
7700
- res.end(fs36.readFileSync(star));
7601
+ res.end(fs38.readFileSync(star));
7701
7602
  } else if (req.url === "/lob-sprite.png" && sprite) {
7702
7603
  res.writeHead(200, { "content-type": "image/png", "cache-control": "no-cache" });
7703
- res.end(fs36.readFileSync(sprite));
7604
+ res.end(fs38.readFileSync(sprite));
7704
7605
  } else if (req.url === "/icon.png") {
7705
7606
  if (icon) {
7706
7607
  res.writeHead(200, { "content-type": "image/png", "cache-control": "no-cache" });
7707
- res.end(fs36.readFileSync(icon));
7608
+ res.end(fs38.readFileSync(icon));
7708
7609
  } else {
7709
7610
  res.writeHead(200, { "content-type": "image/svg+xml", "cache-control": "no-cache" });
7710
7611
  res.end(fallbackIcon);
@@ -7723,127 +7624,15 @@ function serveGlass(port) {
7723
7624
 
7724
7625
  // apps/cli/src/pet.ts
7725
7626
  init_dist2();
7726
- import * as fs38 from "node:fs";
7727
- import * as os8 from "node:os";
7728
- import * as path31 from "node:path";
7627
+ import * as fs39 from "node:fs";
7628
+ import * as os9 from "node:os";
7629
+ import * as path32 from "node:path";
7729
7630
  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
7631
  function petBinaryHome() {
7843
- return path31.join(lobstahHome(), "bin", "LobstahPet");
7632
+ return path32.join(lobstahHome(), "bin", "LobstahPet");
7844
7633
  }
7845
7634
  function petPlistFile() {
7846
- return path31.join(os8.homedir(), "Library", "LaunchAgents", "lobstah.pet.plist");
7635
+ return path32.join(os9.homedir(), "Library", "LaunchAgents", "lobstah.pet.plist");
7847
7636
  }
7848
7637
  function xml2(s) {
7849
7638
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -7865,8 +7654,8 @@ function renderPetPlist(binary, pathEnv, home, logDir) {
7865
7654
  </dict>
7866
7655
  <key>RunAtLoad</key><true/>
7867
7656
  <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>
7657
+ <key>StandardOutPath</key><string>${xml2(path32.posix.join(logDir, "pet.log"))}</string>
7658
+ <key>StandardErrorPath</key><string>${xml2(path32.posix.join(logDir, "pet.err"))}</string>
7870
7659
  </dict>
7871
7660
  </plist>
7872
7661
  `;
@@ -7881,13 +7670,13 @@ function installPet(binaryArg) {
7881
7670
  }
7882
7671
  const candidates = [
7883
7672
  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"),
7673
+ path32.join(process.cwd(), "apps", "pet", ".build", "release", "LobstahPet"),
7674
+ path32.join(process.cwd(), ".build", "release", "LobstahPet"),
7675
+ path32.join(process.cwd(), "apps", "pet", ".build", "debug", "LobstahPet"),
7676
+ path32.join(process.cwd(), ".build", "debug", "LobstahPet"),
7888
7677
  petBinaryHome()
7889
7678
  ].filter((c) => c !== void 0);
7890
- const source = candidates.find((c) => fs38.existsSync(c));
7679
+ const source = candidates.find((c) => fs39.existsSync(c));
7891
7680
  if (!source) {
7892
7681
  throw new Error(
7893
7682
  "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 +7684,33 @@ function installPet(binaryArg) {
7895
7684
  }
7896
7685
  const binary = petBinaryHome();
7897
7686
  const file0 = petPlistFile();
7898
- if (fs38.existsSync(file0)) run2("launchctl", ["unload", file0]);
7687
+ if (fs39.existsSync(file0)) run2("launchctl", ["unload", file0]);
7899
7688
  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 });
7689
+ if (path32.resolve(source) !== path32.resolve(binary)) {
7690
+ fs39.mkdirSync(path32.dirname(binary), { recursive: true });
7691
+ fs39.rmSync(binary, { force: true });
7692
+ fs39.copyFileSync(source, binary);
7693
+ fs39.chmodSync(binary, 493);
7694
+ const bundle = path32.join(path32.dirname(source), "LobstahPet_LobstahPet.bundle");
7695
+ if (fs39.existsSync(bundle)) {
7696
+ const dest = path32.join(path32.dirname(binary), "LobstahPet_LobstahPet.bundle");
7697
+ fs39.rmSync(dest, { recursive: true, force: true });
7698
+ fs39.cpSync(bundle, dest, { recursive: true });
7699
+ }
7700
+ }
7701
+ const logDir = path32.join(lobstahHome(), "logs");
7702
+ fs39.mkdirSync(logDir, { recursive: true });
7914
7703
  const file = petPlistFile();
7915
- fs38.mkdirSync(path31.dirname(file), { recursive: true });
7916
- fs38.writeFileSync(file, renderPetPlist(binary, servicePathEnv(process.execPath), lobstahHome(), logDir));
7704
+ fs39.mkdirSync(path32.dirname(file), { recursive: true });
7705
+ fs39.writeFileSync(file, renderPetPlist(binary, servicePathEnv(process.execPath), lobstahHome(), logDir));
7917
7706
  const load = run2("launchctl", ["load", file]);
7918
7707
  return { file, binary, loaded: load.ok, detail: load.ok ? "label lobstah.pet \u2014 starts at login" : load.out };
7919
7708
  }
7920
7709
  function uninstallPet() {
7921
7710
  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);
7711
+ if (process.platform === "darwin" && fs39.existsSync(file)) run2("launchctl", ["unload", file]);
7712
+ const removed = fs39.existsSync(file);
7713
+ if (removed) fs39.unlinkSync(file);
7925
7714
  run2("pkill", ["-f", "LobstahPet"]);
7926
7715
  return { file, removed };
7927
7716
  }
@@ -7929,8 +7718,8 @@ function uninstallPet() {
7929
7718
  // apps/cli/src/repos.ts
7930
7719
  init_dist();
7931
7720
  init_dist2();
7932
- import * as fs39 from "node:fs";
7933
- import * as path32 from "node:path";
7721
+ import * as fs40 from "node:fs";
7722
+ import * as path33 from "node:path";
7934
7723
  import { spawnSync as spawnSync8 } from "node:child_process";
7935
7724
  function git3(dir, ...args) {
7936
7725
  const res = spawnSync8("git", ["-C", dir, ...args], { encoding: "utf8" });
@@ -7946,41 +7735,41 @@ var LOCKFILE_SETUP = [
7946
7735
  ["poetry.lock", ["poetry install"]]
7947
7736
  ];
7948
7737
  function detectRepo(dir) {
7949
- const abs = path32.resolve(dir);
7738
+ const abs = path33.resolve(dir);
7950
7739
  if (git3(abs, "rev-parse", "--is-inside-work-tree") !== "true") return void 0;
7951
7740
  const top = git3(abs, "rev-parse", "--show-toplevel");
7952
7741
  const canon2 = (p) => {
7953
- const real = path32.resolve(fs39.realpathSync.native(p));
7742
+ const real = path33.resolve(fs40.realpathSync.native(p));
7954
7743
  return process.platform === "win32" ? real.toLowerCase() : real;
7955
7744
  };
7956
7745
  if (!top || canon2(top) !== canon2(abs)) return void 0;
7957
7746
  const origin = git3(abs, "remote", "get-url", "origin");
7958
7747
  const headRef = git3(abs, "symbolic-ref", "--short", "refs/remotes/origin/HEAD");
7959
7748
  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 };
7749
+ const setup = LOCKFILE_SETUP.find(([f]) => fs40.existsSync(path33.join(abs, f)))?.[1];
7750
+ return { key: path33.basename(abs), path: abs, trunk, origin, setup };
7962
7751
  }
7963
7752
  function scanForRepos(roots) {
7964
7753
  const found = [];
7965
7754
  for (const root of roots) {
7966
- const abs = path32.resolve(root);
7967
- if (!fs39.existsSync(abs)) continue;
7755
+ const abs = path33.resolve(root);
7756
+ if (!fs40.existsSync(abs)) continue;
7968
7757
  const self = detectRepo(abs);
7969
7758
  if (self) {
7970
7759
  found.push(self);
7971
7760
  continue;
7972
7761
  }
7973
- for (const entry of fs39.readdirSync(abs, { withFileTypes: true })) {
7762
+ for (const entry of fs40.readdirSync(abs, { withFileTypes: true })) {
7974
7763
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
7975
- const child = detectRepo(path32.join(abs, entry.name));
7764
+ const child = detectRepo(path33.join(abs, entry.name));
7976
7765
  if (child) found.push(child);
7977
7766
  }
7978
7767
  }
7979
7768
  return found;
7980
7769
  }
7981
7770
  function configuredRepoKeys(file = configPath()) {
7982
- if (!fs39.existsSync(file)) return /* @__PURE__ */ new Set();
7983
- const raw = parse(fs39.readFileSync(file, "utf8"));
7771
+ if (!fs40.existsSync(file)) return /* @__PURE__ */ new Set();
7772
+ const raw = parse(fs40.readFileSync(file, "utf8"));
7984
7773
  return new Set(Object.keys(raw.repos ?? {}));
7985
7774
  }
7986
7775
  function appendRepoBlock(repo, opts = {}, file = configPath()) {
@@ -7988,7 +7777,7 @@ function appendRepoBlock(repo, opts = {}, file = configPath()) {
7988
7777
  if (repo.origin) lines.push(`origin = ${JSON.stringify(repo.origin)}`);
7989
7778
  if (repo.setup) lines.push(`setup = [${repo.setup.map((s) => JSON.stringify(s)).join(", ")}]`);
7990
7779
  if (opts.pickup) lines.push(`pickup = true`);
7991
- fs39.appendFileSync(file, `${lines.join("\n")}
7780
+ fs40.appendFileSync(file, `${lines.join("\n")}
7992
7781
  `);
7993
7782
  }
7994
7783
 
@@ -8077,7 +7866,7 @@ var COMMANDS = {
8077
7866
  daemon: { subverbs: ["install", "uninstall"], flags: { "--interval": { value: "<ms>" } } },
8078
7867
  pick: { subverbs: ["once", "install", "uninstall"], flags: {} },
8079
7868
  doctor: { flags: {} },
8080
- glass: { flags: { "--port": { value: "<n>" } } },
7869
+ glass: { subverbs: ["stop", "status", "install", "uninstall"], flags: { "--port": { value: "<n>" }, "--detach": {} } },
8081
7870
  pet: { subverbs: ["install", "uninstall"], flags: { "--binary": { value: "<path>" } } },
8082
7871
  repos: { subverbs: ["add"], flags: { "--pickup": {}, "--key": { value: "<k>" } }, positionals: "[<path>]" },
8083
7872
  init: { flags: { "--scan": {}, "--pickup": {} }, positionals: "[<dir>...]" },
@@ -8179,12 +7968,13 @@ Quitting the pet sticks until next login; uninstall removes the agent.`,
8179
7968
  glass: `The spyglass: tend as a live localhost web page \u2014 attention, dispatches,
8180
7969
  traps with lifecycle and mail, notices, merge view; filters and a
8181
7970
  table/cards toggle. Read-only and binds 127.0.0.1 only: looking through it
8182
- consumes no cursor and steers nothing.`,
7971
+ consumes no cursor and steers nothing. --detach starts it in the background;
7972
+ stop and status manage that process. install and uninstall manage a user service.`,
8183
7973
  repos: `List configured repos, or detect one and append its [repos.*] block.`,
8184
7974
  init: `Create ~/.lobstah + config; --scan appends a [repos.*] block per repo found
8185
7975
  under the given directories.`,
8186
7976
  version: `The installed lobstah version.`,
8187
- "man:manual": `The lobsterman's manual.`,
7977
+ "man:manual": `The lobstah man's manual.`,
8188
7978
  "man:tend": `The whole-fleet pass: verdict, unanswered questions, each work item's chain,
8189
7979
  PR, and merge gate. Pure disk read.`,
8190
7980
  "man:report": `The delta since the last report: landed, arisen, still-waiting, verdict.
@@ -8192,7 +7982,7 @@ Advances the cursor unless --peek \u2014 the acknowledgment man wait's timeout
8192
7982
  digest defers to. --grounds scopes digest and cursor to one helm's territory
8193
7983
  (that helm's alone). Reserved for the claimed helm; identify with --session
8194
7984
  (defaults --grounds to its own).`,
8195
- "man:helm": `Take the helm: sign this session on as the one lobsterman for its grounds.
7985
+ "man:helm": `Take the helm: sign this session on as the one lobstah man for its grounds.
8196
7986
  Prints the charter, enables the Stop hook, and gates the digest.
8197
7987
  Sign-on records who the man is (harness, directory, host; --label names it).
8198
7988
  A live foreign holder refuses without --take; a stale one is claimable.
@@ -8307,15 +8097,15 @@ function detectHarness(opts) {
8307
8097
 
8308
8098
  // apps/cli/src/watchers.ts
8309
8099
  init_dist2();
8310
- import * as fs40 from "node:fs";
8311
- import * as path33 from "node:path";
8100
+ import * as fs41 from "node:fs";
8101
+ import * as path34 from "node:path";
8312
8102
  var STALE_MS = 5e3;
8313
8103
  var HEARTBEAT_MS = 1500;
8314
- var watcherDir = () => path33.join(lobstahHome(), "watchers");
8315
- var watcherFile = (sessionId) => path33.join(watcherDir(), `${encodeURIComponent(sessionId)}.json`);
8104
+ var watcherDir = () => path34.join(lobstahHome(), "watchers");
8105
+ var watcherFile = (sessionId) => path34.join(watcherDir(), `${encodeURIComponent(sessionId)}.json`);
8316
8106
  function liveRegistration(sessionId, now2 = Date.now()) {
8317
8107
  try {
8318
- const w = JSON.parse(fs40.readFileSync(watcherFile(sessionId), "utf8"));
8108
+ const w = JSON.parse(fs41.readFileSync(watcherFile(sessionId), "utf8"));
8319
8109
  const beat = Date.parse(w.heartbeatAt);
8320
8110
  if (w.sessionId !== sessionId || !Number.isInteger(w.pid) || w.pid <= 0 || !Number.isFinite(beat) || now2 - beat > STALE_MS) return void 0;
8321
8111
  return w;
@@ -8327,14 +8117,22 @@ function liveWatcher(sessionId, kind, trapId, now2 = Date.now()) {
8327
8117
  const w = liveRegistration(sessionId, now2);
8328
8118
  return w?.kind === kind && (!trapId || w.trapId === trapId) ? w : void 0;
8329
8119
  }
8120
+ async function awaitWatcher(sessionId, kind, graceMs, trapId, pollMs = 100) {
8121
+ const deadline = Date.now() + Math.max(0, graceMs);
8122
+ for (; ; ) {
8123
+ const w = liveWatcher(sessionId, kind, trapId);
8124
+ if (w || Date.now() >= deadline) return w;
8125
+ await new Promise((resolve6) => setTimeout(resolve6, Math.min(pollMs, Math.max(1, deadline - Date.now()))));
8126
+ }
8127
+ }
8330
8128
  function armWatcher(sessionId, kind, trapId) {
8331
- fs40.mkdirSync(watcherDir(), { recursive: true });
8129
+ fs41.mkdirSync(watcherDir(), { recursive: true });
8332
8130
  const file = watcherFile(sessionId);
8333
8131
  const own = () => ({ sessionId, kind, pid: process.pid, heartbeatAt: (/* @__PURE__ */ new Date()).toISOString(), ...trapId ? { trapId } : {} });
8334
8132
  let claimed = false;
8335
8133
  for (let attempt = 0; attempt < 3; attempt++) {
8336
8134
  try {
8337
- fs40.writeFileSync(file, `${JSON.stringify(own())}
8135
+ fs41.writeFileSync(file, `${JSON.stringify(own())}
8338
8136
  `, { flag: "wx" });
8339
8137
  claimed = true;
8340
8138
  break;
@@ -8343,7 +8141,7 @@ function armWatcher(sessionId, kind, trapId) {
8343
8141
  const live = liveRegistration(sessionId);
8344
8142
  if (live) throw new Error(`watcher already armed for session ${sessionId} (pid ${live.pid})`);
8345
8143
  try {
8346
- fs40.rmSync(file);
8144
+ fs41.rmSync(file);
8347
8145
  } catch {
8348
8146
  }
8349
8147
  }
@@ -8351,7 +8149,7 @@ function armWatcher(sessionId, kind, trapId) {
8351
8149
  if (!claimed) throw new Error(`could not arm watcher for session ${sessionId}`);
8352
8150
  const ownsFile = () => {
8353
8151
  try {
8354
- return JSON.parse(fs40.readFileSync(file, "utf8")).pid === process.pid;
8152
+ return JSON.parse(fs41.readFileSync(file, "utf8")).pid === process.pid;
8355
8153
  } catch {
8356
8154
  return false;
8357
8155
  }
@@ -8360,12 +8158,12 @@ function armWatcher(sessionId, kind, trapId) {
8360
8158
  if (!ownsFile()) return;
8361
8159
  const tmp = `${file}.tmp-${process.pid}`;
8362
8160
  try {
8363
- fs40.writeFileSync(tmp, `${JSON.stringify(own())}
8161
+ fs41.writeFileSync(tmp, `${JSON.stringify(own())}
8364
8162
  `);
8365
- fs40.renameSync(tmp, file);
8163
+ fs41.renameSync(tmp, file);
8366
8164
  } catch {
8367
8165
  try {
8368
- fs40.rmSync(tmp, { force: true });
8166
+ fs41.rmSync(tmp, { force: true });
8369
8167
  } catch {
8370
8168
  }
8371
8169
  }
@@ -8377,7 +8175,7 @@ function armWatcher(sessionId, kind, trapId) {
8377
8175
  if (stopped) return;
8378
8176
  stopped = true;
8379
8177
  clearInterval(timer);
8380
- if (ownsFile()) fs40.rmSync(file, { force: true });
8178
+ if (ownsFile()) fs41.rmSync(file, { force: true });
8381
8179
  process.off("exit", stop);
8382
8180
  process.off("SIGINT", interrupt);
8383
8181
  process.off("SIGTERM", terminate);
@@ -8467,18 +8265,19 @@ host processes:
8467
8265
  pet install|uninstall the desktop pet (macOS): a login LaunchAgent
8468
8266
  walks attention questions across the screen
8469
8267
 
8470
- lobsterman (orchestrator sessions \u2014 bare \`lobstah man\` prints the manual):
8268
+ lobstah man (orchestrator sessions \u2014 bare \`lobstah man\` prints the manual):
8471
8269
  man tend [--json] tend the whole string: fleet verdict (daemon
8472
8270
  up, stalled vs idle), unanswered questions
8473
8271
  with ages, each work item's dispatch chain,
8474
8272
  PR, and merge-gate status from pick's last
8475
8273
  observation. Pure disk read \u2014 no forge calls.
8476
- glass [--port <n>] the spyglass: tend as a live localhost web
8274
+ glass [--port <n>] [--detach] the spyglass: tend as a live localhost web
8477
8275
  page \u2014 attention, dispatches, traps with
8478
8276
  their lifecycle and mail, notices, merge
8479
8277
  view. Read-only; consumes no cursor.
8278
+ stop | status | install | uninstall manage it.
8480
8279
  Port: --port, else $LOBSTAH_GLASS_PORT,
8481
- else 4949 (the pet reads the same var).
8280
+ else [glass].port (default 4949).
8482
8281
  man wait [--timeout <secs>] [--peek]
8483
8282
  block until a dispatch or watched source
8484
8283
  needs attention, then print the event and
@@ -8515,7 +8314,7 @@ lobsterman (orchestrator sessions \u2014 bare \`lobstah man\` prints the manual)
8515
8314
  helm registration, LOBSTAH_MAN=1, or a
8516
8315
  .lobstah-man file. Hookless sessions use the
8517
8316
  foreground verbs: soak --wait (worker),
8518
- man wait (lobsterman).
8317
+ man wait (lobstah man).
8519
8318
 
8520
8319
  workers (dispatched agents; injected into every brief):
8521
8320
  report <uuid> <verb> [--pr <url>] [--no-watch] [--] [note]
@@ -8565,6 +8364,9 @@ function gateHelm(who, grounds) {
8565
8364
  const refusal = helmGate(liveHelms(loadConfig().helm.ttlSecs * 1e3), who?.id, grounds);
8566
8365
  if (refusal) throw new Error(explainRefusal(refusal, who));
8567
8366
  }
8367
+ function armGraceNote(graceSecs) {
8368
+ return `(No watcher registered within ${graceSecs}s of this stop; one started in the last few seconds would have been accepted.)`;
8369
+ }
8568
8370
  function hookParkMode(configured, harness) {
8569
8371
  return configured ?? (harness === "claude" || !harness && !!process.env.CLAUDE_CODE_SESSION_ID ? "arm" : "block");
8570
8372
  }
@@ -8668,7 +8470,7 @@ function emitWatchAttention(attns, sessionId) {
8668
8470
  function findLane(id) {
8669
8471
  for (const lane of ["work", "chore"]) {
8670
8472
  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`))) {
8473
+ 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
8474
  return lane;
8673
8475
  }
8674
8476
  }
@@ -8677,7 +8479,7 @@ function findLane(id) {
8677
8479
  function anythingInFlight() {
8678
8480
  const dispatches = ["work", "chore"].some((lane) => {
8679
8481
  const dirs = laneDirs(lane);
8680
- return [dirs.queue, dirs.active].some((dir) => fs45.readdirSync(dir).some((file) => !file.startsWith(".")));
8482
+ return [dirs.queue, dirs.active].some((dir) => fs46.readdirSync(dir).some((file) => !file.startsWith(".")));
8681
8483
  });
8682
8484
  return dispatches || listWatches().some((watch2) => watch2.owner === "man");
8683
8485
  }
@@ -8691,12 +8493,16 @@ function inheritedAttachments(id) {
8691
8493
  }
8692
8494
  function rowsFor(lane, bucket) {
8693
8495
  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);
8496
+ 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
8497
  const sliced = bucket === "done" ? entries.slice(0, 10) : entries;
8696
8498
  return sliced.map(({ f, m }) => {
8697
8499
  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() };
8500
+ const queued = bucket === "queue";
8501
+ const log = readStatusLog(id, lane);
8502
+ const claimedAt = bucket === "active" ? readSessionClaim(id, lane)?.at : void 0;
8503
+ const state = displayState({ log, lastEventAt: lastEventAt(id, lane), queued, claimedAt });
8504
+ const updated = (queued && state === "queued" ? queuedAt(id, lane) : void 0) ?? (log.length === 0 ? claimedAt : void 0) ?? new Date(m).toISOString();
8505
+ return { id, lane, bucket, state, updated };
8700
8506
  });
8701
8507
  }
8702
8508
  async function mainCli() {
@@ -8783,21 +8589,24 @@ ${usageFor(cmd)}`);
8783
8589
  const d = {
8784
8590
  id: opt("--id") ?? randomUUID4(),
8785
8591
  repo,
8786
- brief: briefText ?? fs45.readFileSync(briefFile, "utf8"),
8592
+ brief: briefText ?? fs46.readFileSync(briefFile, "utf8"),
8787
8593
  harness: opt("--harness"),
8788
8594
  model: opt("--model"),
8789
8595
  effort: opt("--effort"),
8790
8596
  followUp: opt("--follow-up"),
8791
8597
  for: address
8792
8598
  };
8599
+ if (d.harness) d.harnessExplicit = true;
8600
+ if (d.model) d.modelExplicit = true;
8793
8601
  const lane = has("--chore") ? "chore" : "work";
8794
8602
  const attachments = [
8795
8603
  ...inheritedAttachments(d.followUp) ?? [],
8796
8604
  ...copyFiles(values("--attach"), dispatchAttachmentsDir(d.id, lane)) ?? []
8797
8605
  ];
8798
8606
  if (attachments.length > 0) d.attachments = attachments;
8607
+ d.queuedAt = (/* @__PURE__ */ new Date()).toISOString();
8799
8608
  enqueue(d, lane);
8800
- console.log(toonKV({ id: d.id, repo, lane, ...address ? { for: address } : {}, queued: (/* @__PURE__ */ new Date()).toISOString() }));
8609
+ console.log(toonKV({ id: d.id, repo, lane, ...address ? { for: address } : {}, queued: d.queuedAt }));
8801
8610
  for (const w of warnings) console.log(toonKV({ warning: w }));
8802
8611
  console.log(
8803
8612
  toonHelp([
@@ -8826,8 +8635,10 @@ ${usageFor(cmd)}`);
8826
8635
  }
8827
8636
  const lane = findLane(id);
8828
8637
  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 }));
8638
+ const since = queuedAt(id, lane);
8639
+ const claimedAt = readSessionClaim(id, lane)?.at;
8640
+ const state = displayState({ log, lastEventAt: lastEventAt(id, lane), queued: since !== void 0, claimedAt });
8641
+ 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
8642
  console.log(
8832
8643
  toonHelp(
8833
8644
  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 +8651,8 @@ ${usageFor(cmd)}`);
8840
8651
  if (!id) throw new Error("logs requires a dispatch id");
8841
8652
  const lane = findLane(id);
8842
8653
  const file = eventsPath(id, lane);
8843
- if (fs45.existsSync(file)) {
8844
- const raw = fs45.readFileSync(file, "utf8");
8654
+ if (fs46.existsSync(file)) {
8655
+ const raw = fs46.readFileSync(file, "utf8");
8845
8656
  const lines = raw.split("\n").filter((l) => l.length > 0);
8846
8657
  const LIMIT = 50;
8847
8658
  if (!has("--full") && lines.length > LIMIT) {
@@ -8854,15 +8665,15 @@ ${usageFor(cmd)}`);
8854
8665
  }
8855
8666
  }
8856
8667
  if (has("--follow")) {
8857
- let size = fs45.existsSync(file) ? fs45.statSync(file).size : 0;
8668
+ let size = fs46.existsSync(file) ? fs46.statSync(file).size : 0;
8858
8669
  setInterval(() => {
8859
- if (!fs45.existsSync(file)) return;
8860
- const now2 = fs45.statSync(file).size;
8670
+ if (!fs46.existsSync(file)) return;
8671
+ const now2 = fs46.statSync(file).size;
8861
8672
  if (now2 > size) {
8862
- const fd = fs45.openSync(file, "r");
8673
+ const fd = fs46.openSync(file, "r");
8863
8674
  const buf = Buffer.alloc(now2 - size);
8864
- fs45.readSync(fd, buf, 0, buf.length, size);
8865
- fs45.closeSync(fd);
8675
+ fs46.readSync(fd, buf, 0, buf.length, size);
8676
+ fs46.closeSync(fd);
8866
8677
  process.stdout.write(buf.toString("utf8"));
8867
8678
  size = now2;
8868
8679
  }
@@ -8961,8 +8772,13 @@ ${[text, block].filter(Boolean).join("\n\n")}`, from, attachments);
8961
8772
  }
8962
8773
  const { harness = "claude", sessionId } = resolveSessionHarness(id, loadConfig(), lane);
8963
8774
  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();
8775
+ if (harness === "codex" && codexDesktopThread(sessionId)) {
8776
+ throw new Error(
8777
+ `${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`
8778
+ );
8779
+ }
8780
+ const worktree = path39.join(lobstahHome(), "worktrees", id);
8781
+ const cwd = fs46.existsSync(worktree) ? worktree : process.cwd();
8966
8782
  const invocation = harness === "codex" ? codexInvocation(["resume", sessionId]) : { file: "claude", argv: ["--resume", sessionId] };
8967
8783
  if (!invocation) throw new Error("no codex CLI found \u2014 neither on PATH nor vendored by @openai/codex-sdk");
8968
8784
  if (has("--print")) {
@@ -8980,23 +8796,25 @@ ${[text, block].filter(Boolean).join("\n\n")}`, from, attachments);
8980
8796
  gateHelm(callerSession(opt("--session")));
8981
8797
  }
8982
8798
  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"));
8799
+ const activeDir = path39.join(laneDirs(lane).active, id);
8800
+ if (!fs46.existsSync(activeDir)) throw new Error(`${id} is not active \u2014 swap only applies to in-flight dispatches`);
8801
+ const descFile = path39.join(activeDir, "descriptor.json");
8802
+ const descriptor = JSON.parse(fs46.readFileSync(descFile, "utf8"));
8987
8803
  const fromHarness = resolveSessionHarness(id, loadConfig(), lane).harness ?? descriptor.harness ?? "default";
8988
8804
  for (const key of ["harness", "model", "effort"]) {
8989
8805
  const v = opt(`--${key}`);
8990
8806
  if (v) descriptor[key] = v;
8991
8807
  }
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"));
8808
+ if (opt("--harness")) descriptor.harnessExplicit = true;
8809
+ if (opt("--model")) descriptor.modelExplicit = true;
8810
+ fs46.writeFileSync(descFile, JSON.stringify(descriptor, null, 2));
8811
+ const worktree = path39.join(lobstahHome(), "worktrees", id);
8812
+ fs46.writeFileSync(path39.join(activeDir, "handoff"), handoffNote(fromHarness, worktreeProgress(worktree)));
8813
+ const runnerFile = path39.join(activeDir, "runner.json");
8814
+ if (fs46.existsSync(runnerFile)) {
8815
+ const runner = JSON.parse(fs46.readFileSync(runnerFile, "utf8"));
8998
8816
  if (pidAlive(runner.pid, runner.processStartTime)) killGroup(runner.pid, "SIGKILL");
8999
- fs45.unlinkSync(runnerFile);
8817
+ fs46.unlinkSync(runnerFile);
9000
8818
  }
9001
8819
  appendStatus(id, lane, "working", `swapped to ${descriptor.harness ?? "default"} \u2014 awaiting respawn`);
9002
8820
  console.log(toonKV({ id, harness: descriptor.harness ?? "default", model: descriptor.model, swapped: true }));
@@ -9012,7 +8830,12 @@ ${[text, block].filter(Boolean).join("\n\n")}`, from, attachments);
9012
8830
  console.log(
9013
8831
  toonKV({
9014
8832
  id,
9015
- state: reconcile({ log, lastEventAt: lastEventAt(id, lane) }),
8833
+ state: displayState({
8834
+ log,
8835
+ lastEventAt: lastEventAt(id, lane),
8836
+ queued: queuedAt(id, lane) !== void 0,
8837
+ claimedAt: readSessionClaim(id, lane)?.at
8838
+ }),
9016
8839
  branch: ev.branch,
9017
8840
  prUrl: ev.prUrl,
9018
8841
  sessionId: ev.sessionId,
@@ -9166,7 +8989,7 @@ ${usageFor("attention")}`);
9166
8989
  ${errs.map((e) => `- ${e}`).join("\n")}`);
9167
8990
  const grounds = resolveGrounds(cfg, opt("--grounds"));
9168
8991
  const { harness } = detectHarness({ flag: opt("--harness"), sessionId });
9169
- const identity = { harness, cwd: process.cwd(), host: os10.hostname(), label: opt("--label"), window: captureWindow() };
8992
+ const identity = { harness, cwd: process.cwd(), host: os11.hostname(), label: opt("--label"), window: captureWindow() };
9170
8993
  const res = takeHelm({ sessionId, grounds, ttlMs: cfg.helm.ttlSecs * 1e3, take: has("--take"), identity });
9171
8994
  if ("held" in res) {
9172
8995
  const ageSecs = Math.max(0, Math.round((Date.now() - (Date.parse(res.held.heartbeatAt) || 0)) / 1e3));
@@ -9202,7 +9025,7 @@ ${errs.map((e) => `- ${e}`).join("\n")}`);
9202
9025
  gateHelm(callerSession(opt("--session")));
9203
9026
  }
9204
9027
  const lane = findLane(id);
9205
- if (fs45.existsSync(path38.join(laneDirs(lane).active, id))) {
9028
+ if (fs46.existsSync(path39.join(laneDirs(lane).active, id))) {
9206
9029
  requestCancel(id, lane);
9207
9030
  console.log(toonKV({ id, cancel: "requested", note: "the claimant (daemon or trap) winds it down at its next check" }));
9208
9031
  break;
@@ -9211,7 +9034,7 @@ ${errs.map((e) => `- ${e}`).join("\n")}`);
9211
9034
  console.log(toonKV({ id, cancel: "finalized", note: "cancelled before claim \u2014 recorded as failed in done/" }));
9212
9035
  break;
9213
9036
  }
9214
- if (fs45.existsSync(path38.join(laneDirs(lane).active, id))) {
9037
+ if (fs46.existsSync(path39.join(laneDirs(lane).active, id))) {
9215
9038
  requestCancel(id, lane);
9216
9039
  console.log(toonKV({ id, cancel: "requested", note: "claimed while cancelling \u2014 the claimant winds it down" }));
9217
9040
  break;
@@ -9226,6 +9049,7 @@ ${usageFor("man:wait")}`);
9226
9049
  }
9227
9050
  const caller = callerSession(opt("--session"));
9228
9051
  const sid = caller?.id;
9052
+ const waiter = !has("--peek") && sid ? armWatcher(sid, "man") : void 0;
9229
9053
  const cfgWait = loadConfig();
9230
9054
  let groundsName = opt("--grounds");
9231
9055
  gateHelm(caller, groundsName);
@@ -9234,7 +9058,6 @@ ${usageFor("man:wait")}`);
9234
9058
  heartbeatHelm(callerHelm.sessionId);
9235
9059
  groundsName ??= callerHelm.grounds;
9236
9060
  }
9237
- const waiter = !has("--peek") && sid ? armWatcher(sid, "man") : void 0;
9238
9061
  try {
9239
9062
  const timeoutSecs = Number(opt("--timeout") ?? "0");
9240
9063
  const deadline = timeoutSecs > 0 ? Date.now() + timeoutSecs * 1e3 : Number.POSITIVE_INFINITY;
@@ -9314,20 +9137,20 @@ ${usageFor("man:wait")}`);
9314
9137
  }
9315
9138
  }
9316
9139
  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");
9140
+ const file = has("--global") ? path39.join(os11.homedir(), ".claude", "settings.json") : path39.join(".claude", has("--shared") ? "settings.json" : "settings.local.json");
9318
9141
  let existing;
9319
9142
  try {
9320
- existing = JSON.parse(fs45.readFileSync(file, "utf8"));
9143
+ existing = JSON.parse(fs46.readFileSync(file, "utf8"));
9321
9144
  } catch {
9322
9145
  existing = void 0;
9323
9146
  }
9324
9147
  const { settings, changed } = mergeHaulHook(existing);
9325
9148
  if (changed) {
9326
- fs45.mkdirSync(path38.dirname(file), { recursive: true });
9327
- fs45.writeFileSync(file, `${JSON.stringify(settings, null, 2)}
9149
+ fs46.mkdirSync(path39.dirname(file), { recursive: true });
9150
+ fs46.writeFileSync(file, `${JSON.stringify(settings, null, 2)}
9328
9151
  `);
9329
9152
  }
9330
- if (has("--marker")) fs45.writeFileSync(".lobstah-man", "");
9153
+ if (has("--marker")) fs46.writeFileSync(".lobstah-man", "");
9331
9154
  console.log(
9332
9155
  toonKV({
9333
9156
  hook: "lobstah man haul",
@@ -9346,8 +9169,10 @@ ${usageFor("man:wait")}`);
9346
9169
  const cfg = loadConfig();
9347
9170
  if (!has("--park") && hookParkMode(cfg.helm.park, trapReg.harness) === "arm") {
9348
9171
  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.` }));
9172
+ if (!anythingInFlight()) break;
9173
+ const graceSecs = cfg.helm.armGraceSecs;
9174
+ if (await awaitWatcher(trapReg.sessionId, "trap", graceSecs * 1e3, trapReg.trapId)) break;
9175
+ 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
9176
  break;
9352
9177
  }
9353
9178
  await soakPark(trapReg.trapId, opt("--timeout"));
@@ -9365,7 +9190,7 @@ ${usageFor("man:wait")}`);
9365
9190
  if (helm) heartbeatHelm(helm.sessionId);
9366
9191
  const cfgHaul = loadConfig();
9367
9192
  if (!helm && liveHelms(cfgHaul.helm.ttlSecs * 1e3).length > 0) break;
9368
- if (!helm && process.env.LOBSTAH_MAN !== "1" && !fs45.existsSync(".lobstah-man")) break;
9193
+ if (!helm && process.env.LOBSTAH_MAN !== "1" && !fs46.existsSync(".lobstah-man")) break;
9369
9194
  const dueDigest = () => helm ? dueHelmDigest(helm, cfgHaul.helm.reportSecs) : void 0;
9370
9195
  const blockDigest = (d) => {
9371
9196
  advanceCursor(helm.grounds, d.now);
@@ -9415,8 +9240,9 @@ Handle anything actionable; the Stop hook checks again at turn end.`
9415
9240
  ].join("\n"));
9416
9241
  break;
9417
9242
  }
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.`);
9243
+ const graceSecs = cfgHaul.helm.armGraceSecs;
9244
+ if (await awaitWatcher(hook.session_id, "man", graceSecs * 1e3)) break;
9245
+ 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
9246
  break;
9421
9247
  }
9422
9248
  const timeoutSecs = Number(opt("--timeout") ?? "14000");
@@ -9469,7 +9295,7 @@ Handle anything actionable; the Stop hook checks again at turn end.`
9469
9295
  const hook = readHookStdin();
9470
9296
  if (!hook?.session_id) break;
9471
9297
  const behind = pluginBehindLine(lobstahVersion());
9472
- const context = buildBriefContext(hook.session_id, hook.cwd ?? process.cwd()) + (behind ? `
9298
+ const context = await buildBriefContext(hook.session_id, hook.cwd ?? process.cwd()) + (behind ? `
9473
9299
  ${behind}` : "");
9474
9300
  console.log(
9475
9301
  JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: context } })
@@ -9612,11 +9438,50 @@ ${usageFor("soak")}`);
9612
9438
  ${usageFor("pet")}`);
9613
9439
  }
9614
9440
  case "glass": {
9615
- const port = Number(opt("--port") ?? process.env.LOBSTAH_GLASS_PORT ?? "4949");
9616
- serveGlass(port);
9441
+ readGlassState();
9442
+ const requestedPort = opt("--port") === void 0 ? void 0 : Number(opt("--port"));
9443
+ if (requestedPort !== void 0 && (!Number.isInteger(requestedPort) || requestedPort < 1 || requestedPort > 65535)) {
9444
+ throw new UsageError("glass --port must be an integer from 1 to 65535");
9445
+ }
9446
+ if (pos[0] === "stop") {
9447
+ const stopped = await stopGlass();
9448
+ console.log(toonKV({ glass: stopped.stopped ? "stopped" : "not running", ...stopped }));
9449
+ break;
9450
+ }
9451
+ if (pos[0] === "status") {
9452
+ const status = await glassStatus(requestedPort ?? glassPort());
9453
+ console.log(toonKV({ glass: status.info ? "running" : "stopped", port: status.port, ...status.info ? { ...status.info.pid ? { pid: status.info.pid } : {}, version: status.info.version } : {} }));
9454
+ break;
9455
+ }
9456
+ const port = requestedPort ?? glassPort();
9457
+ if (pos[0] === "install") {
9458
+ const res = installService("glass", port);
9459
+ console.log(toonKV({ service: "glass", ...res }));
9460
+ break;
9461
+ }
9462
+ if (pos[0] === "uninstall") {
9463
+ const res = uninstallService("glass");
9464
+ console.log(toonKV({ service: "glass", ...res }));
9465
+ break;
9466
+ }
9467
+ if (has("--detach")) {
9468
+ const { info, already } = await startDetachedGlass(port);
9469
+ console.log(toonKV(glassLines(port, info, already)));
9470
+ break;
9471
+ }
9472
+ const existing = await probeGlass(port);
9473
+ if (existing) {
9474
+ console.log(toonKV(glassLines(port, existing, true)));
9475
+ break;
9476
+ }
9477
+ const server = serveGlass(port);
9478
+ await new Promise((resolve6, reject) => {
9479
+ server.once("listening", () => resolve6());
9480
+ server.once("error", reject);
9481
+ });
9617
9482
  console.log(
9618
9483
  toonKV({
9619
- glass: `http://127.0.0.1:${port}`,
9484
+ glass: glassUrl(port),
9620
9485
  mode: "read-only \u2014 looking consumes nothing",
9621
9486
  stop: "ctrl-c"
9622
9487
  })
@@ -9624,7 +9489,7 @@ ${usageFor("pet")}`);
9624
9489
  return;
9625
9490
  }
9626
9491
  case "doctor": {
9627
- const rows = runDoctor();
9492
+ const rows = await runDoctor();
9628
9493
  console.log(toonTable("doctor", rows, ["check", "status", "detail"]));
9629
9494
  if (rows.some((r) => r.status === "fail")) process.exitCode = 1;
9630
9495
  break;
@@ -9647,7 +9512,7 @@ ${usageFor("pet")}`);
9647
9512
  path: r.path,
9648
9513
  trunk: r.trunk,
9649
9514
  pickup: r.pickup ?? false,
9650
- exists: fs45.existsSync(r.path)
9515
+ exists: fs46.existsSync(r.path)
9651
9516
  }));
9652
9517
  console.log(toonTable("repos", rows, ["key", "path", "trunk", "pickup", "exists"]));
9653
9518
  break;
@@ -9715,7 +9580,7 @@ ${usageFor("pet")}`);
9715
9580
  break;
9716
9581
  }
9717
9582
  case "init": {
9718
- if (!fs45.existsSync(configPath())) {
9583
+ if (!fs46.existsSync(configPath())) {
9719
9584
  const exampleRepo = has("--scan") ? "" : `[repos.example]
9720
9585
  path = "~/src/example"
9721
9586
  trunk = "main"
@@ -9724,7 +9589,7 @@ trunk = "main"
9724
9589
  # pickup = true # opt into [pickup.github] multi-repo tracker pickup
9725
9590
 
9726
9591
  `;
9727
- fs45.writeFileSync(
9592
+ fs46.writeFileSync(
9728
9593
  configPath(),
9729
9594
  `# lobstah workspace definitions \u2014 the descriptor's repo key resolves here.
9730
9595
  # Top-level keys must come before any [section].
@@ -9761,12 +9626,12 @@ wallClockSecs = 3600
9761
9626
  let reference = "https://github.com/aequitas-labs/lobstah/blob/main/docs/configuration.md";
9762
9627
  try {
9763
9628
  const local = fileURLToPath4(new URL("../docs/configuration.md", import.meta.url));
9764
- if (fs45.existsSync(local)) reference = local;
9629
+ if (fs46.existsSync(local)) reference = local;
9765
9630
  } catch {
9766
9631
  }
9767
9632
  console.log(
9768
9633
  toonKV({
9769
- home: path38.dirname(configPath()),
9634
+ home: path39.dirname(configPath()),
9770
9635
  config: configPath(),
9771
9636
  ...added.length > 0 ? { added: added.join(", ") } : {},
9772
9637
  ...unmarked.length > 0 ? { note: `none marked pickable \u2014 set pickup = true per [repos.*] (or rerun with --pickup)` } : {},