lobstah 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +389 -71
- package/dist/runner.js +236 -15
- package/docs/harness/claude-code.md +2 -0
- package/docs/harness/codex.md +4 -3
- package/docs/man.md +21 -8
- package/package.json +1 -1
package/dist/runner.js
CHANGED
|
@@ -857,6 +857,26 @@ var GB = 1024 ** 3;
|
|
|
857
857
|
// packages/core/dist/watch.js
|
|
858
858
|
import * as fs3 from "node:fs";
|
|
859
859
|
import * as path3 from "node:path";
|
|
860
|
+
|
|
861
|
+
// packages/core/dist/gh-errors.js
|
|
862
|
+
function firstMeaningfulLine(...outputs) {
|
|
863
|
+
for (const out of outputs) {
|
|
864
|
+
for (const raw of (out ?? "").split("\n")) {
|
|
865
|
+
let line = raw.trim();
|
|
866
|
+
if (!line || /^(warning:|hint:|\(node:|\{|\})/i.test(line))
|
|
867
|
+
continue;
|
|
868
|
+
const m = /^error:\s*(.*)$/.exec(line);
|
|
869
|
+
if (m)
|
|
870
|
+
line = m[1].replace(/^"(.*)"$/, "$1").trim();
|
|
871
|
+
line = line.replace(/^(gh|GraphQL):\s*/i, "");
|
|
872
|
+
if (line)
|
|
873
|
+
return line;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return void 0;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// packages/core/dist/watch.js
|
|
860
880
|
function watchesDir() {
|
|
861
881
|
return path3.join(lobstahHome(), "watches");
|
|
862
882
|
}
|
|
@@ -1577,6 +1597,7 @@ var COMPILED_BINARY = (() => {
|
|
|
1577
1597
|
})();
|
|
1578
1598
|
|
|
1579
1599
|
// packages/core/dist/pr.js
|
|
1600
|
+
import { spawnSync } from "node:child_process";
|
|
1580
1601
|
function parsePrRef(s) {
|
|
1581
1602
|
const m = /^(?:pr:)?([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)#(\d+)$/.exec(s.trim()) ?? /^https?:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/(\d+)(?:[/?#].*)?$/.exec(s.trim());
|
|
1582
1603
|
if (!m)
|
|
@@ -1587,6 +1608,59 @@ function parsePrRef(s) {
|
|
|
1587
1608
|
}
|
|
1588
1609
|
var PR_VIEW_FIELDS = "title,state,isDraft,headRefOid,baseRefName,baseRefOid,headRefName,mergeStateStatus,reviewDecision,statusCheckRollup,mergedAt,closedAt,updatedAt,reviews";
|
|
1589
1610
|
var PR_VIEW_FIELDS_NO_CHECKS = PR_VIEW_FIELDS.split(",").filter((f) => f !== "statusCheckRollup").join(",");
|
|
1611
|
+
var FORBIDDEN = /resource not accessible by (integration|personal access token)/i;
|
|
1612
|
+
function ghPrView(ref) {
|
|
1613
|
+
const view1 = (fields2) => spawnSync("gh", ["pr", "view", String(ref.number), "--repo", `${ref.owner}/${ref.repo}`, "--json", fields2], {
|
|
1614
|
+
encoding: "utf8",
|
|
1615
|
+
timeout: 6e4
|
|
1616
|
+
});
|
|
1617
|
+
const reason = (r) => firstMeaningfulLine(r.stderr) ?? `gh exited ${r.status}`;
|
|
1618
|
+
const unknownBase = (r) => r.status !== 0 && /unknown json field.*baseRefOid/i.test(r.stderr ?? "");
|
|
1619
|
+
const withoutBase = (fields2) => fields2.split(",").filter((f) => f !== "baseRefOid").join(",");
|
|
1620
|
+
let fields = PR_VIEW_FIELDS;
|
|
1621
|
+
let res = view1(fields);
|
|
1622
|
+
if (unknownBase(res))
|
|
1623
|
+
res = view1(fields = withoutBase(fields));
|
|
1624
|
+
if (res.error)
|
|
1625
|
+
throw new Error(`gh: ${res.error.message}`);
|
|
1626
|
+
let checksError;
|
|
1627
|
+
if (res.status !== 0 && FORBIDDEN.test(res.stderr ?? "")) {
|
|
1628
|
+
checksError = reason(res);
|
|
1629
|
+
const retry = view1(fields === PR_VIEW_FIELDS ? PR_VIEW_FIELDS_NO_CHECKS : withoutBase(PR_VIEW_FIELDS_NO_CHECKS));
|
|
1630
|
+
if (retry.error)
|
|
1631
|
+
throw new Error(`gh: ${retry.error.message}`);
|
|
1632
|
+
if (retry.status !== 0) {
|
|
1633
|
+
throw new Error(FORBIDDEN.test(retry.stderr ?? "") ? `${reason(retry)} (fails even without check results)` : reason(retry));
|
|
1634
|
+
}
|
|
1635
|
+
res = retry;
|
|
1636
|
+
}
|
|
1637
|
+
if (res.status !== 0)
|
|
1638
|
+
throw new Error(reason(res));
|
|
1639
|
+
const view = JSON.parse(res.stdout);
|
|
1640
|
+
if (checksError)
|
|
1641
|
+
view.checksError = checksError;
|
|
1642
|
+
if (view.state === "OPEN") {
|
|
1643
|
+
const threads = ghUnresolvedThreads(ref);
|
|
1644
|
+
if (threads !== void 0)
|
|
1645
|
+
view.unresolvedThreads = threads;
|
|
1646
|
+
}
|
|
1647
|
+
return view;
|
|
1648
|
+
}
|
|
1649
|
+
var THREADS_QUERY = "query($owner:String!,$repo:String!,$n:Int!){repository(owner:$owner,name:$repo){pullRequest(number:$n){reviewThreads(first:100){nodes{isResolved}}}}}";
|
|
1650
|
+
function ghUnresolvedThreads(ref) {
|
|
1651
|
+
const res = spawnSync("gh", ["api", "graphql", "-f", `query=${THREADS_QUERY}`, "-f", `owner=${ref.owner}`, "-f", `repo=${ref.repo}`, "-F", `n=${ref.number}`], { encoding: "utf8", timeout: 6e4 });
|
|
1652
|
+
if (res.error || res.status !== 0)
|
|
1653
|
+
return void 0;
|
|
1654
|
+
return parseUnresolvedThreads(res.stdout);
|
|
1655
|
+
}
|
|
1656
|
+
function parseUnresolvedThreads(stdout) {
|
|
1657
|
+
try {
|
|
1658
|
+
const nodes = JSON.parse(stdout).data?.repository?.pullRequest?.reviewThreads?.nodes;
|
|
1659
|
+
return Array.isArray(nodes) ? nodes.filter((t) => t.isResolved === false).length : void 0;
|
|
1660
|
+
} catch {
|
|
1661
|
+
return void 0;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1590
1664
|
|
|
1591
1665
|
// packages/core/dist/prs.js
|
|
1592
1666
|
import * as fs14 from "node:fs";
|
|
@@ -1594,6 +1668,20 @@ import * as path11 from "node:path";
|
|
|
1594
1668
|
function prsDir() {
|
|
1595
1669
|
return path11.join(lobstahHome(), "prs");
|
|
1596
1670
|
}
|
|
1671
|
+
function prRecordFile(key) {
|
|
1672
|
+
const ref = parsePrRef(key);
|
|
1673
|
+
if (!ref)
|
|
1674
|
+
throw new Error(`not a PR key: ${key}`);
|
|
1675
|
+
const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1676
|
+
return path11.join(prsDir(), `${safe(ref.owner)}__${safe(ref.repo)}__${ref.number}.json`);
|
|
1677
|
+
}
|
|
1678
|
+
function readPr(key) {
|
|
1679
|
+
try {
|
|
1680
|
+
return JSON.parse(fs14.readFileSync(prRecordFile(key), "utf8"));
|
|
1681
|
+
} catch {
|
|
1682
|
+
return void 0;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1597
1685
|
function readPrs() {
|
|
1598
1686
|
let files;
|
|
1599
1687
|
try {
|
|
@@ -1655,7 +1743,7 @@ function pushRule(branch, id = "<dispatch id>") {
|
|
|
1655
1743
|
import * as fs15 from "node:fs";
|
|
1656
1744
|
import * as os3 from "node:os";
|
|
1657
1745
|
import * as path12 from "node:path";
|
|
1658
|
-
import { spawnSync } from "node:child_process";
|
|
1746
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1659
1747
|
var 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;
|
|
1660
1748
|
function harnessFromSessionId(sessionId) {
|
|
1661
1749
|
const version = sessionId ? UUID.exec(sessionId)?.[1] : void 0;
|
|
@@ -1717,7 +1805,7 @@ function isUnresumable(error) {
|
|
|
1717
1805
|
function worktreeProgress(worktree, trunk) {
|
|
1718
1806
|
if (!worktree || !fs15.existsSync(worktree))
|
|
1719
1807
|
return "No worktree progress recorded.";
|
|
1720
|
-
const git2 = (...a) =>
|
|
1808
|
+
const git2 = (...a) => spawnSync2("git", a, { cwd: worktree, encoding: "utf8" }).stdout?.trim() ?? "";
|
|
1721
1809
|
const commits = trunk ? git2("log", "--oneline", `origin/${trunk}..HEAD`) : git2("log", "--oneline", "-15");
|
|
1722
1810
|
const status = git2("status", "--short");
|
|
1723
1811
|
return `Commits so far:
|
|
@@ -1827,7 +1915,7 @@ function isFinished(id, lane) {
|
|
|
1827
1915
|
// packages/runner/src/run.ts
|
|
1828
1916
|
import * as fs19 from "node:fs";
|
|
1829
1917
|
import * as path16 from "node:path";
|
|
1830
|
-
import { execFileSync } from "node:child_process";
|
|
1918
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1831
1919
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1832
1920
|
|
|
1833
1921
|
// packages/adapters/dist/claude.js
|
|
@@ -2346,7 +2434,7 @@ function loadAdapter(name) {
|
|
|
2346
2434
|
import * as fs17 from "node:fs";
|
|
2347
2435
|
import * as path14 from "node:path";
|
|
2348
2436
|
import { createHash } from "node:crypto";
|
|
2349
|
-
import { exec, execFile } from "node:child_process";
|
|
2437
|
+
import { exec, execFile, execFileSync } from "node:child_process";
|
|
2350
2438
|
import { promisify } from "node:util";
|
|
2351
2439
|
var run = promisify(execFile);
|
|
2352
2440
|
var shell = promisify(exec);
|
|
@@ -2372,7 +2460,12 @@ var LOCK_CONTENTION = [
|
|
|
2372
2460
|
// held by another process ("fetching ref <ref> failed: reference already
|
|
2373
2461
|
// exists"). The files backend maps the lock's EEXIST to this reason; older
|
|
2374
2462
|
// git printed "Unable to create '<ref>.lock': File exists" instead.
|
|
2375
|
-
/reference already exists
|
|
2463
|
+
/reference already exists/,
|
|
2464
|
+
// Windows: two fetches unpack the same loose object, and one cannot write
|
|
2465
|
+
// the file the other holds open ("unable to write file
|
|
2466
|
+
// .git/objects/<xx>/<rest>: Permission denied", then "unpack-objects
|
|
2467
|
+
// failed"). Only an object path matches, not any denied write.
|
|
2468
|
+
/unable to write file \S*objects[\\/][0-9a-f]{2}[\\/][0-9a-f]{38,}: Permission denied/
|
|
2376
2469
|
];
|
|
2377
2470
|
function gitStderr(err) {
|
|
2378
2471
|
const stderr = err.stderr;
|
|
@@ -2399,6 +2492,112 @@ ${gitStderr(err)}`, { cause: err });
|
|
|
2399
2492
|
}
|
|
2400
2493
|
}
|
|
2401
2494
|
}
|
|
2495
|
+
var REPO_LOCK_WAIT_MS = 5 * 6e4;
|
|
2496
|
+
var repoQueues = /* @__PURE__ */ new Map();
|
|
2497
|
+
function pidAlive(pid) {
|
|
2498
|
+
try {
|
|
2499
|
+
process.kill(pid, 0);
|
|
2500
|
+
return true;
|
|
2501
|
+
} catch (err) {
|
|
2502
|
+
return err.code === "EPERM";
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
var BUSY = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
2506
|
+
function takeRepoLock(file, mine) {
|
|
2507
|
+
try {
|
|
2508
|
+
fs17.writeFileSync(file, mine, { flag: "wx" });
|
|
2509
|
+
return { state: "held" };
|
|
2510
|
+
} catch (err) {
|
|
2511
|
+
const code = err.code ?? "";
|
|
2512
|
+
if (code !== "EEXIST" && !BUSY.has(code))
|
|
2513
|
+
throw err;
|
|
2514
|
+
}
|
|
2515
|
+
let raw = "";
|
|
2516
|
+
let age = 0;
|
|
2517
|
+
try {
|
|
2518
|
+
raw = fs17.readFileSync(file, "utf8");
|
|
2519
|
+
age = Date.now() - fs17.statSync(file).mtimeMs;
|
|
2520
|
+
} catch {
|
|
2521
|
+
return { state: "retry" };
|
|
2522
|
+
}
|
|
2523
|
+
let holder;
|
|
2524
|
+
try {
|
|
2525
|
+
holder = JSON.parse(raw);
|
|
2526
|
+
} catch {
|
|
2527
|
+
holder = void 0;
|
|
2528
|
+
}
|
|
2529
|
+
const stale = age > REPO_LOCK_WAIT_MS || (holder ? !pidAlive(holder.pid) : age > 5e3);
|
|
2530
|
+
if (stale) {
|
|
2531
|
+
try {
|
|
2532
|
+
if (fs17.readFileSync(file, "utf8") === raw)
|
|
2533
|
+
fs17.rmSync(file, { force: true });
|
|
2534
|
+
} catch {
|
|
2535
|
+
}
|
|
2536
|
+
return { state: "retry" };
|
|
2537
|
+
}
|
|
2538
|
+
return { state: "wait", ...holder ? { holder: holder.pid } : {} };
|
|
2539
|
+
}
|
|
2540
|
+
function releaseRepoLock(file, mine) {
|
|
2541
|
+
try {
|
|
2542
|
+
if (fs17.readFileSync(file, "utf8") === mine)
|
|
2543
|
+
fs17.rmSync(file, { force: true, recursive: true, maxRetries: 5 });
|
|
2544
|
+
} catch {
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
async function withRepoLock(common, fn) {
|
|
2548
|
+
const file = path14.join(common, "lobstah-git.lock");
|
|
2549
|
+
const mine = JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2550
|
+
const deadline = Date.now() + REPO_LOCK_WAIT_MS;
|
|
2551
|
+
let held = false;
|
|
2552
|
+
while (Date.now() < deadline) {
|
|
2553
|
+
const took = takeRepoLock(file, mine);
|
|
2554
|
+
if (took.state === "held") {
|
|
2555
|
+
held = true;
|
|
2556
|
+
break;
|
|
2557
|
+
}
|
|
2558
|
+
if (took.state === "wait")
|
|
2559
|
+
await new Promise((r) => setTimeout(r, 50 + Math.random() * 50));
|
|
2560
|
+
}
|
|
2561
|
+
try {
|
|
2562
|
+
await fn();
|
|
2563
|
+
} finally {
|
|
2564
|
+
if (held)
|
|
2565
|
+
releaseRepoLock(file, mine);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
async function oneAtATime(cwd, fn) {
|
|
2569
|
+
const common = await git(cwd, "rev-parse", "--path-format=absolute", "--git-common-dir");
|
|
2570
|
+
const key = realpath(common);
|
|
2571
|
+
const run2 = (repoQueues.get(key) ?? Promise.resolve()).then(() => withRepoLock(common, () => fn(common)));
|
|
2572
|
+
const tail = run2.catch(() => {
|
|
2573
|
+
});
|
|
2574
|
+
repoQueues.set(key, tail);
|
|
2575
|
+
try {
|
|
2576
|
+
await run2;
|
|
2577
|
+
} finally {
|
|
2578
|
+
if (repoQueues.get(key) === tail)
|
|
2579
|
+
repoQueues.delete(key);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
function fetchStamps(common) {
|
|
2583
|
+
try {
|
|
2584
|
+
return JSON.parse(fs17.readFileSync(path14.join(common, "lobstah-fetch.json"), "utf8"));
|
|
2585
|
+
} catch {
|
|
2586
|
+
return {};
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
async function fetchShared(cwd, remote, ref) {
|
|
2590
|
+
const asked = Date.now();
|
|
2591
|
+
const key = `${remote} ${ref}`;
|
|
2592
|
+
await oneAtATime(cwd, async (common) => {
|
|
2593
|
+
if ((fetchStamps(common)[key] ?? -Infinity) >= asked)
|
|
2594
|
+
return;
|
|
2595
|
+
const started = Date.now();
|
|
2596
|
+
await gitShared(cwd, "fetch", remote, ref);
|
|
2597
|
+
const stamps = { ...fetchStamps(common), [key]: started };
|
|
2598
|
+
fs17.writeFileSync(path14.join(common, "lobstah-fetch.json"), JSON.stringify(stamps, null, 2));
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2402
2601
|
async function allocate(repo, id, fromRemoteBranch = repo.trunk) {
|
|
2403
2602
|
const dir = worktreePath(id);
|
|
2404
2603
|
if (fs17.existsSync(dir)) {
|
|
@@ -2409,8 +2608,10 @@ async function allocate(repo, id, fromRemoteBranch = repo.trunk) {
|
|
|
2409
2608
|
throw new Error(`repo path ${repo.path} missing and no origin configured`);
|
|
2410
2609
|
await run("git", ["clone", repo.origin, repo.path], { env: process.env });
|
|
2411
2610
|
}
|
|
2412
|
-
await
|
|
2413
|
-
await
|
|
2611
|
+
await fetchShared(repo.path, "origin", fromRemoteBranch);
|
|
2612
|
+
await oneAtATime(repo.path, async () => {
|
|
2613
|
+
await gitShared(repo.path, "worktree", "add", "--no-track", dir, "-b", `lobstah/${id}`, `origin/${fromRemoteBranch}`);
|
|
2614
|
+
});
|
|
2414
2615
|
await runSetup(repo, dir);
|
|
2415
2616
|
return dir;
|
|
2416
2617
|
}
|
|
@@ -2544,7 +2745,7 @@ async function chooseWorktree(input) {
|
|
|
2544
2745
|
return { reuse: true, path: found.path, owner: found.owner, from: found.from };
|
|
2545
2746
|
}
|
|
2546
2747
|
async function prepareReuse(repo, dir) {
|
|
2547
|
-
await
|
|
2748
|
+
await fetchShared(dir, "origin", repo.trunk);
|
|
2548
2749
|
if (!repo.setup?.length)
|
|
2549
2750
|
return { setupRan: false };
|
|
2550
2751
|
if (recordedSetupHash(dir) === setupHash(repo, dir))
|
|
@@ -2874,6 +3075,7 @@ function planStart(input) {
|
|
|
2874
3075
|
function startWallClock(opts) {
|
|
2875
3076
|
const now2 = opts.now ?? Date.now;
|
|
2876
3077
|
const tickMs = Math.max(1, Math.min(opts.tickMs ?? 5e3, opts.limitMs));
|
|
3078
|
+
const sleepGapMs = opts.sleepGapMs ?? Math.max(6e4, tickMs * 6);
|
|
2877
3079
|
let elapsed = opts.initialElapsedMs ?? 0;
|
|
2878
3080
|
const maxMs = Math.max(opts.limitMs, opts.maxMs ?? opts.limitMs);
|
|
2879
3081
|
let windowMs = Math.min(maxMs, Math.max(opts.limitMs, opts.initialWindowMs ?? opts.limitMs));
|
|
@@ -2887,7 +3089,8 @@ function startWallClock(opts) {
|
|
|
2887
3089
|
} catch {
|
|
2888
3090
|
paused = false;
|
|
2889
3091
|
}
|
|
2890
|
-
|
|
3092
|
+
const step = Math.max(0, t - last);
|
|
3093
|
+
if (!paused) elapsed += step > sleepGapMs ? Math.min(step, tickMs) : step;
|
|
2891
3094
|
last = t;
|
|
2892
3095
|
if (!done && elapsed >= windowMs && elapsed < maxMs) {
|
|
2893
3096
|
let advanced = false;
|
|
@@ -3094,13 +3297,23 @@ function keepRemote(opts) {
|
|
|
3094
3297
|
}
|
|
3095
3298
|
|
|
3096
3299
|
// packages/runner/src/run.ts
|
|
3300
|
+
function livePrState(url) {
|
|
3301
|
+
const ref = parsePrRef(url);
|
|
3302
|
+
if (!ref) return void 0;
|
|
3303
|
+
try {
|
|
3304
|
+
return ghPrView(ref).state;
|
|
3305
|
+
} catch {
|
|
3306
|
+
return void 0;
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3097
3309
|
var defaultDeps = {
|
|
3098
3310
|
loadAdapter,
|
|
3099
3311
|
allocate,
|
|
3100
3312
|
chooseWorktree,
|
|
3101
3313
|
prepareReuse,
|
|
3102
3314
|
collectEvidence,
|
|
3103
|
-
reap: async () => 0
|
|
3315
|
+
reap: async () => 0,
|
|
3316
|
+
prState: livePrState
|
|
3104
3317
|
};
|
|
3105
3318
|
var DONE_WAIT_MS = 2e3;
|
|
3106
3319
|
function within(p, ms, fallback) {
|
|
@@ -3141,6 +3354,14 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
3141
3354
|
attempts
|
|
3142
3355
|
};
|
|
3143
3356
|
fs19.writeFileSync(path16.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
|
|
3357
|
+
if (descriptor.systemRepair && descriptor.pr?.url) {
|
|
3358
|
+
const state = deps.prState(descriptor.pr.url) ?? readPr(parsePrRef(descriptor.pr.url)?.key ?? "")?.state;
|
|
3359
|
+
if (state === "MERGED" || state === "CLOSED") {
|
|
3360
|
+
status("failed", `cancelled: ${descriptor.pr.url} is ${state.toLowerCase()}; nothing to repair`);
|
|
3361
|
+
complete(id, lane);
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3144
3365
|
const plan = planStart({
|
|
3145
3366
|
id,
|
|
3146
3367
|
lane,
|
|
@@ -3240,7 +3461,7 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
3240
3461
|
})();
|
|
3241
3462
|
const head = () => {
|
|
3242
3463
|
try {
|
|
3243
|
-
return
|
|
3464
|
+
return execFileSync2("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
3244
3465
|
} catch {
|
|
3245
3466
|
return void 0;
|
|
3246
3467
|
}
|
|
@@ -3412,10 +3633,10 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
3412
3633
|
}
|
|
3413
3634
|
|
|
3414
3635
|
// packages/runner/src/reap.ts
|
|
3415
|
-
import { execFileSync as
|
|
3636
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
3416
3637
|
function listProcesses() {
|
|
3417
3638
|
if (process.platform === "win32") {
|
|
3418
|
-
const out2 =
|
|
3639
|
+
const out2 = execFileSync3(
|
|
3419
3640
|
"powershell.exe",
|
|
3420
3641
|
[
|
|
3421
3642
|
"-NoProfile",
|
|
@@ -3427,7 +3648,7 @@ function listProcesses() {
|
|
|
3427
3648
|
);
|
|
3428
3649
|
return out2.split(/\r?\n/).map((l) => l.trim().split(/\s+/).map(Number)).filter(([pid, ppid]) => Number.isInteger(pid) && Number.isInteger(ppid)).map(([pid, ppid]) => ({ pid, ppid }));
|
|
3429
3650
|
}
|
|
3430
|
-
const out =
|
|
3651
|
+
const out = execFileSync3("ps", ["-A", "-o", "pid=,ppid=,pgid="], {
|
|
3431
3652
|
encoding: "utf8",
|
|
3432
3653
|
timeout: 3e4,
|
|
3433
3654
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -3470,7 +3691,7 @@ async function stopPids(pids, graceMs = 2e3) {
|
|
|
3470
3691
|
if (process.platform === "win32") {
|
|
3471
3692
|
for (const pid of pids) {
|
|
3472
3693
|
try {
|
|
3473
|
-
|
|
3694
|
+
execFileSync3("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true, timeout: 3e4 });
|
|
3474
3695
|
} catch {
|
|
3475
3696
|
}
|
|
3476
3697
|
}
|
|
@@ -66,6 +66,8 @@ For the glass's ↗ open button, give the trap its own session link with
|
|
|
66
66
|
`claude://claude.ai/...` link in the app; copy that link from the session.
|
|
67
67
|
Its app id is not the CLI session id. In the VS Code extension, use
|
|
68
68
|
`vscode://anthropic.claude-code/open?session=<session-id>` with the id above.
|
|
69
|
+
A CLI session in a terminal, including the Claude desktop app's terminal
|
|
70
|
+
panel, passes no link; soak ignores a link that does not fit the session.
|
|
69
71
|
The glass checks the stored link before showing it. `lobstah focus <trap>`
|
|
70
72
|
uses the same focus steps from the terminal.
|
|
71
73
|
|
package/docs/harness/codex.md
CHANGED
|
@@ -124,9 +124,10 @@ Codex task ids are UUIDv7 (for example `01a0ceb8-b9bd-7d42-…`); Claude Code
|
|
|
124
124
|
session ids are UUIDv4. When both harnesses' variables are set, lobstah uses
|
|
125
125
|
this format to tell which one signed on.
|
|
126
126
|
|
|
127
|
-
For the glass's ↗ open button
|
|
128
|
-
task id in the session-start brief and
|
|
129
|
-
`lobstah soak --link <url>`.
|
|
127
|
+
For the glass's ↗ open button in the Codex desktop app, form
|
|
128
|
+
`codex://threads/<task-id>` from the task id in the session-start brief and
|
|
129
|
+
pass it with `lobstah soak --link <url>`. A Codex CLI session in a terminal
|
|
130
|
+
passes no link; soak ignores a link that does not fit the session. The glass checks the stored link before
|
|
130
131
|
showing it. `lobstah focus <trap>` uses the same focus steps from the
|
|
131
132
|
terminal.
|
|
132
133
|
|
package/docs/man.md
CHANGED
|
@@ -462,9 +462,10 @@ the helm files to keep.
|
|
|
462
462
|
unacked first, up to 8, then "+N more", which opens the Reports tab. A
|
|
463
463
|
report's card or row shows its title, then who filed it (a trap's name, a
|
|
464
464
|
headless dispatch's id, nothing for the helm), its age, and `acked`. A
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
465
|
+
report opens on its own page, `/report/<key>`, in a new tab: from its card
|
|
466
|
+
or row, a lob, the desktop pet, or its dispatch's modal. The page renders
|
|
467
|
+
once and does not refresh; opening it does not ack the report.
|
|
468
|
+
`#report/<key>` goes to that page. The page shows headings, lists,
|
|
468
469
|
tables, fenced code, links (in a new tab), bold, italics, and images.
|
|
469
470
|
Raw HTML in the markdown shows as text. The glass serves the markdown and
|
|
470
471
|
the images read-only, and an image only by basename from that report's
|
|
@@ -508,7 +509,9 @@ the same rule.
|
|
|
508
509
|
`attachments/`. The claimed helm alone may ask.
|
|
509
510
|
- **Standing.** A decision stands until it is answered or the helm withdraws
|
|
510
511
|
it with `lobstah man ask --withdraw <key>`, which removes its directory. A
|
|
511
|
-
newer `man ask` on the same dispatch
|
|
512
|
+
newer `man ask` on the same dispatch keeps the older decisions: each shows
|
|
513
|
+
as its own card. `lobstah man ask ... --replace <key>` replaces that one
|
|
514
|
+
standing decision; a key that is not standing is refused.
|
|
512
515
|
- **Attention.** A standing decision is attention of kind `decision`, in
|
|
513
516
|
the default `attentionKinds`. It makes the verdict `needs-attention`. A
|
|
514
517
|
worker's raw `needs-decision` or `blocked` stays a `question` until the
|
|
@@ -558,9 +561,11 @@ the same rule.
|
|
|
558
561
|
[--text <text>] [--attach <file>]...` answers the same way, with the same
|
|
559
562
|
checks.
|
|
560
563
|
- **The pet.** The desktop pet shows a decision as its title only. A click
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
the
|
|
564
|
+
brings up the live helm's session through the pet's focus ladder: the
|
|
565
|
+
Claude desktop app for a desktop helm, the exact terminal tab for a terminal
|
|
566
|
+
helm. With no live helm, it opens the glass at the card. Either way it acks
|
|
567
|
+
the item for the pet (`lobstah attention ack <key> --by pet`). The card
|
|
568
|
+
stays in the glass until the decision is answered or withdrawn.
|
|
564
569
|
- **Cull.** `lobstah cull` removes a decision once its answer is older than
|
|
565
570
|
the retention window, with the `decision-answer` request and its files. A
|
|
566
571
|
standing decision is never culled.
|
|
@@ -806,7 +811,15 @@ lobstah soak --name amber-gull # choose or change this trap's two-word name
|
|
|
806
811
|
a VS Code extension URL of the form
|
|
807
812
|
`vscode://anthropic.claude-code/open?session=<id>`, or a Codex task URL of
|
|
808
813
|
the form `codex://threads/<task-id>`. It rejects all other schemes and
|
|
809
|
-
malformed links.
|
|
814
|
+
malformed links. A link must also fit the session's surface, taken from
|
|
815
|
+
Claude Code's `CLAUDE_CODE_ENTRYPOINT` and the recorded window: a `vscode://`
|
|
816
|
+
link only in the VS Code extension (`claude-vscode`), a `claude://` link only
|
|
817
|
+
in the Claude desktop app's Code tab, a `codex://` link only in the Codex app.
|
|
818
|
+
A CLI session in a terminal, including the Claude desktop app's terminal
|
|
819
|
+
panel, has no link: soak ignores one and prints `link: ignored — <why>`. The
|
|
820
|
+
glass and `lobstah focus` skip a stored link that contradicts the trap's
|
|
821
|
+
window and focus the window instead. The glass checks a stored link again
|
|
822
|
+
before rendering it.
|
|
810
823
|
`lobstah focus <trap>` accepts the trap id with or without `wt:` and reports
|
|
811
824
|
the focus step or why it could not focus. It does not revive a signed-off trap.
|
|
812
825
|
|