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 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.dispatch && opts.replace !== false ? standingDecisions().filter((d) => d.key !== key && d.dispatch === opts.dispatch && d.lane === opts.lane).map((d) => d.key) : [];
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 gitShared(repo.path, "fetch", "origin", fromRemoteBranch);
5646
- await gitShared(repo.path, "worktree", "add", "--no-track", dir, "-b", `lobstah/${id}`, `origin/${fromRemoteBranch}`);
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 gitShared(dir, "fetch", "origin", repo.trunk);
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 += t - last;
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 execFileSync8 } from "node:child_process";
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 execFileSync8("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
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 execFileSync9 } from "node:child_process";
7601
+ import { execFileSync as execFileSync10 } from "node:child_process";
7405
7602
  function listProcesses() {
7406
7603
  if (process.platform === "win32") {
7407
- const out2 = execFileSync9("powershell.exe", [
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 = execFileSync9("ps", ["-A", "-o", "pid=,ppid=,pgid="], {
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
- execFileSync9("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true, timeout: 3e4 });
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 execFileSync3 } from "node:child_process";
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 = execFileSync3("powershell.exe", ["-NoProfile", "-Command", `(Get-Process -Id ${pid}).StartTime.Ticks`], { encoding: "utf8" }).trim();
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 execFileSync3("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8" }).trim() || void 0;
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 pidAlive(pid, recordedStart) {
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
- execFileSync3("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
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 ? pidAlive(st.runner.pid, st.runner.processStartTime) : void 0;
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) && !pidAlive(st.runner.pid, st.runner.processStartTime));
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 (pidAlive(prior.pid, prior.processStartTime)) {
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 execFileSync4 } from "node:child_process";
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 execFileSync4("git", args, { cwd: worktree, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }).trim();
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 execFileSync5 } from "node:child_process";
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 = execFileSync5("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }).trim();
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
- if (!hasOpenCatch(reg)) continue;
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: reg.claimed,
10227
- lane: laneOf(reg.claimed) ?? "work",
10428
+ ...catchId ? { dispatchId: catchId } : {},
10429
+ lane,
10228
10430
  worktree: reg.worktree,
10229
- repo: storedDescriptor(reg.claimed, laneOf(reg.claimed) ?? "work")?.repo ?? reg.repo
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
- return { label: worker.label, dispatchId: worker.dispatchId, ...forgeRepo ? { forgeRepo } : {}, branches, prs };
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
- return { heldBy: worker.label, reason: `${worker.label} (dispatch ${worker.dispatchId.slice(0, 8)}) ${how}${where}` };
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 held = repairHold(pr, records ??= readPrs(), workers ??= (opts.workerHolds ?? readWorkerHolds)());
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 claim = readSessionClaim(chain.latest, "work");
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 pidAlive2(state.pid) ? state : void 0;
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 pidAlive2(pid) {
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 (pidAlive2(pid)) {
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 execFileSync6 } from "node:child_process";
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 = execFileSync6("du", ["-sk", target], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
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
- execFileSync6("git", ["worktree", "remove", "--force", dir], { cwd: repo.path, stdio: "ignore" });
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 = execFileSync6("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
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
- execFileSync6("git", ["--git-dir", common, "worktree", "remove", "--force", dir], { stdio: "ignore" });
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 execFileSync7 } from "node:child_process";
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 execFileSync7("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
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 pidAlive3(pid) {
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 || pidAlive3(hb.pid));
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. A newer ask on the same dispatch replaces it. The answer
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>). The answer wakes man
15322
- wait as a decision-answer event.
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 pidAlive(runner.pid, runner.processStartTime);
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 (pidAlive(runner.pid, runner.processStartTime)) killGroup(runner.pid, "SIGKILL");
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) => spawnSync("git", a, { cwd: worktree, encoding: "utf8" }).stdout?.trim() ?? "";
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 gitShared(repo.path, "fetch", "origin", fromRemoteBranch);
2413
- await gitShared(repo.path, "worktree", "add", "--no-track", dir, "-b", `lobstah/${id}`, `origin/${fromRemoteBranch}`);
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 gitShared(dir, "fetch", "origin", repo.trunk);
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
- if (!paused) elapsed += t - last;
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 execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
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 execFileSync2 } from "node:child_process";
3636
+ import { execFileSync as execFileSync3 } from "node:child_process";
3416
3637
  function listProcesses() {
3417
3638
  if (process.platform === "win32") {
3418
- const out2 = execFileSync2(
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 = execFileSync2("ps", ["-A", "-o", "pid=,ppid=,pgid="], {
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
- execFileSync2("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true, timeout: 3e4 });
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 replaces the older decision.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lobstah",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Harness-agnostic, token-efficient supervision framework for coding agents",
5
5
  "license": "MIT",
6
6
  "author": "aequitas labs LLC",