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