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/README.md +128 -32
- package/dist/main.js +877 -1012
- package/dist/runner.js +116 -18
- package/docs/assets/lob-crawl.svg +14 -0
- package/docs/configuration.md +8 -1
- package/docs/design.md +63 -5
- package/docs/harness/claude-code.md +104 -0
- package/docs/harness/codex.md +189 -0
- package/docs/{lobsterman.md → man.md} +40 -14
- package/docs/vocabulary.md +8 -5
- package/package.json +1 -1
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 =
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
2880
|
-
import * as
|
|
2881
|
-
import * as
|
|
2882
|
-
import { spawn as
|
|
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 =
|
|
2885
|
-
|
|
2886
|
-
const auth =
|
|
2887
|
-
const realAuth =
|
|
2888
|
-
if (!
|
|
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
|
-
|
|
3022
|
+
fs42.symlinkSync(realAuth, auth);
|
|
2891
3023
|
} catch {
|
|
2892
|
-
|
|
3024
|
+
fs42.copyFileSync(realAuth, auth);
|
|
2893
3025
|
}
|
|
2894
3026
|
}
|
|
2895
|
-
const cfg =
|
|
2896
|
-
if (!
|
|
2897
|
-
|
|
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 =
|
|
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
|
|
3112
|
-
import * as
|
|
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
|
|
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 (
|
|
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 (!
|
|
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
|
|
3337
|
-
import * as
|
|
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 =
|
|
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(
|
|
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 =
|
|
3353
|
-
if (!
|
|
3354
|
-
|
|
3355
|
-
const brief =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
3531
|
+
const wtFile = path37.join(activeDir, "worktree.json");
|
|
3381
3532
|
let cwd;
|
|
3382
|
-
if (
|
|
3383
|
-
cwd = JSON.parse(
|
|
3384
|
-
if (!
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
3397
|
-
const workerPath =
|
|
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
|
|
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
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
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
|
|
3476
|
-
import * as
|
|
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 =
|
|
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
|
-
|
|
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
|
|
3506
|
-
import * as
|
|
3507
|
-
import * as
|
|
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
|
|
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:
|
|
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
|
|
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("~/") ? `${
|
|
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
|
-
|
|
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(
|
|
5792
|
-
at:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
6482
|
-
import * as
|
|
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 =
|
|
6487
|
-
if (stat.isDirectory()) return
|
|
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(
|
|
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(
|
|
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 =
|
|
6520
|
-
const m =
|
|
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 =
|
|
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 =
|
|
6532
|
-
items.push({ kind: "worktree", id, target: p, ageDays: Math.floor((now2 -
|
|
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
|
|
6538
|
-
const target =
|
|
6539
|
-
const directory =
|
|
6540
|
-
if (directory && !
|
|
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(
|
|
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,
|
|
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:
|
|
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 =
|
|
6927
|
+
const descFile = path27.join(laneDirs(lane).done, id, "descriptor.json");
|
|
6571
6928
|
try {
|
|
6572
|
-
const desc = JSON.parse(
|
|
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
|
-
|
|
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"))
|
|
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 =
|
|
6590
|
-
for (const f of
|
|
6591
|
-
if (f === item.id || f.startsWith(`${item.id}.`))
|
|
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
|
|
6954
|
+
var MANUAL = `the lobstah man's manual
|
|
6598
6955
|
|
|
6599
|
-
You are the
|
|
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>]
|
|
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
|
|
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/
|
|
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
|
|
6671
|
-
import * as
|
|
6672
|
-
import { spawnSync as
|
|
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
|
|
6677
|
-
import * as
|
|
6678
|
-
import * as
|
|
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(
|
|
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 ??
|
|
7056
|
+
const home = opts.home ?? os6.homedir();
|
|
6697
7057
|
const root = env.CLAUDE_PLUGIN_ROOT;
|
|
6698
7058
|
if (root) {
|
|
6699
|
-
const version2 = manifestVersion(
|
|
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 ??
|
|
7062
|
+
const claudeDir = env.CLAUDE_CONFIG_DIR ?? path28.join(home, ".claude");
|
|
6703
7063
|
const installed = readJson2(
|
|
6704
|
-
|
|
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(
|
|
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 ??
|
|
7079
|
+
const codexDir = env.CODEX_HOME ?? path28.join(opts.home ?? os6.homedir(), ".codex");
|
|
6720
7080
|
let cfg;
|
|
6721
7081
|
try {
|
|
6722
|
-
cfg = parse(
|
|
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 =
|
|
7088
|
+
const cache = path28.join(codexDir, "plugins", "cache", "lobstah", "lobstah");
|
|
6729
7089
|
let dirs;
|
|
6730
7090
|
try {
|
|
6731
|
-
dirs =
|
|
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 =
|
|
6737
|
-
const version = manifestVersion(
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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 =
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
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 (!
|
|
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(
|
|
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 =
|
|
6872
|
-
const stale =
|
|
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(
|
|
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
|
|
6896
|
-
import * as
|
|
6897
|
-
import * as
|
|
6898
|
-
import * as
|
|
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-
|
|
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(
|
|
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
|
|
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
|
|
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 (
|
|
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 =
|
|
7059
|
-
return
|
|
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,
|
|
7063
|
-
(folder) => listDir(folder).filter((file) => file.endsWith(".meta.json")).flatMap((file) => readJson3(
|
|
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 =
|
|
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 =
|
|
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:
|
|
7442
|
+
return { file: path31.basename(file), state, from: p.from ?? "unknown", at: p.at ?? "", text: p.text ?? "", attachments };
|
|
7079
7443
|
} catch {
|
|
7080
|
-
return { file:
|
|
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(
|
|
7085
|
-
...listDir(
|
|
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(
|
|
7095
|
-
if (d) rows.push({ lane, bucket: "queued", d, sort: mtime(
|
|
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(
|
|
7099
|
-
if (d) rows.push({ lane, bucket: "active", d, sort: mtime(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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) =>
|
|
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 =
|
|
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)=>({'&':'&','<':'<','>':'>','"':'"'}[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,'"');
|
|
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,''')+","+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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
7727
|
-
import * as
|
|
7728
|
-
import * as
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
7632
|
+
return path32.join(lobstahHome(), "bin", "LobstahPet");
|
|
7844
7633
|
}
|
|
7845
7634
|
function petPlistFile() {
|
|
7846
|
-
return
|
|
7635
|
+
return path32.join(os9.homedir(), "Library", "LaunchAgents", "lobstah.pet.plist");
|
|
7847
7636
|
}
|
|
7848
7637
|
function xml2(s) {
|
|
7849
7638
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -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(
|
|
7869
|
-
<key>StandardErrorPath</key><string>${xml2(
|
|
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
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
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) =>
|
|
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 (
|
|
7687
|
+
if (fs39.existsSync(file0)) run2("launchctl", ["unload", file0]);
|
|
7899
7688
|
run2("pkill", ["-f", "LobstahPet"]);
|
|
7900
|
-
if (
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
const bundle =
|
|
7906
|
-
if (
|
|
7907
|
-
const dest =
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
}
|
|
7911
|
-
}
|
|
7912
|
-
const logDir =
|
|
7913
|
-
|
|
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
|
-
|
|
7916
|
-
|
|
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" &&
|
|
7923
|
-
const removed =
|
|
7924
|
-
if (removed)
|
|
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
|
|
7933
|
-
import * as
|
|
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 =
|
|
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 =
|
|
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]) =>
|
|
7961
|
-
return { key:
|
|
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 =
|
|
7967
|
-
if (!
|
|
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
|
|
7762
|
+
for (const entry of fs40.readdirSync(abs, { withFileTypes: true })) {
|
|
7974
7763
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
7975
|
-
const child = detectRepo(
|
|
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 (!
|
|
7983
|
-
const raw = parse(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
8311
|
-
import * as
|
|
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 = () =>
|
|
8315
|
-
var watcherFile = (sessionId) =>
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
8161
|
+
fs41.writeFileSync(tmp, `${JSON.stringify(own())}
|
|
8364
8162
|
`);
|
|
8365
|
-
|
|
8163
|
+
fs41.renameSync(tmp, file);
|
|
8366
8164
|
} catch {
|
|
8367
8165
|
try {
|
|
8368
|
-
|
|
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())
|
|
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
|
-
|
|
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>]
|
|
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
|
|
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 (
|
|
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 (
|
|
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) =>
|
|
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 =
|
|
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
|
|
8699
|
-
|
|
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 ??
|
|
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:
|
|
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
|
|
8830
|
-
|
|
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 (
|
|
8844
|
-
const raw =
|
|
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 =
|
|
8668
|
+
let size = fs46.existsSync(file) ? fs46.statSync(file).size : 0;
|
|
8858
8669
|
setInterval(() => {
|
|
8859
|
-
if (!
|
|
8860
|
-
const now2 =
|
|
8670
|
+
if (!fs46.existsSync(file)) return;
|
|
8671
|
+
const now2 = fs46.statSync(file).size;
|
|
8861
8672
|
if (now2 > size) {
|
|
8862
|
-
const fd =
|
|
8673
|
+
const fd = fs46.openSync(file, "r");
|
|
8863
8674
|
const buf = Buffer.alloc(now2 - size);
|
|
8864
|
-
|
|
8865
|
-
|
|
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
|
-
|
|
8965
|
-
|
|
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 =
|
|
8984
|
-
if (!
|
|
8985
|
-
const descFile =
|
|
8986
|
-
const descriptor = JSON.parse(
|
|
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
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
const
|
|
8996
|
-
|
|
8997
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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 (
|
|
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 (
|
|
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") ?
|
|
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(
|
|
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
|
-
|
|
9327
|
-
|
|
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"))
|
|
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()
|
|
9350
|
-
|
|
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" && !
|
|
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
|
-
|
|
9419
|
-
|
|
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
|
-
|
|
9616
|
-
|
|
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:
|
|
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:
|
|
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 (!
|
|
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
|
-
|
|
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 (
|
|
9629
|
+
if (fs46.existsSync(local)) reference = local;
|
|
9765
9630
|
} catch {
|
|
9766
9631
|
}
|
|
9767
9632
|
console.log(
|
|
9768
9633
|
toonKV({
|
|
9769
|
-
home:
|
|
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)` } : {},
|