lobstah 0.6.2 → 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 +321 -64
- package/dist/runner.js +236 -15
- package/docs/man.md +3 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -649,6 +649,9 @@ function checkOptions(raw) {
|
|
|
649
649
|
return options;
|
|
650
650
|
}
|
|
651
651
|
function askDecision(opts) {
|
|
652
|
+
if (opts.replace !== void 0 && !standingDecisions().some((d) => d.key === opts.replace)) {
|
|
653
|
+
throw new DecisionError(`no standing decision ${opts.replace} to replace \u2014 \`lobstah man tend\` lists them`);
|
|
654
|
+
}
|
|
652
655
|
const title = checkTitle(opts.title);
|
|
653
656
|
const options = checkOptions(opts.options ?? []);
|
|
654
657
|
let detail = opts.detailText ?? "";
|
|
@@ -709,7 +712,7 @@ ${title}`).digest("hex").slice(0, 16)
|
|
|
709
712
|
writeAtomic(path6.join(dir, DETAIL_MD), detail);
|
|
710
713
|
writeAtomic(path6.join(dir, DECISION_JSON), `${JSON.stringify(meta, null, 2)}
|
|
711
714
|
`);
|
|
712
|
-
const replaced = opts.
|
|
715
|
+
const replaced = opts.replace !== void 0 && opts.replace !== key ? [opts.replace] : [];
|
|
713
716
|
for (const old of replaced)
|
|
714
717
|
fs6.rmSync(decisionDir(old), { recursive: true, force: true });
|
|
715
718
|
return { meta, replaced };
|
|
@@ -4205,6 +4208,35 @@ function bounceTrapMessages(trapId) {
|
|
|
4205
4208
|
}
|
|
4206
4209
|
return msgs.length;
|
|
4207
4210
|
}
|
|
4211
|
+
function messageTexts(id, lane) {
|
|
4212
|
+
const dir = inboxDir(id, lane);
|
|
4213
|
+
const texts = [];
|
|
4214
|
+
for (const d of [dir, path19.join(dir, "handled")]) {
|
|
4215
|
+
let files;
|
|
4216
|
+
try {
|
|
4217
|
+
files = fs21.readdirSync(d).filter((f) => f.endsWith(".msg"));
|
|
4218
|
+
} catch {
|
|
4219
|
+
continue;
|
|
4220
|
+
}
|
|
4221
|
+
for (const f of files) {
|
|
4222
|
+
try {
|
|
4223
|
+
texts.push(fs21.readFileSync(path19.join(d, f), "utf8"));
|
|
4224
|
+
} catch {
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
return texts;
|
|
4229
|
+
}
|
|
4230
|
+
function trapMessageTexts(trapId, sinceMs = 0) {
|
|
4231
|
+
return messageTexts(trapKey(trapId), "work").flatMap((raw) => {
|
|
4232
|
+
try {
|
|
4233
|
+
const m = JSON.parse(raw);
|
|
4234
|
+
return (Date.parse(m.at ?? "") || 0) >= sinceMs ? [m.text ?? ""] : [];
|
|
4235
|
+
} catch {
|
|
4236
|
+
return sinceMs > 0 ? [] : [raw];
|
|
4237
|
+
}
|
|
4238
|
+
});
|
|
4239
|
+
}
|
|
4208
4240
|
var metaName, trapKey;
|
|
4209
4241
|
var init_inbox = __esm({
|
|
4210
4242
|
"packages/core/dist/inbox.js"() {
|
|
@@ -5602,7 +5634,7 @@ var init_dist2 = __esm({
|
|
|
5602
5634
|
import * as fs30 from "node:fs";
|
|
5603
5635
|
import * as path26 from "node:path";
|
|
5604
5636
|
import { createHash as createHash6 } from "node:crypto";
|
|
5605
|
-
import { exec, execFile } from "node:child_process";
|
|
5637
|
+
import { exec, execFile, execFileSync as execFileSync3 } from "node:child_process";
|
|
5606
5638
|
import { promisify } from "node:util";
|
|
5607
5639
|
async function git2(cwd, ...args) {
|
|
5608
5640
|
const { stdout } = await run("git", args, { cwd, env: process.env });
|
|
@@ -5632,6 +5664,140 @@ ${gitStderr(err)}`, { cause: err });
|
|
|
5632
5664
|
}
|
|
5633
5665
|
}
|
|
5634
5666
|
}
|
|
5667
|
+
function pidAlive(pid) {
|
|
5668
|
+
try {
|
|
5669
|
+
process.kill(pid, 0);
|
|
5670
|
+
return true;
|
|
5671
|
+
} catch (err) {
|
|
5672
|
+
return err.code === "EPERM";
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
function takeRepoLock(file, mine) {
|
|
5676
|
+
try {
|
|
5677
|
+
fs30.writeFileSync(file, mine, { flag: "wx" });
|
|
5678
|
+
return { state: "held" };
|
|
5679
|
+
} catch (err) {
|
|
5680
|
+
const code = err.code ?? "";
|
|
5681
|
+
if (code !== "EEXIST" && !BUSY.has(code))
|
|
5682
|
+
throw err;
|
|
5683
|
+
}
|
|
5684
|
+
let raw = "";
|
|
5685
|
+
let age2 = 0;
|
|
5686
|
+
try {
|
|
5687
|
+
raw = fs30.readFileSync(file, "utf8");
|
|
5688
|
+
age2 = Date.now() - fs30.statSync(file).mtimeMs;
|
|
5689
|
+
} catch {
|
|
5690
|
+
return { state: "retry" };
|
|
5691
|
+
}
|
|
5692
|
+
let holder;
|
|
5693
|
+
try {
|
|
5694
|
+
holder = JSON.parse(raw);
|
|
5695
|
+
} catch {
|
|
5696
|
+
holder = void 0;
|
|
5697
|
+
}
|
|
5698
|
+
const stale = age2 > REPO_LOCK_WAIT_MS || (holder ? !pidAlive(holder.pid) : age2 > 5e3);
|
|
5699
|
+
if (stale) {
|
|
5700
|
+
try {
|
|
5701
|
+
if (fs30.readFileSync(file, "utf8") === raw)
|
|
5702
|
+
fs30.rmSync(file, { force: true });
|
|
5703
|
+
} catch {
|
|
5704
|
+
}
|
|
5705
|
+
return { state: "retry" };
|
|
5706
|
+
}
|
|
5707
|
+
return { state: "wait", ...holder ? { holder: holder.pid } : {} };
|
|
5708
|
+
}
|
|
5709
|
+
function releaseRepoLock(file, mine) {
|
|
5710
|
+
try {
|
|
5711
|
+
if (fs30.readFileSync(file, "utf8") === mine)
|
|
5712
|
+
fs30.rmSync(file, { force: true, recursive: true, maxRetries: 5 });
|
|
5713
|
+
} catch {
|
|
5714
|
+
}
|
|
5715
|
+
}
|
|
5716
|
+
async function withRepoLock(common, fn) {
|
|
5717
|
+
const file = path26.join(common, "lobstah-git.lock");
|
|
5718
|
+
const mine = JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5719
|
+
const deadline = Date.now() + REPO_LOCK_WAIT_MS;
|
|
5720
|
+
let held = false;
|
|
5721
|
+
while (Date.now() < deadline) {
|
|
5722
|
+
const took = takeRepoLock(file, mine);
|
|
5723
|
+
if (took.state === "held") {
|
|
5724
|
+
held = true;
|
|
5725
|
+
break;
|
|
5726
|
+
}
|
|
5727
|
+
if (took.state === "wait")
|
|
5728
|
+
await new Promise((r) => setTimeout(r, 50 + Math.random() * 50));
|
|
5729
|
+
}
|
|
5730
|
+
try {
|
|
5731
|
+
await fn();
|
|
5732
|
+
} finally {
|
|
5733
|
+
if (held)
|
|
5734
|
+
releaseRepoLock(file, mine);
|
|
5735
|
+
}
|
|
5736
|
+
}
|
|
5737
|
+
function withRepoLockSync(cwd, fn) {
|
|
5738
|
+
let common;
|
|
5739
|
+
try {
|
|
5740
|
+
common = execFileSync3("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
5741
|
+
} catch {
|
|
5742
|
+
return fn();
|
|
5743
|
+
}
|
|
5744
|
+
const file = path26.join(common, "lobstah-git.lock");
|
|
5745
|
+
const mine = JSON.stringify({ pid: process.pid, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5746
|
+
const deadline = Date.now() + REPO_LOCK_WAIT_MS;
|
|
5747
|
+
const pause = new Int32Array(new SharedArrayBuffer(4));
|
|
5748
|
+
let held = false;
|
|
5749
|
+
while (Date.now() < deadline) {
|
|
5750
|
+
const took = takeRepoLock(file, mine);
|
|
5751
|
+
if (took.state === "held") {
|
|
5752
|
+
held = true;
|
|
5753
|
+
break;
|
|
5754
|
+
}
|
|
5755
|
+
if (took.state === "wait") {
|
|
5756
|
+
if (took.holder === process.pid)
|
|
5757
|
+
break;
|
|
5758
|
+
Atomics.wait(pause, 0, 0, 50 + Math.random() * 50);
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5761
|
+
try {
|
|
5762
|
+
return fn();
|
|
5763
|
+
} finally {
|
|
5764
|
+
if (held)
|
|
5765
|
+
releaseRepoLock(file, mine);
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
5768
|
+
async function oneAtATime(cwd, fn) {
|
|
5769
|
+
const common = await git2(cwd, "rev-parse", "--path-format=absolute", "--git-common-dir");
|
|
5770
|
+
const key = realpath(common);
|
|
5771
|
+
const run4 = (repoQueues.get(key) ?? Promise.resolve()).then(() => withRepoLock(common, () => fn(common)));
|
|
5772
|
+
const tail = run4.catch(() => {
|
|
5773
|
+
});
|
|
5774
|
+
repoQueues.set(key, tail);
|
|
5775
|
+
try {
|
|
5776
|
+
await run4;
|
|
5777
|
+
} finally {
|
|
5778
|
+
if (repoQueues.get(key) === tail)
|
|
5779
|
+
repoQueues.delete(key);
|
|
5780
|
+
}
|
|
5781
|
+
}
|
|
5782
|
+
function fetchStamps(common) {
|
|
5783
|
+
try {
|
|
5784
|
+
return JSON.parse(fs30.readFileSync(path26.join(common, "lobstah-fetch.json"), "utf8"));
|
|
5785
|
+
} catch {
|
|
5786
|
+
return {};
|
|
5787
|
+
}
|
|
5788
|
+
}
|
|
5789
|
+
async function fetchShared(cwd, remote, ref) {
|
|
5790
|
+
const asked = Date.now();
|
|
5791
|
+
const key = `${remote} ${ref}`;
|
|
5792
|
+
await oneAtATime(cwd, async (common) => {
|
|
5793
|
+
if ((fetchStamps(common)[key] ?? -Infinity) >= asked)
|
|
5794
|
+
return;
|
|
5795
|
+
const started = Date.now();
|
|
5796
|
+
await gitShared(cwd, "fetch", remote, ref);
|
|
5797
|
+
const stamps = { ...fetchStamps(common), [key]: started };
|
|
5798
|
+
fs30.writeFileSync(path26.join(common, "lobstah-fetch.json"), JSON.stringify(stamps, null, 2));
|
|
5799
|
+
});
|
|
5800
|
+
}
|
|
5635
5801
|
async function allocate(repo, id, fromRemoteBranch = repo.trunk) {
|
|
5636
5802
|
const dir = worktreePath(id);
|
|
5637
5803
|
if (fs30.existsSync(dir)) {
|
|
@@ -5642,8 +5808,10 @@ async function allocate(repo, id, fromRemoteBranch = repo.trunk) {
|
|
|
5642
5808
|
throw new Error(`repo path ${repo.path} missing and no origin configured`);
|
|
5643
5809
|
await run("git", ["clone", repo.origin, repo.path], { env: process.env });
|
|
5644
5810
|
}
|
|
5645
|
-
await
|
|
5646
|
-
await
|
|
5811
|
+
await fetchShared(repo.path, "origin", fromRemoteBranch);
|
|
5812
|
+
await oneAtATime(repo.path, async () => {
|
|
5813
|
+
await gitShared(repo.path, "worktree", "add", "--no-track", dir, "-b", `lobstah/${id}`, `origin/${fromRemoteBranch}`);
|
|
5814
|
+
});
|
|
5647
5815
|
await runSetup(repo, dir);
|
|
5648
5816
|
return dir;
|
|
5649
5817
|
}
|
|
@@ -5760,7 +5928,7 @@ async function chooseWorktree(input) {
|
|
|
5760
5928
|
return { reuse: true, path: found.path, owner: found.owner, from: found.from };
|
|
5761
5929
|
}
|
|
5762
5930
|
async function prepareReuse(repo, dir) {
|
|
5763
|
-
await
|
|
5931
|
+
await fetchShared(dir, "origin", repo.trunk);
|
|
5764
5932
|
if (!repo.setup?.length)
|
|
5765
5933
|
return { setupRan: false };
|
|
5766
5934
|
if (recordedSetupHash(dir) === setupHash(repo, dir))
|
|
@@ -5876,7 +6044,7 @@ async function removeIfSafe(dir, opts = {}) {
|
|
|
5876
6044
|
}
|
|
5877
6045
|
return { removed: true, primary, deletedBranches, keptBranches };
|
|
5878
6046
|
}
|
|
5879
|
-
var run, shell, LOCK_CONTENTION, ATTEMPTS, LOCKFILES, short;
|
|
6047
|
+
var run, shell, LOCK_CONTENTION, ATTEMPTS, REPO_LOCK_WAIT_MS, repoQueues, BUSY, LOCKFILES, short;
|
|
5880
6048
|
var init_dist3 = __esm({
|
|
5881
6049
|
"packages/worktree/dist/index.js"() {
|
|
5882
6050
|
"use strict";
|
|
@@ -5901,9 +6069,17 @@ var init_dist3 = __esm({
|
|
|
5901
6069
|
// held by another process ("fetching ref <ref> failed: reference already
|
|
5902
6070
|
// exists"). The files backend maps the lock's EEXIST to this reason; older
|
|
5903
6071
|
// git printed "Unable to create '<ref>.lock': File exists" instead.
|
|
5904
|
-
/reference already exists
|
|
6072
|
+
/reference already exists/,
|
|
6073
|
+
// Windows: two fetches unpack the same loose object, and one cannot write
|
|
6074
|
+
// the file the other holds open ("unable to write file
|
|
6075
|
+
// .git/objects/<xx>/<rest>: Permission denied", then "unpack-objects
|
|
6076
|
+
// failed"). Only an object path matches, not any denied write.
|
|
6077
|
+
/unable to write file \S*objects[\\/][0-9a-f]{2}[\\/][0-9a-f]{38,}: Permission denied/
|
|
5905
6078
|
];
|
|
5906
6079
|
ATTEMPTS = 6;
|
|
6080
|
+
REPO_LOCK_WAIT_MS = 5 * 6e4;
|
|
6081
|
+
repoQueues = /* @__PURE__ */ new Map();
|
|
6082
|
+
BUSY = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
5907
6083
|
LOCKFILES = [
|
|
5908
6084
|
"pnpm-lock.yaml",
|
|
5909
6085
|
"package-lock.json",
|
|
@@ -6813,6 +6989,7 @@ var init_plan = __esm({
|
|
|
6813
6989
|
function startWallClock(opts) {
|
|
6814
6990
|
const now2 = opts.now ?? Date.now;
|
|
6815
6991
|
const tickMs = Math.max(1, Math.min(opts.tickMs ?? 5e3, opts.limitMs));
|
|
6992
|
+
const sleepGapMs = opts.sleepGapMs ?? Math.max(6e4, tickMs * 6);
|
|
6816
6993
|
let elapsed = opts.initialElapsedMs ?? 0;
|
|
6817
6994
|
const maxMs = Math.max(opts.limitMs, opts.maxMs ?? opts.limitMs);
|
|
6818
6995
|
let windowMs = Math.min(maxMs, Math.max(opts.limitMs, opts.initialWindowMs ?? opts.limitMs));
|
|
@@ -6826,8 +7003,9 @@ function startWallClock(opts) {
|
|
|
6826
7003
|
} catch {
|
|
6827
7004
|
paused = false;
|
|
6828
7005
|
}
|
|
7006
|
+
const step = Math.max(0, t - last);
|
|
6829
7007
|
if (!paused)
|
|
6830
|
-
elapsed +=
|
|
7008
|
+
elapsed += step > sleepGapMs ? Math.min(step, tickMs) : step;
|
|
6831
7009
|
last = t;
|
|
6832
7010
|
if (!done && elapsed >= windowMs && elapsed < maxMs) {
|
|
6833
7011
|
let advanced = false;
|
|
@@ -7067,8 +7245,18 @@ var init_remote = __esm({
|
|
|
7067
7245
|
// packages/runner/dist/run.js
|
|
7068
7246
|
import * as fs70 from "node:fs";
|
|
7069
7247
|
import * as path57 from "node:path";
|
|
7070
|
-
import { execFileSync as
|
|
7248
|
+
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
7071
7249
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
7250
|
+
function livePrState(url) {
|
|
7251
|
+
const ref = parsePrRef(url);
|
|
7252
|
+
if (!ref)
|
|
7253
|
+
return void 0;
|
|
7254
|
+
try {
|
|
7255
|
+
return ghPrView(ref).state;
|
|
7256
|
+
} catch {
|
|
7257
|
+
return void 0;
|
|
7258
|
+
}
|
|
7259
|
+
}
|
|
7072
7260
|
function within(p, ms, fallback) {
|
|
7073
7261
|
let timer;
|
|
7074
7262
|
const late = new Promise((resolve13) => timer = setTimeout(() => resolve13(fallback), ms));
|
|
@@ -7109,6 +7297,14 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
7109
7297
|
attempts
|
|
7110
7298
|
};
|
|
7111
7299
|
fs70.writeFileSync(path57.join(activeDir, "runner.json"), JSON.stringify(runnerInfo, null, 2));
|
|
7300
|
+
if (descriptor.systemRepair && descriptor.pr?.url) {
|
|
7301
|
+
const state = deps.prState(descriptor.pr.url) ?? readPr(parsePrRef(descriptor.pr.url)?.key ?? "")?.state;
|
|
7302
|
+
if (state === "MERGED" || state === "CLOSED") {
|
|
7303
|
+
status("failed", `cancelled: ${descriptor.pr.url} is ${state.toLowerCase()}; nothing to repair`);
|
|
7304
|
+
complete(id, lane);
|
|
7305
|
+
return;
|
|
7306
|
+
}
|
|
7307
|
+
}
|
|
7112
7308
|
const plan = planStart({
|
|
7113
7309
|
id,
|
|
7114
7310
|
lane,
|
|
@@ -7213,7 +7409,7 @@ async function main(activeDir, lane, seams = {}) {
|
|
|
7213
7409
|
})();
|
|
7214
7410
|
const head = () => {
|
|
7215
7411
|
try {
|
|
7216
|
-
return
|
|
7412
|
+
return execFileSync9("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
7217
7413
|
} catch {
|
|
7218
7414
|
return void 0;
|
|
7219
7415
|
}
|
|
@@ -7393,7 +7589,8 @@ var init_run = __esm({
|
|
|
7393
7589
|
chooseWorktree,
|
|
7394
7590
|
prepareReuse,
|
|
7395
7591
|
collectEvidence,
|
|
7396
|
-
reap: async () => 0
|
|
7592
|
+
reap: async () => 0,
|
|
7593
|
+
prState: livePrState
|
|
7397
7594
|
};
|
|
7398
7595
|
DONE_WAIT_MS = 2e3;
|
|
7399
7596
|
short3 = (s) => s.slice(0, 8);
|
|
@@ -7401,10 +7598,10 @@ var init_run = __esm({
|
|
|
7401
7598
|
});
|
|
7402
7599
|
|
|
7403
7600
|
// packages/runner/dist/reap.js
|
|
7404
|
-
import { execFileSync as
|
|
7601
|
+
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
7405
7602
|
function listProcesses() {
|
|
7406
7603
|
if (process.platform === "win32") {
|
|
7407
|
-
const out2 =
|
|
7604
|
+
const out2 = execFileSync10("powershell.exe", [
|
|
7408
7605
|
"-NoProfile",
|
|
7409
7606
|
"-NonInteractive",
|
|
7410
7607
|
"-Command",
|
|
@@ -7412,7 +7609,7 @@ function listProcesses() {
|
|
|
7412
7609
|
], { encoding: "utf8", windowsHide: true, timeout: 3e4, stdio: ["ignore", "pipe", "ignore"] });
|
|
7413
7610
|
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 }));
|
|
7414
7611
|
}
|
|
7415
|
-
const out =
|
|
7612
|
+
const out = execFileSync10("ps", ["-A", "-o", "pid=,ppid=,pgid="], {
|
|
7416
7613
|
encoding: "utf8",
|
|
7417
7614
|
timeout: 3e4,
|
|
7418
7615
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -7452,7 +7649,7 @@ async function stopPids(pids, graceMs = 2e3) {
|
|
|
7452
7649
|
if (process.platform === "win32") {
|
|
7453
7650
|
for (const pid of pids) {
|
|
7454
7651
|
try {
|
|
7455
|
-
|
|
7652
|
+
execFileSync10("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true, timeout: 3e4 });
|
|
7456
7653
|
} catch {
|
|
7457
7654
|
}
|
|
7458
7655
|
}
|
|
@@ -7556,7 +7753,7 @@ import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
|
7556
7753
|
|
|
7557
7754
|
// packages/supervisor/dist/liveness.js
|
|
7558
7755
|
init_dist2();
|
|
7559
|
-
import { execFileSync as
|
|
7756
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
7560
7757
|
function classify(input) {
|
|
7561
7758
|
const { hasRunner, alive: alive2, lastVerb, lastEventAt: lastEventAt2, startedAt, now: now2, wedgeThresholdMs } = input;
|
|
7562
7759
|
if (lastVerb && TERMINAL_VERBS.includes(lastVerb))
|
|
@@ -7577,15 +7774,15 @@ function classify(input) {
|
|
|
7577
7774
|
function processStartTime(pid) {
|
|
7578
7775
|
try {
|
|
7579
7776
|
if (process.platform === "win32") {
|
|
7580
|
-
const out =
|
|
7777
|
+
const out = execFileSync4("powershell.exe", ["-NoProfile", "-Command", `(Get-Process -Id ${pid}).StartTime.Ticks`], { encoding: "utf8" }).trim();
|
|
7581
7778
|
return out || void 0;
|
|
7582
7779
|
}
|
|
7583
|
-
return
|
|
7780
|
+
return execFileSync4("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8" }).trim() || void 0;
|
|
7584
7781
|
} catch {
|
|
7585
7782
|
return void 0;
|
|
7586
7783
|
}
|
|
7587
7784
|
}
|
|
7588
|
-
function
|
|
7785
|
+
function pidAlive2(pid, recordedStart) {
|
|
7589
7786
|
try {
|
|
7590
7787
|
process.kill(pid, 0);
|
|
7591
7788
|
} catch {
|
|
@@ -7599,7 +7796,7 @@ function pidAlive(pid, recordedStart) {
|
|
|
7599
7796
|
function killGroup(pid, signal = "SIGTERM") {
|
|
7600
7797
|
if (process.platform === "win32") {
|
|
7601
7798
|
try {
|
|
7602
|
-
|
|
7799
|
+
execFileSync4("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
7603
7800
|
} catch {
|
|
7604
7801
|
}
|
|
7605
7802
|
return;
|
|
@@ -7820,7 +8017,7 @@ function sessionOf(st) {
|
|
|
7820
8017
|
return void 0;
|
|
7821
8018
|
}
|
|
7822
8019
|
}
|
|
7823
|
-
function reconcileOne(st, cfg, log, spawnHeadless = spawnRunner) {
|
|
8020
|
+
function reconcileOne(st, cfg, log, spawnHeadless = spawnRunner, resumeGrace = false) {
|
|
7824
8021
|
const hasDescriptor = fs32.existsSync(path28.join(st.dir, "descriptor.json"));
|
|
7825
8022
|
if (!hasDescriptor) {
|
|
7826
8023
|
const age2 = Date.now() - fs32.statSync(st.dir).mtimeMs;
|
|
@@ -7830,7 +8027,7 @@ function reconcileOne(st, cfg, log, spawnHeadless = spawnRunner) {
|
|
|
7830
8027
|
}
|
|
7831
8028
|
const statusLog = readStatusLog(st.id, st.lane);
|
|
7832
8029
|
const lastVerb = statusLog.at(-1)?.verb;
|
|
7833
|
-
const alive2 = st.runner ?
|
|
8030
|
+
const alive2 = st.runner ? pidAlive2(st.runner.pid, st.runner.processStartTime) : void 0;
|
|
7834
8031
|
const sessionClaim = readSessionClaim(st.id, st.lane);
|
|
7835
8032
|
if (sessionClaim) {
|
|
7836
8033
|
if (lastVerb === "done" || lastVerb === "failed") {
|
|
@@ -7900,6 +8097,8 @@ function reconcileOne(st, cfg, log, spawnHeadless = spawnRunner) {
|
|
|
7900
8097
|
break;
|
|
7901
8098
|
}
|
|
7902
8099
|
case "wedged": {
|
|
8100
|
+
if (resumeGrace)
|
|
8101
|
+
break;
|
|
7903
8102
|
const attempts = (st.runner?.attempts ?? 0) + 1;
|
|
7904
8103
|
if (st.runner)
|
|
7905
8104
|
killGroup(st.runner.pid, "SIGKILL");
|
|
@@ -7933,7 +8132,7 @@ function wakeReason(id, lane, now2 = Date.now()) {
|
|
|
7933
8132
|
}
|
|
7934
8133
|
function wakeParked(lane, free, log, spawnHeadless = spawnRunner, now2 = Date.now()) {
|
|
7935
8134
|
let woke = 0;
|
|
7936
|
-
const parked = listActive(lane).filter((st) => st.runner && !isTrapCatch(st.id, lane) && isParked(st.id, lane) && !cancelRequested(st.id, lane) && !
|
|
8135
|
+
const parked = listActive(lane).filter((st) => st.runner && !isTrapCatch(st.id, lane) && isParked(st.id, lane) && !cancelRequested(st.id, lane) && !pidAlive2(st.runner.pid, st.runner.processStartTime));
|
|
7937
8136
|
for (const st of parked) {
|
|
7938
8137
|
if (woke >= free)
|
|
7939
8138
|
break;
|
|
@@ -8068,10 +8267,12 @@ function tick(log = () => {
|
|
|
8068
8267
|
ensureLayout();
|
|
8069
8268
|
const now2 = hooks.now?.() ?? Date.now();
|
|
8070
8269
|
const ttl = cfg.soak.ttlSecs * 1e3;
|
|
8270
|
+
const wedgeMs = cfg.limits.wedgeThresholdSecs * 1e3;
|
|
8071
8271
|
const home = lobstahHome();
|
|
8072
8272
|
const timing = tickTimes.get(home);
|
|
8073
|
-
const resumed = timing && now2 - timing.last > ttl ? now2 : timing?.resumed;
|
|
8273
|
+
const resumed = timing && now2 - timing.last > Math.min(ttl, wedgeMs) ? now2 : timing?.resumed;
|
|
8074
8274
|
tickTimes.set(home, { last: now2, resumed });
|
|
8275
|
+
const wedgeGrace = resumed !== void 0 && now2 - resumed <= wedgeMs;
|
|
8075
8276
|
writeHeartbeat(cfg);
|
|
8076
8277
|
hooks.prWatches?.(hooks.now?.() ?? Date.now(), log);
|
|
8077
8278
|
for (const action of resumed !== void 0 && now2 - resumed <= ttl ? [] : sweepGhostTraps(ttl, now2, cfg.soak.pausedTtlSecs * 1e3)) {
|
|
@@ -8086,7 +8287,7 @@ function tick(log = () => {
|
|
|
8086
8287
|
const skipFor = (lane) => lane === "work" ? workSkip : choreSkip;
|
|
8087
8288
|
for (const lane of ["chore", "work"]) {
|
|
8088
8289
|
for (const st of listActive(lane))
|
|
8089
|
-
reconcileOne(st, cfg, log, hooks.spawnRunner);
|
|
8290
|
+
reconcileOne(st, cfg, log, hooks.spawnRunner, wedgeGrace);
|
|
8090
8291
|
}
|
|
8091
8292
|
retentionPass(cfg, hooks, log);
|
|
8092
8293
|
const hasHeadlessSlot = (lane) => slotUsage(lane).headless < (lane === "work" ? cfg.limits.maxConcurrent : cfg.limits.choreConcurrent);
|
|
@@ -8115,7 +8316,7 @@ function tick(log = () => {
|
|
|
8115
8316
|
inFlight++;
|
|
8116
8317
|
const st = listActive(lane).find((s) => s.id === id);
|
|
8117
8318
|
if (st)
|
|
8118
|
-
reconcileOne(st, cfg, log, hooks.spawnRunner);
|
|
8319
|
+
reconcileOne(st, cfg, log, hooks.spawnRunner, wedgeGrace);
|
|
8119
8320
|
}
|
|
8120
8321
|
}
|
|
8121
8322
|
if (cfg.notifyCommand) {
|
|
@@ -8135,7 +8336,7 @@ function acquireDaemonLock() {
|
|
|
8135
8336
|
const lockPath = path28.join(lobstahHome(), "daemon.lock");
|
|
8136
8337
|
try {
|
|
8137
8338
|
const prior = JSON.parse(fs32.readFileSync(lockPath, "utf8"));
|
|
8138
|
-
if (
|
|
8339
|
+
if (pidAlive2(prior.pid, prior.processStartTime)) {
|
|
8139
8340
|
throw new Error(`another daemon (pid ${prior.pid}) already supervises ${lobstahHome()} \u2014 run a second instance against its own LOBSTAH_HOME instead`);
|
|
8140
8341
|
}
|
|
8141
8342
|
} catch (err) {
|
|
@@ -8862,7 +9063,7 @@ import * as path32 from "node:path";
|
|
|
8862
9063
|
init_dist2();
|
|
8863
9064
|
import * as fs36 from "node:fs";
|
|
8864
9065
|
import * as path31 from "node:path";
|
|
8865
|
-
import { execFileSync as
|
|
9066
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
8866
9067
|
function age(seconds) {
|
|
8867
9068
|
if (seconds < 60)
|
|
8868
9069
|
return `${seconds}s`;
|
|
@@ -8872,7 +9073,7 @@ function age(seconds) {
|
|
|
8872
9073
|
}
|
|
8873
9074
|
function git3(worktree, ...args) {
|
|
8874
9075
|
try {
|
|
8875
|
-
return
|
|
9076
|
+
return execFileSync5("git", args, { cwd: worktree, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }).trim();
|
|
8876
9077
|
} catch {
|
|
8877
9078
|
return void 0;
|
|
8878
9079
|
}
|
|
@@ -9973,7 +10174,7 @@ function worktreeView(id, lane) {
|
|
|
9973
10174
|
init_dist2();
|
|
9974
10175
|
import * as fs45 from "node:fs";
|
|
9975
10176
|
import * as path39 from "node:path";
|
|
9976
|
-
import { execFileSync as
|
|
10177
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
9977
10178
|
var GIT_CACHE_MS = 6e4;
|
|
9978
10179
|
var gitCache = /* @__PURE__ */ new Map();
|
|
9979
10180
|
function headStamp(cwd) {
|
|
@@ -10001,7 +10202,7 @@ function git4(cwd, ...args) {
|
|
|
10001
10202
|
if (hit && stamp !== "" && hit.stamp === stamp && now2 - hit.at < GIT_CACHE_MS) return hit.out;
|
|
10002
10203
|
let out;
|
|
10003
10204
|
try {
|
|
10004
|
-
out =
|
|
10205
|
+
out = execFileSync6("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }).trim();
|
|
10005
10206
|
} catch {
|
|
10006
10207
|
out = void 0;
|
|
10007
10208
|
}
|
|
@@ -10220,13 +10421,15 @@ function terminal2(id, lane) {
|
|
|
10220
10421
|
function liveWorkers() {
|
|
10221
10422
|
const out = [];
|
|
10222
10423
|
for (const reg of listTraps()) {
|
|
10223
|
-
|
|
10424
|
+
const catchId = hasOpenCatch(reg) ? reg.claimed : void 0;
|
|
10425
|
+
const lane = catchId ? laneOf(catchId) ?? "work" : "work";
|
|
10224
10426
|
out.push({
|
|
10225
10427
|
label: `wt:${reg.name ?? reg.trapId}`,
|
|
10226
|
-
dispatchId:
|
|
10227
|
-
lane
|
|
10428
|
+
...catchId ? { dispatchId: catchId } : {},
|
|
10429
|
+
lane,
|
|
10228
10430
|
worktree: reg.worktree,
|
|
10229
|
-
repo: storedDescriptor(
|
|
10431
|
+
repo: (catchId ? storedDescriptor(catchId, lane)?.repo : void 0) ?? reg.repo,
|
|
10432
|
+
trapId: reg.trapId
|
|
10230
10433
|
});
|
|
10231
10434
|
}
|
|
10232
10435
|
for (const lane of ["work", "chore"]) {
|
|
@@ -10254,6 +10457,7 @@ function git5(cwd, args) {
|
|
|
10254
10457
|
const res = spawnSync4("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: 1e4 });
|
|
10255
10458
|
return res.status === 0 && !res.error ? res.stdout.trim() : void 0;
|
|
10256
10459
|
}
|
|
10460
|
+
var TRAP_MESSAGE_HOLD_MS = 24 * 36e5;
|
|
10257
10461
|
var GIT_CACHE_MS2 = 6e4;
|
|
10258
10462
|
var gitCache2 = /* @__PURE__ */ new Map();
|
|
10259
10463
|
function gitFacts(worktree, branch, now2 = Date.now()) {
|
|
@@ -10271,7 +10475,7 @@ function gitFacts(worktree, branch, now2 = Date.now()) {
|
|
|
10271
10475
|
function workerHold(worker) {
|
|
10272
10476
|
const branches = /* @__PURE__ */ new Map();
|
|
10273
10477
|
const prs = /* @__PURE__ */ new Map();
|
|
10274
|
-
const evidence = readEvidence(worker.dispatchId, worker.lane);
|
|
10478
|
+
const evidence = worker.dispatchId ? readEvidence(worker.dispatchId, worker.lane) : {};
|
|
10275
10479
|
const origin = worker.repo ? loadConfig().repos[worker.repo]?.origin : void 0;
|
|
10276
10480
|
let forgeRepo = origin ? githubRepoFromOrigin(origin) : void 0;
|
|
10277
10481
|
if (worker.worktree && fs46.existsSync(worker.worktree)) {
|
|
@@ -10292,10 +10496,28 @@ function workerHold(worker) {
|
|
|
10292
10496
|
}
|
|
10293
10497
|
const named = evidence.prUrl ? parsePrRef(evidence.prUrl) : void 0;
|
|
10294
10498
|
if (named) prs.set(named.key, "works on this PR");
|
|
10295
|
-
const owned = chainPr(worker.dispatchId, worker.lane);
|
|
10499
|
+
const owned = worker.dispatchId ? chainPr(worker.dispatchId, worker.lane) : void 0;
|
|
10296
10500
|
const ownedRef = owned ? parsePrRef(owned.url) : void 0;
|
|
10297
10501
|
if (ownedRef && !prs.has(ownedRef.key)) prs.set(ownedRef.key, "its chain owns this PR");
|
|
10298
|
-
|
|
10502
|
+
const said = [
|
|
10503
|
+
...worker.dispatchId ? [storedDescriptor(worker.dispatchId, worker.lane)?.brief ?? "", ...messageTexts(worker.dispatchId, worker.lane)] : [],
|
|
10504
|
+
...worker.trapId ? trapMessageTexts(worker.trapId, Date.now() - TRAP_MESSAGE_HOLD_MS) : []
|
|
10505
|
+
].join("\n");
|
|
10506
|
+
return {
|
|
10507
|
+
label: worker.label,
|
|
10508
|
+
...worker.dispatchId ? { dispatchId: worker.dispatchId } : {},
|
|
10509
|
+
...worker.trapId ? { trapId: worker.trapId } : {},
|
|
10510
|
+
...forgeRepo ? { forgeRepo } : {},
|
|
10511
|
+
branches,
|
|
10512
|
+
prs,
|
|
10513
|
+
said
|
|
10514
|
+
};
|
|
10515
|
+
}
|
|
10516
|
+
function namesPr(text, pr, sameRepo) {
|
|
10517
|
+
if (!pr.repo || pr.number === void 0 || !text) return false;
|
|
10518
|
+
const repo = pr.repo.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10519
|
+
const n = String(pr.number);
|
|
10520
|
+
return new RegExp(`github\\.com/${repo}/pull/${n}(?![0-9])`, "i").test(text) || new RegExp(`(?<![\\w.-])${repo}#${n}(?![0-9])`, "i").test(text) || sameRepo && new RegExp(`(?<![\\w/#&])#${n}(?![0-9])`).test(text);
|
|
10299
10521
|
}
|
|
10300
10522
|
function readWorkerHolds() {
|
|
10301
10523
|
return liveWorkers().map(workerHold);
|
|
@@ -10308,10 +10530,11 @@ function repairHold(pr, records, workers) {
|
|
|
10308
10530
|
for (const target of targets) {
|
|
10309
10531
|
for (const worker of workers) {
|
|
10310
10532
|
if (worker.forgeRepo && worker.forgeRepo !== target.repo) continue;
|
|
10311
|
-
const how = worker.prs.get(target.key) ?? (target.head ? worker.branches.get(target.head) : void 0);
|
|
10533
|
+
const how = worker.prs.get(target.key) ?? (target.head ? worker.branches.get(target.head) : void 0) ?? (namesPr(worker.said, target, worker.forgeRepo === target.repo) ? "was told to work on this PR" : void 0);
|
|
10312
10534
|
if (!how) continue;
|
|
10313
10535
|
const where = target.below ? `; #${target.number} is below this PR` : "";
|
|
10314
|
-
|
|
10536
|
+
const dispatch = worker.dispatchId ? ` (dispatch ${worker.dispatchId.slice(0, 8)})` : "";
|
|
10537
|
+
return { heldBy: worker.label, reason: `${worker.label}${dispatch} ${how}${where}` };
|
|
10315
10538
|
}
|
|
10316
10539
|
}
|
|
10317
10540
|
return void 0;
|
|
@@ -10463,6 +10686,11 @@ function holdCancelledRepair(id, now2 = /* @__PURE__ */ new Date()) {
|
|
|
10463
10686
|
}
|
|
10464
10687
|
return held;
|
|
10465
10688
|
}
|
|
10689
|
+
function trapAddressOf(id) {
|
|
10690
|
+
const claim = readSessionClaim(id, "work");
|
|
10691
|
+
const deliveredTo = readEvidence(id, "work").deliveredTo;
|
|
10692
|
+
return claim?.by.startsWith("wt:") ? claim.by : deliveredTo?.startsWith("wt:") ? deliveredTo : void 0;
|
|
10693
|
+
}
|
|
10466
10694
|
function deliverPrRepairs(log, cap = 3, opts = {}) {
|
|
10467
10695
|
const config = loadConfig();
|
|
10468
10696
|
const cfg = config.watch;
|
|
@@ -10565,7 +10793,12 @@ function deliverPrRepairs(log, cap = 3, opts = {}) {
|
|
|
10565
10793
|
});
|
|
10566
10794
|
};
|
|
10567
10795
|
if (watch2.heldAt) return wait(watchHold(watch2));
|
|
10568
|
-
const
|
|
10796
|
+
const ownerTrap = trapAddressOf(chainState(owner).latest);
|
|
10797
|
+
const held = repairHold(
|
|
10798
|
+
pr,
|
|
10799
|
+
records ??= readPrs(),
|
|
10800
|
+
(workers ??= (opts.workerHolds ?? readWorkerHolds)()).filter((w2) => !(w2.dispatchId === void 0 && w2.trapId !== void 0 && `wt:${w2.trapId}` === ownerTrap))
|
|
10801
|
+
);
|
|
10569
10802
|
if (held) return wait(held);
|
|
10570
10803
|
const changed = Math.max(
|
|
10571
10804
|
Date.parse(pr.headSince ?? pr.observedAt) || now2,
|
|
@@ -10592,9 +10825,7 @@ function deliverPrRepairs(log, cap = 3, opts = {}) {
|
|
|
10592
10825
|
}
|
|
10593
10826
|
const target = storedDescriptor(chain.latest, "work");
|
|
10594
10827
|
if (!target) return;
|
|
10595
|
-
const
|
|
10596
|
-
const deliveredTo = readEvidence(chain.latest, "work").deliveredTo;
|
|
10597
|
-
const address = claim?.by.startsWith("wt:") ? claim.by : deliveredTo?.startsWith("wt:") ? deliveredTo : void 0;
|
|
10828
|
+
const address = trapAddressOf(chain.latest);
|
|
10598
10829
|
const trap = address ? readTrap(address.slice(3)) : void 0;
|
|
10599
10830
|
const liveTrap2 = trap && now2 - trapLastSeen(trap) <= config.soak.ttlSecs * 1e3 ? trap : void 0;
|
|
10600
10831
|
const waitSecs = Math.max(0, Number.isFinite(cfg.repairTrapWaitSecs) ? cfg.repairTrapWaitSecs : 600);
|
|
@@ -11673,7 +11904,7 @@ function liveState(text) {
|
|
|
11673
11904
|
return void 0;
|
|
11674
11905
|
}
|
|
11675
11906
|
if (!state || !Number.isInteger(state.pid) || state.pid <= 0 || !Number.isInteger(state.port)) return void 0;
|
|
11676
|
-
return
|
|
11907
|
+
return pidAlive3(state.pid) ? state : void 0;
|
|
11677
11908
|
}
|
|
11678
11909
|
function readGlassState() {
|
|
11679
11910
|
return liveState(readStateText());
|
|
@@ -11701,7 +11932,7 @@ function writeGlassState(state) {
|
|
|
11701
11932
|
function removeGlassState() {
|
|
11702
11933
|
fs51.rmSync(stateFile(), { force: true });
|
|
11703
11934
|
}
|
|
11704
|
-
function
|
|
11935
|
+
function pidAlive3(pid) {
|
|
11705
11936
|
try {
|
|
11706
11937
|
process.kill(pid, 0);
|
|
11707
11938
|
return true;
|
|
@@ -11802,7 +12033,7 @@ async function terminate(pid, timeoutMs) {
|
|
|
11802
12033
|
} catch {
|
|
11803
12034
|
}
|
|
11804
12035
|
const deadline = Date.now() + timeoutMs;
|
|
11805
|
-
while (
|
|
12036
|
+
while (pidAlive3(pid)) {
|
|
11806
12037
|
if (Date.now() >= deadline) return false;
|
|
11807
12038
|
await delay(50);
|
|
11808
12039
|
}
|
|
@@ -11957,7 +12188,7 @@ ${help}`;
|
|
|
11957
12188
|
init_dist2();
|
|
11958
12189
|
import * as fs52 from "node:fs";
|
|
11959
12190
|
import * as path45 from "node:path";
|
|
11960
|
-
import { execFileSync as
|
|
12191
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
11961
12192
|
var DAY = 864e5;
|
|
11962
12193
|
function walkBytes(target) {
|
|
11963
12194
|
const stat = fs52.lstatSync(target);
|
|
@@ -11976,7 +12207,7 @@ var sizing = {
|
|
|
11976
12207
|
worktree(target, platform = process.platform) {
|
|
11977
12208
|
if (platform !== "win32") {
|
|
11978
12209
|
try {
|
|
11979
|
-
const out =
|
|
12210
|
+
const out = execFileSync7("du", ["-sk", target], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
11980
12211
|
const kb = Number(out.trim().split(/\s+/)[0]);
|
|
11981
12212
|
if (Number.isFinite(kb)) return kb * 1024;
|
|
11982
12213
|
} catch {
|
|
@@ -12202,20 +12433,20 @@ function removeWorktree(id, dir) {
|
|
|
12202
12433
|
const desc = JSON.parse(fs52.readFileSync(descFile, "utf8"));
|
|
12203
12434
|
const repo = loadConfig().repos[desc.repo];
|
|
12204
12435
|
if (repo) {
|
|
12205
|
-
|
|
12436
|
+
execFileSync7("git", ["worktree", "remove", "--force", dir], { cwd: repo.path, stdio: "ignore" });
|
|
12206
12437
|
return;
|
|
12207
12438
|
}
|
|
12208
12439
|
} catch {
|
|
12209
12440
|
}
|
|
12210
12441
|
}
|
|
12211
12442
|
try {
|
|
12212
|
-
const common =
|
|
12443
|
+
const common = execFileSync7("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
12213
12444
|
cwd: dir,
|
|
12214
12445
|
encoding: "utf8",
|
|
12215
12446
|
stdio: ["ignore", "pipe", "ignore"]
|
|
12216
12447
|
}).trim();
|
|
12217
12448
|
if (common) {
|
|
12218
|
-
|
|
12449
|
+
execFileSync7("git", ["--git-dir", common, "worktree", "remove", "--force", dir], { stdio: "ignore" });
|
|
12219
12450
|
return;
|
|
12220
12451
|
}
|
|
12221
12452
|
} catch {
|
|
@@ -12266,8 +12497,9 @@ function runCull(days, apply, freeBytes = statfsFreeBytes, now2 = Date.now()) {
|
|
|
12266
12497
|
|
|
12267
12498
|
// apps/cli/src/release.ts
|
|
12268
12499
|
init_dist2();
|
|
12500
|
+
init_dist3();
|
|
12269
12501
|
import * as fs53 from "node:fs";
|
|
12270
|
-
import { execFileSync as
|
|
12502
|
+
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
12271
12503
|
var DAY2 = 864e5;
|
|
12272
12504
|
function listIds(dir) {
|
|
12273
12505
|
try {
|
|
@@ -12346,7 +12578,7 @@ function mergedPrs(all) {
|
|
|
12346
12578
|
}
|
|
12347
12579
|
function git7(cwd, ...args) {
|
|
12348
12580
|
try {
|
|
12349
|
-
return
|
|
12581
|
+
return execFileSync8("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
12350
12582
|
} catch {
|
|
12351
12583
|
return void 0;
|
|
12352
12584
|
}
|
|
@@ -12356,7 +12588,7 @@ function unsafe(dir, fetched) {
|
|
|
12356
12588
|
if (status === void 0) return "unpushed work: not a readable git checkout";
|
|
12357
12589
|
if (status !== "") return "unpushed work: uncommitted changes";
|
|
12358
12590
|
const common = git7(dir, "rev-parse", "--path-format=absolute", "--git-common-dir") ?? dir;
|
|
12359
|
-
if (!fetched.has(common)) fetched.set(common, git7(dir, "fetch", "--quiet", "origin") !== void 0);
|
|
12591
|
+
if (!fetched.has(common)) fetched.set(common, withRepoLockSync(dir, () => git7(dir, "fetch", "--quiet", "origin")) !== void 0);
|
|
12360
12592
|
if (!fetched.get(common)) return "unpushed work: fetch failed, remote state unknown";
|
|
12361
12593
|
const remote = git7(dir, "branch", "-r", "--contains", "HEAD");
|
|
12362
12594
|
if (remote === void 0 || remote === "") return "unpushed work: HEAD is not on the remote";
|
|
@@ -12585,7 +12817,6 @@ function answerKey(key, req) {
|
|
|
12585
12817
|
lane: q.lane,
|
|
12586
12818
|
...q.repo ? { repo: q.repo } : {},
|
|
12587
12819
|
askedBy: "worker",
|
|
12588
|
-
replace: false,
|
|
12589
12820
|
maxBytes
|
|
12590
12821
|
});
|
|
12591
12822
|
try {
|
|
@@ -14115,7 +14346,7 @@ async function awaitHeartbeat(sinceMs, oldPid, timeoutMs = RESTART_WAIT_MS) {
|
|
|
14115
14346
|
}
|
|
14116
14347
|
return void 0;
|
|
14117
14348
|
}
|
|
14118
|
-
function
|
|
14349
|
+
function pidAlive4(pid) {
|
|
14119
14350
|
try {
|
|
14120
14351
|
process.kill(pid, 0);
|
|
14121
14352
|
return true;
|
|
@@ -14128,7 +14359,7 @@ function daemonStatus(installed, now2 = Date.now()) {
|
|
|
14128
14359
|
const hb = readHeartbeat();
|
|
14129
14360
|
const ageSecs = hb ? Math.max(0, Math.round((now2 - (Date.parse(hb.heartbeat) || 0)) / 1e3)) : void 0;
|
|
14130
14361
|
const fresh = ageSecs !== void 0 && ageSecs * 1e3 < HEARTBEAT_STALE_MS3;
|
|
14131
|
-
const running = fresh && (hb?.pid === void 0 ||
|
|
14362
|
+
const running = fresh && (hb?.pid === void 0 || pidAlive4(hb.pid));
|
|
14132
14363
|
const parked = parkedDispatches(now2);
|
|
14133
14364
|
const cfg = loadConfig();
|
|
14134
14365
|
const work = slotUsage("work");
|
|
@@ -14362,6 +14593,7 @@ function observePr(ref, view, opts = {}) {
|
|
|
14362
14593
|
if (id && lane) mergeEvidence(id, lane, { pr });
|
|
14363
14594
|
const was = before?.state ?? legacyBefore?.state;
|
|
14364
14595
|
const terminal3 = pr.state === "MERGED" || pr.state === "CLOSED";
|
|
14596
|
+
if (terminal3) cancelQueuedRepairs(ref.key);
|
|
14365
14597
|
const endedAt = Date.parse(view.mergedAt ?? view.closedAt ?? "");
|
|
14366
14598
|
const recentEnd = Number.isFinite(endedAt) && now2.getTime() - endedAt <= FIRST_SIGHT_NOTICE_MS;
|
|
14367
14599
|
if (terminal3 && (was === "OPEN" || was === void 0 && recentEnd)) {
|
|
@@ -14378,6 +14610,15 @@ function observePr(ref, view, opts = {}) {
|
|
|
14378
14610
|
}
|
|
14379
14611
|
return after;
|
|
14380
14612
|
}
|
|
14613
|
+
function cancelQueuedRepairs(key) {
|
|
14614
|
+
const cancelled = [];
|
|
14615
|
+
for (const id of pendingIds("chore")) {
|
|
14616
|
+
const d = queuedDescriptor(id, "chore");
|
|
14617
|
+
if (!d?.systemRepair || !d.pr?.url || parsePrRef(d.pr.url)?.key !== key) continue;
|
|
14618
|
+
if (cancelQueued(id, "chore")) cancelled.push(id);
|
|
14619
|
+
}
|
|
14620
|
+
return cancelled;
|
|
14621
|
+
}
|
|
14381
14622
|
function rawConfig() {
|
|
14382
14623
|
try {
|
|
14383
14624
|
return parse(fs64.readFileSync(configPath(), "utf8"));
|
|
@@ -14849,6 +15090,7 @@ var COMMANDS = {
|
|
|
14849
15090
|
"--option": { value: "<label>", repeatable: true },
|
|
14850
15091
|
"--attach": { value: "<file>", repeatable: true },
|
|
14851
15092
|
"--withdraw": { value: "<key>" },
|
|
15093
|
+
"--replace": { value: "<key>" },
|
|
14852
15094
|
"--grounds": { value: "<name>" },
|
|
14853
15095
|
"--session": { value: "<id>" }
|
|
14854
15096
|
},
|
|
@@ -14984,7 +15226,7 @@ work in flight requires a live watcher or the hook blocks with the arm command.
|
|
|
14984
15226
|
--attach copies images the page names by bare filename; --title overrides its first # heading.
|
|
14985
15227
|
The glass shows it on the deck; \`lobstah attention ack <key>\` acks it.`,
|
|
14986
15228
|
"man:ask": `Put a decision to the human: a card in the glass (decisions/<rid>/) until
|
|
14987
|
-
answered or withdrawn.
|
|
15229
|
+
answered or withdrawn. Asks stand side by side; --replace <key> replaces one. The answer
|
|
14988
15230
|
wakes man wait as decision-answer. Reserved for the claimed helm.`,
|
|
14989
15231
|
"man:answer": `Answer a decision (or a raw question's <lane>:<id>) as the glass does.
|
|
14990
15232
|
Stores the answer; the helm's man wait receives it as decision-answer.`,
|
|
@@ -15315,11 +15557,12 @@ lobstah man (orchestrator sessions \u2014 bare \`lobstah man\` prints the manual
|
|
|
15315
15557
|
man file <file.md> [--attach <file> ...] [--title <text>]
|
|
15316
15558
|
file the helm's own report under its
|
|
15317
15559
|
grounds; the glass renders it on the deck.
|
|
15318
|
-
man ask [<dispatch-id>] --title <q> [--detail <f.md>] [--option <label> ...] [--attach <file> ...]
|
|
15560
|
+
man ask [<dispatch-id>] --title <q> [--detail <f.md>] [--option <label> ...] [--attach <file> ...] [--replace <key>]
|
|
15319
15561
|
put a decision to the human: a card in the
|
|
15320
15562
|
glass until answered or withdrawn
|
|
15321
|
-
(--withdraw <key>).
|
|
15322
|
-
|
|
15563
|
+
(--withdraw <key>). Asks stand side by side;
|
|
15564
|
+
--replace <key> replaces one. The answer
|
|
15565
|
+
wakes man wait as a decision-answer event.
|
|
15323
15566
|
man answer <key> [--option <label>] [--text <text>] [--attach <file> ...]
|
|
15324
15567
|
answer a decision from the terminal.
|
|
15325
15568
|
man helm [--session <id>] [--grounds <name>] [--take] [--harness claude|codex]
|
|
@@ -15606,7 +15849,7 @@ function liveChainMember(member) {
|
|
|
15606
15849
|
if (isParked(member.id, member.lane)) return true;
|
|
15607
15850
|
try {
|
|
15608
15851
|
const runner = JSON.parse(fs72.readFileSync(path59.join(dir, "runner.json"), "utf8"));
|
|
15609
|
-
return
|
|
15852
|
+
return pidAlive2(runner.pid, runner.processStartTime);
|
|
15610
15853
|
} catch {
|
|
15611
15854
|
return false;
|
|
15612
15855
|
}
|
|
@@ -16072,6 +16315,19 @@ ${text}`, from);
|
|
|
16072
16315
|
${usageFor("report")}`);
|
|
16073
16316
|
}
|
|
16074
16317
|
}
|
|
16318
|
+
if (verb === "done") {
|
|
16319
|
+
const unread = unhandled(id, lane);
|
|
16320
|
+
if (unread.length > 0) {
|
|
16321
|
+
for (const m of unread) {
|
|
16322
|
+
console.log(`--- message ${m.file}`);
|
|
16323
|
+
console.log(m.text);
|
|
16324
|
+
acknowledge(id, lane, m.file);
|
|
16325
|
+
}
|
|
16326
|
+
throw new Error(
|
|
16327
|
+
`not reported done: ${unread.length} unread message(s) for ${id} arrived while you worked. They are printed above and now marked read. Act on them, then report done again.`
|
|
16328
|
+
);
|
|
16329
|
+
}
|
|
16330
|
+
}
|
|
16075
16331
|
const reportFile = opt("--report");
|
|
16076
16332
|
const attachFiles = values("--attach");
|
|
16077
16333
|
if (reportFile !== void 0 && verb !== "done" && verb !== "failed")
|
|
@@ -16208,7 +16464,7 @@ ${usageFor("report")}`);
|
|
|
16208
16464
|
const runnerFile = path59.join(activeDir, "runner.json");
|
|
16209
16465
|
if (fs72.existsSync(runnerFile)) {
|
|
16210
16466
|
const runner = JSON.parse(fs72.readFileSync(runnerFile, "utf8"));
|
|
16211
|
-
if (
|
|
16467
|
+
if (pidAlive2(runner.pid, runner.processStartTime)) killGroup(runner.pid, "SIGKILL");
|
|
16212
16468
|
fs72.unlinkSync(runnerFile);
|
|
16213
16469
|
}
|
|
16214
16470
|
appendStatus(id, lane, "working", `swapped to ${descriptor.harness ?? "default"} \u2014 awaiting respawn`);
|
|
@@ -16381,6 +16637,7 @@ ${usageFor("man:ask")}`);
|
|
|
16381
16637
|
...repo ? { repo } : {},
|
|
16382
16638
|
...grounds ? { grounds } : {},
|
|
16383
16639
|
askedBy: "helm",
|
|
16640
|
+
...opt("--replace") !== void 0 ? { replace: opt("--replace") } : {},
|
|
16384
16641
|
maxBytes: loadConfig().limits.attachmentMaxBytes
|
|
16385
16642
|
});
|
|
16386
16643
|
} catch (err) {
|
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
|
}
|
package/docs/man.md
CHANGED
|
@@ -509,7 +509,9 @@ the same rule.
|
|
|
509
509
|
`attachments/`. The claimed helm alone may ask.
|
|
510
510
|
- **Standing.** A decision stands until it is answered or the helm withdraws
|
|
511
511
|
it with `lobstah man ask --withdraw <key>`, which removes its directory. A
|
|
512
|
-
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.
|
|
513
515
|
- **Attention.** A standing decision is attention of kind `decision`, in
|
|
514
516
|
the default `attentionKinds`. It makes the verdict `needs-attention`. A
|
|
515
517
|
worker's raw `needs-decision` or `blocked` stays a `question` until the
|