@mrciphersmith/keryx 0.2.121 → 0.2.123

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.
Files changed (2) hide show
  1. package/dist/cli.js +2435 -455
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4194,7 +4194,19 @@ var init_exfil = __esm(() => {
4194
4194
 
4195
4195
  // src/lib/fs.ts
4196
4196
  import { randomUUID as randomUUID2 } from "crypto";
4197
+ import {
4198
+ chmodSync as chmodSync2,
4199
+ mkdirSync as mkdirSync2,
4200
+ readFileSync as readFileSync3,
4201
+ renameSync as renameSync2,
4202
+ rmdirSync,
4203
+ rmSync,
4204
+ statSync as statSync2,
4205
+ utimesSync,
4206
+ writeFileSync as writeFileSync2
4207
+ } from "fs";
4197
4208
  import { access, mkdir, readFile, rename, rm, stat, utimes, writeFile } from "fs/promises";
4209
+ import { hostname } from "os";
4198
4210
  import path8 from "path";
4199
4211
  async function pathExists(filePath) {
4200
4212
  try {
@@ -4319,7 +4331,197 @@ function processIsAlive(pid) {
4319
4331
  function delay(ms) {
4320
4332
  return new Promise((resolve) => setTimeout(resolve, ms));
4321
4333
  }
4322
- var DEFAULT_LOCK_STALE_MS = 30000;
4334
+ function readLeaseOwnerSync(lockPath) {
4335
+ try {
4336
+ const value = JSON.parse(readFileSync3(path8.join(lockPath, LEASE_OWNER_FILE), "utf8"));
4337
+ if (value === null || typeof value !== "object")
4338
+ return;
4339
+ const record = value;
4340
+ const wellFormed = typeof record.token === "string" && record.token.length > 0 && Number.isSafeInteger(record.pid) && Number(record.pid) > 0 && typeof record.host === "string" && typeof record.heartbeatAt === "string" && !Number.isNaN(Date.parse(record.heartbeatAt));
4341
+ return wellFormed ? record : undefined;
4342
+ } catch {
4343
+ return;
4344
+ }
4345
+ }
4346
+ function inspectLeaseSync(lockPath, opts) {
4347
+ const now = (opts.now ?? Date.now)();
4348
+ let dirMtimeMs;
4349
+ try {
4350
+ dirMtimeMs = statSync2(lockPath).mtimeMs;
4351
+ } catch (error) {
4352
+ if (isNotFound(error))
4353
+ return { state: "free" };
4354
+ throw error;
4355
+ }
4356
+ const holder = readLeaseOwnerSync(lockPath);
4357
+ if (holder === undefined) {
4358
+ return { state: now - dirMtimeMs <= opts.staleMs ? "live" : "gone", holder: undefined };
4359
+ }
4360
+ if (opts.reclaimIf?.(holder) === true) {
4361
+ return { state: "gone", holder };
4362
+ }
4363
+ if (now - Date.parse(holder.heartbeatAt) <= opts.staleMs) {
4364
+ return { state: "live", holder };
4365
+ }
4366
+ const thisHost = opts.host ?? hostname();
4367
+ const isAlive = opts.isAlive ?? processIsAlive;
4368
+ return { state: holder.host === thisHost && isAlive(holder.pid) ? "stale" : "gone", holder };
4369
+ }
4370
+ function acquireLeaseSync(lockPath, owner, opts) {
4371
+ return acquireOrReclaim(lockPath, owner, opts, false);
4372
+ }
4373
+ function reclaimStaleLeaseSync(lockPath, owner, opts) {
4374
+ return acquireOrReclaim(lockPath, owner, opts, true);
4375
+ }
4376
+ function acquireOrReclaim(lockPath, owner, opts, takeStale) {
4377
+ const now = opts.now ?? Date.now;
4378
+ mkdirSync2(path8.dirname(lockPath), { recursive: true, mode: 448 });
4379
+ for (let attempt = 0;attempt < MAX_LEASE_ATTEMPTS; attempt += 1) {
4380
+ const createdIno = tryCreateLease(lockPath, owner, opts.raceHook);
4381
+ if (createdIno !== undefined) {
4382
+ return { ok: true, handle: createLeaseHandle(lockPath, owner, createdIno, now, opts.raceHook) };
4383
+ }
4384
+ const seen = inspectLeaseSync(lockPath, opts);
4385
+ if (seen.state === "free")
4386
+ continue;
4387
+ if (seen.state === "live" || seen.state === "stale" && !takeStale) {
4388
+ return { ok: false, holder: seen.holder, state: seen.state };
4389
+ }
4390
+ removeLeaseDirIfUnchanged(lockPath, seen.holder?.token);
4391
+ }
4392
+ const last = inspectLeaseSync(lockPath, opts);
4393
+ return {
4394
+ ok: false,
4395
+ holder: last.state === "free" ? undefined : last.holder,
4396
+ state: last.state === "stale" ? "stale" : "live"
4397
+ };
4398
+ }
4399
+ function tryCreateLease(lockPath, owner, raceHook) {
4400
+ try {
4401
+ mkdirSync2(lockPath, { mode: 448 });
4402
+ } catch (error) {
4403
+ if (isAlreadyExistsError(error))
4404
+ return;
4405
+ throw error;
4406
+ }
4407
+ let createdIno;
4408
+ try {
4409
+ createdIno = statSync2(lockPath).ino;
4410
+ } catch (error) {
4411
+ if (isNotFound(error))
4412
+ return;
4413
+ throw error;
4414
+ }
4415
+ raceHook?.("create");
4416
+ const ownerPath = path8.join(lockPath, LEASE_OWNER_FILE);
4417
+ try {
4418
+ chmodSync2(lockPath, 448);
4419
+ writeFileSync2(ownerPath, JSON.stringify(owner), { encoding: "utf8", flag: "wx", mode: 384 });
4420
+ } catch (error) {
4421
+ const stillOurs = inodeOf(lockPath) === createdIno;
4422
+ if (stillOurs) {
4423
+ if (!isAlreadyExistsError(error))
4424
+ rmSync(ownerPath, { force: true });
4425
+ try {
4426
+ rmdirSync(lockPath);
4427
+ } catch {}
4428
+ }
4429
+ if (!stillOurs || isAlreadyExistsError(error))
4430
+ return;
4431
+ throw error;
4432
+ }
4433
+ return createdIno;
4434
+ }
4435
+ function inodeOf(target) {
4436
+ try {
4437
+ return statSync2(target).ino;
4438
+ } catch {
4439
+ return;
4440
+ }
4441
+ }
4442
+ function moveLeaseAside(lockPath, removeIf, keepOnConflict) {
4443
+ const aside = `${lockPath}.aside.${process.pid}.${randomUUID2()}`;
4444
+ try {
4445
+ renameSync2(lockPath, aside);
4446
+ } catch {
4447
+ return;
4448
+ }
4449
+ if (!removeIf(readLeaseOwnerSync(aside))) {
4450
+ try {
4451
+ renameSync2(aside, lockPath);
4452
+ return;
4453
+ } catch {
4454
+ if (keepOnConflict)
4455
+ return;
4456
+ }
4457
+ }
4458
+ rmSync(aside, { recursive: true, force: true });
4459
+ }
4460
+ function removeLeaseDirIfUnchanged(lockPath, expectedToken) {
4461
+ moveLeaseAside(lockPath, (moved) => moved === undefined || moved.token === expectedToken, false);
4462
+ }
4463
+ function createLeaseHandle(lockPath, initial, createdIno, now, raceHook) {
4464
+ let current = { ...initial };
4465
+ let released = false;
4466
+ const ownerPath = path8.join(lockPath, LEASE_OWNER_FILE);
4467
+ const ownership = () => {
4468
+ let ino;
4469
+ try {
4470
+ ino = statSync2(lockPath).ino;
4471
+ } catch (error) {
4472
+ const code = error?.code;
4473
+ return code === "ENOENT" || code === "ENOTDIR" ? "lost" : "unknown";
4474
+ }
4475
+ if (ino !== createdIno)
4476
+ return "lost";
4477
+ const record = readLeaseOwnerSync(lockPath);
4478
+ if (record === undefined)
4479
+ return "unknown";
4480
+ return record.token === initial.token ? "held" : "lost";
4481
+ };
4482
+ const holds = () => ownership() === "held";
4483
+ return {
4484
+ get owner() {
4485
+ return current;
4486
+ },
4487
+ refresh(patch) {
4488
+ if (released || !holds())
4489
+ return false;
4490
+ raceHook?.("refresh");
4491
+ const next = { ...current, ...patch, token: initial.token, heartbeatAt: new Date(now()).toISOString() };
4492
+ const tmp = path8.join(lockPath, `.${LEASE_OWNER_FILE}.${randomUUID2()}.tmp`);
4493
+ try {
4494
+ writeFileSync2(tmp, JSON.stringify(next), { encoding: "utf8", flag: "wx", mode: 384 });
4495
+ if (!holds()) {
4496
+ rmSync(tmp, { force: true });
4497
+ return false;
4498
+ }
4499
+ renameSync2(tmp, ownerPath);
4500
+ } catch {
4501
+ rmSync(tmp, { force: true });
4502
+ return false;
4503
+ }
4504
+ current = next;
4505
+ try {
4506
+ const touched = new Date;
4507
+ utimesSync(lockPath, touched, touched);
4508
+ } catch {}
4509
+ return true;
4510
+ },
4511
+ holds,
4512
+ ownership,
4513
+ release() {
4514
+ if (released)
4515
+ return;
4516
+ released = true;
4517
+ if (!holds())
4518
+ return;
4519
+ raceHook?.("release");
4520
+ moveLeaseAside(lockPath, (moved) => moved?.token === initial.token, true);
4521
+ }
4522
+ };
4523
+ }
4524
+ var DEFAULT_LOCK_STALE_MS = 30000, LEASE_OWNER_FILE = "owner.json", MAX_LEASE_ATTEMPTS = 3;
4323
4525
  var init_fs = () => {};
4324
4526
 
4325
4527
  // src/lib/contained-path.ts
@@ -6599,7 +6801,7 @@ var init_self_protect = __esm(() => {
6599
6801
  });
6600
6802
 
6601
6803
  // src/lib/descriptor-read.ts
6602
- import { constants, fstatSync, lstatSync as lstatSync2, statSync as statSync2 } from "fs";
6804
+ import { constants, fstatSync, lstatSync as lstatSync2, statSync as statSync3 } from "fs";
6603
6805
  import path18 from "path";
6604
6806
  import { dlopen, FFIType, ptr } from "bun:ffi";
6605
6807
  function readDescriptorChain(workspaceRoot, absolutePath, options = {}) {
@@ -6637,7 +6839,7 @@ function readDescriptorChain(workspaceRoot, absolutePath, options = {}) {
6637
6839
  const name = components.at(-1);
6638
6840
  if (!name)
6639
6841
  throw new DescriptorReadError("invalid-path", "safe source path has no final component");
6640
- const expected = options.expectedTargetIdentity ?? statSync2(absolutePath);
6842
+ const expected = options.expectedTargetIdentity ?? statSync3(absolutePath);
6641
6843
  const finalFlags = constants.O_RDONLY | constants.O_NOFOLLOW | (options.requireRegularFile ? constants.O_NONBLOCK ?? 0 : 0);
6642
6844
  const file = openAt(libc, directoryFd, name, finalFlags);
6643
6845
  opened.push(file);
@@ -25250,12 +25452,12 @@ var init_guarded_owner_writer = () => {};
25250
25452
 
25251
25453
  // src/session/store.ts
25252
25454
  import {
25253
- chmodSync as chmodSync3,
25455
+ chmodSync as chmodSync4,
25254
25456
  existsSync as existsSync15,
25255
- mkdirSync as mkdirSync4,
25457
+ mkdirSync as mkdirSync5,
25256
25458
  readdirSync,
25257
- renameSync as renameSync3,
25258
- writeFileSync as writeFileSync5
25459
+ renameSync as renameSync4,
25460
+ writeFileSync as writeFileSync6
25259
25461
  } from "fs";
25260
25462
  import path98 from "path";
25261
25463
  import { randomUUID as randomUUID9 } from "crypto";
@@ -25336,7 +25538,7 @@ function ensureDir(dir, dataDir) {
25336
25538
  if (shared) {
25337
25539
  ensureKeryxConfigDir();
25338
25540
  }
25339
- mkdirSync4(dir, { recursive: true, mode: 448 });
25541
+ mkdirSync5(dir, { recursive: true, mode: 448 });
25340
25542
  if (process.platform === "win32") {
25341
25543
  return;
25342
25544
  }
@@ -25355,13 +25557,13 @@ function ensureDir(dir, dataDir) {
25355
25557
  }
25356
25558
  function tighten(target) {
25357
25559
  try {
25358
- chmodSync3(target, 448);
25560
+ chmodSync4(target, 448);
25359
25561
  } catch {}
25360
25562
  }
25361
25563
  function atomicWriteText(file, body) {
25362
25564
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
25363
- writeFileSync5(tmp, body, { encoding: "utf8", mode: 384 });
25364
- renameSync3(tmp, file);
25565
+ writeFileSync6(tmp, body, { encoding: "utf8", mode: 384 });
25566
+ renameSync4(tmp, file);
25365
25567
  }
25366
25568
  function atomicWriteJson(file, value) {
25367
25569
  atomicWriteText(file, `${JSON.stringify(value, null, 2)}
@@ -28474,23 +28676,48 @@ async function openSlate(opts) {
28474
28676
  async function closeSlate(dir, mintAttemptId) {
28475
28677
  await archiveSlate(dir, mintAttemptId());
28476
28678
  }
28679
+ function detachSlateSession(ref) {
28680
+ if (ref !== undefined) {
28681
+ ref.detached = true;
28682
+ }
28683
+ }
28684
+ function isSlateSessionDetached(ref) {
28685
+ return ref.detached === true;
28686
+ }
28687
+ function slateSessionDir(ref) {
28688
+ return isSlateSessionDetached(ref) ? undefined : ref.dir;
28689
+ }
28690
+ async function writeSlateSession(ref, update) {
28691
+ if (isSlateSessionDetached(ref)) {
28692
+ return;
28693
+ }
28694
+ return writeSlate(ref.dir, update);
28695
+ }
28696
+ async function readSlateSession(ref) {
28697
+ if (isSlateSessionDetached(ref)) {
28698
+ return;
28699
+ }
28700
+ return readSlate(ref.dir);
28701
+ }
28477
28702
  async function ensureSlateOpened(ref, mintAttemptId, runtime) {
28703
+ if (isSlateSessionDetached(ref)) {
28704
+ return;
28705
+ }
28478
28706
  if (ref.opened) {
28479
28707
  const live = await readSlate(ref.dir);
28480
28708
  if (live !== undefined) {
28481
28709
  return;
28482
28710
  }
28483
28711
  }
28484
- await openSlate({
28485
- dir: ref.dir,
28486
- cwd: ref.cwd,
28487
- mintAttemptId,
28488
- ...runtime !== undefined ? { runtime } : {}
28489
- });
28712
+ const anchors = await computeAnchors({ cwd: ref.cwd, ...runtime !== undefined ? { runtime } : {} });
28713
+ if (isSlateSessionDetached(ref)) {
28714
+ return;
28715
+ }
28716
+ await openSlateAtomic(ref.dir, mintAttemptId, () => ({ anchors, course: {}, seeds: [] }));
28490
28717
  ref.opened = true;
28491
28718
  }
28492
28719
  async function closeSlateSession(ref, mintAttemptId) {
28493
- if (ref === undefined) {
28720
+ if (ref === undefined || isSlateSessionDetached(ref)) {
28494
28721
  return;
28495
28722
  }
28496
28723
  await closeSlate(ref.dir, mintAttemptId);
@@ -28540,6 +28767,12 @@ async function recordSlateTouch(dir, touched, extra) {
28540
28767
  });
28541
28768
  return { changed, slate };
28542
28769
  }
28770
+ async function recordSlateSessionTouch(ref, touched, extra) {
28771
+ if (isSlateSessionDetached(ref)) {
28772
+ return;
28773
+ }
28774
+ return recordSlateTouch(ref.dir, touched, extra);
28775
+ }
28543
28776
  function mintTimestampAttemptId(now = new Date) {
28544
28777
  timestampMintCounter += 1;
28545
28778
  return `${now.toISOString().replace(/:/g, "-")}-${timestampMintCounter}`;
@@ -29024,7 +29257,7 @@ async function resolveTerminalStateSnapshots(options) {
29024
29257
  const ref = options.slateSession;
29025
29258
  if (ref !== undefined && ref.opened) {
29026
29259
  try {
29027
- const slate = await readSlate(ref.dir);
29260
+ const slate = await readSlateSession(ref);
29028
29261
  if (slate !== undefined) {
29029
29262
  return { courseSnapshot: slate.course, anchorsSnapshot: slate.anchors };
29030
29263
  }
@@ -29044,9 +29277,10 @@ async function emitTerminalState(io, deps, options, reason) {
29044
29277
  };
29045
29278
  io.onTerminalState?.(state);
29046
29279
  const ref = options.slateSession;
29047
- if (ref !== undefined && ref.opened) {
29280
+ const terminalDir = ref !== undefined && ref.opened ? slateSessionDir(ref) : undefined;
29281
+ if (terminalDir !== undefined) {
29048
29282
  try {
29049
- await writeTerminalState(ref.dir, state);
29283
+ await writeTerminalState(terminalDir, state);
29050
29284
  } catch {}
29051
29285
  }
29052
29286
  const block = renderTerminalStateBlock(state);
@@ -29082,7 +29316,7 @@ async function closeSlateOnFlowDone(io, deps, options) {
29082
29316
  return;
29083
29317
  }
29084
29318
  try {
29085
- const slate = await readSlate(ref.dir);
29319
+ const slate = await readSlateSession(ref);
29086
29320
  const course = await courseFromSlate(ref.cwd, slate);
29087
29321
  if (isCourseDone(course)) {
29088
29322
  if (slate !== undefined) {
@@ -29147,7 +29381,7 @@ async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
29147
29381
  if (options.slateSession !== undefined) {
29148
29382
  try {
29149
29383
  if (options.skipCloseTrigger !== true && isClosePhrase(userLine)) {
29150
- const liveSlate = await readSlate(options.slateSession.dir);
29384
+ const liveSlate = await readSlateSession(options.slateSession);
29151
29385
  if (liveSlate !== undefined) {
29152
29386
  await dispatchWrapUpBestEffort(io, options, "explicit", options.slateSession.cwd, options.slateSession.dir, liveSlate);
29153
29387
  }
@@ -29159,7 +29393,7 @@ async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
29159
29393
  model: deps.modelId
29160
29394
  });
29161
29395
  if (!wasOpened && options.slateSession.opened) {
29162
- const freshSlate = await readSlate(options.slateSession.dir);
29396
+ const freshSlate = await readSlateSession(options.slateSession);
29163
29397
  if (freshSlate !== undefined) {
29164
29398
  history.push({ role: "user", content: renderAnchorsBlock(freshSlate.anchors), provenance: "project", ts: now() });
29165
29399
  io.onHistoryChange?.("tool");
@@ -29577,10 +29811,10 @@ ${modelOutput}` : modelOutput,
29577
29811
  if (options.slateSession !== undefined && options.slateSession.opened === true) {
29578
29812
  try {
29579
29813
  const touchedPaths = extractTouchedFromToolInput(call.name, parseToolInput3(call.input));
29580
- const touch = await recordSlateTouch(options.slateSession.dir, touchedPaths, {
29814
+ const touch = await recordSlateSessionTouch(options.slateSession, touchedPaths, {
29581
29815
  runtime: { provider: deps.providerId, model: deps.modelId }
29582
29816
  });
29583
- if (touch.changed) {
29817
+ if (touch?.changed === true) {
29584
29818
  anchorsToAnnounce = touch.slate.anchors;
29585
29819
  }
29586
29820
  } catch (err) {
@@ -40654,7 +40888,7 @@ var init_deep_enrich = __esm(() => {
40654
40888
  });
40655
40889
 
40656
40890
  // src/wiki/enrich.ts
40657
- import { existsSync as existsSync23, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
40891
+ import { existsSync as existsSync23, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
40658
40892
  import { readFile as readFile77, writeFile as writeFile42 } from "fs/promises";
40659
40893
  import path137 from "path";
40660
40894
  function resolveEnrichProviderModel(input) {
@@ -40672,7 +40906,7 @@ function loadResumeState(cwd) {
40672
40906
  if (!existsSync23(file)) {
40673
40907
  return { updatedAt: new Date().toISOString(), completed: [], failed: [] };
40674
40908
  }
40675
- const raw = JSON.parse(readFileSync6(file, "utf8"));
40909
+ const raw = JSON.parse(readFileSync7(file, "utf8"));
40676
40910
  if (raw === null || typeof raw !== "object") {
40677
40911
  return { updatedAt: new Date().toISOString(), completed: [], failed: [] };
40678
40912
  }
@@ -40695,8 +40929,8 @@ function isStringRecord(value) {
40695
40929
  function saveResumeState(cwd, state) {
40696
40930
  try {
40697
40931
  const file = resumeStatePath(cwd);
40698
- mkdirSync5(path137.dirname(file), { recursive: true });
40699
- writeFileSync6(file, `${JSON.stringify({ ...state, updatedAt: new Date().toISOString() }, null, 2)}
40932
+ mkdirSync6(path137.dirname(file), { recursive: true });
40933
+ writeFileSync7(file, `${JSON.stringify({ ...state, updatedAt: new Date().toISOString() }, null, 2)}
40700
40934
  `, "utf8");
40701
40935
  } catch {}
40702
40936
  }
@@ -42980,6 +43214,103 @@ async function startStdioTransport(server) {
42980
43214
  await server.connect(transport);
42981
43215
  }
42982
43216
 
43217
+ // src/tui/debug-log.ts
43218
+ import path192 from "path";
43219
+ function isDebugEnabled() {
43220
+ return activeRun !== undefined;
43221
+ }
43222
+ function currentDebugRun() {
43223
+ return activeRun;
43224
+ }
43225
+ function debugEvent(kind, data = {}) {
43226
+ const run = activeRun;
43227
+ if (run === undefined) {
43228
+ return;
43229
+ }
43230
+ seq += 1;
43231
+ try {
43232
+ appendOwnerOnlyLine(run.shellLog, safeStringify({ ts: new Date().toISOString(), seq, pid: process.pid, kind, ...data }));
43233
+ } catch {}
43234
+ }
43235
+ function callerStack() {
43236
+ const raw = new Error("stack").stack ?? "";
43237
+ return raw.split(`
43238
+ `).slice(1).filter((line) => !line.includes("debug-log.ts")).slice(0, 14).map((line) => line.trim()).join(" | ");
43239
+ }
43240
+ function startDebugRun(opts = {}) {
43241
+ const stamp = (opts.now ?? new Date).toISOString().replace(/[:.]/g, "-");
43242
+ const name = `${stamp}-${process.pid}`;
43243
+ const dir = ensureKeryxSubdir(["debug", name], opts.configDir);
43244
+ const run = {
43245
+ dir,
43246
+ shellLog: path192.join(dir, "shell.ndjson"),
43247
+ watcherLog: path192.join(dir, "watcher.ndjson")
43248
+ };
43249
+ try {
43250
+ writeOwnerOnlyFile(path192.join(path192.dirname(dir), "latest.txt"), `${dir}
43251
+ `);
43252
+ } catch {}
43253
+ activeRun = run;
43254
+ seq = 0;
43255
+ return run;
43256
+ }
43257
+ function summarizeInputChunk(chunk) {
43258
+ const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
43259
+ const text = buf.toString("utf8");
43260
+ let printable = 0;
43261
+ let control = 0;
43262
+ const sequences = [];
43263
+ for (let i = 0;i < text.length; i++) {
43264
+ const ch = text[i] ?? "";
43265
+ const code = ch.charCodeAt(0);
43266
+ if (ch === "\x1B") {
43267
+ let j = i + 1;
43268
+ while (j < text.length && j - i < 48) {
43269
+ const c = text.charCodeAt(j);
43270
+ if (j > i + 1 && c >= 64 && c <= 126 && text[j] !== "[" && text[j] !== "<" && text[j] !== ";") {
43271
+ break;
43272
+ }
43273
+ j++;
43274
+ }
43275
+ if (sequences.length < 12) {
43276
+ sequences.push(Buffer.from(text.slice(i, j + 1)).toString("hex"));
43277
+ }
43278
+ control++;
43279
+ i = j;
43280
+ } else if (code < 32 || code === 127) {
43281
+ control++;
43282
+ if (sequences.length < 12) {
43283
+ sequences.push(Buffer.from(ch).toString("hex"));
43284
+ }
43285
+ } else {
43286
+ printable++;
43287
+ }
43288
+ }
43289
+ return { bytes: buf.length, printable, control, sequences };
43290
+ }
43291
+ function safeStringify(value) {
43292
+ const seen = new WeakSet;
43293
+ return JSON.stringify(value, (_key, v) => {
43294
+ if (typeof v === "bigint") {
43295
+ return v.toString();
43296
+ }
43297
+ if (v instanceof Error) {
43298
+ return { name: v.name, message: v.message, stack: v.stack };
43299
+ }
43300
+ if (v !== null && typeof v === "object") {
43301
+ if (seen.has(v)) {
43302
+ return "[circular]";
43303
+ }
43304
+ seen.add(v);
43305
+ }
43306
+ return v;
43307
+ });
43308
+ }
43309
+ var activeRun, seq = 0;
43310
+ var init_debug_log = __esm(() => {
43311
+ init_config_dir();
43312
+ });
43313
+
42983
43314
  // src/lib/oauth/device-code.ts
42984
43315
  function requestInit(base, signal) {
42985
43316
  return signal === undefined ? base : { ...base, signal };
@@ -43472,6 +43803,327 @@ var init_login = __esm(() => {
43472
43803
  SESSION_REFRESHED_GRANTS = ["grok", "github-copilot"];
43473
43804
  });
43474
43805
 
43806
+ // src/tui/debug-watcher.ts
43807
+ import { spawn as spawn6, spawnSync as spawnSync4 } from "child_process";
43808
+ import { closeSync as closeSync3, constants as constants2, openSync as openSync4, readdirSync as readdirSync3, readFileSync as readFileSync11, readlinkSync as readlinkSync3, statSync as statSync7 } from "fs";
43809
+ import path196 from "path";
43810
+ function parseEpollFdinfo(text) {
43811
+ const out = [];
43812
+ for (const line of text.split(`
43813
+ `)) {
43814
+ const match = /^tfd:\s*(\d+)\s+events:\s*([0-9a-fA-F]+)/.exec(line.trim());
43815
+ if (match?.[1] !== undefined && match[2] !== undefined) {
43816
+ out.push({ tfd: Number(match[1]), events: Number.parseInt(match[2], 16) });
43817
+ }
43818
+ }
43819
+ return out;
43820
+ }
43821
+ function ttyReaderArmed(ttyFds, epoll) {
43822
+ if (ttyFds.length === 0) {
43823
+ return;
43824
+ }
43825
+ return epoll.some((entry) => ttyFds.includes(entry.tfd) && (entry.events & EPOLLIN) !== 0);
43826
+ }
43827
+ function detectStall(history, window = 3) {
43828
+ if (history.length < window) {
43829
+ return;
43830
+ }
43831
+ const recent = history.slice(-window);
43832
+ if (recent.every((s) => s.alive && s.readerArmed === false)) {
43833
+ return "reader-not-polled";
43834
+ }
43835
+ if (recent.every((s) => s.alive && (s.pending ?? 0) > 0) && recent.every((s, i) => i === 0 || (s.pending ?? 0) >= (recent[i - 1]?.pending ?? 0))) {
43836
+ return "input-not-drained";
43837
+ }
43838
+ if (recent.every((s) => s.alive && (s.heartbeatAgeMs ?? 0) > 5000)) {
43839
+ return "event-loop-silent";
43840
+ }
43841
+ return;
43842
+ }
43843
+ function isAlive(pid) {
43844
+ try {
43845
+ process.kill(pid, 0);
43846
+ return true;
43847
+ } catch {
43848
+ return false;
43849
+ }
43850
+ }
43851
+ function readText(file) {
43852
+ try {
43853
+ return readFileSync11(file, "utf8");
43854
+ } catch {
43855
+ return;
43856
+ }
43857
+ }
43858
+ function listFds(pid) {
43859
+ const out = [];
43860
+ let entries;
43861
+ try {
43862
+ entries = readdirSync3(`/proc/${pid}/fd`);
43863
+ } catch {
43864
+ return out;
43865
+ }
43866
+ for (const name of entries) {
43867
+ const fd = Number(name);
43868
+ let link;
43869
+ try {
43870
+ link = readlinkSync3(`/proc/${pid}/fd/${name}`);
43871
+ } catch {
43872
+ continue;
43873
+ }
43874
+ const flags = /flags:\s*(\S+)/.exec(readText(`/proc/${pid}/fdinfo/${name}`) ?? "")?.[1];
43875
+ out.push({ fd, link, flags });
43876
+ }
43877
+ return out;
43878
+ }
43879
+ function epollEntries(pid, fds) {
43880
+ return fds.filter((f) => f.link === "anon_inode:[eventpoll]").map((f) => ({ epfd: f.fd, entries: parseEpollFdinfo(readText(`/proc/${pid}/fdinfo/${f.fd}`) ?? "") }));
43881
+ }
43882
+ function threads(pid) {
43883
+ let tids;
43884
+ try {
43885
+ tids = readdirSync3(`/proc/${pid}/task`);
43886
+ } catch {
43887
+ return [];
43888
+ }
43889
+ return tids.map((tid) => {
43890
+ const base = `/proc/${pid}/task/${tid}`;
43891
+ const stat = readText(`${base}/stat`) ?? "";
43892
+ const state = /\)\s+(\S)/.exec(stat)?.[1] ?? "?";
43893
+ return { tid, comm: (readText(`${base}/comm`) ?? "").trim(), wchan: (readText(`${base}/wchan`) ?? "").trim(), state };
43894
+ });
43895
+ }
43896
+ function ttyHolders(ttyPath) {
43897
+ const out = [];
43898
+ let pids;
43899
+ try {
43900
+ pids = readdirSync3("/proc").filter((name) => /^\d+$/.test(name));
43901
+ } catch {
43902
+ return out;
43903
+ }
43904
+ for (const pid of pids) {
43905
+ let fdNames;
43906
+ try {
43907
+ fdNames = readdirSync3(`/proc/${pid}/fd`);
43908
+ } catch {
43909
+ continue;
43910
+ }
43911
+ const fds = [];
43912
+ for (const fd of fdNames) {
43913
+ try {
43914
+ if (readlinkSync3(`/proc/${pid}/fd/${fd}`) === ttyPath)
43915
+ fds.push(Number(fd));
43916
+ } catch {}
43917
+ }
43918
+ if (fds.length === 0)
43919
+ continue;
43920
+ const cmd = (readText(`/proc/${pid}/cmdline`) ?? "").split("\x00").join(" ").trim().slice(0, 300);
43921
+ const ppid = /\)\s+\S\s+(\d+)/.exec(readText(`/proc/${pid}/stat`) ?? "")?.[1];
43922
+ out.push({ pid: Number(pid), ...ppid !== undefined ? { ppid } : {}, cmd, fds });
43923
+ }
43924
+ return out;
43925
+ }
43926
+ function decodeTermios(buf) {
43927
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
43928
+ const iflag = view.getUint32(0, true);
43929
+ const lflag = view.getUint32(12, true);
43930
+ return {
43931
+ iflag,
43932
+ lflag,
43933
+ vmin: buf[CC_OFFSET + VMIN] ?? 0,
43934
+ vtime: buf[CC_OFFSET + VTIME] ?? 0,
43935
+ icanon: (lflag & ICANON) !== 0,
43936
+ echo: (lflag & ECHO) !== 0
43937
+ };
43938
+ }
43939
+ async function makeTtyProbes() {
43940
+ const none = { pending: () => {
43941
+ return;
43942
+ }, termios: () => {
43943
+ return;
43944
+ } };
43945
+ if (process.platform !== "linux") {
43946
+ return none;
43947
+ }
43948
+ try {
43949
+ const ffi = await import("bun:ffi");
43950
+ const libc = ffi.dlopen("libc.so.6", {
43951
+ ioctl: { args: [ffi.FFIType.i32, ffi.FFIType.u64, ffi.FFIType.ptr], returns: ffi.FFIType.i32 },
43952
+ tcgetattr: { args: [ffi.FFIType.i32, ffi.FFIType.ptr], returns: ffi.FFIType.i32 }
43953
+ });
43954
+ return {
43955
+ pending: (fd) => {
43956
+ const out = new Int32Array(1);
43957
+ const rc = libc.symbols.ioctl(fd, FIONREAD, ffi.ptr(out));
43958
+ return rc === 0 ? out[0] ?? 0 : undefined;
43959
+ },
43960
+ termios: (fd) => {
43961
+ const buf = new Uint8Array(TERMIOS_SIZE);
43962
+ return libc.symbols.tcgetattr(fd, ffi.ptr(buf)) === 0 ? decodeTermios(buf) : undefined;
43963
+ }
43964
+ };
43965
+ } catch {
43966
+ return none;
43967
+ }
43968
+ }
43969
+ function spawnDebugWatcher(run) {
43970
+ const entry = process.argv[1];
43971
+ const args = [
43972
+ ...entry !== undefined ? [entry] : [],
43973
+ "shell",
43974
+ "--debug-watcher",
43975
+ "--pid",
43976
+ String(process.pid),
43977
+ "--dir",
43978
+ run.dir
43979
+ ];
43980
+ try {
43981
+ const child = spawn6(process.execPath, args, { detached: true, stdio: "ignore", env: process.env });
43982
+ child.unref();
43983
+ return child.pid;
43984
+ } catch {
43985
+ return;
43986
+ }
43987
+ }
43988
+ async function runDebugWatcher(argv) {
43989
+ const pidArg = argv[argv.indexOf("--pid") + 1];
43990
+ const dirArg = argv[argv.indexOf("--dir") + 1];
43991
+ const pid = Number(pidArg);
43992
+ if (!Number.isInteger(pid) || pid <= 0 || dirArg === undefined || !argv.includes("--dir")) {
43993
+ process.stderr.write(`usage: keryx shell --debug-watcher --pid <pid> --dir <debug run dir>
43994
+ `);
43995
+ process.exitCode = 2;
43996
+ return;
43997
+ }
43998
+ const logFile = path196.join(dirArg, "watcher.ndjson");
43999
+ const shellLog = path196.join(dirArg, "shell.ndjson");
44000
+ const log = (kind, data = {}) => {
44001
+ try {
44002
+ appendOwnerOnlyLine(logFile, JSON.stringify({ ts: new Date().toISOString(), watcher: process.pid, target: pid, kind, ...data }));
44003
+ } catch {}
44004
+ };
44005
+ const linux = process.platform === "linux" && isAlive(pid) && readText(`/proc/${pid}/stat`) !== undefined;
44006
+ const probes = await makeTtyProbes();
44007
+ const pendingOf = probes.pending;
44008
+ let ttyPath;
44009
+ let ttyFd;
44010
+ if (linux) {
44011
+ try {
44012
+ ttyPath = readlinkSync3(`/proc/${pid}/fd/0`);
44013
+ if (isTtyLink(ttyPath)) {
44014
+ ttyFd = openSync4(ttyPath, constants2.O_RDONLY | constants2.O_NONBLOCK | constants2.O_NOCTTY);
44015
+ }
44016
+ } catch (error) {
44017
+ log("tty.open-failed", { error: String(error) });
44018
+ }
44019
+ }
44020
+ log("watcher.start", { linux, ttyPath, fionread: ttyFd !== undefined && pendingOf(ttyFd) !== undefined });
44021
+ const history = [];
44022
+ let lastLogged = "";
44023
+ let lastTermios = "";
44024
+ let lastPeriodic = 0;
44025
+ let lastSignal = 0;
44026
+ const fullSnapshot = () => {
44027
+ const fds = listFds(pid);
44028
+ let stty;
44029
+ if (ttyPath !== undefined) {
44030
+ const res = spawnSync4("stty", ["-a", "-F", ttyPath], { encoding: "utf8", timeout: 2000 });
44031
+ stty = (res.stdout ?? "").trim() || (res.stderr ?? "").trim();
44032
+ }
44033
+ return {
44034
+ stat: (readText(`/proc/${pid}/stat`) ?? "").trim(),
44035
+ status: (readText(`/proc/${pid}/status`) ?? "").split(`
44036
+ `).filter((l) => /^(State|Threads|SigBlk|SigIgn|SigCgt|VmRSS):/.test(l)),
44037
+ fds,
44038
+ epoll: epollEntries(pid, fds),
44039
+ threads: threads(pid),
44040
+ stty
44041
+ };
44042
+ };
44043
+ const tick = () => {
44044
+ const alive = isAlive(pid);
44045
+ const sample = { alive };
44046
+ if (alive && linux) {
44047
+ const fds = listFds(pid);
44048
+ const ttyFds = fds.filter((f) => isTtyLink(f.link)).map((f) => f.fd);
44049
+ const entries = epollEntries(pid, fds).flatMap((e) => e.entries);
44050
+ sample.readerArmed = ttyReaderArmed(ttyFds, entries);
44051
+ }
44052
+ if (alive && ttyFd !== undefined) {
44053
+ sample.pending = pendingOf(ttyFd);
44054
+ }
44055
+ try {
44056
+ sample.heartbeatAgeMs = Date.now() - statSync7(shellLog).mtimeMs;
44057
+ } catch {
44058
+ sample.heartbeatAgeMs = undefined;
44059
+ }
44060
+ history.push(sample);
44061
+ if (history.length > 30)
44062
+ history.shift();
44063
+ if (!alive) {
44064
+ log("target.exit");
44065
+ return false;
44066
+ }
44067
+ const key = JSON.stringify({ a: sample.readerArmed, p: (sample.pending ?? 0) > 0 });
44068
+ const now = Date.now();
44069
+ if (key !== lastLogged || now - lastPeriodic > 1e4) {
44070
+ log("sample", { ...sample });
44071
+ lastLogged = key;
44072
+ lastPeriodic = now;
44073
+ }
44074
+ const stall = detectStall(history);
44075
+ if (stall !== undefined && now - lastSignal > 15000) {
44076
+ lastSignal = now;
44077
+ log("stall.detected", { reason: stall, recent: history.slice(-5), snapshot: linux ? fullSnapshot() : {} });
44078
+ if (stall !== "event-loop-silent" && process.platform !== "win32") {
44079
+ try {
44080
+ process.kill(pid, "SIGUSR2");
44081
+ log("recover.signal-sent", { signal: "SIGUSR2" });
44082
+ } catch (error) {
44083
+ log("recover.signal-failed", { error: String(error) });
44084
+ }
44085
+ setTimeout(() => {
44086
+ log("recover.after", { snapshot: linux ? fullSnapshot() : {}, pending: ttyFd !== undefined ? pendingOf(ttyFd) : undefined });
44087
+ }, 2000);
44088
+ }
44089
+ }
44090
+ return true;
44091
+ };
44092
+ const termiosTimer = ttyFd === undefined ? undefined : setInterval(() => {
44093
+ const termios = probes.termios(ttyFd);
44094
+ const key = JSON.stringify(termios);
44095
+ if (termios !== undefined && key !== lastTermios) {
44096
+ const suspicious = termios.vmin === 0 || termios.icanon;
44097
+ log("termios", {
44098
+ ...termios,
44099
+ suspicious,
44100
+ ...suspicious && lastTermios !== "" && ttyPath !== undefined ? { ttyHolders: ttyHolders(ttyPath) } : {}
44101
+ });
44102
+ lastTermios = key;
44103
+ }
44104
+ }, 50);
44105
+ await new Promise((resolve) => {
44106
+ const timer = setInterval(() => {
44107
+ if (!tick()) {
44108
+ clearInterval(timer);
44109
+ if (termiosTimer !== undefined)
44110
+ clearInterval(termiosTimer);
44111
+ resolve();
44112
+ }
44113
+ }, 1000);
44114
+ });
44115
+ if (ttyFd !== undefined) {
44116
+ try {
44117
+ closeSync3(ttyFd);
44118
+ } catch {}
44119
+ }
44120
+ log("watcher.stop");
44121
+ }
44122
+ var EPOLLIN = 1, FIONREAD = 21531, isTtyLink = (link) => link.startsWith("/dev/pts/") || /^\/dev\/tty/.test(link), TERMIOS_SIZE = 60, CC_OFFSET = 17, VTIME = 5, VMIN = 6, ICANON = 2, ECHO = 8;
44123
+ var init_debug_watcher = __esm(() => {
44124
+ init_config_dir();
44125
+ });
44126
+
43475
44127
  // src/cli.ts
43476
44128
  init_single_turn();
43477
44129
  init_model_turn_port();
@@ -43568,23 +44220,23 @@ import path24 from "path";
43568
44220
  // src/lib/project-registry.ts
43569
44221
  init_config_dir();
43570
44222
  import {
43571
- chmodSync as chmodSync2,
44223
+ chmodSync as chmodSync3,
43572
44224
  closeSync as closeSync2,
43573
44225
  copyFileSync,
43574
44226
  existsSync as existsSync4,
43575
44227
  fsyncSync,
43576
44228
  openSync as openSync2,
43577
44229
  realpathSync as realpathSync2,
43578
- renameSync as renameSync2,
43579
- statSync as statSync4,
44230
+ renameSync as renameSync3,
44231
+ statSync as statSync5,
43580
44232
  unlinkSync as unlinkSync2,
43581
- writeFileSync as writeFileSync3
44233
+ writeFileSync as writeFileSync4
43582
44234
  } from "fs";
43583
44235
  import path23 from "path";
43584
44236
  import { randomUUID as randomUUID4 } from "crypto";
43585
44237
 
43586
44238
  // src/lib/file-lock.ts
43587
- import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, rmSync, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
44239
+ import { closeSync, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync4, writeFileSync as writeFileSync3 } from "fs";
43588
44240
  import { randomUUID as randomUUID3 } from "crypto";
43589
44241
  import path22 from "path";
43590
44242
  var LOCK_STALE_MS = 1e4;
@@ -43600,7 +44252,7 @@ function sleepSync(ms) {
43600
44252
  }
43601
44253
  function withFileLock2(lockPath, fn, options = {}) {
43602
44254
  try {
43603
- mkdirSync2(path22.dirname(lockPath), { recursive: true, mode: 448 });
44255
+ mkdirSync3(path22.dirname(lockPath), { recursive: true, mode: 448 });
43604
44256
  } catch {
43605
44257
  return null;
43606
44258
  }
@@ -43616,18 +44268,18 @@ function withFileLock2(lockPath, fn, options = {}) {
43616
44268
  try {
43617
44269
  handle = openSync(lockPath, "wx", 384);
43618
44270
  try {
43619
- writeFileSync2(handle, nonce, { encoding: "utf8" });
44271
+ writeFileSync3(handle, nonce, { encoding: "utf8" });
43620
44272
  } catch (cause) {
43621
44273
  closeSync(handle);
43622
44274
  handle = null;
43623
- rmSync(lockPath, { force: true });
44275
+ rmSync2(lockPath, { force: true });
43624
44276
  throw cause;
43625
44277
  }
43626
44278
  } catch {
43627
- const age = statSync3(lockPath, { throwIfNoEntry: false })?.mtimeMs;
44279
+ const age = statSync4(lockPath, { throwIfNoEntry: false })?.mtimeMs;
43628
44280
  if (age !== undefined && Date.now() - age > LOCK_STALE_MS) {
43629
44281
  try {
43630
- rmSync(lockPath, { force: true });
44282
+ rmSync2(lockPath, { force: true });
43631
44283
  } catch {}
43632
44284
  }
43633
44285
  if (!announced && Date.now() - started > 1000) {
@@ -43644,8 +44296,8 @@ function withFileLock2(lockPath, fn, options = {}) {
43644
44296
  closeSync(handle);
43645
44297
  } catch {}
43646
44298
  try {
43647
- if (readFileSync3(lockPath, "utf8") === nonce) {
43648
- rmSync(lockPath, { force: true });
44299
+ if (readFileSync4(lockPath, "utf8") === nonce) {
44300
+ rmSync2(lockPath, { force: true });
43649
44301
  }
43650
44302
  } catch {}
43651
44303
  }
@@ -43767,7 +44419,7 @@ function tightenBackup(file) {
43767
44419
  return;
43768
44420
  }
43769
44421
  try {
43770
- chmodSync2(file, 384);
44422
+ chmodSync3(file, 384);
43771
44423
  } catch {}
43772
44424
  }
43773
44425
  function quarantineDamagedRegistry(dir, onWarn) {
@@ -43795,13 +44447,13 @@ function saveProjectRegistry(registry, dir, onWarn) {
43795
44447
  };
43796
44448
  const handle = openSync2(temp, "wx", 384);
43797
44449
  try {
43798
- writeFileSync3(handle, `${JSON.stringify(sorted, null, 2)}
44450
+ writeFileSync4(handle, `${JSON.stringify(sorted, null, 2)}
43799
44451
  `, { encoding: "utf8" });
43800
44452
  fsyncSync(handle);
43801
44453
  } finally {
43802
44454
  closeSync2(handle);
43803
44455
  }
43804
- renameSync2(temp, file);
44456
+ renameSync3(temp, file);
43805
44457
  return true;
43806
44458
  } catch {
43807
44459
  try {
@@ -43881,7 +44533,7 @@ function forgetProject(projectId, dir, onWarn) {
43881
44533
  return outcome ?? "write-failed";
43882
44534
  }
43883
44535
  function isInitializedProject(projectPath) {
43884
- return statSync4(path23.join(projectPath, ".metaproject"), { throwIfNoEntry: false })?.isDirectory() === true;
44536
+ return statSync5(path23.join(projectPath, ".metaproject"), { throwIfNoEntry: false })?.isDirectory() === true;
43885
44537
  }
43886
44538
  function listProjects(dir, onWarn) {
43887
44539
  const registry = loadProjectRegistry(dir, onWarn);
@@ -44832,7 +45484,7 @@ async function choice(question, choices, defaultValue) {
44832
45484
 
44833
45485
  // src/lib/project-sandbox-policy.ts
44834
45486
  init_paths();
44835
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
45487
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
44836
45488
  import path29 from "path";
44837
45489
 
44838
45490
  // src/harness/process/sandbox/network-run.ts
@@ -45026,7 +45678,7 @@ function loadProjectSandboxPolicy(cwdOrRoot) {
45026
45678
  if (!existsSync7(file)) {
45027
45679
  return {};
45028
45680
  }
45029
- const raw = JSON.parse(readFileSync4(file, "utf8"));
45681
+ const raw = JSON.parse(readFileSync5(file, "utf8"));
45030
45682
  return sanitizeProjectSandboxPolicy(raw);
45031
45683
  } catch {
45032
45684
  return {};
@@ -45049,8 +45701,8 @@ function writeProjectSandboxPolicySkeletonIfMissing(cwdOrRoot) {
45049
45701
  if (existsSync7(file)) {
45050
45702
  return false;
45051
45703
  }
45052
- mkdirSync3(path29.dirname(file), { recursive: true });
45053
- writeFileSync4(file, projectSandboxPolicySkeleton(), { mode: 420 });
45704
+ mkdirSync4(path29.dirname(file), { recursive: true });
45705
+ writeFileSync5(file, projectSandboxPolicySkeleton(), { mode: 420 });
45054
45706
  return true;
45055
45707
  } catch {
45056
45708
  return false;
@@ -45058,7 +45710,7 @@ function writeProjectSandboxPolicySkeletonIfMissing(cwdOrRoot) {
45058
45710
  }
45059
45711
 
45060
45712
  // src/gdskills/catalog.ts
45061
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
45713
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
45062
45714
  import path30 from "path";
45063
45715
  import { fileURLToPath as fileURLToPath2 } from "url";
45064
45716
  var GDSKILLS_PROFILES = [
@@ -45553,7 +46205,7 @@ function readBundledSkillRouting(category, name) {
45553
46205
  if (file === undefined) {
45554
46206
  throw new Error(`bundled skill "${category}/${name}" ships no SKILL.md under src/gdskills/bundled/skills, and BUNDLED_GDSKILLS reads its description and triggers from there. Declare it with renderedSkill() if it has no file.`);
45555
46207
  }
45556
- const { description, triggers } = parseSkillFrontmatter(readFileSync5(file, "utf8"));
46208
+ const { description, triggers } = parseSkillFrontmatter(readFileSync6(file, "utf8"));
45557
46209
  if (description === undefined || description.length === 0 || triggers === undefined || triggers.length === 0) {
45558
46210
  throw new Error(`${file}: frontmatter must declare a non-empty \`description\` and \`triggers:\` list; the skill router scores both.`);
45559
46211
  }
@@ -50274,7 +50926,7 @@ import path47 from "path";
50274
50926
  // package.json
50275
50927
  var package_default = {
50276
50928
  name: "@mrciphersmith/keryx",
50277
- version: "0.2.121",
50929
+ version: "0.2.123",
50278
50930
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
50279
50931
  private: false,
50280
50932
  publishConfig: {
@@ -60225,7 +60877,7 @@ function escapeRegExp6(value) {
60225
60877
  }
60226
60878
 
60227
60879
  // src/gdskills/bundled-eval.ts
60228
- import { existsSync as existsSync25, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
60880
+ import { existsSync as existsSync25, readFileSync as readFileSync8, readdirSync as readdirSync2 } from "fs";
60229
60881
  import path147 from "path";
60230
60882
 
60231
60883
  // src/gdskills/model-tier.ts
@@ -61164,7 +61816,7 @@ function evaluateBundledTree(root = defaultBundledRoot()) {
61164
61816
  const skill = path147.basename(skillDir);
61165
61817
  const rel = path147.relative(skillsRoot, file).split(path147.sep).join("/");
61166
61818
  const category = path147.dirname(rel).split("/")[0] ?? "";
61167
- const text = readFileSync7(file, "utf8");
61819
+ const text = readFileSync8(file, "utf8");
61168
61820
  const add = (check, line, message) => {
61169
61821
  findings.push({ check, skill, file: rel, line, message });
61170
61822
  };
@@ -61314,7 +61966,7 @@ function evaluateBundledTree(root = defaultBundledRoot()) {
61314
61966
  }
61315
61967
  for (const file of bundledRuleFiles(root)) {
61316
61968
  const rel = path147.relative(root, file).split(path147.sep).join("/");
61317
- const text = readFileSync7(file, "utf8");
61969
+ const text = readFileSync8(file, "utf8");
61318
61970
  const add = (check, line, message) => {
61319
61971
  findings.push({ check, skill: path147.basename(file, path147.extname(file)), file: rel, line, message });
61320
61972
  };
@@ -61324,7 +61976,7 @@ function evaluateBundledTree(root = defaultBundledRoot()) {
61324
61976
  const skillDir = path147.dirname(file);
61325
61977
  const skill = path147.basename(skillDir);
61326
61978
  const rel = path147.relative(skillsRoot, file).split(path147.sep).join("/");
61327
- const text = readFileSync7(file, "utf8");
61979
+ const text = readFileSync8(file, "utf8");
61328
61980
  const add = (check, line, message) => {
61329
61981
  findings.push({ check, skill, file: rel, line, message });
61330
61982
  };
@@ -61353,7 +62005,7 @@ function evaluateBundledTree(root = defaultBundledRoot()) {
61353
62005
  const dir = path147.dirname(file);
61354
62006
  let canonicalComparable;
61355
62007
  try {
61356
- canonicalComparable = buildComparableText(readFileSync7(file, "utf8"));
62008
+ canonicalComparable = buildComparableText(readFileSync8(file, "utf8"));
61357
62009
  } catch {
61358
62010
  continue;
61359
62011
  }
@@ -61366,7 +62018,7 @@ function evaluateBundledTree(root = defaultBundledRoot()) {
61366
62018
  continue;
61367
62019
  let buildComparable;
61368
62020
  try {
61369
- buildComparable = buildComparableText(readFileSync7(buildPath, "utf8"));
62021
+ buildComparable = buildComparableText(readFileSync8(buildPath, "utf8"));
61370
62022
  } catch {
61371
62023
  continue;
61372
62024
  }
@@ -73672,11 +74324,11 @@ function wrapSeatbelt(command, profile) {
73672
74324
  }
73673
74325
 
73674
74326
  // src/harness/process/sandbox/bwrap.ts
73675
- import { statSync as statSync5 } from "fs";
74327
+ import { statSync as statSync6 } from "fs";
73676
74328
  var BWRAP_PROGRAM = "bwrap";
73677
74329
  function inspectMaskTarget(target) {
73678
74330
  try {
73679
- return statSync5(target).isDirectory() ? "dir" : "file";
74331
+ return statSync6(target).isDirectory() ? "dir" : "file";
73680
74332
  } catch {
73681
74333
  return "missing";
73682
74334
  }
@@ -76047,7 +76699,7 @@ function credentialSummary(raw) {
76047
76699
 
76048
76700
  // src/mcp-servers/store.ts
76049
76701
  init_config_dir();
76050
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
76702
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
76051
76703
  import path170 from "path";
76052
76704
  var SERVER_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
76053
76705
  function userConfigFile(configDir) {
@@ -76096,8 +76748,8 @@ function writeDocument(file, doc, scope) {
76096
76748
  writeOwnerOnlyFileAtomic(file, body);
76097
76749
  return;
76098
76750
  }
76099
- mkdirSync6(path170.dirname(file), { recursive: true });
76100
- writeFileSync7(file, body, { encoding: "utf8" });
76751
+ mkdirSync7(path170.dirname(file), { recursive: true });
76752
+ writeFileSync8(file, body, { encoding: "utf8" });
76101
76753
  }
76102
76754
  function addServer(options) {
76103
76755
  if (!SERVER_NAME_PATTERN.test(options.name)) {
@@ -79503,7 +80155,7 @@ Reports the workspace root and one enabled/disabled line per module. Use
79503
80155
  }
79504
80156
 
79505
80157
  // src/commands/harness.ts
79506
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
80158
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync9 } from "fs";
79507
80159
  import path182 from "path";
79508
80160
  import { randomUUID as randomUUID20 } from "crypto";
79509
80161
 
@@ -81416,7 +82068,7 @@ async function harnessCommand(args, deps) {
81416
82068
  const result = await runOffline(input, config, runDeps);
81417
82069
  structured = toStructured(result);
81418
82070
  if (record !== undefined && record.length > 0) {
81419
- writeFileSync8(record, `${JSON.stringify(toRunRecord(result, {
82071
+ writeFileSync9(record, `${JSON.stringify(toRunRecord(result, {
81420
82072
  runId: result.output.runId,
81421
82073
  status: result.output.status,
81422
82074
  recordedAt: result.output.startedAt
@@ -81462,7 +82114,7 @@ function parseReplayArgs(args) {
81462
82114
  function readJsonFile2(file) {
81463
82115
  let text;
81464
82116
  try {
81465
- text = readFileSync8(file, "utf8");
82117
+ text = readFileSync9(file, "utf8");
81466
82118
  } catch (error) {
81467
82119
  return { ok: false, reason: error instanceof Error ? error.message : String(error) };
81468
82120
  }
@@ -81517,7 +82169,7 @@ function harnessReplay(args, deps) {
81517
82169
  return;
81518
82170
  }
81519
82171
  if (parsed.writeFixture !== undefined && parsed.writeFixture.length > 0) {
81520
- writeFileSync8(parsed.writeFixture, `${JSON.stringify(fixture.value, null, 2)}
82172
+ writeFileSync9(parsed.writeFixture, `${JSON.stringify(fixture.value, null, 2)}
81521
82173
  `, {
81522
82174
  encoding: "utf8",
81523
82175
  mode: 384
@@ -81829,7 +82481,7 @@ function readSpecArg(args) {
81829
82481
  if (specPath === undefined) {
81830
82482
  throw new Error("keryx harness: a --spec <path> argument is required when no spec is injected.");
81831
82483
  }
81832
- return JSON.parse(readFileSync8(specPath, "utf8"));
82484
+ return JSON.parse(readFileSync9(specPath, "utf8"));
81833
82485
  }
81834
82486
  var DEFAULT_CHILD_RESULT = `STATUS: DONE
81835
82487
  Extension completed within its granted capabilities.`;
@@ -81936,7 +82588,7 @@ init_providers();
81936
82588
  init_make_provider();
81937
82589
  init_orient();
81938
82590
  init_metaproject_adapter();
81939
- import { randomUUID as randomUUID29 } from "crypto";
82591
+ import { randomUUID as randomUUID30 } from "crypto";
81940
82592
  import * as readline2 from "readline";
81941
82593
 
81942
82594
  // src/commands/agent-approval-context.ts
@@ -88192,7 +88844,7 @@ async function runGoalCommand(params) {
88192
88844
  const wasOpened = slateSession.opened;
88193
88845
  await ensureSlateOpened(slateSession, mintAttemptId, { provider: deps.providerId, model: deps.modelId });
88194
88846
  if (!wasOpened && slateSession.opened) {
88195
- const freshSlate = await readSlate(slateSession.dir);
88847
+ const freshSlate = await readSlateSession(slateSession);
88196
88848
  if (freshSlate !== undefined) {
88197
88849
  history.push({ role: "user", content: renderAnchorsBlock(freshSlate.anchors), provenance: "project" });
88198
88850
  io.onHistoryChange?.("tool");
@@ -88200,17 +88852,17 @@ async function runGoalCommand(params) {
88200
88852
  }
88201
88853
  if (parsed.workspaceId !== undefined) {
88202
88854
  const workspaceId = parsed.workspaceId;
88203
- await writeSlate(slateSession.dir, (prev) => {
88855
+ await writeSlateSession(slateSession, (prev) => {
88204
88856
  const base = prev ?? { anchors: { root: "", touched: [] }, course: {}, seeds: [] };
88205
88857
  return { ...base, workspaceId };
88206
88858
  });
88207
88859
  }
88208
88860
  if (parsed.auto !== undefined) {
88209
- const forCourse = await readSlate(slateSession.dir);
88861
+ const forCourse = await readSlateSession(slateSession);
88210
88862
  let flowRefForBinding = forCourse?.course.flowRef;
88211
88863
  if (forCourse !== undefined && forCourse.course.flowRef === undefined) {
88212
88864
  const flowId = await autoProvisionFlow(cwd, parsed.text);
88213
- await writeSlate(slateSession.dir, (prev) => {
88865
+ await writeSlateSession(slateSession, (prev) => {
88214
88866
  if (!prev)
88215
88867
  throw new Error(`SLATE-27 bind: no open slate in ${slateSession.dir}`);
88216
88868
  return { ...prev, course: { ...prev.course, flowRef: flowId } };
@@ -88233,7 +88885,7 @@ async function runGoalCommand(params) {
88233
88885
  delete slateSession.autoGoalRounds;
88234
88886
  let roundsLeft = roundsCap;
88235
88887
  let round = 1;
88236
- while (roundsLeft > 0 && slateSession.opened) {
88888
+ while (roundsLeft > 0 && slateSession.opened && !isSlateSessionDetached(slateSession)) {
88237
88889
  roundsLeft -= 1;
88238
88890
  round += 1;
88239
88891
  const continuationText = await buildContinuationMessage(cwd, slateSession, round, roundsCap);
@@ -88246,8 +88898,18 @@ async function runGoalCommand(params) {
88246
88898
  break;
88247
88899
  }
88248
88900
  }
88901
+ if (isSlateSessionDetached(slateSession)) {
88902
+ systemLine(io, `/goal --auto: stopped \u2014 another shell took this session over.
88903
+ `);
88904
+ return;
88905
+ }
88249
88906
  const wasOpenBeforeVerifier = slateSession.opened;
88250
88907
  const verdict = await runGoalVerifier(deps, parsed.text, cwd, slateSession, history, io, mintAttemptId);
88908
+ if (isSlateSessionDetached(slateSession)) {
88909
+ systemLine(io, `/goal --auto: stopped \u2014 another shell took this session over.
88910
+ `);
88911
+ return;
88912
+ }
88251
88913
  if (verdict === undefined) {
88252
88914
  systemLine(io, `/goal --auto: verifier unavailable \u2014 outcome not independently checked.
88253
88915
  `);
@@ -88262,12 +88924,12 @@ async function runGoalCommand(params) {
88262
88924
  if (!wasOpenBeforeVerifier) {
88263
88925
  try {
88264
88926
  await ensureSlateOpened(slateSession, mintAttemptId, { provider: deps.providerId, model: deps.modelId });
88265
- const reopened = await readSlate(slateSession.dir);
88927
+ const reopened = await readSlateSession(slateSession);
88266
88928
  if (reopened !== undefined) {
88267
88929
  history.push({ role: "user", content: renderAnchorsBlock(reopened.anchors), provenance: "project" });
88268
88930
  io.onHistoryChange?.("tool");
88269
88931
  }
88270
- await writeSlate(slateSession.dir, (prev) => {
88932
+ await writeSlateSession(slateSession, (prev) => {
88271
88933
  if (!prev)
88272
88934
  throw new Error(`SLATE-27 verifier-reopen: no open slate in ${slateSession.dir}`);
88273
88935
  return {
@@ -88297,7 +88959,7 @@ async function runGoalCommand(params) {
88297
88959
 
88298
88960
  // src/tui/tui-shell.ts
88299
88961
  init_metaproject_adapter();
88300
- import { spawnSync as spawnSync2 } from "child_process";
88962
+ import { spawnSync as spawnSync3 } from "child_process";
88301
88963
 
88302
88964
  // src/tui/transcript-blocks.ts
88303
88965
  var EVICTED_BLOCK_TEXT = "(output no longer retained)";
@@ -90550,6 +91212,9 @@ function classifyBusyDispatch(params) {
90550
91212
  return "not-a-command";
90551
91213
  }
90552
91214
 
91215
+ // src/tui/tui-shell.ts
91216
+ init_debug_log();
91217
+
90553
91218
  // src/tui/boot-animation.ts
90554
91219
  var DEFAULT_BOOT_DURATION_MS = 350;
90555
91220
  var WORDMARK = ["========================", " K E R Y X ", "========================"];
@@ -94050,10 +94715,10 @@ function describeElicitationPrompt(tool, inputJson) {
94050
94715
  // src/lib/permission-mode-config.ts
94051
94716
  init_permission_mode();
94052
94717
  init_config_dir();
94053
- import path192 from "path";
94718
+ import path193 from "path";
94054
94719
  var EMPTY3 = { schemaVersion: 1, projects: {} };
94055
94720
  function permissionModeConfigPath(dir) {
94056
- return path192.join(keryxConfigDir(dir), "permission-mode.json");
94721
+ return path193.join(keryxConfigDir(dir), "permission-mode.json");
94057
94722
  }
94058
94723
  function withRegistryLock2(dir, fn) {
94059
94724
  return withFileLock2(`${permissionModeConfigPath(dir)}.lock`, fn, {
@@ -94296,7 +94961,465 @@ function createForegroundAgentIoFacade(owner, token, io) {
94296
94961
  };
94297
94962
  }
94298
94963
 
94964
+ // src/session/lease.ts
94965
+ init_fs();
94966
+ init_paths();
94967
+ init_store3();
94968
+ import { randomUUID as randomUUID29 } from "crypto";
94969
+ import { hostname as hostname2 } from "os";
94970
+ import path194 from "path";
94971
+ var SESSION_LEASE_STALE_MS = 15000;
94972
+ var SESSION_LEASE_HEARTBEAT_MS = 5000;
94973
+ var instanceId;
94974
+ function processInstanceId() {
94975
+ instanceId ??= randomUUID29();
94976
+ return instanceId;
94977
+ }
94978
+ var MIN_ENV_STALE_MS = 250;
94979
+ var MIN_ENV_HEARTBEAT_MS = 50;
94980
+ function envInt(name) {
94981
+ const raw = process.env[name];
94982
+ return raw !== undefined && /^\d+$/.test(raw) ? Number(raw) : undefined;
94983
+ }
94984
+ function leaseTimingEnvEnabled() {
94985
+ return process.env.KERYX_TEST_LEASE_TIMING === "1";
94986
+ }
94987
+ function resolveSessionLeaseTiming(timing = {}) {
94988
+ const fromEnv = leaseTimingEnvEnabled();
94989
+ const envStale = fromEnv ? envInt("KERYX_SESSION_LEASE_STALE_MS") : undefined;
94990
+ const staleMs = timing.staleMs ?? (envStale !== undefined && envStale >= MIN_ENV_STALE_MS ? envStale : SESSION_LEASE_STALE_MS);
94991
+ const maxHeartbeat = Math.floor(staleMs / 3);
94992
+ const envHeartbeat = fromEnv ? envInt("KERYX_SESSION_LEASE_HEARTBEAT_MS") : undefined;
94993
+ const heartbeatMs = timing.heartbeatMs ?? (envHeartbeat !== undefined && envHeartbeat >= MIN_ENV_HEARTBEAT_MS && envHeartbeat <= maxHeartbeat ? envHeartbeat : Math.max(1, Math.min(SESSION_LEASE_HEARTBEAT_MS, maxHeartbeat)));
94994
+ return { ...timing, staleMs, heartbeatMs };
94995
+ }
94996
+ var resolveTiming = resolveSessionLeaseTiming;
94997
+ function isMine(holder) {
94998
+ return holder !== undefined && holder.instanceId === processInstanceId() && holder.pid === process.pid;
94999
+ }
95000
+ function leaseOptions(timing) {
95001
+ return {
95002
+ staleMs: timing.staleMs,
95003
+ ...timing.now !== undefined ? { now: timing.now } : {},
95004
+ ...timing.isAlive !== undefined ? { isAlive: timing.isAlive } : {},
95005
+ ...timing.host !== undefined ? { host: timing.host } : {}
95006
+ };
95007
+ }
95008
+ function sessionLeasePath(cwd, sessionId, dataDir) {
95009
+ return path194.join(sessionDir(resolveProjectRoot2(cwd), sessionId, dataDir), "active.lease");
95010
+ }
95011
+ function sessionLeaseState(cwd, sessionId, dataDir, timing) {
95012
+ const resolved = resolveTiming(timing);
95013
+ const seen = inspectLeaseSync(sessionLeasePath(cwd, sessionId, dataDir), leaseOptions(resolved));
95014
+ if (seen.state === "free" || seen.state === "gone")
95015
+ return { state: "free" };
95016
+ if (isMine(seen.holder))
95017
+ return { state: "mine", holder: seen.holder };
95018
+ return { state: seen.state, ...seen.holder !== undefined ? { holder: seen.holder } : {} };
95019
+ }
95020
+ function latestUnleasedSession(cwd, dataDir, timing) {
95021
+ let skipped;
95022
+ for (const summary of listSessions(cwd, dataDir)) {
95023
+ const { state, holder } = sessionLeaseState(cwd, summary.id, dataDir, timing);
95024
+ if (state === "free" || state === "mine") {
95025
+ return { summary, ...skipped !== undefined ? { skipped } : {} };
95026
+ }
95027
+ skipped ??= { summary, holder, state };
95028
+ }
95029
+ return { summary: undefined, ...skipped !== undefined ? { skipped } : {} };
95030
+ }
95031
+ function describeLeaseHolder(holder) {
95032
+ if (holder === undefined)
95033
+ return "another instance (still starting)";
95034
+ if (typeof holder.name === "string" && holder.name.length > 0)
95035
+ return `@${holder.name} (pid ${holder.pid})`;
95036
+ return `instance ${String(holder.instanceId).slice(0, 8)} (pid ${holder.pid})`;
95037
+ }
95038
+
95039
+ class SessionLeasedError extends Error {
95040
+ summary;
95041
+ holder;
95042
+ state;
95043
+ constructor(summary, holder, state, opts = {}) {
95044
+ const where = `session ${shortSessionId(summary.id)} is open in ${describeLeaseHolder(holder)}`;
95045
+ const hint = opts.takeOverRefused === true ? "; the holder is live, so --take-over is refused; use --fork" : state === "stale" ? "; use --fork or --take-over" : "; use --fork";
95046
+ super(`${where}${hint}`);
95047
+ this.name = "SessionLeasedError";
95048
+ this.summary = summary;
95049
+ this.holder = holder;
95050
+ this.state = state;
95051
+ }
95052
+ }
95053
+ var heldByThisProcess = new Map;
95054
+ function wrapHandle(sessionId, lockPath, inner, heartbeatMs) {
95055
+ let released = false;
95056
+ let lost = false;
95057
+ let lostTo;
95058
+ const lostListeners = new Set;
95059
+ let timer;
95060
+ const onExit = () => {
95061
+ handle.release();
95062
+ };
95063
+ const stopHeartbeat = () => {
95064
+ if (timer !== undefined)
95065
+ clearInterval(timer);
95066
+ timer = undefined;
95067
+ };
95068
+ const markLost = () => {
95069
+ if (lost || released)
95070
+ return;
95071
+ lost = true;
95072
+ lostTo = readLeaseOwnerSync(lockPath);
95073
+ stopHeartbeat();
95074
+ if (heldByThisProcess.get(lockPath) === handle)
95075
+ heldByThisProcess.delete(lockPath);
95076
+ const listeners = [...lostListeners];
95077
+ lostListeners.clear();
95078
+ for (const cb of listeners) {
95079
+ try {
95080
+ cb();
95081
+ } catch {}
95082
+ }
95083
+ };
95084
+ const handle = {
95085
+ sessionId,
95086
+ lockPath,
95087
+ get owner() {
95088
+ return inner.owner;
95089
+ },
95090
+ get released() {
95091
+ return released;
95092
+ },
95093
+ get heartbeatTimer() {
95094
+ return timer;
95095
+ },
95096
+ get lost() {
95097
+ return lost;
95098
+ },
95099
+ get lostTo() {
95100
+ return lostTo;
95101
+ },
95102
+ refresh(patch) {
95103
+ if (released || lost)
95104
+ return false;
95105
+ if (inner.refresh(patch))
95106
+ return true;
95107
+ if (inner.ownership() === "lost")
95108
+ markLost();
95109
+ return false;
95110
+ },
95111
+ checkLost() {
95112
+ if (!lost && !released && inner.ownership() === "lost")
95113
+ markLost();
95114
+ return lost;
95115
+ },
95116
+ onLost(cb) {
95117
+ if (lost) {
95118
+ cb();
95119
+ return () => {};
95120
+ }
95121
+ lostListeners.add(cb);
95122
+ return () => {
95123
+ lostListeners.delete(cb);
95124
+ };
95125
+ },
95126
+ release() {
95127
+ if (released)
95128
+ return;
95129
+ released = true;
95130
+ stopHeartbeat();
95131
+ lostListeners.clear();
95132
+ process.off("exit", onExit);
95133
+ if (heldByThisProcess.get(lockPath) === handle)
95134
+ heldByThisProcess.delete(lockPath);
95135
+ if (!lost)
95136
+ inner.release();
95137
+ }
95138
+ };
95139
+ timer = setInterval(() => {
95140
+ handle.refresh();
95141
+ }, heartbeatMs);
95142
+ timer.unref?.();
95143
+ process.on("exit", onExit);
95144
+ heldByThisProcess.set(lockPath, handle);
95145
+ return handle;
95146
+ }
95147
+ function mintOwner(now) {
95148
+ const at = new Date(now()).toISOString();
95149
+ return {
95150
+ schemaVersion: 1,
95151
+ token: randomUUID29(),
95152
+ pid: process.pid,
95153
+ host: hostname2(),
95154
+ instanceId: processInstanceId(),
95155
+ name: null,
95156
+ acquiredAt: at,
95157
+ heartbeatAt: at
95158
+ };
95159
+ }
95160
+ function leaseSession(cwd, summary, dataDir, timing, takeOver) {
95161
+ const lockPath = sessionLeasePath(cwd, summary.id, dataDir);
95162
+ const existing = heldByThisProcess.get(lockPath);
95163
+ if (existing !== undefined && !existing.released && !existing.lost)
95164
+ return { handle: existing, minted: false };
95165
+ const owner = mintOwner(timing.now ?? Date.now);
95166
+ const opts = {
95167
+ ...leaseOptions(timing),
95168
+ reclaimIf: (holder) => isMine(holder)
95169
+ };
95170
+ const result = takeOver ? reclaimStaleLeaseSync(lockPath, owner, opts) : acquireLeaseSync(lockPath, owner, opts);
95171
+ if (!result.ok) {
95172
+ throw new SessionLeasedError(summary, result.holder, result.state, {
95173
+ takeOverRefused: takeOver && result.state === "live"
95174
+ });
95175
+ }
95176
+ return { handle: wrapHandle(summary.id, lockPath, result.handle, timing.heartbeatMs), minted: true };
95177
+ }
95178
+ function describeLeaseLoss(handle) {
95179
+ const short = shortSessionId(handle.sessionId);
95180
+ const by = handle.lostTo !== undefined ? describeLeaseHolder(handle.lostTo) : "another instance";
95181
+ return `session ${short} was taken over by ${by}; this shell no longer saves it. ` + `Use /new, or restart with -r ${short} --fork to keep your turns.
95182
+ `;
95183
+ }
95184
+ var LOST_LEASE_COMPACT_REFUSAL = `Not compacted: another shell took this session over, and this shell no longer saves it. Use /new.
95185
+ `;
95186
+ function watchLeaseLoss(notify) {
95187
+ let current;
95188
+ let unsubscribe;
95189
+ return {
95190
+ track(next) {
95191
+ if (next === current)
95192
+ return;
95193
+ unsubscribe?.();
95194
+ unsubscribe = undefined;
95195
+ current = next;
95196
+ if (next !== undefined) {
95197
+ unsubscribe = next.onLost(() => {
95198
+ notify(describeLeaseLoss(next), next);
95199
+ });
95200
+ }
95201
+ },
95202
+ canPersist() {
95203
+ return current === undefined || !current.checkLost();
95204
+ }
95205
+ };
95206
+ }
95207
+ function whilePersisting(value, canPersist) {
95208
+ return value !== undefined && !canPersist() ? undefined : value;
95209
+ }
95210
+ function gateBoxByLease(box, canPersist) {
95211
+ let value = box.current;
95212
+ Object.defineProperty(box, "current", {
95213
+ configurable: true,
95214
+ enumerable: true,
95215
+ get: () => whilePersisting(value, canPersist),
95216
+ set: (next) => {
95217
+ value = next;
95218
+ }
95219
+ });
95220
+ }
95221
+ function releaseSessionLease(handle) {
95222
+ handle?.release();
95223
+ }
95224
+ function openLeasedSession(opts) {
95225
+ const { takeOver, fork, staleMs, heartbeatMs, now, isAlive, host, resumeId: rawResumeId, continueLast, ...base } = opts;
95226
+ const timing = resolveTiming({
95227
+ ...staleMs !== undefined ? { staleMs } : {},
95228
+ ...heartbeatMs !== undefined ? { heartbeatMs } : {},
95229
+ ...now !== undefined ? { now } : {},
95230
+ ...isAlive !== undefined ? { isAlive } : {},
95231
+ ...host !== undefined ? { host } : {}
95232
+ });
95233
+ const { cwd, dataDir } = base;
95234
+ const resumeId = rawResumeId !== undefined && rawResumeId.length > 0 ? rawResumeId : undefined;
95235
+ const openWith = (target, lease) => {
95236
+ try {
95237
+ const opened = openSession({ ...base, ...target !== undefined ? { resumeId: target } : {} });
95238
+ return { ...opened, lease: lease?.handle ?? leaseSession(cwd, opened.handle.summary, dataDir, timing, false).handle };
95239
+ } catch (error) {
95240
+ if (lease?.minted === true)
95241
+ releaseSessionLease(lease.handle);
95242
+ throw error;
95243
+ }
95244
+ };
95245
+ if (fork === true) {
95246
+ if (resumeId === undefined) {
95247
+ throw new Error("--fork needs a session id (-r <id>).");
95248
+ }
95249
+ const forked = forkSession({
95250
+ cwd,
95251
+ sourceIdOrPrefix: resumeId,
95252
+ ...dataDir !== undefined ? { dataDir } : {}
95253
+ });
95254
+ const summary = forked.handle.summary;
95255
+ return openWith(summary.id, leaseSession(cwd, summary, dataDir, timing, false));
95256
+ }
95257
+ if (resumeId !== undefined) {
95258
+ const found = findSession(cwd, resumeId, dataDir);
95259
+ if (found === undefined) {
95260
+ return openWith(resumeId);
95261
+ }
95262
+ return openWith(found.id, leaseSession(cwd, found, dataDir, timing, takeOver === true));
95263
+ }
95264
+ let skipped;
95265
+ if (continueLast === true) {
95266
+ for (let attempt = 0;attempt < 3; attempt += 1) {
95267
+ const pick = latestUnleasedSession(cwd, dataDir, timing);
95268
+ skipped ??= pick.skipped;
95269
+ if (pick.summary === undefined)
95270
+ break;
95271
+ let lease;
95272
+ try {
95273
+ lease = leaseSession(cwd, pick.summary, dataDir, timing, false);
95274
+ } catch (error) {
95275
+ if (!(error instanceof SessionLeasedError))
95276
+ throw error;
95277
+ skipped ??= { summary: error.summary, holder: error.holder, state: error.state };
95278
+ continue;
95279
+ }
95280
+ return { ...openWith(pick.summary.id, lease), ...skipped !== undefined ? { skipped } : {} };
95281
+ }
95282
+ }
95283
+ return { ...openWith(undefined), ...skipped !== undefined ? { skipped } : {} };
95284
+ }
95285
+ function switchLeasedSession(current, openTarget) {
95286
+ const next = openTarget();
95287
+ if (current !== undefined && current !== next.lease) {
95288
+ releaseSessionLease(current);
95289
+ }
95290
+ return next;
95291
+ }
95292
+
95293
+ // src/session/lease-choice.ts
95294
+ init_store3();
95295
+ function leasedChoiceRows(error) {
95296
+ return [
95297
+ { choice: "fork", label: "fork (default): continue in a copy of the session" },
95298
+ { choice: "view", label: "view (read-only), then start a new session" },
95299
+ { choice: "cancel", label: "cancel" },
95300
+ ...error.state === "stale" ? [{ choice: "take-over", label: "take over (the holder is stale)" }] : []
95301
+ ];
95302
+ }
95303
+ function describeLeasedSession(error) {
95304
+ const stale = error.state === "stale" ? " (stale)" : "";
95305
+ return `Session ${shortSessionId(error.summary.id)} \xB7 ${error.summary.title} is open in ${describeLeaseHolder(error.holder)}${stale}.`;
95306
+ }
95307
+ function describeSkippedSession(skipped) {
95308
+ const stale = skipped.state === "stale" ? " (stale)" : "";
95309
+ return `Skipped session ${shortSessionId(skipped.summary.id)} \xB7 ${skipped.summary.title}: open in ${describeLeaseHolder(skipped.holder)}${stale}
95310
+ `;
95311
+ }
95312
+
95313
+ // src/tui/tui-session-lease.ts
95314
+ function leaseMarker(state) {
95315
+ if (state === "live")
95316
+ return "\u25CF live";
95317
+ if (state === "stale")
95318
+ return "\u25CC stale";
95319
+ return "";
95320
+ }
95321
+ function leaseStateLookup(cwd, dataDir) {
95322
+ return (sessionId) => {
95323
+ try {
95324
+ return sessionLeaseState(cwd, sessionId, dataDir).state;
95325
+ } catch {
95326
+ return "free";
95327
+ }
95328
+ };
95329
+ }
95330
+ function withLeaseMarker(label, state) {
95331
+ const marker = leaseMarker(state);
95332
+ return marker.length > 0 ? `${label} ${marker}` : label;
95333
+ }
95334
+ function leasedChoiceRequest(error) {
95335
+ const descriptions = {
95336
+ fork: "Continue in a copy of the session; the other shell keeps the original",
95337
+ view: "Show the session read-only, then start a new session",
95338
+ cancel: "Exit without opening a session",
95339
+ "take-over": "Reclaim the lease: the holder has stopped refreshing it"
95340
+ };
95341
+ return {
95342
+ title: describeLeasedSession(error),
95343
+ subtitle: "Esc = cancel",
95344
+ cancelId: "cancel",
95345
+ options: leasedChoiceRows(error).map((row) => ({
95346
+ id: row.choice,
95347
+ label: row.label,
95348
+ description: descriptions[row.choice],
95349
+ ...row.choice === "fork" ? { recommended: true } : {}
95350
+ }))
95351
+ };
95352
+ }
95353
+ function toLeasedChoice(error, id) {
95354
+ const offered = leasedChoiceRows(error).find((row) => row.choice === id);
95355
+ return offered?.choice ?? "cancel";
95356
+ }
95357
+ async function resolveLeasedStartup(first, deps) {
95358
+ let error = first;
95359
+ for (;; ) {
95360
+ const choice = await deps.choose(error);
95361
+ if (choice === "cancel") {
95362
+ return { kind: "cancelled" };
95363
+ }
95364
+ if (choice === "view") {
95365
+ try {
95366
+ deps.showReadOnly(deps.exportSession(error.summary.id));
95367
+ } catch (cause) {
95368
+ deps.notice(`Could not render the session: ${cause instanceof Error ? cause.message : String(cause)}`);
95369
+ }
95370
+ return { kind: "opened", opened: deps.open({}) };
95371
+ }
95372
+ try {
95373
+ const target = choice === "fork" ? { resumeId: error.summary.id, fork: true } : { resumeId: error.summary.id, takeOver: true };
95374
+ return { kind: "opened", opened: deps.open(target) };
95375
+ } catch (cause) {
95376
+ if (!(cause instanceof SessionLeasedError)) {
95377
+ throw cause;
95378
+ }
95379
+ deps.notice(cause.message);
95380
+ error = cause;
95381
+ }
95382
+ }
95383
+ }
95384
+ function createTuiLeaseHolder() {
95385
+ let current;
95386
+ let lossListener;
95387
+ const watch = watchLeaseLoss((message) => lossListener?.(message));
95388
+ const set = (next) => {
95389
+ current = next;
95390
+ watch.track(next);
95391
+ };
95392
+ return {
95393
+ get current() {
95394
+ return current;
95395
+ },
95396
+ hold(next) {
95397
+ if (current !== undefined && current !== next) {
95398
+ releaseSessionLease(current);
95399
+ }
95400
+ set(next);
95401
+ },
95402
+ switchTo(openTarget) {
95403
+ const next = switchLeasedSession(current, openTarget);
95404
+ set(next.lease);
95405
+ return next;
95406
+ },
95407
+ release() {
95408
+ const held = current;
95409
+ set(undefined);
95410
+ releaseSessionLease(held);
95411
+ },
95412
+ canPersist() {
95413
+ return watch.canPersist();
95414
+ },
95415
+ onLost(listener) {
95416
+ lossListener = listener;
95417
+ }
95418
+ };
95419
+ }
95420
+
94299
95421
  // src/tui/herdr-report.ts
95422
+ init_debug_log();
94300
95423
  import net from "net";
94301
95424
  var SOURCE = "herdr:keryx";
94302
95425
  var AGENT = "keryx";
@@ -94336,6 +95459,7 @@ function createHerdrReporter(clock = () => Date.now(), connect = writeOnce) {
94336
95459
  if (state === last)
94337
95460
  return;
94338
95461
  last = state;
95462
+ debugEvent("herdr.report", { state });
94339
95463
  send("pane.report_agent", { state });
94340
95464
  },
94341
95465
  release() {
@@ -94371,6 +95495,14 @@ function writeOnce(socketPath, request) {
94371
95495
  }
94372
95496
 
94373
95497
  // src/tui/composer-choice.ts
95498
+ init_debug_log();
95499
+ var DEFAULT_ACCEPT_DELAY_MS = 400;
95500
+ function resolveAcceptDelay(request) {
95501
+ if (request.acceptDelayMs !== undefined) {
95502
+ return Math.max(0, request.acceptDelayMs);
95503
+ }
95504
+ return DEFAULT_ACCEPT_DELAY_MS;
95505
+ }
94374
95506
  var MAX_SUBTITLE_CHARS = 8000;
94375
95507
  var MAX_SUBTITLE_ROWS = 10;
94376
95508
  var SUBTITLE_INLINE_MAX_CHARS = 76;
@@ -94408,12 +95540,17 @@ function lockDock(dock) {
94408
95540
  async function showComposerChoice(otui, r, dock, request) {
94409
95541
  const enqueue = request.enqueue !== false;
94410
95542
  if (!enqueue && dockBusy(dock)) {
95543
+ debugEvent("choice.busy-cancel", { title: request.title });
94411
95544
  request.onBusy?.();
94412
95545
  return request.cancelId;
94413
95546
  }
95547
+ const queuedAt = Date.now();
95548
+ debugEvent("choice.request", { title: request.title, waitingBehind: dockDepth.get(dock) ?? 0 });
94414
95549
  const unlock = await lockDock(dock);
95550
+ debugEvent("choice.lock-acquired", { title: request.title, waitedMs: Date.now() - queuedAt });
94415
95551
  try {
94416
95552
  if (request.signal?.aborted === true) {
95553
+ debugEvent("choice.aborted-before-open", { title: request.title });
94417
95554
  return request.cancelId;
94418
95555
  }
94419
95556
  return await presentComposerChoice(otui, r, dock, request);
@@ -94423,6 +95560,7 @@ async function showComposerChoice(otui, r, dock, request) {
94423
95560
  }
94424
95561
  function presentComposerChoice(otui, r, dock, request) {
94425
95562
  if (dock.visible === true) {
95563
+ debugEvent("choice.dock-already-visible", { title: request.title });
94426
95564
  request.onBusy?.();
94427
95565
  return Promise.resolve(request.cancelId);
94428
95566
  }
@@ -94434,6 +95572,9 @@ function presentComposerChoice(otui, r, dock, request) {
94434
95572
  const recommendedIdx = options.findIndex((o) => o.recommended === true);
94435
95573
  let selected = recommendedIdx >= 0 ? recommendedIdx : 0;
94436
95574
  dock.visible = true;
95575
+ const openedAt = Date.now();
95576
+ const acceptDelayMs = resolveAcceptDelay(request);
95577
+ let lastTypedAt = 0;
94437
95578
  request.onOpen?.();
94438
95579
  const theme = getTheme();
94439
95580
  const rawSubtitle = request.subtitle !== undefined && request.subtitle.length > 0 ? request.subtitle : undefined;
@@ -94501,13 +95642,14 @@ function presentComposerChoice(otui, r, dock, request) {
94501
95642
  };
94502
95643
  let settled = false;
94503
95644
  const onAbort = () => {
94504
- finish(request.cancelId);
95645
+ finish(request.cancelId, "abort");
94505
95646
  };
94506
- const finish = (id) => {
95647
+ const finish = (id, via = "key") => {
94507
95648
  if (settled) {
94508
95649
  return;
94509
95650
  }
94510
95651
  settled = true;
95652
+ debugEvent("choice.finish", { title: request.title, id, via, openMs: Date.now() - openedAt });
94511
95653
  cleanup();
94512
95654
  resolve(id);
94513
95655
  };
@@ -94518,7 +95660,7 @@ function presentComposerChoice(otui, r, dock, request) {
94518
95660
  width: "100%",
94519
95661
  flexDirection: "column",
94520
95662
  onMouseDown: () => {
94521
- finish(o.id);
95663
+ finish(o.id, "click");
94522
95664
  }
94523
95665
  });
94524
95666
  const label = new otui.TextRenderable(r, { id: `ch-opt-l-${i}-${Date.now()}`, content: o.displayLabel });
@@ -94576,8 +95718,11 @@ function presentComposerChoice(otui, r, dock, request) {
94576
95718
  dock.visible = false;
94577
95719
  }
94578
95720
  const onKey = (key) => {
95721
+ if (!key.ctrl && typeof key.sequence === "string" && key.sequence.length === 1 && key.sequence >= " ") {
95722
+ lastTypedAt = Date.now();
95723
+ }
94579
95724
  if (key.name === "escape") {
94580
- finish(request.cancelId);
95725
+ finish(request.cancelId, "escape");
94581
95726
  key.preventDefault();
94582
95727
  key.stopPropagation();
94583
95728
  return;
@@ -94614,6 +95759,17 @@ function presentComposerChoice(otui, r, dock, request) {
94614
95759
  return;
94615
95760
  }
94616
95761
  if (key.name === "return" || key.name === "linefeed" || key.name === "kpenter") {
95762
+ const now = Date.now();
95763
+ if (now - openedAt < acceptDelayMs || now - lastTypedAt < acceptDelayMs) {
95764
+ debugEvent("choice.enter-ignored", {
95765
+ title: request.title,
95766
+ sinceOpenMs: now - openedAt,
95767
+ sinceTypingMs: lastTypedAt === 0 ? undefined : now - lastTypedAt
95768
+ });
95769
+ key.preventDefault();
95770
+ key.stopPropagation();
95771
+ return;
95772
+ }
94617
95773
  const chosen = options[selected];
94618
95774
  if (chosen !== undefined) {
94619
95775
  finish(chosen.id);
@@ -94627,13 +95783,20 @@ function presentComposerChoice(otui, r, dock, request) {
94627
95783
  request.signal.addEventListener("abort", onAbort, { once: true });
94628
95784
  }
94629
95785
  optionsScroll.focus();
95786
+ debugEvent("choice.open", {
95787
+ title: request.title,
95788
+ options: options.map((o) => o.id),
95789
+ selected: options[selected]?.id,
95790
+ scrollableSubtitle: hasScrollableSubtitle,
95791
+ acceptDelayMs
95792
+ });
94630
95793
  });
94631
95794
  }
94632
95795
 
94633
95796
  // src/lib/version-check.ts
94634
95797
  init_config_dir();
94635
95798
  init_fs();
94636
- import path193 from "path";
95799
+ import path195 from "path";
94637
95800
  var REGISTRY_URL = "https://registry.npmjs.org/@mrciphersmith%2Fkeryx/latest";
94638
95801
  var FIXED_INSTALL_COMMAND = "npm install -g @mrciphersmith/keryx@latest";
94639
95802
  var RESPONSE_BODY_LIMIT_BYTES = 64 * 1024;
@@ -94835,7 +95998,7 @@ async function checkVersion(options) {
94835
95998
  const now = options.now ?? Date.now;
94836
95999
  const timestamp = now();
94837
96000
  const configDir = ensureKeryxConfigDir(options.cacheDir);
94838
- const cacheFile = path193.join(configDir, "version-check.json");
96001
+ const cacheFile = path195.join(configDir, "version-check.json");
94839
96002
  const cache = parseCache(cacheFile);
94840
96003
  if (cache?.latestVersion !== undefined && cache.successAt !== undefined && timestamp - cache.successAt >= 0 && timestamp - cache.successAt < SUCCESS_CACHE_TTL_MS) {
94841
96004
  return resultFor(options.currentVersion, current, cache.latestVersion, "cache");
@@ -94903,6 +96066,364 @@ async function checkVersion(options) {
94903
96066
  return resultFor(options.currentVersion, current, latestVersion, "registry");
94904
96067
  }
94905
96068
 
96069
+ // src/tui/shell-chrome.ts
96070
+ init_debug_log();
96071
+
96072
+ // src/tui/renderer-debug.ts
96073
+ init_debug_log();
96074
+ import { spawnSync as spawnSync2 } from "child_process";
96075
+ import { readFileSync as readFileSync10, readlinkSync as readlinkSync2 } from "fs";
96076
+
96077
+ // src/tui/stdin-guard.ts
96078
+ init_debug_log();
96079
+ import { openSync as openSync3, readlinkSync } from "fs";
96080
+ import tty from "tty";
96081
+ var resolveStdin = (source) => typeof source === "function" ? source() : source;
96082
+ function stdinIsDead(stdin) {
96083
+ return stdin.destroyed === true || stdin.readableEnded === true;
96084
+ }
96085
+ function openTerminalInput() {
96086
+ let target = "/dev/tty";
96087
+ if (process.platform === "linux") {
96088
+ try {
96089
+ const link = readlinkSync("/proc/self/fd/0");
96090
+ if (link.startsWith("/dev/pts/") || link.startsWith("/dev/tty")) {
96091
+ target = link;
96092
+ }
96093
+ } catch {}
96094
+ }
96095
+ try {
96096
+ const fd = openSync3(target, "r");
96097
+ const stream = new tty.ReadStream(fd);
96098
+ stream.setRawMode(false);
96099
+ stream.setRawMode(true);
96100
+ debugEvent("stdin.reopened", { path: target, fd });
96101
+ return stream;
96102
+ } catch (error) {
96103
+ debugEvent("stdin.reopen-failed", { path: target, error: error instanceof Error ? error.message : String(error) });
96104
+ return;
96105
+ }
96106
+ }
96107
+ var rendererListener;
96108
+ function stdinSnapshot(stdin) {
96109
+ const safe = (read) => {
96110
+ try {
96111
+ return read();
96112
+ } catch (error) {
96113
+ return `error: ${error instanceof Error ? error.message : String(error)}`;
96114
+ }
96115
+ };
96116
+ return {
96117
+ isTTY: safe(() => stdin.isTTY === true),
96118
+ isRaw: safe(() => stdin.isRaw),
96119
+ paused: safe(() => stdin.isPaused()),
96120
+ flowing: safe(() => stdin.readableFlowing),
96121
+ ended: safe(() => stdin.readableEnded),
96122
+ destroyed: safe(() => stdin.destroyed),
96123
+ buffered: safe(() => stdin.readableLength),
96124
+ dataListeners: safe(() => stdin.listenerCount("data")),
96125
+ readableListeners: safe(() => stdin.listenerCount("readable")),
96126
+ rendererAttached: safe(() => rendererListener === undefined ? undefined : stdin.listeners("data").includes(rendererListener))
96127
+ };
96128
+ }
96129
+ function stdinLooksStalled(snapshot) {
96130
+ return snapshot.paused === true || snapshot.flowing === false || snapshot.ended === true || snapshot.destroyed === true || snapshot.dataListeners === 0 || snapshot.rendererAttached === false || snapshot.isTTY === true && snapshot.isRaw === false;
96131
+ }
96132
+ function registerRendererInputListener(listener) {
96133
+ rendererListener = listener;
96134
+ }
96135
+ function recoverStdin(stdin, opts = {}) {
96136
+ const actions = [];
96137
+ const listener = opts.listener ?? rendererListener;
96138
+ const step = (name, run) => {
96139
+ try {
96140
+ run();
96141
+ actions.push(name);
96142
+ } catch (error) {
96143
+ actions.push(`${name} failed: ${error instanceof Error ? error.message : String(error)}`);
96144
+ }
96145
+ };
96146
+ if (stdin.isTTY === true && typeof stdin.setRawMode === "function" && stdin.isRaw !== true) {
96147
+ step("setRawMode(true)", () => stdin.setRawMode?.(true));
96148
+ }
96149
+ if (listener !== undefined && !stdin.listeners("data").includes(listener)) {
96150
+ step("re-attach renderer data listener", () => stdin.on("data", listener));
96151
+ }
96152
+ if (opts.cycle === true) {
96153
+ step("pause()", () => stdin.pause());
96154
+ }
96155
+ step("resume()", () => stdin.resume());
96156
+ return actions;
96157
+ }
96158
+ function instrumentStdin(stdin) {
96159
+ const target = stdin;
96160
+ const restores = [];
96161
+ const wrap = (method, describe = () => ({})) => {
96162
+ const original = target[method];
96163
+ if (typeof original !== "function") {
96164
+ return;
96165
+ }
96166
+ const fn = original;
96167
+ target[method] = function(...args) {
96168
+ debugEvent(`stdin.${method}`, { ...describe(args), before: stdinSnapshot(stdin), stack: callerStack() });
96169
+ return fn.apply(this, args);
96170
+ };
96171
+ restores.push(() => {
96172
+ target[method] = original;
96173
+ });
96174
+ };
96175
+ const eventName = (args) => ({ event: String(args[0]) });
96176
+ wrap("pause");
96177
+ wrap("resume");
96178
+ wrap("setRawMode", (args) => ({ mode: args[0] }));
96179
+ wrap("removeListener", eventName);
96180
+ wrap("off", eventName);
96181
+ wrap("removeAllListeners", eventName);
96182
+ wrap("destroy");
96183
+ wrap("unref");
96184
+ wrap("unpipe");
96185
+ wrap("setEncoding", (args) => ({ encoding: args[0] }));
96186
+ const lifecycle = [];
96187
+ for (const event of ["end", "close", "error", "pause", "resume"]) {
96188
+ const handler = (...args) => {
96189
+ debugEvent(`stdin.event.${event}`, { args: args.map((a) => a instanceof Error ? a : String(a)), state: stdinSnapshot(stdin) });
96190
+ };
96191
+ lifecycle.push([event, handler]);
96192
+ }
96193
+ const onData = (chunk) => {
96194
+ debugEvent("stdin.data", summarizeInputChunk(chunk));
96195
+ };
96196
+ lifecycle.push(["data", onData]);
96197
+ for (const [event, handler] of lifecycle) {
96198
+ if (event === "error" && stdin.listenerCount("error") === 0) {
96199
+ continue;
96200
+ }
96201
+ stdin.on(event, handler);
96202
+ }
96203
+ debugEvent("stdin.instrumented", { state: stdinSnapshot(stdin) });
96204
+ return () => {
96205
+ for (const restore of restores)
96206
+ restore();
96207
+ for (const [event, handler] of lifecycle)
96208
+ stdin.off?.(event, handler);
96209
+ };
96210
+ }
96211
+ function startStdinHeartbeat(source, opts = {}) {
96212
+ const intervalMs = opts.intervalMs ?? 1000;
96213
+ const cooldownMs = opts.cooldownMs ?? 1e4;
96214
+ let lastRecovery = 0;
96215
+ const timer = setInterval(() => {
96216
+ const stdin = resolveStdin(source);
96217
+ const state = stdinSnapshot(stdin);
96218
+ const stalled = stdinLooksStalled(state);
96219
+ debugEvent("heartbeat", { stdin: state, stalled, ...opts.extra?.() ?? {} });
96220
+ if (stalled && (opts.shouldRecover?.() ?? true) && Date.now() - lastRecovery > cooldownMs) {
96221
+ lastRecovery = Date.now();
96222
+ const actions = opts.recover?.() ?? recoverStdin(stdin, { cycle: true });
96223
+ debugEvent("stdin.stall.recovered-in-process", { before: state, actions, after: stdinSnapshot(resolveStdin(source)) });
96224
+ }
96225
+ }, intervalMs);
96226
+ timer.unref?.();
96227
+ return () => clearInterval(timer);
96228
+ }
96229
+ function installInputRecoverySignal(source, onRecovered, shouldRecover = () => true, recover) {
96230
+ if (process.platform === "win32") {
96231
+ return () => {};
96232
+ }
96233
+ const handler = () => {
96234
+ const stdin = resolveStdin(source);
96235
+ if (!shouldRecover()) {
96236
+ debugEvent("signal.SIGUSR2.skipped", { reason: "renderer suspended or destroyed", stdin: stdinSnapshot(stdin) });
96237
+ return;
96238
+ }
96239
+ const before = stdinSnapshot(stdin);
96240
+ const actions = recover?.() ?? recoverStdin(stdin, { cycle: true });
96241
+ const after = stdinSnapshot(resolveStdin(source));
96242
+ debugEvent("signal.SIGUSR2.recover", { before, actions, after, debug: isDebugEnabled() });
96243
+ try {
96244
+ onRecovered?.(actions);
96245
+ } catch {}
96246
+ };
96247
+ process.on("SIGUSR2", handler);
96248
+ return () => {
96249
+ process.off("SIGUSR2", handler);
96250
+ };
96251
+ }
96252
+
96253
+ // src/tui/renderer-debug.ts
96254
+ var MAX_REOPENS_PER_MINUTE = 5;
96255
+ function eofDiagnostics() {
96256
+ if (process.platform !== "linux") {
96257
+ return {};
96258
+ }
96259
+ let tty;
96260
+ try {
96261
+ tty = readlinkSync2("/proc/self/fd/0");
96262
+ } catch {
96263
+ tty = undefined;
96264
+ }
96265
+ let stat;
96266
+ try {
96267
+ stat = readFileSync10("/proc/self/stat", "utf8").trim();
96268
+ } catch {
96269
+ stat = "";
96270
+ }
96271
+ const fields = stat.replace(/^.*\)\s+/, "").split(" ");
96272
+ const stty = tty !== undefined ? spawnSync2("stty", ["-a", "-F", tty], { encoding: "utf8", timeout: 1500 }).stdout?.trim() : undefined;
96273
+ return { tty, pgrp: fields[2], session: fields[3], tpgid: fields[5], stty };
96274
+ }
96275
+ function attachRendererGuards(r, initial = process.stdin, deps = {}) {
96276
+ const internals = r;
96277
+ const listener = internals.stdinListener;
96278
+ registerRendererInputListener(listener);
96279
+ const shouldRecover = () => !r.isDestroyed && internals.controlState !== "explicit_suspended";
96280
+ const teardown = [];
96281
+ let current = initial;
96282
+ let unwatch = () => {};
96283
+ let undoInstrument = () => {};
96284
+ const reopenTimes = [];
96285
+ const reopen = (reason) => {
96286
+ const now = Date.now();
96287
+ while (reopenTimes.length > 0 && now - (reopenTimes[0] ?? 0) > 60000)
96288
+ reopenTimes.shift();
96289
+ if (reopenTimes.length >= MAX_REOPENS_PER_MINUTE) {
96290
+ debugEvent("stdin.reopen-gave-up", { reason, reopensLastMinute: reopenTimes.length });
96291
+ return ["reopen skipped: rate limit"];
96292
+ }
96293
+ reopenTimes.push(now);
96294
+ const next = (deps.openInput ?? openTerminalInput)();
96295
+ if (next === undefined) {
96296
+ return ["reopen failed"];
96297
+ }
96298
+ const previous = current;
96299
+ unwatch();
96300
+ undoInstrument();
96301
+ undoInstrument = () => {};
96302
+ if (listener !== undefined) {
96303
+ try {
96304
+ previous.off?.("data", listener);
96305
+ } catch {}
96306
+ next.on("data", listener);
96307
+ }
96308
+ current = next;
96309
+ internals.stdin = next;
96310
+ watch(next);
96311
+ if (isDebugEnabled()) {
96312
+ undoInstrument = instrumentStdin(next);
96313
+ }
96314
+ next.resume();
96315
+ debugEvent("stdin.replaced", { reason, state: stdinSnapshot(next) });
96316
+ r.requestRender();
96317
+ return [`reopened terminal input (${reason})`];
96318
+ };
96319
+ function watch(stream) {
96320
+ const onDead = (event) => () => {
96321
+ if (current !== stream) {
96322
+ return;
96323
+ }
96324
+ if (isDebugEnabled()) {
96325
+ debugEvent(`stdin.dead.${event}`, { state: stdinSnapshot(stream), ...eofDiagnostics() });
96326
+ }
96327
+ if (!shouldRecover()) {
96328
+ return;
96329
+ }
96330
+ setTimeout(() => {
96331
+ if (current === stream && shouldRecover()) {
96332
+ reopen(event);
96333
+ }
96334
+ }, 0);
96335
+ };
96336
+ const onEnd = onDead("end");
96337
+ const onClose = onDead("close");
96338
+ stream.on("end", onEnd);
96339
+ stream.on("close", onClose);
96340
+ unwatch = () => {
96341
+ stream.off?.("end", onEnd);
96342
+ stream.off?.("close", onClose);
96343
+ };
96344
+ }
96345
+ watch(current);
96346
+ const recover = () => stdinIsDead(current) ? reopen("recover") : recoverStdin(current, { cycle: true });
96347
+ teardown.push(installInputRecoverySignal(() => current, () => {
96348
+ r.requestRender();
96349
+ }, shouldRecover, recover));
96350
+ teardown.push(() => {
96351
+ unwatch();
96352
+ undoInstrument();
96353
+ registerRendererInputListener(undefined);
96354
+ if (current !== initial) {
96355
+ current.destroy?.();
96356
+ }
96357
+ });
96358
+ if (!isDebugEnabled()) {
96359
+ return () => {
96360
+ for (const fn of teardown.splice(0))
96361
+ fn();
96362
+ };
96363
+ }
96364
+ debugEvent("renderer.created", {
96365
+ controlState: internals.controlState,
96366
+ width: r.width,
96367
+ height: r.height,
96368
+ hasStdinListener: internals.stdinListener !== undefined
96369
+ });
96370
+ undoInstrument = instrumentStdin(current);
96371
+ teardown.push(startStdinHeartbeat(() => current, {
96372
+ shouldRecover,
96373
+ recover,
96374
+ extra: () => ({
96375
+ controlState: internals.controlState,
96376
+ focused: internals.currentFocusedRenderable?.id
96377
+ })
96378
+ }));
96379
+ const onKey = (key) => {
96380
+ const printable = key.sequence.length === 1 && key.sequence >= " " && !key.ctrl && !key.meta;
96381
+ queueMicrotask(() => {
96382
+ debugEvent("key", {
96383
+ name: printable ? "<printable>" : key.name,
96384
+ ctrl: key.ctrl,
96385
+ meta: key.meta,
96386
+ shift: key.shift,
96387
+ prevented: key.defaultPrevented,
96388
+ stopped: key.propagationStopped,
96389
+ focused: internals.currentFocusedRenderable?.id
96390
+ });
96391
+ });
96392
+ };
96393
+ internals._internalKeyInput?.onInternal("keypress", onKey);
96394
+ teardown.push(() => internals._internalKeyInput?.offInternal("keypress", onKey));
96395
+ const emitter = r;
96396
+ for (const event of ["resize", "focus", "blur", "capabilities"]) {
96397
+ const fn = (...args) => {
96398
+ debugEvent(`renderer.${event}`, event === "capabilities" ? {} : { args: args.map(String) });
96399
+ };
96400
+ emitter.on?.(event, fn);
96401
+ teardown.push(() => emitter.off?.(event, fn));
96402
+ }
96403
+ const target = r;
96404
+ for (const method of ["suspend", "resume", "pause", "stop", "destroy"]) {
96405
+ const original = target[method];
96406
+ if (typeof original !== "function")
96407
+ continue;
96408
+ const fn = original;
96409
+ target[method] = function(...args) {
96410
+ debugEvent(`renderer.${method}`, { controlState: internals.controlState, stack: callerStack() });
96411
+ return fn.apply(this, args);
96412
+ };
96413
+ teardown.push(() => {
96414
+ target[method] = original;
96415
+ });
96416
+ }
96417
+ return () => {
96418
+ debugEvent("renderer.teardown");
96419
+ for (const fn of teardown.splice(0)) {
96420
+ try {
96421
+ fn();
96422
+ } catch {}
96423
+ }
96424
+ };
96425
+ }
96426
+
94906
96427
  // src/tui/shell-chrome.ts
94907
96428
  function onKeypress3(r, handler) {
94908
96429
  r._internalKeyInput.onInternal("keypress", handler);
@@ -95008,14 +96529,20 @@ function prefixFilter(commands, query) {
95008
96529
  return commands.filter((c) => c.name.slice(1).toLowerCase().startsWith(needle));
95009
96530
  }
95010
96531
  async function createShellRenderer(otui, opts = {}) {
95011
- return await otui.createCliRenderer({
96532
+ const guards = {};
96533
+ const renderer = await otui.createCliRenderer({
95012
96534
  exitOnCtrlC: true,
95013
96535
  screenMode: "alternate-screen",
95014
96536
  clearOnShutdown: true,
95015
96537
  useMouse: true,
95016
96538
  backgroundColor: getTheme().bg,
95017
- ...opts.onDestroy !== undefined ? { onDestroy: opts.onDestroy } : {}
96539
+ onDestroy: () => {
96540
+ guards.detach?.();
96541
+ opts.onDestroy?.();
96542
+ }
95018
96543
  });
96544
+ guards.detach = attachRendererGuards(renderer);
96545
+ return renderer;
95019
96546
  }
95020
96547
  async function createShellChrome(otui, renderer, opts) {
95021
96548
  const r = renderer;
@@ -95178,10 +96705,12 @@ async function createShellChrome(otui, renderer, opts) {
95178
96705
  };
95179
96706
  const withOverlay = async (run) => {
95180
96707
  overlayDepth += 1;
96708
+ debugEvent("overlay.enter", { depth: overlayDepth, dockVisible: dock.visible });
95181
96709
  try {
95182
96710
  return await run();
95183
96711
  } finally {
95184
96712
  overlayDepth -= 1;
96713
+ debugEvent("overlay.exit", { depth: overlayDepth, dockVisible: dock.visible });
95185
96714
  }
95186
96715
  };
95187
96716
  const addOverlaySource = (isActive) => {
@@ -95430,9 +96959,11 @@ async function createShellChrome(otui, renderer, opts) {
95430
96959
  });
95431
96960
  textarea.onSubmit = () => {
95432
96961
  if (overlayActive()) {
96962
+ debugEvent("composer.submit-blocked", { reason: "overlay active", dockVisible: dock.visible, overlayDepth });
95433
96963
  return;
95434
96964
  }
95435
96965
  const line = input.value.trim();
96966
+ debugEvent("composer.submit", { chars: line.length, slash: line.startsWith("/") });
95436
96967
  input.value = "";
95437
96968
  hideMenu();
95438
96969
  syncComposerHeight();
@@ -95550,6 +97081,15 @@ async function createShellChrome(otui, renderer, opts) {
95550
97081
  appliedTheme = theme;
95551
97082
  };
95552
97083
  const unsubTheme = onThemeChange((theme) => applyTheme(theme));
97084
+ {
97085
+ const run = currentDebugRun();
97086
+ if (run !== undefined) {
97087
+ setTimeout(() => {
97088
+ if (alive)
97089
+ showToast(`debug log: ${run.dir}`);
97090
+ }, 0);
97091
+ }
97092
+ }
95553
97093
  return {
95554
97094
  renderer: r,
95555
97095
  root: rootRow,
@@ -97782,7 +99322,7 @@ function parseGitHubRemote(remote) {
97782
99322
  }
97783
99323
  function runGitText(args, cwd) {
97784
99324
  try {
97785
- const proc = spawnSync2("git", args, {
99325
+ const proc = spawnSync3("git", args, {
97786
99326
  cwd,
97787
99327
  encoding: "utf8",
97788
99328
  stdio: ["ignore", "pipe", "pipe"],
@@ -97799,7 +99339,7 @@ function runGitText(args, cwd) {
97799
99339
  }
97800
99340
  function runGhJson(repo, branch, cwd) {
97801
99341
  try {
97802
- const proc = spawnSync2("gh", ["pr", "view", branch, "--repo", repo, "--json", "url"], {
99342
+ const proc = spawnSync3("gh", ["pr", "view", branch, "--repo", repo, "--json", "url"], {
97803
99343
  cwd,
97804
99344
  encoding: "utf8",
97805
99345
  stdio: ["ignore", "pipe", "pipe"],
@@ -98114,6 +99654,7 @@ function attachBlockIo(io, addBlock, chrome = {}, thinkDisplay = () => "auto") {
98114
99654
  });
98115
99655
  };
98116
99656
  io.onToolCall = (name, input) => {
99657
+ debugEvent("tool.call", { name, inputChars: input.length });
98117
99658
  chrome.onToolCall?.(name, input);
98118
99659
  const args = summarizeToolArgs(input);
98119
99660
  addBlock({
@@ -98125,6 +99666,7 @@ function attachBlockIo(io, addBlock, chrome = {}, thinkDisplay = () => "auto") {
98125
99666
  }, { hint: "ctrl+o", tone: "cyan" });
98126
99667
  };
98127
99668
  io.onToolResult = (name, result) => {
99669
+ debugEvent("tool.result", { name, isError: result.isError === true, outputChars: result.output.length });
98128
99670
  chrome.onToolResult?.(name, result);
98129
99671
  const { summary, lineCount, hidden } = collapseToolOutput(result.output);
98130
99672
  const more = hidden > 0 ? ` \xB7 +${hidden} more` : "";
@@ -98280,8 +99822,8 @@ async function applyRuntimeSwitchToSlate(params) {
98280
99822
  if (params.slateSession === undefined || !params.slateSession.opened) {
98281
99823
  return false;
98282
99824
  }
98283
- const result = await recordSlateTouch(params.slateSession.dir, [], { runtime: params.runtime });
98284
- if (!result.changed) {
99825
+ const result = await recordSlateSessionTouch(params.slateSession, [], { runtime: params.runtime });
99826
+ if (result === undefined || !result.changed) {
98285
99827
  return false;
98286
99828
  }
98287
99829
  params.history.push({
@@ -98292,6 +99834,9 @@ async function applyRuntimeSwitchToSlate(params) {
98292
99834
  params.onHistoryChange?.("tool");
98293
99835
  return true;
98294
99836
  }
99837
+ function freshSlateSessionRef(sessionDir, cwd) {
99838
+ return { dir: sessionDir, cwd, opened: false };
99839
+ }
98295
99840
  function fmtTokens(n) {
98296
99841
  return n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n);
98297
99842
  }
@@ -99248,21 +100793,40 @@ function formatSessionDate(iso) {
99248
100793
  const short = iso.trim().replace("T", " ");
99249
100794
  return short.length >= 16 ? short.slice(0, 16) : short;
99250
100795
  }
99251
- function pickSessionInTui(otui, rOrChrome, sessions) {
99252
- const chrome = isModalChrome(rOrChrome) ? rOrChrome : undefined;
99253
- const r = chrome !== undefined ? chrome.renderer : rOrChrome;
99254
- const all = sessions.map((s) => {
100796
+ function sessionPickerOptions(sessions, leaseState = () => "free") {
100797
+ return sessions.map((s) => {
99255
100798
  const created = formatSessionDate(s.createdAt);
99256
100799
  const updated = formatSessionDate(s.updatedAt);
99257
100800
  const short = shortSessionId(s.id);
99258
100801
  const title = s.title.length > 52 ? `${s.title.slice(0, 49)}\u2026` : s.title;
100802
+ const label = withLeaseMarker(`${short} \xB7 ${title}`, leaseState(s.id));
99259
100803
  return {
99260
100804
  value: s.id,
99261
- label: `${short} \xB7 ${title}`,
100805
+ label,
99262
100806
  description: `${s.projectPath} \xB7 created ${created} \xB7 updated ${updated}`,
99263
- search: `${s.id} ${short} ${s.projectPath} ${s.title} ${created} ${updated}`.toLowerCase()
100807
+ search: `${s.id} ${label} ${s.projectPath} ${s.title} ${created} ${updated}`.toLowerCase()
99264
100808
  };
99265
100809
  });
100810
+ }
100811
+ function startupSessionChoices(rows, leaseState = () => "free") {
100812
+ return [
100813
+ {
100814
+ id: "__new__",
100815
+ label: "New session",
100816
+ description: "Start fresh (old sessions stay on disk)",
100817
+ recommended: true
100818
+ },
100819
+ ...rows.map((s) => ({
100820
+ id: s.id,
100821
+ label: withLeaseMarker(s.title.length > 40 ? `${s.title.slice(0, 37)}\u2026` : s.title, leaseState(s.id)),
100822
+ description: `${shortSessionId(s.id)} \xB7 ctx ${s.messageCount} \xB7 ${s.updatedAt.slice(0, 16).replace("T", " ")}`
100823
+ }))
100824
+ ];
100825
+ }
100826
+ function pickSessionInTui(otui, rOrChrome, sessions, leaseState) {
100827
+ const chrome = isModalChrome(rOrChrome) ? rOrChrome : undefined;
100828
+ const r = chrome !== undefined ? chrome.renderer : rOrChrome;
100829
+ const all = sessionPickerOptions(sessions, leaseState);
99266
100830
  const listSpec = {
99267
100831
  idPrefix: "sp",
99268
100832
  items: all,
@@ -99369,11 +100933,13 @@ async function launchTuiAgentShell(opts) {
99369
100933
  let liveJobs;
99370
100934
  let detachExternal;
99371
100935
  const foregroundOperation = createForegroundOperationOwner();
100936
+ const sessionLease = createTuiLeaseHolder();
99372
100937
  try {
99373
100938
  const r = renderer = await createShellRenderer(otui, {
99374
100939
  onDestroy: () => {
99375
100940
  foregroundOperation.cancel("renderer destroyed");
99376
100941
  foregroundOperation.dispose();
100942
+ sessionLease.release();
99377
100943
  mountedChrome?.destroy();
99378
100944
  setAskUserHost(undefined);
99379
100945
  setSubagentFleetListener(undefined);
@@ -99406,7 +100972,8 @@ async function launchTuiAgentShell(opts) {
99406
100972
  saveShellConfig(sel.baseUrl === undefined ? { provider: sel.provider, model: sel.model } : { provider: sel.provider, model: sel.model, baseUrl: sel.baseUrl });
99407
100973
  let currentSel = sel;
99408
100974
  const suggestionGate = new NextStepSuggestionGate;
99409
- let deps = await opts.makeAgentDeps(sel, () => slateSession);
100975
+ const liveSlateSession = () => whilePersisting(slateSession, () => sessionLease.canPersist());
100976
+ let deps = await opts.makeAgentDeps(sel, liveSlateSession);
99410
100977
  liveDeps = deps;
99411
100978
  let reasoningOverride;
99412
100979
  let thinkDisplayMode = resolveThinkDisplayMode(loadShellConfig().thinkDisplay);
@@ -99432,6 +100999,7 @@ async function launchTuiAgentShell(opts) {
99432
100999
  let busyStartedAt = 0;
99433
101000
  const setBusyPhase = (phase) => {
99434
101001
  busyPhase = phase;
101002
+ debugEvent("busy.phase", { phase });
99435
101003
  chrome.setBusyPhase(phase);
99436
101004
  };
99437
101005
  const startBusy = (phase = "waiting for model") => {
@@ -99478,7 +101046,7 @@ async function launchTuiAgentShell(opts) {
99478
101046
  }));
99479
101047
  };
99480
101048
  const refreshWorkspaceSidebar = async () => {
99481
- const dir = slateSession?.dir;
101049
+ const dir = liveSlateSession()?.dir;
99482
101050
  const workspaceId = dir !== undefined ? (await readSlate(dir).catch(() => {
99483
101051
  return;
99484
101052
  }))?.workspaceId : undefined;
@@ -99654,6 +101222,7 @@ async function launchTuiAgentShell(opts) {
99654
101222
  })();
99655
101223
  };
99656
101224
  const setMainAgent = (status, detail) => {
101225
+ debugEvent("main-agent", { status, detail });
99657
101226
  fleet.upsert({
99658
101227
  id: MAIN_AGENT_ID,
99659
101228
  label: "main",
@@ -100099,7 +101668,13 @@ async function launchTuiAgentShell(opts) {
100099
101668
  let nextArchiveIndex = 0;
100100
101669
  let sessionPersistTimer;
100101
101670
  let slateSession;
101671
+ const bindSlateToLiveSession = () => {
101672
+ slateSession = freshSlateSessionRef(liveSession.dir, sessionCwd);
101673
+ };
100102
101674
  const onContextCompaction = (r) => {
101675
+ if (!sessionLease.canPersist()) {
101676
+ return;
101677
+ }
100103
101678
  const persisted = persistCompacted(liveSession, r.context, archive, {
100104
101679
  provider: currentSel.provider,
100105
101680
  model: currentSel.model
@@ -100131,7 +101706,7 @@ async function launchTuiAgentShell(opts) {
100131
101706
  return;
100132
101707
  }
100133
101708
  chrome.hideMenu();
100134
- const pickId = await chrome.withOverlay(() => pickSessionInTui(otui, chrome, rows));
101709
+ const pickId = await chrome.withOverlay(() => pickSessionInTui(otui, chrome, rows, leaseState));
100135
101710
  input.focus();
100136
101711
  if (pickId === undefined) {
100137
101712
  return;
@@ -100144,98 +101719,142 @@ async function launchTuiAgentShell(opts) {
100144
101719
  }
100145
101720
  return found;
100146
101721
  };
101722
+ const leasedOpen = (target = {}) => sessionLease.switchTo(() => openLeasedSession({
101723
+ cwd: sessionCwd,
101724
+ ...target,
101725
+ provider: currentSel.provider,
101726
+ model: currentSel.model
101727
+ }));
101728
+ const leaseState = leaseStateLookup(sessionCwd);
101729
+ const showOpenedNotes = (opened) => {
101730
+ if (opened.skipped !== undefined) {
101731
+ transcript.add(new otui.TextRenderable(r, {
101732
+ id: `sessskip${uid++}`,
101733
+ content: otui.t`${otui.yellow(describeSkippedSession(opened.skipped).replace(/\n+$/, ""))}`,
101734
+ marginTop: 1
101735
+ }));
101736
+ }
101737
+ if (opened.archiveDegraded !== undefined) {
101738
+ transcript.add(new otui.TextRenderable(r, {
101739
+ id: `sessdeg${uid++}`,
101740
+ content: otui.t`${otui.yellow(`archive unavailable \u2014 resumed from the active context (${opened.archiveDegraded})`)}`,
101741
+ marginTop: 1
101742
+ }));
101743
+ }
101744
+ if (opened.resumed) {
101745
+ transcript.add(new otui.TextRenderable(r, {
101746
+ id: `sess${uid++}`,
101747
+ content: otui.t`${otui.dim(`session ${shortSessionId(liveSession.summary.id)} \xB7 ${liveSession.summary.title} \xB7 ctx ${history.length} \xB7 archive ${archive.length}`)}`,
101748
+ marginTop: 1
101749
+ }));
101750
+ for (const m of history.filter((x) => x.role === "user").slice(-5)) {
101751
+ const t = m.content.length > 100 ? `${m.content.slice(0, 97)}\u2026` : m.content;
101752
+ transcript.add(new otui.TextRenderable(r, {
101753
+ id: `sessu${uid++}`,
101754
+ content: otui.t`${otui.dim(` \u276F ${t}`)}`
101755
+ }));
101756
+ }
101757
+ }
101758
+ };
101759
+ let startupCancelled = false;
101760
+ let viewedReadOnly = false;
100147
101761
  try {
100148
101762
  if (opts.session?.pickOnStart === true && opts.session.resumeId === undefined) {
100149
101763
  const rows = listSessions(sessionCwd).slice(0, 12);
100150
101764
  if (rows.length === 0) {
100151
- applyOpened(openSession({
100152
- cwd: sessionCwd,
100153
- provider: currentSel.provider,
100154
- model: currentSel.model
100155
- }));
101765
+ applyOpened(leasedOpen());
100156
101766
  } else {
100157
101767
  chrome.hideMenu();
100158
101768
  const pickId = await showComposerChoice(otui, r, chrome.dock, {
100159
101769
  title: "Resume session (this project)",
100160
101770
  subtitle: "Esc = new session",
100161
101771
  cancelId: "__new__",
100162
- options: [
100163
- {
100164
- id: "__new__",
100165
- label: "New session",
100166
- description: "Start fresh (old sessions stay on disk)",
100167
- recommended: true
100168
- },
100169
- ...rows.map((s) => ({
100170
- id: s.id,
100171
- label: s.title.length > 40 ? `${s.title.slice(0, 37)}\u2026` : s.title,
100172
- description: `${shortSessionId(s.id)} \xB7 ctx ${s.messageCount} \xB7 ${s.updatedAt.slice(0, 16).replace("T", " ")}`
100173
- }))
100174
- ]
101772
+ options: startupSessionChoices(rows, leaseState)
100175
101773
  });
100176
101774
  input.focus();
100177
101775
  if (pickId === "__new__") {
100178
- applyOpened(openSession({
100179
- cwd: sessionCwd,
100180
- provider: currentSel.provider,
100181
- model: currentSel.model
100182
- }));
101776
+ applyOpened(leasedOpen());
100183
101777
  } else {
100184
- applyOpened(openSession({
100185
- cwd: sessionCwd,
100186
- resumeId: pickId,
100187
- provider: currentSel.provider,
100188
- model: currentSel.model
100189
- }));
101778
+ const opened = leasedOpen({ resumeId: pickId });
101779
+ applyOpened(opened);
101780
+ showOpenedNotes(opened);
100190
101781
  }
100191
101782
  }
100192
101783
  } else {
100193
- const opened = openSession({
100194
- cwd: sessionCwd,
101784
+ const opened = leasedOpen({
100195
101785
  ...opts.session?.continueLast === true ? { continueLast: true } : {},
100196
101786
  ...opts.session?.resumeId !== undefined ? { resumeId: opts.session.resumeId } : {},
100197
- provider: currentSel.provider,
100198
- model: currentSel.model
101787
+ ...opts.session?.fork === true ? { fork: true } : {},
101788
+ ...opts.session?.takeOver === true ? { takeOver: true } : {}
100199
101789
  });
100200
101790
  applyOpened(opened);
100201
- if (opened.archiveDegraded !== undefined) {
100202
- transcript.add(new otui.TextRenderable(r, {
100203
- id: `sessdeg${uid++}`,
100204
- content: otui.t`${otui.yellow(`archive unavailable \u2014 resumed from the active context (${opened.archiveDegraded})`)}`,
100205
- marginTop: 1
100206
- }));
100207
- }
100208
- if (opened.resumed) {
100209
- transcript.add(new otui.TextRenderable(r, {
100210
- id: `sess${uid++}`,
100211
- content: otui.t`${otui.dim(`session ${shortSessionId(liveSession.summary.id)} \xB7 ${liveSession.summary.title} \xB7 ctx ${history.length} \xB7 archive ${archive.length}`)}`,
100212
- marginTop: 1
100213
- }));
100214
- for (const m of history.filter((x) => x.role === "user").slice(-5)) {
100215
- const t = m.content.length > 100 ? `${m.content.slice(0, 97)}\u2026` : m.content;
100216
- transcript.add(new otui.TextRenderable(r, {
100217
- id: `sessu${uid++}`,
100218
- content: otui.t`${otui.dim(` \u276F ${t}`)}`
100219
- }));
101791
+ showOpenedNotes(opened);
101792
+ }
101793
+ } catch (cause) {
101794
+ let failure = cause;
101795
+ if (cause instanceof SessionLeasedError) {
101796
+ try {
101797
+ const outcome = await resolveLeasedStartup(cause, {
101798
+ choose: async (error) => {
101799
+ chrome.hideMenu();
101800
+ const id = await showComposerChoice(otui, r, chrome.dock, leasedChoiceRequest(error));
101801
+ input.focus();
101802
+ return toLeasedChoice(error, id);
101803
+ },
101804
+ open: (target) => leasedOpen(target),
101805
+ exportSession: (sessionId) => exportSessionMarkdown(sessionCwd, sessionId),
101806
+ showReadOnly: (markdown) => {
101807
+ viewedReadOnly = true;
101808
+ transcript.add(new otui.TextRenderable(r, {
101809
+ id: `sessview${uid++}`,
101810
+ content: otui.t`${otui.dim(`${markdown.replace(/\n+$/, "")}
101811
+
101812
+ \u2014 read-only view; a new session starts below \u2014`)}`,
101813
+ marginTop: 1
101814
+ }));
101815
+ },
101816
+ notice: (text) => {
101817
+ transcript.add(new otui.TextRenderable(r, {
101818
+ id: `sesslease${uid++}`,
101819
+ content: otui.t`${otui.yellow(text)}`,
101820
+ marginTop: 1
101821
+ }));
101822
+ }
101823
+ });
101824
+ if (outcome.kind === "cancelled") {
101825
+ startupCancelled = true;
101826
+ } else {
101827
+ applyOpened(outcome.opened);
101828
+ showOpenedNotes(outcome.opened);
100220
101829
  }
101830
+ failure = undefined;
101831
+ } catch (leaseCause) {
101832
+ failure = leaseCause;
100221
101833
  }
100222
101834
  }
100223
- } catch (cause) {
100224
- transcript.add(new otui.TextRenderable(r, {
100225
- id: `sesserr${uid++}`,
100226
- content: otui.t`${otui.red(cause instanceof Error ? cause.message : String(cause))}`,
100227
- marginTop: 1
100228
- }));
100229
- applyOpened(openSession({
100230
- cwd: sessionCwd,
100231
- provider: currentSel.provider,
100232
- model: currentSel.model
100233
- }));
101835
+ if (failure !== undefined) {
101836
+ transcript.add(new otui.TextRenderable(r, {
101837
+ id: `sesserr${uid++}`,
101838
+ content: otui.t`${otui.red(failure instanceof Error ? failure.message : String(failure))}`,
101839
+ marginTop: 1
101840
+ }));
101841
+ applyOpened(leasedOpen());
101842
+ }
101843
+ }
101844
+ if (startupCancelled) {
101845
+ r.off("theme_mode", onThemeMode);
101846
+ r.destroy();
101847
+ await done;
101848
+ return true;
100234
101849
  }
100235
- slateSession = { dir: liveSession.dir, cwd: sessionCwd, opened: false };
101850
+ bindSlateToLiveSession();
100236
101851
  if (history.length === 0) {
100237
101852
  removeSplash = mountEmptyTranscriptSplash(otui, r, transcript);
100238
101853
  }
101854
+ if (viewedReadOnly) {
101855
+ removeSplash?.();
101856
+ removeSplash = undefined;
101857
+ }
100239
101858
  refreshWorkspaceSidebar();
100240
101859
  refreshReviewSidebar();
100241
101860
  const paintSessionHeader = () => {
@@ -100245,7 +101864,19 @@ async function launchTuiAgentShell(opts) {
100245
101864
  const cx = liveSession.summary.compactCount > 0 ? ` \xB7 c\xD7${liveSession.summary.compactCount}` : "";
100246
101865
  chrome.setTitle(`keryx \xB7 ${title} \xB7 ${sid}${cx} \xB7 ${label}`);
100247
101866
  };
101867
+ sessionLease.onLost((message) => {
101868
+ detachSlateSession(slateSession);
101869
+ slateSession = undefined;
101870
+ if (sessionPersistTimer !== undefined) {
101871
+ clearTimeout(sessionPersistTimer);
101872
+ sessionPersistTimer = undefined;
101873
+ }
101874
+ io.onSystem?.(message);
101875
+ });
100248
101876
  const saveSession = () => {
101877
+ if (!sessionLease.canPersist()) {
101878
+ return;
101879
+ }
100249
101880
  liveSession = persistHistory(liveSession, history, {
100250
101881
  archive,
100251
101882
  provider: currentSel.provider,
@@ -100298,12 +101929,17 @@ async function launchTuiAgentShell(opts) {
100298
101929
  usage.resetUsage();
100299
101930
  };
100300
101931
  const startNewSession = (note) => {
101932
+ let opened;
101933
+ try {
101934
+ opened = leasedOpen();
101935
+ } catch (cause) {
101936
+ io.onSystem?.(`Could not start a new session: ${cause instanceof Error ? cause.message : String(cause)}
101937
+ Staying in the current session.
101938
+ `);
101939
+ return false;
101940
+ }
100301
101941
  resetSessionSurface();
100302
- liveSession = createSession({
100303
- cwd: sessionCwd,
100304
- provider: currentSel.provider,
100305
- model: currentSel.model
100306
- });
101942
+ liveSession = opened.handle;
100307
101943
  history = [];
100308
101944
  archive = [];
100309
101945
  nextArchiveIndex = 0;
@@ -100312,6 +101948,7 @@ async function launchTuiAgentShell(opts) {
100312
101948
  io.onSystem?.(`${note}
100313
101949
  `);
100314
101950
  }
101951
+ return true;
100315
101952
  };
100316
101953
  const resumeSessionInteractive = async () => {
100317
101954
  const found = await pickRecentSession();
@@ -100320,19 +101957,18 @@ async function launchTuiAgentShell(opts) {
100320
101957
  }
100321
101958
  let opened;
100322
101959
  try {
100323
- opened = openSession({
100324
- cwd: sessionCwd,
100325
- resumeId: found.id,
100326
- provider: currentSel.provider,
100327
- model: currentSel.model
100328
- });
101960
+ opened = leasedOpen({ resumeId: found.id });
100329
101961
  } catch (cause) {
101962
+ const hint = cause instanceof SessionLeasedError ? `To continue it in this shell, start one with: keryx shell -r ${shortSessionId(found.id)} --fork
101963
+ ` : "";
100330
101964
  io.onSystem?.(`Could not resume ${shortSessionId(found.id)}: ${cause instanceof Error ? cause.message : String(cause)}
100331
- Staying in the current session.
101965
+ ` + hint + `Staying in the current session.
100332
101966
  `);
100333
101967
  return;
100334
101968
  }
100335
101969
  applyOpened(opened, true);
101970
+ bindSlateToLiveSession();
101971
+ refreshWorkspaceSidebar();
100336
101972
  paintSessionHeader();
100337
101973
  if (opened.archiveDegraded !== undefined) {
100338
101974
  io.onSystem?.(`Archive unavailable \u2014 resumed from the active context (${opened.archiveDegraded})
@@ -100394,7 +102030,7 @@ Staying in the current session.
100394
102030
  };
100395
102031
  const showWorkspace = () => {
100396
102032
  (async () => {
100397
- const dir = slateSession?.dir;
102033
+ const dir = liveSlateSession()?.dir;
100398
102034
  const workspaceId = dir !== undefined ? (await readSlate(dir).catch(() => {
100399
102035
  return;
100400
102036
  }))?.workspaceId : undefined;
@@ -100613,7 +102249,7 @@ Staying in the current session.
100613
102249
  };
100614
102250
  const switchTo = async (ns) => {
100615
102251
  currentSel = ns;
100616
- deps = { ...await opts.makeAgentDeps(ns, () => slateSession), onContextCompaction };
102252
+ deps = { ...await opts.makeAgentDeps(ns, liveSlateSession), onContextCompaction };
100617
102253
  liveDeps = deps;
100618
102254
  saveShellConfig(ns.baseUrl === undefined ? { provider: ns.provider, model: ns.model } : { provider: ns.provider, model: ns.model, baseUrl: ns.baseUrl });
100619
102255
  updateModelLabels();
@@ -100901,7 +102537,7 @@ Staying in the current session.
100901
102537
  });
100902
102538
  let answer = "";
100903
102539
  try {
100904
- const base = await opts.makeAgentDeps(currentSel, () => slateSession);
102540
+ const base = await opts.makeAgentDeps(currentSel, liveSlateSession);
100905
102541
  const tools = base.tools.filter((t) => t.definition.risk === "read" && !SIDE_WORKER_DENIED_TOOL_NAMES.has(t.definition.name));
100906
102542
  const sideDeps = {
100907
102543
  ...base,
@@ -101018,6 +102654,7 @@ Staying in the current session.
101018
102654
  await closeSlateSession(slateSession, mintTimestampAttemptId);
101019
102655
  await deps.sweepBackgroundJobs?.();
101020
102656
  jobs.removeAll();
102657
+ sessionLease.release();
101021
102658
  r.off("theme_mode", onThemeMode);
101022
102659
  r.destroy();
101023
102660
  })();
@@ -101206,6 +102843,7 @@ Staying in the current session.
101206
102843
  await closeSlateSession(slateSession, mintTimestampAttemptId);
101207
102844
  await deps.sweepBackgroundJobs?.();
101208
102845
  jobs.removeAll();
102846
+ sessionLease.release();
101209
102847
  r.off("theme_mode", onThemeMode);
101210
102848
  r.destroy();
101211
102849
  })();
@@ -101230,8 +102868,10 @@ Staying in the current session.
101230
102868
  if (command.name === "/clear" || command.name === "/new") {
101231
102869
  (async () => {
101232
102870
  await closeSlateSession(slateSession, mintTimestampAttemptId);
101233
- startNewSession();
101234
- slateSession = { dir: liveSession.dir, cwd: sessionCwd, opened: false };
102871
+ if (!startNewSession()) {
102872
+ return;
102873
+ }
102874
+ bindSlateToLiveSession();
101235
102875
  refreshWorkspaceSidebar();
101236
102876
  refreshReviewSidebar();
101237
102877
  sessions.clear();
@@ -101261,6 +102901,10 @@ Staying in the current session.
101261
102901
  return;
101262
102902
  }
101263
102903
  if (command.name === "/compact") {
102904
+ if (!sessionLease.canPersist()) {
102905
+ io.onSystem?.(LOST_LEASE_COMPACT_REFUSAL);
102906
+ return;
102907
+ }
101264
102908
  const focus = line.trim().split(/\s+/).slice(1).join(" ").trim();
101265
102909
  const packed = compactSession(liveSession, history, archive, {
101266
102910
  keepLastUserTurns: 3,
@@ -101795,6 +103439,7 @@ ${tail}`,
101795
103439
  } catch {
101796
103440
  return false;
101797
103441
  } finally {
103442
+ sessionLease.release();
101798
103443
  await herdr.release();
101799
103444
  try {
101800
103445
  renderer?.destroy();
@@ -102195,6 +103840,7 @@ init_patch_risk();
102195
103840
  init_permission_mode();
102196
103841
  init_agent();
102197
103842
  init_slate_lifecycle();
103843
+ init_slate_lifecycle();
102198
103844
  function reportMcpProblems(runtime) {
102199
103845
  for (const problem of runtime.problems()) {
102200
103846
  console.error(`keryx mcp: ${problem.file} ${problem.message}`);
@@ -102241,6 +103887,132 @@ function readlineAgentHelpText() {
102241
103887
  ` + renderCommandHelp("agent", READLINE_AGENT_COMMANDS) + `Sessions are per-project: keryx shell -c | -r [id] | keryx sessions list
102242
103888
  `;
102243
103889
  }
103890
+ function bareResumeTarget(cwd, emit) {
103891
+ const pick = latestUnleasedSession(cwd);
103892
+ if (pick.skipped !== undefined) {
103893
+ emit(describeSkippedSession(pick.skipped));
103894
+ }
103895
+ return pick.summary?.id;
103896
+ }
103897
+ function leasedOpenOptions(session, cwd, provider, model) {
103898
+ return {
103899
+ cwd,
103900
+ ...session?.continueLast === true ? { continueLast: true } : {},
103901
+ ...session?.resumeId !== undefined ? { resumeId: session.resumeId } : {},
103902
+ ...session?.fork === true ? { fork: true } : {},
103903
+ ...session?.takeOver === true ? { takeOver: true } : {},
103904
+ provider,
103905
+ model
103906
+ };
103907
+ }
103908
+ function openFreshLeasedSession(open, cwd, provider, model) {
103909
+ try {
103910
+ const opened = open({ cwd, provider, model });
103911
+ return { handle: opened.handle, lease: opened.lease };
103912
+ } catch {
103913
+ return;
103914
+ }
103915
+ }
103916
+ async function resolveLeasedChoice(io, error) {
103917
+ const rows = leasedChoiceRows(error);
103918
+ io.write(`${describeLeasedSession(error)}
103919
+ ` + rows.map((row, index) => ` ${index + 1}) ${row.label}
103920
+ `).join(""));
103921
+ for (;; ) {
103922
+ io.write(`Choose [1-${rows.length}, default 1]: `);
103923
+ const line = await io.readLine();
103924
+ if (line === undefined) {
103925
+ io.write(`
103926
+ `);
103927
+ return "cancel";
103928
+ }
103929
+ const answer = line.trim().toLowerCase();
103930
+ if (answer.length === 0) {
103931
+ return "fork";
103932
+ }
103933
+ const byNumber = /^\d+$/.test(answer) ? rows[Number(answer) - 1] : undefined;
103934
+ const byName = rows.find((row) => row.choice === answer || row.choice.replace("-", " ") === answer || row.choice.replace("-", "") === answer);
103935
+ const picked = byNumber ?? byName;
103936
+ if (picked !== undefined) {
103937
+ return picked.choice;
103938
+ }
103939
+ io.write(`Not one of the choices: ${line.trim()}
103940
+ `);
103941
+ }
103942
+ }
103943
+ async function runWithLeaseChoice(session, run, runtime) {
103944
+ let current = session;
103945
+ for (;; ) {
103946
+ try {
103947
+ await run(current);
103948
+ return;
103949
+ } catch (error) {
103950
+ if (!(error instanceof SessionLeasedError)) {
103951
+ throw error;
103952
+ }
103953
+ if (!runtime.isTty) {
103954
+ runtime.writeError(`${error.message}
103955
+ `);
103956
+ return 1;
103957
+ }
103958
+ const choice = await resolveLeasedChoice(runtime.io, error);
103959
+ const base = {
103960
+ cwd: current.cwd,
103961
+ ...current.enabled !== undefined ? { enabled: current.enabled } : {},
103962
+ ...current.leaseBox !== undefined ? { leaseBox: current.leaseBox } : {},
103963
+ ...current.openLeased !== undefined ? { openLeased: current.openLeased } : {}
103964
+ };
103965
+ if (choice === "cancel") {
103966
+ return 0;
103967
+ }
103968
+ if (choice === "view") {
103969
+ try {
103970
+ const render = runtime.exportSession ?? ((cwd, id) => exportSessionMarkdown(cwd, id));
103971
+ runtime.io.write(`${render(current.cwd, error.summary.id)}
103972
+ `);
103973
+ } catch (cause) {
103974
+ runtime.io.write(`Could not render the session: ${cause instanceof Error ? cause.message : String(cause)}
103975
+ `);
103976
+ }
103977
+ current = base;
103978
+ } else if (choice === "fork") {
103979
+ current = { ...base, resumeId: error.summary.id, fork: true };
103980
+ } else {
103981
+ current = { ...base, resumeId: error.summary.id, takeOver: true };
103982
+ }
103983
+ }
103984
+ }
103985
+ }
103986
+ async function runLeasedChatShell(io, deps, run = runShell, notes = []) {
103987
+ const system = (text) => {
103988
+ if (io.onSystem !== undefined) {
103989
+ io.onSystem(text);
103990
+ } else {
103991
+ io.write(text);
103992
+ }
103993
+ };
103994
+ for (const note of notes) {
103995
+ system(note);
103996
+ }
103997
+ if (deps.session === undefined) {
103998
+ await run(io, deps);
103999
+ return;
104000
+ }
104001
+ let iterator;
104002
+ const runtime = {
104003
+ isTty: true,
104004
+ io: {
104005
+ write: system,
104006
+ readLine: async () => {
104007
+ iterator ??= io.lines[Symbol.asyncIterator]();
104008
+ const next = await iterator.next();
104009
+ return next.done === true ? undefined : next.value;
104010
+ }
104011
+ },
104012
+ writeError: system
104013
+ };
104014
+ return runWithLeaseChoice(deps.session, (session) => run(io, { ...deps, session }), runtime);
104015
+ }
102244
104016
  async function runShell(io, deps) {
102245
104017
  let providerName = deps.initial.provider;
102246
104018
  let modelName = deps.initial.model;
@@ -102259,17 +104031,22 @@ async function runShell(io, deps) {
102259
104031
  let live;
102260
104032
  let history = [];
102261
104033
  let archive = [];
104034
+ let lease;
104035
+ const leaseWatch = watchLeaseLoss((message) => system(message));
104036
+ const holdLease = (next) => {
104037
+ lease = next;
104038
+ leaseWatch.track(next);
104039
+ if (deps.session?.leaseBox !== undefined)
104040
+ deps.session.leaseBox.current = next;
104041
+ };
104042
+ const openLeased = deps.session?.openLeased ?? openLeasedSession;
102262
104043
  if (sessionsOn) {
102263
104044
  try {
102264
- const resumeId = deps.session?.resumeId;
102265
- if (resumeId === undefined && deps.session?.continueLast !== true) {}
102266
- const opened = openSession({
102267
- cwd: sessionCwd,
102268
- ...deps.session?.continueLast === true ? { continueLast: true } : {},
102269
- ...resumeId !== undefined ? { resumeId } : {},
102270
- provider: providerName,
102271
- model: modelName
102272
- });
104045
+ const opened = openLeased(leasedOpenOptions(deps.session, sessionCwd, providerName, modelName));
104046
+ holdLease(opened.lease);
104047
+ if (opened.skipped !== undefined) {
104048
+ system(describeSkippedSession(opened.skipped));
104049
+ }
102273
104050
  live = opened.handle;
102274
104051
  history = opened.history;
102275
104052
  archive = opened.archive.length > 0 ? [...opened.archive] : [...opened.history];
@@ -102282,7 +104059,12 @@ async function runShell(io, deps) {
102282
104059
  `);
102283
104060
  }
102284
104061
  } catch (cause) {
102285
- live = createSession({ cwd: sessionCwd, provider: providerName, model: modelName });
104062
+ if (cause instanceof SessionLeasedError) {
104063
+ throw cause;
104064
+ }
104065
+ const fresh = openFreshLeasedSession(openLeased, sessionCwd, providerName, modelName);
104066
+ holdLease(fresh?.lease);
104067
+ live = fresh?.handle ?? createSession({ cwd: sessionCwd, provider: providerName, model: modelName });
102286
104068
  history = [];
102287
104069
  archive = [];
102288
104070
  system(`${cause instanceof Error ? cause.message : String(cause)}
@@ -102290,8 +104072,12 @@ Starting a new session.
102290
104072
  `);
102291
104073
  }
102292
104074
  }
104075
+ const releaseLease = () => {
104076
+ releaseSessionLease(lease);
104077
+ holdLease(undefined);
104078
+ };
102293
104079
  const save = () => {
102294
- if (live === undefined) {
104080
+ if (live === undefined || !leaseWatch.canPersist()) {
102295
104081
  return;
102296
104082
  }
102297
104083
  try {
@@ -102318,6 +104104,7 @@ Starting a new session.
102318
104104
  const command = parts[0] ?? "";
102319
104105
  const argument = parts.slice(1).join(" ");
102320
104106
  if (command === "/exit" || command === "/quit") {
104107
+ releaseLease();
102321
104108
  return;
102322
104109
  }
102323
104110
  if (command === "/help") {
@@ -102377,7 +104164,16 @@ ${formatThemeList(getThemeId())}`);
102377
104164
  }
102378
104165
  if (command === "/clear" || command === "/new") {
102379
104166
  if (sessionsOn) {
102380
- live = createSession({ cwd: sessionCwd, provider: providerName, model: modelName });
104167
+ try {
104168
+ const next = switchLeasedSession(lease, () => openLeased({ cwd: sessionCwd, provider: providerName, model: modelName }));
104169
+ holdLease(next.lease);
104170
+ live = next.handle;
104171
+ } catch (cause) {
104172
+ system(`${cause instanceof Error ? cause.message : String(cause)}
104173
+ Kept the current session.
104174
+ `);
104175
+ continue;
104176
+ }
102381
104177
  history = [];
102382
104178
  archive = [];
102383
104179
  system(`New session ${shortSessionId(live.summary.id)} (previous kept on disk)
@@ -102393,6 +104189,10 @@ ${formatThemeList(getThemeId())}`);
102393
104189
  `);
102394
104190
  continue;
102395
104191
  }
104192
+ if (!leaseWatch.canPersist()) {
104193
+ system(LOST_LEASE_COMPACT_REFUSAL);
104194
+ continue;
104195
+ }
102396
104196
  const focus = argument.trim();
102397
104197
  const packed = compactSession(live, history, archive, {
102398
104198
  keepLastUserTurns: 3,
@@ -102528,6 +104328,7 @@ ${formatThemeList(getThemeId())}`);
102528
104328
 
102529
104329
  `);
102530
104330
  }
104331
+ releaseLease();
102531
104332
  }
102532
104333
  function realMakeProvider(write) {
102533
104334
  return (name, model, baseUrl) => {
@@ -103144,7 +104945,7 @@ ${text.trim()}`;
103144
104945
  deps = {
103145
104946
  ...deps,
103146
104947
  onContextCompaction: (r) => {
103147
- if (live === undefined) {
104948
+ if (live === undefined || !leaseWatch.canPersist()) {
103148
104949
  return;
103149
104950
  }
103150
104951
  const persisted = persistCompacted(live, r.context, archive, {
@@ -103157,17 +104958,44 @@ ${text.trim()}`;
103157
104958
  `);
103158
104959
  }
103159
104960
  };
104961
+ let lease;
104962
+ const leaseWatch = watchLeaseLoss((message) => {
104963
+ detachSlateSession(slateSession);
104964
+ slateSession = undefined;
104965
+ slateSessionBox.current = undefined;
104966
+ agentIo.onSystem?.(message);
104967
+ });
104968
+ gateBoxByLease(slateSessionBox, () => leaseWatch.canPersist());
104969
+ const holdLease = (next) => {
104970
+ lease = next;
104971
+ leaseWatch.track(next);
104972
+ if (sessionOpts?.leaseBox !== undefined)
104973
+ sessionOpts.leaseBox.current = next;
104974
+ };
104975
+ const openLeased = sessionOpts?.openLeased ?? openLeasedSession;
104976
+ const switchNewSession = () => {
104977
+ try {
104978
+ const next = switchLeasedSession(lease, () => openLeased({ cwd: sessionCwd, provider: deps.providerId, model: deps.modelId }));
104979
+ holdLease(next.lease);
104980
+ return next.handle;
104981
+ } catch (cause) {
104982
+ agentIo.onSystem?.(`${cause instanceof Error ? cause.message : String(cause)}
104983
+ Kept the current session.
104984
+ `);
104985
+ return false;
104986
+ }
104987
+ };
104988
+ const releaseLease = () => {
104989
+ releaseSessionLease(lease);
104990
+ holdLease(undefined);
104991
+ };
103160
104992
  if (sessionsOn) {
103161
104993
  try {
103162
- const resumeId = sessionOpts?.resumeId;
103163
- if (resumeId === undefined && sessionOpts?.continueLast !== true && sessionOpts !== undefined) {}
103164
- const opened = openSession({
103165
- cwd: sessionCwd,
103166
- ...sessionOpts?.continueLast === true ? { continueLast: true } : {},
103167
- ...resumeId !== undefined ? { resumeId } : {},
103168
- provider: deps.providerId,
103169
- model: deps.modelId
103170
- });
104994
+ const opened = openLeased(leasedOpenOptions(sessionOpts, sessionCwd, deps.providerId, deps.modelId));
104995
+ holdLease(opened.lease);
104996
+ if (opened.skipped !== undefined) {
104997
+ agentIo.onSystem?.(describeSkippedSession(opened.skipped));
104998
+ }
103171
104999
  live = opened.handle;
103172
105000
  history = opened.history;
103173
105001
  archive = opened.archive.length > 0 ? [...opened.archive] : [...opened.history];
@@ -103180,7 +105008,12 @@ ${text.trim()}`;
103180
105008
  `);
103181
105009
  }
103182
105010
  } catch (cause) {
103183
- live = createSession({
105011
+ if (cause instanceof SessionLeasedError) {
105012
+ throw cause;
105013
+ }
105014
+ const fresh = openFreshLeasedSession(openLeased, sessionCwd, deps.providerId, deps.modelId);
105015
+ holdLease(fresh?.lease);
105016
+ live = fresh?.handle ?? createSession({
103184
105017
  cwd: sessionCwd,
103185
105018
  provider: deps.providerId,
103186
105019
  model: deps.modelId
@@ -103196,7 +105029,7 @@ New session ${shortSessionId(live.summary.id)}.
103196
105029
  slateSession = live !== undefined ? { dir: live.dir, cwd: sessionCwd, opened: false } : undefined;
103197
105030
  slateSessionBox.current = slateSession;
103198
105031
  const save = () => {
103199
- if (live === undefined) {
105032
+ if (live === undefined || !leaseWatch.canPersist()) {
103200
105033
  return;
103201
105034
  }
103202
105035
  try {
@@ -103272,6 +105105,7 @@ ${GUTTER}${turnSeparator()}
103272
105105
  if (line === undefined) {
103273
105106
  await closeSlateSession(slateSession, mintTimestampAttemptId);
103274
105107
  await deps.sweepBackgroundJobs?.();
105108
+ releaseLease();
103275
105109
  return;
103276
105110
  }
103277
105111
  rich.safeBoundary?.();
@@ -103282,6 +105116,7 @@ ${GUTTER}${turnSeparator()}
103282
105116
  if (command === "/exit" || command === "/quit") {
103283
105117
  await closeSlateSession(slateSession, mintTimestampAttemptId);
103284
105118
  await deps.sweepBackgroundJobs?.();
105119
+ releaseLease();
103285
105120
  return;
103286
105121
  }
103287
105122
  if (command === "/help") {
@@ -103326,13 +105161,14 @@ ${GUTTER}${turnSeparator()}
103326
105161
  `);
103327
105162
  }
103328
105163
  } else if (command === "/new" || command === "/clear") {
105164
+ const nextLive = sessionsOn ? switchNewSession() : undefined;
105165
+ if (nextLive === false) {
105166
+ rich.printPrompt();
105167
+ continue;
105168
+ }
103329
105169
  await closeSlateSession(slateSession, mintTimestampAttemptId);
103330
- if (sessionsOn) {
103331
- live = createSession({
103332
- cwd: sessionCwd,
103333
- provider: deps.providerId,
103334
- model: deps.modelId
103335
- });
105170
+ if (nextLive !== undefined) {
105171
+ live = nextLive;
103336
105172
  history = [];
103337
105173
  archive = [];
103338
105174
  nextArchiveIndex = 0;
@@ -103350,6 +105186,8 @@ ${GUTTER}${turnSeparator()}
103350
105186
  if (live === undefined) {
103351
105187
  agentIo.onSystem?.(`No persistent session.
103352
105188
  `);
105189
+ } else if (!leaseWatch.canPersist()) {
105190
+ agentIo.onSystem?.(LOST_LEASE_COMPACT_REFUSAL);
103353
105191
  } else {
103354
105192
  const packed = compactSession(live, history, archive, {
103355
105193
  keepLastUserTurns: 3,
@@ -103636,6 +105474,14 @@ async function resolveTuiStartup(opts) {
103636
105474
  appliedKeys
103637
105475
  };
103638
105476
  }
105477
+
105478
+ class ShellFlagError extends Error {
105479
+ exitCode = 2;
105480
+ constructor(message) {
105481
+ super(`${message}. See keryx shell --help.`);
105482
+ this.name = "ShellFlagError";
105483
+ }
105484
+ }
103639
105485
  function parseShellCliFlags(args) {
103640
105486
  if (args.includes("--help") || args.includes("-h")) {
103641
105487
  return { help: true, wantTui: true };
@@ -103658,11 +105504,14 @@ function parseShellCliFlags(args) {
103658
105504
  let continueLast;
103659
105505
  let resumeId;
103660
105506
  let resumePick;
105507
+ let fork;
105508
+ let takeOver;
103661
105509
  let permissionModeFlag;
103662
105510
  let denyTools;
103663
105511
  let printPromptArg;
103664
105512
  let eventsFile;
103665
105513
  let eventsMaxField;
105514
+ let debug;
103666
105515
  for (let i = 0;i < args.length; i++) {
103667
105516
  const arg = args[i];
103668
105517
  if (arg === "--provider") {
@@ -103693,6 +105542,10 @@ function parseShellCliFlags(args) {
103693
105542
  } else {
103694
105543
  resumePick = true;
103695
105544
  }
105545
+ } else if (arg === "--fork") {
105546
+ fork = true;
105547
+ } else if (arg === "--take-over") {
105548
+ takeOver = true;
103696
105549
  } else if (arg === "--permission-mode") {
103697
105550
  const next = valueAfter(i++);
103698
105551
  if (!isPermissionMode(next))
@@ -103713,6 +105566,8 @@ function parseShellCliFlags(args) {
103713
105566
  i += 1;
103714
105567
  } else if (arg === "--events-file") {
103715
105568
  eventsFile = valueAfter(i++);
105569
+ } else if (arg === "--debug") {
105570
+ debug = true;
103716
105571
  } else if (arg === "--events-max-field") {
103717
105572
  const raw = valueAfter(i++);
103718
105573
  const parsed = Number(raw);
@@ -103726,6 +105581,18 @@ function parseShellCliFlags(args) {
103726
105581
  if (continueLast && (resumeId !== undefined || resumePick)) {
103727
105582
  invalid("--continue and --resume cannot be combined");
103728
105583
  }
105584
+ const leaseFlag = fork === true ? "--fork" : takeOver === true ? "--take-over" : undefined;
105585
+ if (leaseFlag !== undefined) {
105586
+ if (continueLast === true) {
105587
+ throw new ShellFlagError(`${leaseFlag} cannot be combined with --continue; use -r <id> ${leaseFlag}`);
105588
+ }
105589
+ if (fork === true && takeOver === true) {
105590
+ throw new ShellFlagError("--fork and --take-over cannot be combined; choose one");
105591
+ }
105592
+ if (resumeId === undefined) {
105593
+ throw new ShellFlagError(`${leaseFlag} needs an explicit session id; use -r <id> ${leaseFlag}`);
105594
+ }
105595
+ }
103729
105596
  if (printPromptArg !== undefined && modeFlag === false) {
103730
105597
  invalid("--print is agent-mode only and cannot be combined with --chat");
103731
105598
  }
@@ -103738,11 +105605,14 @@ function parseShellCliFlags(args) {
103738
105605
  ...continueLast === true ? { continueLast: true } : {},
103739
105606
  ...resumeId !== undefined ? { resumeId } : {},
103740
105607
  ...resumePick === true ? { resumePick: true } : {},
105608
+ ...fork === true ? { fork: true } : {},
105609
+ ...takeOver === true ? { takeOver: true } : {},
103741
105610
  ...permissionModeFlag !== undefined ? { permissionModeFlag } : {},
103742
105611
  ...denyTools !== undefined ? { denyTools } : {},
103743
105612
  ...printPromptArg !== undefined ? { printPrompt: printPromptArg } : {},
103744
105613
  ...eventsFile !== undefined ? { eventsFile } : {},
103745
- ...eventsMaxField !== undefined ? { eventsMaxField } : {}
105614
+ ...eventsMaxField !== undefined ? { eventsMaxField } : {},
105615
+ ...debug === true ? { debug: true } : {}
103746
105616
  };
103747
105617
  }
103748
105618
  function chooseShellSurface(flags, isTty) {
@@ -103755,6 +105625,11 @@ function chooseShellSurface(flags, isTty) {
103755
105625
  return flags.modeFlag === false ? "tui-chat" : "tui-agent";
103756
105626
  }
103757
105627
  async function shellCommand(args, runtime = {}) {
105628
+ if (args[0] === "--debug-watcher") {
105629
+ await Promise.resolve().then(() => init_debug_watcher());
105630
+ await runDebugWatcher(args.slice(1));
105631
+ return;
105632
+ }
103758
105633
  const flags = parseShellCliFlags(args);
103759
105634
  if (flags.help) {
103760
105635
  console.log(`Usage: keryx shell [options]
@@ -103765,21 +105640,55 @@ async function shellCommand(args, runtime = {}) {
103765
105640
  --base-url <url> Override the provider endpoint
103766
105641
  --agent | --chat Choose agent (default) or chat mode
103767
105642
  --tui | --no-tui Prefer TUI (default) or readline; last flag wins
103768
- --continue, -c Continue the latest project session
105643
+ --continue, -c Continue the most recent session no other shell has open
103769
105644
  --resume, -r [id-or-title] Resume a session, or open the session picker
105645
+ --fork With -r <id>: continue in a fork of that session
105646
+ --take-over With -r <id>: take the session from a stale holder
103770
105647
  --permission-mode <mode> Agent permissions: ask, trust or auto
103771
105648
  --deny-tools <a,b> Withhold these tools from the session entirely
103772
105649
  --ask | --trust | --auto Permission shortcuts; last flag wins
103773
105650
  --print, -p <prompt> Run one agent turn on this prompt and exit
103774
105651
  --events-file <path> Append an NDJSON transcript (turns, tools, usage)
103775
105652
  --events-max-field <n> Per-field character cap in that transcript (default 4000)
105653
+ --debug Log the session (input, keys, dialogs, tools) and start a watcher
105654
+ that re-arms terminal input if it stops; see debug/latest.txt
103776
105655
 
103777
105656
  Session continuation and resume are mutually exclusive. Permission flags
103778
- are ignored in chat mode. Without a TTY, resume without an ID uses the latest session.
105657
+ are ignored in chat mode. Without a TTY, resume without an ID uses the latest
105658
+ session no other shell has open. A session open in another shell is refused;
105659
+ --fork and --take-over (stale holders only) need an explicit -r <id> and
105660
+ cannot be combined with each other or with --continue.
105661
+
105662
+ A shell that stops reacting to keys can be recovered from another terminal
105663
+ with: kill -USR2 <keryx pid>
103779
105664
 
103780
105665
  Example: keryx shell --provider ollama --model llama3.1:latest`);
103781
105666
  return;
103782
105667
  }
105668
+ if (flags.debug === true) {
105669
+ await Promise.resolve().then(() => init_debug_log());
105670
+ await Promise.resolve().then(() => init_debug_watcher());
105671
+ const run = startDebugRun(runtime.cacheDir !== undefined ? { configDir: runtime.cacheDir } : {});
105672
+ const watcherPid = spawnDebugWatcher(run);
105673
+ debugEvent("session.start", {
105674
+ version: package_default.version,
105675
+ argv: args,
105676
+ bun: typeof Bun !== "undefined" ? Bun.version : undefined,
105677
+ platform: process.platform,
105678
+ execPath: process.execPath,
105679
+ cwd: process.cwd(),
105680
+ stdinTTY: process.stdin.isTTY === true,
105681
+ stdoutTTY: process.stdout.isTTY === true,
105682
+ env: Object.fromEntries(["TERM", "TERM_PROGRAM", "COLORTERM", "TMUX", "HERDR_ENV", "HERDR_PANE_ID", "SSH_TTY", "LANG"].map((k) => [k, process.env[k]])),
105683
+ watcherPid,
105684
+ dir: run.dir
105685
+ });
105686
+ process.on("exit", (code) => {
105687
+ debugEvent("session.exit", { code });
105688
+ process.stderr.write(`keryx debug log: ${run.dir}
105689
+ `);
105690
+ });
105691
+ }
103783
105692
  const versionCheck = (runtime.checkVersion ?? (() => checkVersion({
103784
105693
  currentVersion: package_default.version,
103785
105694
  ...runtime.cacheDir !== undefined ? { cacheDir: runtime.cacheDir } : {}
@@ -103788,6 +105697,10 @@ Example: keryx shell --provider ollama --model llama3.1:latest`);
103788
105697
  const modelArg = flags.modelArg;
103789
105698
  let baseUrl = flags.baseUrl;
103790
105699
  let modeFlag = flags.modeFlag;
105700
+ const leaseFlagOpts = {
105701
+ ...flags.fork === true ? { fork: true } : {},
105702
+ ...flags.takeOver === true ? { takeOver: true } : {}
105703
+ };
103791
105704
  {
103792
105705
  await Promise.resolve().then(() => init_login());
103793
105706
  const oauthFetch = (input, init) => globalThis.fetch(input, init);
@@ -103887,7 +105800,7 @@ Example: keryx shell --provider ollama --model llama3.1:latest`);
103887
105800
  sessionOverride: reasoningSessionOverride,
103888
105801
  globalEffort: loadShellConfig(runtime.cacheDir).reasoningEffort
103889
105802
  }),
103890
- idSeq: () => randomUUID29(),
105803
+ idSeq: () => randomUUID30(),
103891
105804
  askUser: invokeAskUserHost,
103892
105805
  sweepBackgroundJobs: () => jobRegistry.sweepAll(),
103893
105806
  jobRegistry,
@@ -103918,29 +105831,40 @@ Example: keryx shell --provider ollama --model llama3.1:latest`);
103918
105831
  });
103919
105832
  const tuiInitial = startup.initial;
103920
105833
  const tuiDetected = startup.detected;
105834
+ const chatLeaseBox = { current: undefined };
103921
105835
  try {
103922
105836
  if (surface === "tui-chat") {
103923
105837
  const chatFactory = realMakeProvider(() => {});
103924
105838
  let chatResumeId = flags.resumeId;
105839
+ const chatNotes = [];
103925
105840
  if (flags.resumePick === true && chatResumeId === undefined) {
103926
- chatResumeId = latestSession(cwd)?.id;
105841
+ chatResumeId = bareResumeTarget(cwd, (text) => {
105842
+ chatNotes.push(text);
105843
+ });
103927
105844
  }
103928
105845
  if (await (runtime.launchChat ?? launchTuiChatShell)({
103929
105846
  detected: tuiDetected,
103930
105847
  redetect,
103931
105848
  ...tuiInitial !== undefined ? { initial: tuiInitial } : {},
103932
- runShell,
105849
+ runShell: async (chatIo, chatDeps) => {
105850
+ const code = await runLeasedChatShell(chatIo, chatDeps, runShell, chatNotes.splice(0));
105851
+ if (code !== undefined) {
105852
+ process.exitCode = code;
105853
+ }
105854
+ },
103933
105855
  makeShellDeps: (sel) => {
103934
105856
  const resolvedModelParams = resolveProviderModelParamsByName(sel.provider, loadShellConfig(runtime.cacheDir), runtime.cacheDir);
103935
105857
  return {
103936
105858
  makeProvider: chatFactory,
103937
105859
  clock: () => new Date().toISOString(),
103938
- idSeq: () => randomUUID29(),
105860
+ idSeq: () => randomUUID30(),
103939
105861
  initial: Object.keys(resolvedModelParams).length > 0 ? { ...sel, modelParams: resolvedModelParams } : sel,
103940
105862
  session: {
103941
105863
  cwd,
103942
105864
  ...flags.continueLast === true ? { continueLast: true } : {},
103943
- ...chatResumeId !== undefined ? { resumeId: chatResumeId } : {}
105865
+ ...chatResumeId !== undefined ? { resumeId: chatResumeId } : {},
105866
+ ...leaseFlagOpts,
105867
+ leaseBox: chatLeaseBox
103944
105868
  }
103945
105869
  };
103946
105870
  },
@@ -103960,7 +105884,8 @@ Example: keryx shell --provider ollama --model llama3.1:latest`);
103960
105884
  cwd,
103961
105885
  ...flags.continueLast === true ? { continueLast: true } : {},
103962
105886
  ...flags.resumeId !== undefined ? { resumeId: flags.resumeId } : {},
103963
- ...flags.resumePick === true ? { pickOnStart: true } : {}
105887
+ ...flags.resumePick === true ? { pickOnStart: true } : {},
105888
+ ...leaseFlagOpts
103964
105889
  },
103965
105890
  ...flags.permissionModeFlag !== undefined ? { initialPermissionMode: flags.permissionModeFlag } : {},
103966
105891
  versionCheck
@@ -103968,6 +105893,8 @@ Example: keryx shell --provider ollama --model llama3.1:latest`);
103968
105893
  return;
103969
105894
  }
103970
105895
  } finally {
105896
+ releaseSessionLease(chatLeaseBox.current);
105897
+ chatLeaseBox.current = undefined;
103971
105898
  if (mcpRuntime !== undefined) {
103972
105899
  await mcpRuntime.close();
103973
105900
  reportMcpProblems(mcpRuntime);
@@ -103976,7 +105903,10 @@ Example: keryx shell --provider ollama --model llama3.1:latest`);
103976
105903
  }
103977
105904
  const rl = readline2.createInterface({ input: process.stdin });
103978
105905
  let readlineMcp;
105906
+ const leaseBox = { current: undefined };
103979
105907
  const closeAndExit = (code) => () => {
105908
+ releaseSessionLease(leaseBox.current);
105909
+ leaseBox.current = undefined;
103980
105910
  (async () => {
103981
105911
  try {
103982
105912
  await readlineMcp?.close();
@@ -103995,6 +105925,27 @@ Example: keryx shell --provider ollama --model llama3.1:latest`);
103995
105925
  }
103996
105926
  };
103997
105927
  const { io, emitSystem, printHeader, printPrompt, destroy } = createRichIo(sharedLines, versionCheck);
105928
+ const leaseChoiceRuntime = {
105929
+ isTty: process.stdin.isTTY === true && oneShotPrompt === undefined,
105930
+ io: {
105931
+ write: (text) => {
105932
+ io.write(text);
105933
+ },
105934
+ readLine: async () => {
105935
+ const next = await lineIterator.next();
105936
+ return next.done === true ? undefined : next.value;
105937
+ }
105938
+ },
105939
+ writeError: (text) => {
105940
+ process.stderr.write(text);
105941
+ }
105942
+ };
105943
+ const bareResumeId = () => bareResumeTarget(process.cwd(), emitSystem);
105944
+ const finishLeased = (code) => {
105945
+ if (code !== undefined) {
105946
+ process.exitCode = code;
105947
+ }
105948
+ };
103998
105949
  let provider;
103999
105950
  let model;
104000
105951
  try {
@@ -104034,7 +105985,7 @@ Example: keryx shell --provider ollama --model llama3.1:latest`);
104034
105985
  const deps = {
104035
105986
  makeProvider: baseFactory,
104036
105987
  clock: () => new Date().toISOString(),
104037
- idSeq: () => randomUUID29(),
105988
+ idSeq: () => randomUUID30(),
104038
105989
  initial: {
104039
105990
  provider,
104040
105991
  model,
@@ -104106,7 +106057,7 @@ Example: keryx shell --provider ollama --model llama3.1:latest`);
104106
106057
  sessionOverride: reasoningSessionOverride,
104107
106058
  globalEffort: loadShellConfig(runtime.cacheDir).reasoningEffort
104108
106059
  }),
104109
- idSeq: () => randomUUID29(),
106060
+ idSeq: () => randomUUID30(),
104110
106061
  askUser: invokeAskUserHost,
104111
106062
  sweepBackgroundJobs: () => jobRegistry.sweepAll(),
104112
106063
  ...resetSubagentBudget !== undefined ? { resetSubagentBudget } : {},
@@ -104122,34 +106073,38 @@ Example: keryx shell --provider ollama --model llama3.1:latest`);
104122
106073
  };
104123
106074
  let resumeId = flags.resumeId;
104124
106075
  if (flags.resumePick === true && resumeId === undefined) {
104125
- resumeId = latestSession(process.cwd())?.id;
106076
+ resumeId = bareResumeId();
104126
106077
  }
104127
106078
  const events = flags.eventsFile === undefined ? undefined : createFileEventSink(flags.eventsFile, (message) => {
104128
106079
  emitSystem(`${message}
104129
106080
  `);
104130
106081
  }, flags.eventsMaxField);
106082
+ const runRepl = async (session) => {
106083
+ await runAgentRepl(sharedLines, { printPrompt, safeBoundary: io.onSafeBoundary }, agentDeps, metaprojectPort, session, flags.permissionModeFlag, slateSessionBox, events, runtime.cacheDir);
106084
+ };
104131
106085
  try {
104132
- await runAgentRepl(sharedLines, { printPrompt, safeBoundary: io.onSafeBoundary }, agentDeps, metaprojectPort, {
106086
+ finishLeased(await runWithLeaseChoice({
104133
106087
  cwd: process.cwd(),
104134
106088
  ...flags.continueLast === true ? { continueLast: true } : {},
104135
- ...resumeId !== undefined ? { resumeId } : {}
104136
- }, flags.permissionModeFlag, slateSessionBox, events, runtime.cacheDir);
106089
+ ...resumeId !== undefined ? { resumeId } : {},
106090
+ ...leaseFlagOpts,
106091
+ leaseBox
106092
+ }, runRepl, leaseChoiceRuntime));
104137
106093
  } finally {
104138
106094
  await mcpRuntime.close();
104139
106095
  }
104140
106096
  } else {
104141
106097
  let resumeId = flags.resumeId;
104142
106098
  if (flags.resumePick === true && resumeId === undefined) {
104143
- resumeId = latestSession(process.cwd())?.id;
106099
+ resumeId = bareResumeId();
104144
106100
  }
104145
- await runShell(io, {
104146
- ...deps,
104147
- session: {
104148
- cwd: process.cwd(),
104149
- ...flags.continueLast === true ? { continueLast: true } : {},
104150
- ...resumeId !== undefined ? { resumeId } : {}
104151
- }
104152
- });
106101
+ finishLeased(await runWithLeaseChoice({
106102
+ cwd: process.cwd(),
106103
+ ...flags.continueLast === true ? { continueLast: true } : {},
106104
+ ...resumeId !== undefined ? { resumeId } : {},
106105
+ ...leaseFlagOpts,
106106
+ leaseBox
106107
+ }, (session) => runShell(io, { ...deps, session }), leaseChoiceRuntime));
104153
106108
  }
104154
106109
  } finally {
104155
106110
  try {
@@ -104170,12 +106125,13 @@ async function sessionsCommand(args) {
104170
106125
  const cwd = process.cwd();
104171
106126
  if (sub === "list") {
104172
106127
  const asJson = args.includes("--json");
104173
- const rows = listSessions(cwd);
106128
+ const listed = listSessions(cwd).map((s) => ({ summary: s, lease: leaseColumn(cwd, s.id) }));
104174
106129
  if (asJson) {
106130
+ const rows = listed.map(({ summary, lease }) => ({ ...summary, live: lease === "?" ? null : lease }));
104175
106131
  console.log(JSON.stringify({ schemaVersion: 1, project: resolveProjectRoot2(cwd), sessions: rows }, null, 2));
104176
106132
  return;
104177
106133
  }
104178
- if (rows.length === 0) {
106134
+ if (listed.length === 0) {
104179
106135
  console.log(`No sessions for project ${resolveProjectRoot2(cwd)}`);
104180
106136
  console.log(`(store: ${projectSessionsDir(cwd)})`);
104181
106137
  return;
@@ -104183,10 +106139,10 @@ async function sessionsCommand(args) {
104183
106139
  console.log(`Project: ${resolveProjectRoot2(cwd)}`);
104184
106140
  console.log(`Store: ${projectSessionsDir(cwd)}`);
104185
106141
  console.log("");
104186
- console.log(pad("ID", 10) + pad("UPDATED", 22) + pad("MSGS", 6) + pad("MODEL", 24) + "TITLE");
104187
- for (const s of rows) {
106142
+ console.log(pad("ID", 10) + pad("UPDATED", 22) + pad("MSGS", 6) + pad("LIVE", 7) + pad("MODEL", 24) + "TITLE");
106143
+ for (const { summary: s, lease } of listed) {
104188
106144
  const model = s.provider !== undefined && s.model !== undefined ? `${s.provider}/${s.model}` : s.model ?? "-";
104189
- console.log(pad(shortSessionId(s.id), 10) + pad(s.updatedAt.slice(0, 19).replace("T", " "), 22) + pad(String(s.messageCount), 6) + pad(clip8(model, 22), 24) + (s.parentSessionId !== undefined ? `\u21B3 ${s.title}` : s.title));
106145
+ console.log(pad(shortSessionId(s.id), 10) + pad(s.updatedAt.slice(0, 19).replace("T", " "), 22) + pad(String(s.messageCount), 6) + pad(lease ?? "", 7) + pad(clip8(model, 22), 24) + (s.parentSessionId !== undefined ? `\u21B3 ${s.title}` : s.title));
104190
106146
  }
104191
106147
  console.log("");
104192
106148
  console.log("Resume: keryx shell -r <id> Continue last: keryx shell -c");
@@ -104273,6 +106229,19 @@ async function sessionsCommand(args) {
104273
106229
  printHelp17();
104274
106230
  process.exitCode = 1;
104275
106231
  }
106232
+ function leaseColumn(cwd, sessionId) {
106233
+ let state;
106234
+ try {
106235
+ state = sessionLeaseState(cwd, sessionId).state;
106236
+ } catch {
106237
+ return "?";
106238
+ }
106239
+ if (state === "live" || state === "mine")
106240
+ return "live";
106241
+ if (state === "stale")
106242
+ return "stale";
106243
+ return null;
106244
+ }
104276
106245
  function pad(s, n) {
104277
106246
  return s.length >= n ? `${s.slice(0, n - 1)} ` : s + " ".repeat(n - s.length);
104278
106247
  }
@@ -104290,9 +106259,16 @@ Usage:
104290
106259
  keryx sessions export <id> Export transcript as Markdown
104291
106260
  keryx sessions path Print the on-disk sessions directory
104292
106261
 
106262
+ List columns: ID, UPDATED, MSGS, LIVE, MODEL, TITLE. LIVE is "live" when a
106263
+ shell has the session open, "stale" when its holder stopped heartbeating, and
106264
+ blank when no shell holds it. In --json every row carries "live": "live" |
106265
+ "stale" | null.
106266
+
104293
106267
  Shell:
104294
- keryx shell -c Continue last session in this project
106268
+ keryx shell -c Continue the last session no other shell has open
104295
106269
  keryx shell -r [id] Resume session (id / short id / title)
106270
+ keryx shell -r <id> --fork Fork a session another shell has open
106271
+ keryx shell -r <id> --take-over Take over a session whose holder is stale
104296
106272
  `);
104297
106273
  }
104298
106274
 
@@ -104300,7 +106276,7 @@ Shell:
104300
106276
  init_fs();
104301
106277
  import { readFile as readFile99 } from "fs/promises";
104302
106278
  import { stdin } from "process";
104303
- import path194 from "path";
106279
+ import path197 from "path";
104304
106280
  var MODULES = [
104305
106281
  { name: "gdgraph", flag: "--no-gdgraph", desc: "code graph, symbols, affected context", defaultEnabled: true },
104306
106282
  { name: "gdctx", flag: "--no-gdctx", desc: "token-aware command/read output", defaultEnabled: true },
@@ -104346,8 +106322,8 @@ async function modulesCommand(args = []) {
104346
106322
  return;
104347
106323
  }
104348
106324
  const wantsJson = args.includes("--json") && (sub === undefined || sub === "status" || sub === "list" || sub === "--json");
104349
- const metaprojectRoot = path194.join(process.cwd(), ".metaproject");
104350
- const manifestPath = path194.join(metaprojectRoot, "metaproject.json");
106325
+ const metaprojectRoot = path197.join(process.cwd(), ".metaproject");
106326
+ const manifestPath = path197.join(metaprojectRoot, "metaproject.json");
104351
106327
  if (!await pathExists(manifestPath)) {
104352
106328
  if (wantsJson) {
104353
106329
  console.log(JSON.stringify({ schemaVersion: 1, error: "not-initialized", modules: [] }, null, 2));
@@ -104461,26 +106437,26 @@ function printHelp18() {
104461
106437
  }
104462
106438
 
104463
106439
  // src/commands/serve.ts
104464
- import { randomUUID as randomUUID32 } from "crypto";
106440
+ import { randomUUID as randomUUID33 } from "crypto";
104465
106441
  init_serve_config();
104466
106442
 
104467
106443
  // src/lib/serve-credential.ts
104468
106444
  init_config_dir();
104469
- import { createHash as createHash44, randomBytes as randomBytes5, randomUUID as randomUUID30 } from "crypto";
106445
+ import { createHash as createHash44, randomBytes as randomBytes5, randomUUID as randomUUID31 } from "crypto";
104470
106446
  import {
104471
- chmodSync as chmodSync4,
104472
- closeSync as closeSync3,
106447
+ chmodSync as chmodSync5,
106448
+ closeSync as closeSync4,
104473
106449
  existsSync as existsSync34,
104474
106450
  fsyncSync as fsyncSync2,
104475
- openSync as openSync3,
104476
- renameSync as renameSync4,
104477
- statSync as statSync6,
106451
+ openSync as openSync5,
106452
+ renameSync as renameSync5,
106453
+ statSync as statSync8,
104478
106454
  unlinkSync as unlinkSync3,
104479
- writeFileSync as writeFileSync9
106455
+ writeFileSync as writeFileSync10
104480
106456
  } from "fs";
104481
- import path195 from "path";
106457
+ import path198 from "path";
104482
106458
  function serveCredentialPath(dir) {
104483
- return path195.join(keryxConfigDir(dir), "serve-credentials.json");
106459
+ return path198.join(keryxConfigDir(dir), "serve-credentials.json");
104484
106460
  }
104485
106461
  function constantTimeEqual(a, b) {
104486
106462
  const width = Math.max(a.length, b.length);
@@ -104509,7 +106485,7 @@ function isGroupOrOtherAccessible(file) {
104509
106485
  if (process.platform === "win32") {
104510
106486
  return false;
104511
106487
  }
104512
- const mode = statSync6(file, { throwIfNoEntry: false })?.mode;
106488
+ const mode = statSync8(file, { throwIfNoEntry: false })?.mode;
104513
106489
  return mode !== undefined && (mode & 63) !== 0;
104514
106490
  }
104515
106491
  function readServeCredential(dir) {
@@ -104550,18 +106526,18 @@ function readServeCredential(dir) {
104550
106526
  }
104551
106527
  function writeStore(store, dir) {
104552
106528
  const file = serveCredentialPath(dir);
104553
- const temp = `${file}.${randomUUID30()}.tmp`;
106529
+ const temp = `${file}.${randomUUID31()}.tmp`;
104554
106530
  try {
104555
106531
  ensureKeryxConfigDir(dir);
104556
- const handle = openSync3(temp, "wx", 384);
106532
+ const handle = openSync5(temp, "wx", 384);
104557
106533
  try {
104558
- writeFileSync9(handle, `${JSON.stringify(store, null, 2)}
106534
+ writeFileSync10(handle, `${JSON.stringify(store, null, 2)}
104559
106535
  `, { encoding: "utf8" });
104560
106536
  fsyncSync2(handle);
104561
106537
  } finally {
104562
- closeSync3(handle);
106538
+ closeSync4(handle);
104563
106539
  }
104564
- renameSync4(temp, file);
106540
+ renameSync5(temp, file);
104565
106541
  tighten2(file, 384);
104566
106542
  return true;
104567
106543
  } catch {
@@ -104576,7 +106552,7 @@ function tighten2(target, mode) {
104576
106552
  return;
104577
106553
  }
104578
106554
  try {
104579
- chmodSync4(target, mode);
106555
+ chmodSync5(target, mode);
104580
106556
  } catch {}
104581
106557
  }
104582
106558
  function withServeCredentialLock(dir, fn, onWaiting) {
@@ -104590,7 +106566,7 @@ function mintRecord(now) {
104590
106566
  const salt = randomBytes5(32).toString("hex");
104591
106567
  return {
104592
106568
  token,
104593
- record: { id: randomUUID30(), algorithm: "sha256", salt, hash: hashToken(salt, token), createdAt: now }
106569
+ record: { id: randomUUID31(), algorithm: "sha256", salt, hash: hashToken(salt, token), createdAt: now }
104594
106570
  };
104595
106571
  }
104596
106572
  function issueServeToken(dir, now = () => new Date().toISOString(), onWaiting) {
@@ -104713,23 +106689,23 @@ class AuthFailureThrottle {
104713
106689
  // src/lib/serve-turn-store.ts
104714
106690
  init_config_dir();
104715
106691
  import { createHash as createHash45 } from "crypto";
104716
- import { existsSync as existsSync35, readdirSync as readdirSync3, rmSync as rmSync2 } from "fs";
104717
- import path196 from "path";
106692
+ import { existsSync as existsSync35, readdirSync as readdirSync4, rmSync as rmSync3 } from "fs";
106693
+ import path199 from "path";
104718
106694
  var MAX_TURN_EVENTS = 1e4;
104719
106695
  function turnsRoot(dir) {
104720
- return path196.join(keryxConfigDir(dir), "turns");
106696
+ return path199.join(keryxConfigDir(dir), "turns");
104721
106697
  }
104722
106698
  function turnDir(turnId, dir) {
104723
- return path196.join(turnsRoot(dir), turnId);
106699
+ return path199.join(turnsRoot(dir), turnId);
104724
106700
  }
104725
106701
  function keyPath(project, idempotencyKey, dir) {
104726
106702
  const projectBytes = Buffer.byteLength(project, "utf8");
104727
106703
  const digest = createHash45("sha256").update(`${projectBytes}:${project}\x00${idempotencyKey}`, "utf8").digest("hex");
104728
- return path196.join(turnsRoot(dir), "keys", `${digest}.json`);
106704
+ return path199.join(turnsRoot(dir), "keys", `${digest}.json`);
104729
106705
  }
104730
106706
  function legacyKeyPath(idempotencyKey, dir) {
104731
106707
  const digest = createHash45("sha256").update(idempotencyKey, "utf8").digest("hex");
104732
- return path196.join(turnsRoot(dir), "keys", `${digest}.json`);
106708
+ return path199.join(turnsRoot(dir), "keys", `${digest}.json`);
104733
106709
  }
104734
106710
  function adoptLegacyClaim(project, idempotencyKey, dir) {
104735
106711
  const legacy = legacyKeyPath(idempotencyKey, dir);
@@ -104750,7 +106726,7 @@ function adoptLegacyClaim(project, idempotencyKey, dir) {
104750
106726
  } catch {}
104751
106727
  const record = turnId === undefined ? undefined : readTurnRecord(turnId, dir);
104752
106728
  if (turnId === undefined || record === undefined || !record.ok) {
104753
- rmSync2(legacy, { force: true });
106729
+ rmSync3(legacy, { force: true });
104754
106730
  return null;
104755
106731
  }
104756
106732
  if (record.value.project !== project) {
@@ -104758,7 +106734,7 @@ function adoptLegacyClaim(project, idempotencyKey, dir) {
104758
106734
  }
104759
106735
  writeOwnerOnlyFile(scoped, `${JSON.stringify({ turnId }, null, 2)}
104760
106736
  `);
104761
- rmSync2(legacy, { force: true });
106737
+ rmSync3(legacy, { force: true });
104762
106738
  return turnId;
104763
106739
  }
104764
106740
  function isServerFault(reason) {
@@ -104793,7 +106769,7 @@ function ensureTurnDir(turnId, dir) {
104793
106769
  }
104794
106770
  function createTurnRecord(record, dir) {
104795
106771
  ensureTurnDir(record.turnId, dir);
104796
- writeOwnerOnlyFile(path196.join(turnDir(record.turnId, dir), "turn.json"), `${JSON.stringify(record, null, 2)}
106772
+ writeOwnerOnlyFile(path199.join(turnDir(record.turnId, dir), "turn.json"), `${JSON.stringify(record, null, 2)}
104797
106773
  `);
104798
106774
  }
104799
106775
  function appendTurnEvent(event, dir, opts) {
@@ -104802,12 +106778,12 @@ function appendTurnEvent(event, dir, opts) {
104802
106778
  }
104803
106779
  const line = JSON.stringify(event);
104804
106780
  try {
104805
- appendOwnerOnlyLine(path196.join(turnDir(event.turnId, dir), "events.jsonl"), line);
106781
+ appendOwnerOnlyLine(path199.join(turnDir(event.turnId, dir), "events.jsonl"), line);
104806
106782
  } catch (error) {
104807
106783
  if (error?.code !== "ENOENT") {
104808
106784
  throw error;
104809
106785
  }
104810
- appendOwnerOnlyLine(path196.join(ensureTurnDir(event.turnId, dir), "events.jsonl"), line);
106786
+ appendOwnerOnlyLine(path199.join(ensureTurnDir(event.turnId, dir), "events.jsonl"), line);
104811
106787
  }
104812
106788
  return true;
104813
106789
  }
@@ -104815,7 +106791,7 @@ function readTurnEvents(turnId, after = -1, dir) {
104815
106791
  if (!isTurnId(turnId)) {
104816
106792
  return { ok: false, reason: "not-a-turn-id" };
104817
106793
  }
104818
- const read = readTurnFile(path196.join(turnDir(turnId, dir), "events.jsonl"));
106794
+ const read = readTurnFile(path199.join(turnDir(turnId, dir), "events.jsonl"));
104819
106795
  if (!read.ok) {
104820
106796
  if (isDefiniteAbsence2(read.reason)) {
104821
106797
  return { ok: true, value: [] };
@@ -104843,7 +106819,7 @@ function readTurnRecord(turnId, dir) {
104843
106819
  if (!isTurnId(turnId)) {
104844
106820
  return { ok: false, reason: "not-a-turn-id" };
104845
106821
  }
104846
- const read = readTurnFile(path196.join(turnDir(turnId, dir), "turn.json"));
106822
+ const read = readTurnFile(path199.join(turnDir(turnId, dir), "turn.json"));
104847
106823
  if (!read.ok) {
104848
106824
  return { ok: false, reason: read.reason };
104849
106825
  }
@@ -104862,7 +106838,7 @@ function finishTurn(turnId, result, dir) {
104862
106838
  if (!record.ok) {
104863
106839
  return false;
104864
106840
  }
104865
- writeOwnerOnlyFile(path196.join(turnDir(turnId, dir), "turn.json"), `${JSON.stringify({ ...record.value, result }, null, 2)}
106841
+ writeOwnerOnlyFile(path199.join(turnDir(turnId, dir), "turn.json"), `${JSON.stringify({ ...record.value, result }, null, 2)}
104866
106842
  `);
104867
106843
  return true;
104868
106844
  }
@@ -104902,13 +106878,13 @@ function releaseIdempotencyKey(project, idempotencyKey, turnId, dir) {
104902
106878
  } catch {
104903
106879
  return false;
104904
106880
  }
104905
- rmSync2(file, { force: true });
106881
+ rmSync3(file, { force: true });
104906
106882
  return true;
104907
106883
  }
104908
106884
 
104909
106885
  // src/lib/serve-turn.ts
104910
- import { randomUUID as randomUUID31 } from "crypto";
104911
- import path197 from "path";
106886
+ import { randomUUID as randomUUID32 } from "crypto";
106887
+ import path200 from "path";
104912
106888
  init_service();
104913
106889
  var REMOTE_ORIGIN = "remote:http";
104914
106890
  var MAX_PROMPT_CHARS = 32000;
@@ -104977,9 +106953,9 @@ function isUuid(value) {
104977
106953
  return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(value);
104978
106954
  }
104979
106955
  function resolveProject(declared, dir) {
104980
- const wanted = path197.resolve(declared);
106956
+ const wanted = path200.resolve(declared);
104981
106957
  for (const entry of listProjects(dir, () => {})) {
104982
- if (path197.resolve(entry.path) === wanted) {
106958
+ if (path200.resolve(entry.path) === wanted) {
104983
106959
  return { ok: true, project: entry.path };
104984
106960
  }
104985
106961
  }
@@ -105026,7 +107002,7 @@ async function redactOut(security, text) {
105026
107002
  async function runRemoteTurn(input) {
105027
107003
  const scanRoot = input.scanRoot;
105028
107004
  const security = createSecurityService(scanRoot);
105029
- const newId = input.newId ?? (() => randomUUID31());
107005
+ const newId = input.newId ?? (() => randomUUID32());
105030
107006
  const clock = input.clock ?? (() => new Date().toISOString());
105031
107007
  const turnId = input.turnId ?? newId();
105032
107008
  const sessionId = input.request.sessionId ?? newId();
@@ -105169,7 +107145,7 @@ function outcomeOf(status, gate, unresolvedBlockerIds) {
105169
107145
  }
105170
107146
  function createSubmitTurn(deps) {
105171
107147
  return async (request, project) => {
105172
- const turnId = (deps.newId ?? (() => randomUUID31()))();
107148
+ const turnId = (deps.newId ?? (() => randomUUID32()))();
105173
107149
  const scanned = await scanPrompt(deps.dir, request.prompt);
105174
107150
  if (scanned.rejected) {
105175
107151
  return { kind: "rejected" };
@@ -105857,7 +107833,7 @@ function runConfig(args) {
105857
107833
  return;
105858
107834
  }
105859
107835
  const credential = readServeCredential();
105860
- const credentialId = credential.status === "ok" ? credential.record.id : randomUUID32();
107836
+ const credentialId = credential.status === "ok" ? credential.record.id : randomUUID33();
105861
107837
  const config = defaultServeConfig(credentialId, {
105862
107838
  address: parsed.parsed.values.get("--bind") ?? DEFAULT_SERVE_BIND_ADDRESS,
105863
107839
  port: port ?? DEFAULT_SERVE_PORT,
@@ -106012,10 +107988,10 @@ function printHelp19() {
106012
107988
  }
106013
107989
 
106014
107990
  // src/commands/update.ts
106015
- import { spawn as spawn6 } from "child_process";
107991
+ import { spawn as spawn7 } from "child_process";
106016
107992
  import { chmod as chmod4, mkdir as mkdir63, readFile as readFile100, readdir as readdir37, writeFile as writeFile54 } from "fs/promises";
106017
- import { access as access4, constants as constants2, existsSync as existsSync36 } from "fs";
106018
- import path198 from "path";
107993
+ import { access as access4, constants as constants3, existsSync as existsSync36 } from "fs";
107994
+ import path201 from "path";
106019
107995
  import { fileURLToPath as fileURLToPath8 } from "url";
106020
107996
  init_config2();
106021
107997
  init_config();
@@ -106032,8 +108008,8 @@ async function updateCommand(args = []) {
106032
108008
  return;
106033
108009
  }
106034
108010
  const projectRoot = process.cwd();
106035
- const metaprojectRoot = path198.join(projectRoot, ".metaproject");
106036
- banner("keryx update", `Refreshing the .metaproject workspace in ${path198.basename(projectRoot)}/`);
108011
+ const metaprojectRoot = path201.join(projectRoot, ".metaproject");
108012
+ banner("keryx update", `Refreshing the .metaproject workspace in ${path201.basename(projectRoot)}/`);
106037
108013
  if (!await pathExists(metaprojectRoot)) {
106038
108014
  console.log(` ${style.red(symbols.cross)} Metaproject is not initialized.`);
106039
108015
  console.log(` ${style.cyan(symbols.arrow)} Run ${style.cyan("keryx init")} first.`);
@@ -106106,7 +108082,7 @@ async function updateCommand(args = []) {
106106
108082
  nextSteps(steps);
106107
108083
  }
106108
108084
  async function previewServiceFiles(projectRoot, options) {
106109
- const metaprojectRoot = path198.join(projectRoot, ".metaproject");
108085
+ const metaprojectRoot = path201.join(projectRoot, ".metaproject");
106110
108086
  const manifestState = await readManifest5(metaprojectRoot);
106111
108087
  const manifest = manifestState.manifest;
106112
108088
  const enableTasks = moduleEnabled2(manifest, "tasks") || !options.noTasks;
@@ -106135,12 +108111,12 @@ async function previewServiceFiles(projectRoot, options) {
106135
108111
  });
106136
108112
  }
106137
108113
  async function refreshServiceFiles(projectRoot, options) {
106138
- const metaprojectRoot = path198.join(projectRoot, ".metaproject");
108114
+ const metaprojectRoot = path201.join(projectRoot, ".metaproject");
106139
108115
  const manifestState = await readManifest5(metaprojectRoot);
106140
108116
  const manifest = manifestState.manifest;
106141
108117
  const recoveredManifest = !manifestState.exists || !manifestState.valid;
106142
108118
  if (manifestState.migrated) {
106143
- await writeFile54(path198.join(metaprojectRoot, "metaproject.json"), `${JSON.stringify(manifest, null, 2)}
108119
+ await writeFile54(path201.join(metaprojectRoot, "metaproject.json"), `${JSON.stringify(manifest, null, 2)}
106144
108120
  `, "utf8");
106145
108121
  }
106146
108122
  const enableGdgraph = moduleEnabled2(manifest, "gdgraph");
@@ -106177,9 +108153,9 @@ async function refreshServiceFiles(projectRoot, options) {
106177
108153
  enableSecurity,
106178
108154
  enableSac
106179
108155
  });
106180
- await writeTextIfChanged4(path198.join(metaprojectRoot, "core", "README.md"), renderMetaprojectCoreReadme());
106181
- await writeTextIfChanged4(path198.join(metaprojectRoot, "hooks", "README.md"), renderHooksReadme());
106182
- await writeTextIfChanged4(path198.join(metaprojectRoot, "skills", "project-rules", "README.md"), renderProjectRulesSkillReadme({ sources: ruleSources }));
108156
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "core", "README.md"), renderMetaprojectCoreReadme());
108157
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "hooks", "README.md"), renderHooksReadme());
108158
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "skills", "project-rules", "README.md"), renderProjectRulesSkillReadme({ sources: ruleSources }));
106183
108159
  await writeRoutingEntrypointPair(metaprojectRoot, {
106184
108160
  enableGdgraph,
106185
108161
  enableGdctx,
@@ -106202,7 +108178,7 @@ async function refreshServiceFiles(projectRoot, options) {
106202
108178
  }
106203
108179
  }
106204
108180
  });
106205
- await writeTextIfChanged4(path198.join(metaprojectRoot, "keryx-dashboard.html"), renderMetaprojectDashboardHtml({
108181
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "keryx-dashboard.html"), renderMetaprojectDashboardHtml({
106206
108182
  enableGdgraph,
106207
108183
  enableGdctx,
106208
108184
  enableGdwiki,
@@ -106214,7 +108190,7 @@ async function refreshServiceFiles(projectRoot, options) {
106214
108190
  enableSecurity,
106215
108191
  data: dashboardData
106216
108192
  }));
106217
- await writeTextIfMissing4(path198.join(metaprojectRoot, "README.md"), renderMetaprojectReadme({
108193
+ await writeTextIfMissing4(path201.join(metaprojectRoot, "README.md"), renderMetaprojectReadme({
106218
108194
  enableGdgraph,
106219
108195
  enableGdctx,
106220
108196
  enableGdwiki,
@@ -106227,31 +108203,31 @@ async function refreshServiceFiles(projectRoot, options) {
106227
108203
  }));
106228
108204
  if (enableGdgraph) {
106229
108205
  await installGdgraphCoreScripts2(metaprojectRoot);
106230
- await writeTextIfChanged4(path198.join(metaprojectRoot, "modules", "gdgraph.md"), renderGdgraphManifest());
106231
- await writeTextIfChanged4(path198.join(metaprojectRoot, "core", "gdgraph", "README.md"), renderGdgraphCoreReadme());
106232
- await writeTextIfChanged4(path198.join(metaprojectRoot, "skills", "gdgraph", "SKILL.md"), renderGdgraphSkillReadme());
108206
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "modules", "gdgraph.md"), renderGdgraphManifest());
108207
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "core", "gdgraph", "README.md"), renderGdgraphCoreReadme());
108208
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "skills", "gdgraph", "SKILL.md"), renderGdgraphSkillReadme());
106233
108209
  await seedAssetsLock(metaprojectRoot);
106234
108210
  if (manifest.modules?.gdgraph?.hooks?.gitPostCommit) {
106235
108211
  await installManagedHook2(projectRoot, "post-commit", "gdgraph-post-commit", renderGdgraphPostCommitHook());
106236
108212
  }
106237
108213
  }
106238
108214
  if (enableGdctx) {
106239
- await writeTextIfMissing4(path198.join(metaprojectRoot, "gdctx.config.json"), renderGdctxConfig());
106240
- await writeTextIfChanged4(path198.join(metaprojectRoot, "modules", "gdctx.md"), renderGdctxManifest());
106241
- await writeTextIfChanged4(path198.join(metaprojectRoot, "core", "gdctx", "README.md"), renderGdctxCoreReadme());
106242
- await writeTextIfChanged4(path198.join(metaprojectRoot, "skills", "gdctx", "SKILL.md"), renderGdctxSkillReadme());
108215
+ await writeTextIfMissing4(path201.join(metaprojectRoot, "gdctx.config.json"), renderGdctxConfig());
108216
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "modules", "gdctx.md"), renderGdctxManifest());
108217
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "core", "gdctx", "README.md"), renderGdctxCoreReadme());
108218
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "skills", "gdctx", "SKILL.md"), renderGdctxSkillReadme());
106243
108219
  }
106244
108220
  if (enableGdwiki) {
106245
- await writeTextIfMissing4(path198.join(metaprojectRoot, "wiki", "templates", "page.md"), renderWikiPageTemplate());
106246
- await writeTextIfChanged4(path198.join(metaprojectRoot, "modules", "gdwiki.md"), renderGdwikiManifest());
106247
- await writeTextIfChanged4(path198.join(metaprojectRoot, "skills", "gdwiki", "SKILL.md"), renderGdwikiSkillReadme());
108221
+ await writeTextIfMissing4(path201.join(metaprojectRoot, "wiki", "templates", "page.md"), renderWikiPageTemplate());
108222
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "modules", "gdwiki.md"), renderGdwikiManifest());
108223
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "skills", "gdwiki", "SKILL.md"), renderGdwikiSkillReadme());
106248
108224
  if (manifest.modules?.gdgraph?.hooks?.gitPostCommit) {
106249
108225
  await installManagedHook2(projectRoot, "post-commit", "gdwiki-post-commit", renderGdwikiPostCommitHook());
106250
108226
  }
106251
108227
  }
106252
108228
  if (enableSac) {
106253
- await writeTextIfMissing4(path198.join(metaprojectRoot, "modules", "sac.md"), renderSacManifest());
106254
- await writeTextIfChanged4(path198.join(metaprojectRoot, "skills", "sac", "SKILL.md"), renderSacSkillReadme());
108229
+ await writeTextIfMissing4(path201.join(metaprojectRoot, "modules", "sac.md"), renderSacManifest());
108230
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "skills", "sac", "SKILL.md"), renderSacSkillReadme());
106255
108231
  }
106256
108232
  let gdskillsWarnings = [];
106257
108233
  let gdskillsNotices = [];
@@ -106264,25 +108240,25 @@ async function refreshServiceFiles(projectRoot, options) {
106264
108240
  }
106265
108241
  }
106266
108242
  if (enableHealth) {
106267
- await writeTextIfMissing4(path198.join(metaprojectRoot, "health.config.json"), renderHealthConfig());
106268
- await writeTextIfChanged4(path198.join(metaprojectRoot, "modules", "health.md"), renderHealthManifest());
106269
- await writeTextIfChanged4(path198.join(metaprojectRoot, "core", "health", "README.md"), renderHealthCoreReadme());
106270
- await writeTextIfChanged4(path198.join(metaprojectRoot, "skills", "health", "SKILL.md"), renderHealthSkillReadme());
108243
+ await writeTextIfMissing4(path201.join(metaprojectRoot, "health.config.json"), renderHealthConfig());
108244
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "modules", "health.md"), renderHealthManifest());
108245
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "core", "health", "README.md"), renderHealthCoreReadme());
108246
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "skills", "health", "SKILL.md"), renderHealthSkillReadme());
106271
108247
  if (manifest.modules?.health?.hooks?.gitPostCommit) {
106272
108248
  await installManagedHook2(projectRoot, "post-commit", "health-post-commit", renderHealthPostCommitHook());
106273
108249
  }
106274
108250
  }
106275
108251
  if (enableTesting) {
106276
- await writeTextIfMissing4(path198.join(metaprojectRoot, "testing.config.json"), renderTestingConfig({
108252
+ await writeTextIfMissing4(path201.join(metaprojectRoot, "testing.config.json"), renderTestingConfig({
106277
108253
  postCommitRefresh: Boolean(manifest.modules?.testing?.hooks?.gitPostCommit),
106278
108254
  prePushGate: Boolean(manifest.modules?.testing?.hooks?.prePush)
106279
108255
  }));
106280
- await writeTextIfChanged4(path198.join(metaprojectRoot, "modules", "testing.md"), renderTestingManifest());
106281
- await writeTextIfChanged4(path198.join(metaprojectRoot, "core", "testing", "README.md"), renderTestingCoreReadme());
106282
- await writeTextIfChanged4(path198.join(metaprojectRoot, "skills", "testing", "SKILL.md"), renderTestingSkillReadme());
108256
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "modules", "testing.md"), renderTestingManifest());
108257
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "core", "testing", "README.md"), renderTestingCoreReadme());
108258
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "skills", "testing", "SKILL.md"), renderTestingSkillReadme());
106283
108259
  if (enableGdwiki) {
106284
- await writeTextIfMissing4(path198.join(metaprojectRoot, "wiki", "testing", "README.md"), renderTestingWikiReadme());
106285
- await writeTextIfMissing4(path198.join(metaprojectRoot, "wiki", "testing", "conventions.md"), renderTestingWikiConventions());
108260
+ await writeTextIfMissing4(path201.join(metaprojectRoot, "wiki", "testing", "README.md"), renderTestingWikiReadme());
108261
+ await writeTextIfMissing4(path201.join(metaprojectRoot, "wiki", "testing", "conventions.md"), renderTestingWikiConventions());
106286
108262
  }
106287
108263
  if (manifest.modules?.testing?.hooks?.gitPostCommit) {
106288
108264
  await installManagedHook2(projectRoot, "post-commit", "testing-post-commit", renderTestingPostCommitHook());
@@ -106295,24 +108271,24 @@ async function refreshServiceFiles(projectRoot, options) {
106295
108271
  await installManagedHook2(projectRoot, "post-commit", "metaproject-dashboard-post-commit", renderMetaprojectDashboardPostCommitHook());
106296
108272
  }
106297
108273
  if (enableMemory) {
106298
- await writeTextIfMissing4(path198.join(metaprojectRoot, "memory.config.json"), renderMemoryConfig());
106299
- await writeTextIfMissing4(path198.join(metaprojectRoot, "memory", "templates", "entry.md"), renderMemoryEntryTemplate());
106300
- await writeTextIfChanged4(path198.join(metaprojectRoot, "modules", "memory.md"), renderMemoryManifest());
106301
- await writeTextIfChanged4(path198.join(metaprojectRoot, "core", "memory", "README.md"), renderMemoryCoreReadme());
106302
- await writeTextIfChanged4(path198.join(metaprojectRoot, "skills", "memory", "SKILL.md"), renderMemorySkillReadme());
108274
+ await writeTextIfMissing4(path201.join(metaprojectRoot, "memory.config.json"), renderMemoryConfig());
108275
+ await writeTextIfMissing4(path201.join(metaprojectRoot, "memory", "templates", "entry.md"), renderMemoryEntryTemplate());
108276
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "modules", "memory.md"), renderMemoryManifest());
108277
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "core", "memory", "README.md"), renderMemoryCoreReadme());
108278
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "skills", "memory", "SKILL.md"), renderMemorySkillReadme());
106303
108279
  }
106304
108280
  if (enableTasks) {
106305
- await writeTextIfChanged4(path198.join(metaprojectRoot, "flows", "README.md"), renderFlowsReadme());
106306
- await writeTextIfChanged4(path198.join(metaprojectRoot, "modules", "tasks.md"), renderTasksManifest());
106307
- await writeTextIfChanged4(path198.join(metaprojectRoot, "skills", "flow", "SKILL.md"), renderFlowSkillRouter());
106308
- await writeTextIfChanged4(path198.join(metaprojectRoot, "skills", "flow", "init.md"), renderFlowInitSkill());
106309
- await writeTextIfChanged4(path198.join(metaprojectRoot, "skills", "flow", "manage.md"), renderFlowManageSkill());
106310
- await writeTextIfChanged4(path198.join(metaprojectRoot, "skills", "flow", "complete.md"), renderFlowCompleteSkill());
108281
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "flows", "README.md"), renderFlowsReadme());
108282
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "modules", "tasks.md"), renderTasksManifest());
108283
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "skills", "flow", "SKILL.md"), renderFlowSkillRouter());
108284
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "skills", "flow", "init.md"), renderFlowInitSkill());
108285
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "skills", "flow", "manage.md"), renderFlowManageSkill());
108286
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "skills", "flow", "complete.md"), renderFlowCompleteSkill());
106311
108287
  }
106312
108288
  if (enableSecurity) {
106313
- await writeTextIfMissing4(path198.join(metaprojectRoot, "security.config.json"), renderSecurityConfig());
106314
- await writeTextIfChanged4(path198.join(metaprojectRoot, "modules", "security.md"), renderSecurityManifest());
106315
- await writeTextIfChanged4(path198.join(metaprojectRoot, "core", "security", "README.md"), renderSecurityCoreReadme());
108289
+ await writeTextIfMissing4(path201.join(metaprojectRoot, "security.config.json"), renderSecurityConfig());
108290
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "modules", "security.md"), renderSecurityManifest());
108291
+ await writeTextIfChanged4(path201.join(metaprojectRoot, "core", "security", "README.md"), renderSecurityCoreReadme());
106316
108292
  if (manifest.modules?.security?.hooks?.prePush) {
106317
108293
  await installManagedHook2(projectRoot, "pre-push", "security-pre-push", renderSecurityPrePushHook());
106318
108294
  }
@@ -106365,13 +108341,13 @@ async function refreshServiceFiles(projectRoot, options) {
106365
108341
  };
106366
108342
  }
106367
108343
  async function buildDashboard(projectRoot = process.cwd()) {
106368
- const metaprojectRoot = path198.join(projectRoot, ".metaproject");
108344
+ const metaprojectRoot = path201.join(projectRoot, ".metaproject");
106369
108345
  if (!await pathExists(metaprojectRoot)) {
106370
108346
  throw new Error("Metaproject is not initialized. Run: keryx init");
106371
108347
  }
106372
108348
  const manifest = (await readManifest5(metaprojectRoot)).manifest;
106373
108349
  const data = await collectDashboardData(metaprojectRoot);
106374
- const dashboardPath = path198.join(metaprojectRoot, "keryx-dashboard.html");
108350
+ const dashboardPath = path201.join(metaprojectRoot, "keryx-dashboard.html");
106375
108351
  await writeTextIfChanged4(dashboardPath, renderMetaprojectDashboardHtml({
106376
108352
  enableGdgraph: moduleEnabled2(manifest, "gdgraph"),
106377
108353
  enableGdctx: moduleEnabled2(manifest, "gdctx"),
@@ -106391,7 +108367,7 @@ async function shouldInstallDashboardPostCommitHook(projectRoot, manifest) {
106391
108367
  if (Object.values(modules).some((module) => Boolean(module.hooks?.gitPostCommit))) {
106392
108368
  return true;
106393
108369
  }
106394
- const hookPath = path198.join(projectRoot, ".git", "hooks", "post-commit");
108370
+ const hookPath = path201.join(projectRoot, ".git", "hooks", "post-commit");
106395
108371
  if (!await pathExists(hookPath)) {
106396
108372
  return false;
106397
108373
  }
@@ -106411,11 +108387,11 @@ async function collectDashboardData(metaprojectRoot) {
106411
108387
  if (testing) {
106412
108388
  data.testing = testing;
106413
108389
  }
106414
- const wiki = await collectMarkdownPages(path198.join(metaprojectRoot, "wiki"), "wiki");
108390
+ const wiki = await collectMarkdownPages(path201.join(metaprojectRoot, "wiki"), "wiki");
106415
108391
  if (wiki.length > 0) {
106416
108392
  data.wiki = { pages: wiki };
106417
108393
  }
106418
- const memory = await collectMarkdownPages(path198.join(metaprojectRoot, "memory"), "memory");
108394
+ const memory = await collectMarkdownPages(path201.join(metaprojectRoot, "memory"), "memory");
106419
108395
  if (memory.length > 0) {
106420
108396
  data.memory = { entries: memory };
106421
108397
  }
@@ -106430,7 +108406,7 @@ async function collectDashboardData(metaprojectRoot) {
106430
108406
  return data;
106431
108407
  }
106432
108408
  async function collectTasksDashboardData(metaprojectRoot) {
106433
- const flowsRoot = path198.join(metaprojectRoot, "flows");
108409
+ const flowsRoot = path201.join(metaprojectRoot, "flows");
106434
108410
  if (!await pathExists(flowsRoot)) {
106435
108411
  return null;
106436
108412
  }
@@ -106442,7 +108418,7 @@ async function collectTasksDashboardData(metaprojectRoot) {
106442
108418
  }
106443
108419
  const flows = [];
106444
108420
  for (const dir of dirEntries) {
106445
- const flowPath = path198.join(flowsRoot, dir, "flow.json");
108421
+ const flowPath = path201.join(flowsRoot, dir, "flow.json");
106446
108422
  if (!await pathExists(flowPath)) {
106447
108423
  continue;
106448
108424
  }
@@ -106450,7 +108426,7 @@ async function collectTasksDashboardData(metaprojectRoot) {
106450
108426
  const flow = JSON.parse(await readFile100(flowPath, "utf8"));
106451
108427
  const tasks = Array.isArray(flow.tasks) ? flow.tasks : [];
106452
108428
  let acTotal = 0;
106453
- const acPath = path198.join(flowsRoot, dir, "acceptance-criteria.md");
108429
+ const acPath = path201.join(flowsRoot, dir, "acceptance-criteria.md");
106454
108430
  if (await pathExists(acPath)) {
106455
108431
  const acContent = await readFile100(acPath, "utf8");
106456
108432
  acTotal = (acContent.match(/^- AC\d+:/gm) ?? []).length;
@@ -106504,7 +108480,7 @@ async function collectDashboardDocs(metaprojectRoot, wiki, memory) {
106504
108480
  "data/testing/context.md"
106505
108481
  ];
106506
108482
  for (const href of staticHrefs) {
106507
- const filePath = path198.join(metaprojectRoot, ...href.split("/"));
108483
+ const filePath = path201.join(metaprojectRoot, ...href.split("/"));
106508
108484
  if (!await pathExists(filePath)) {
106509
108485
  continue;
106510
108486
  }
@@ -106521,7 +108497,7 @@ async function collectDashboardDocs(metaprojectRoot, wiki, memory) {
106521
108497
  return docs;
106522
108498
  }
106523
108499
  async function collectHealthDashboardData(metaprojectRoot) {
106524
- const reportPath = path198.join(metaprojectRoot, "data", "health", "artifacts", "latest.json");
108500
+ const reportPath = path201.join(metaprojectRoot, "data", "health", "artifacts", "latest.json");
106525
108501
  if (!await pathExists(reportPath)) {
106526
108502
  return;
106527
108503
  }
@@ -106630,8 +108606,8 @@ function metricToScope(metric) {
106630
108606
  };
106631
108607
  }
106632
108608
  async function collectGraphDashboardData(metaprojectRoot) {
106633
- const nodesPath = path198.join(metaprojectRoot, "data", "gdgraph", "storage", "nodes.jsonl");
106634
- const edgesPath = path198.join(metaprojectRoot, "data", "gdgraph", "storage", "edges.jsonl");
108609
+ const nodesPath = path201.join(metaprojectRoot, "data", "gdgraph", "storage", "nodes.jsonl");
108610
+ const edgesPath = path201.join(metaprojectRoot, "data", "gdgraph", "storage", "edges.jsonl");
106635
108611
  if (!await pathExists(nodesPath) || !await pathExists(edgesPath)) {
106636
108612
  return;
106637
108613
  }
@@ -106682,8 +108658,8 @@ async function collectGraphDashboardData(metaprojectRoot) {
106682
108658
  };
106683
108659
  }
106684
108660
  async function collectTestingDashboardData(metaprojectRoot) {
106685
- const reportPath = path198.join(metaprojectRoot, "data", "testing", "artifacts", "latest.json");
106686
- const contextPath = path198.join(metaprojectRoot, "data", "testing", "context.md");
108661
+ const reportPath = path201.join(metaprojectRoot, "data", "testing", "artifacts", "latest.json");
108662
+ const contextPath = path201.join(metaprojectRoot, "data", "testing", "context.md");
106687
108663
  if (await pathExists(reportPath)) {
106688
108664
  const report = JSON.parse(await readFile100(reportPath, "utf8"));
106689
108665
  const totalTests = numberOrUndefined(report.total);
@@ -106712,7 +108688,7 @@ async function collectMarkdownPages(root, hrefPrefix) {
106712
108688
  const files = await listMarkdownFiles(root);
106713
108689
  const pages = [];
106714
108690
  for (const filePath of files.slice(0, 40)) {
106715
- const relativePath = path198.relative(root, filePath).split(path198.sep).join("/");
108691
+ const relativePath = path201.relative(root, filePath).split(path201.sep).join("/");
106716
108692
  if (relativePath === "index.md" || relativePath.startsWith("templates/")) {
106717
108693
  continue;
106718
108694
  }
@@ -106733,7 +108709,7 @@ async function listMarkdownFiles(root) {
106733
108709
  const entries = await readdir37(root, { withFileTypes: true });
106734
108710
  const files = [];
106735
108711
  for (const entry of entries) {
106736
- const fullPath = path198.join(root, entry.name);
108712
+ const fullPath = path201.join(root, entry.name);
106737
108713
  if (entry.isDirectory()) {
106738
108714
  files.push(...await listMarkdownFiles(fullPath));
106739
108715
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -106781,7 +108757,7 @@ async function writeRecoveredManifest(metaprojectRoot, modules) {
106781
108757
  const manifest = {
106782
108758
  schemaVersion: 1,
106783
108759
  standardVersion: STANDARD_VERSION,
106784
- name: `${path198.basename(path198.dirname(metaprojectRoot))}-metaproject`,
108760
+ name: `${path201.basename(path201.dirname(metaprojectRoot))}-metaproject`,
106785
108761
  createdBy: "keryx",
106786
108762
  profiles: computeProfiles(enabledModuleKeys),
106787
108763
  paths: {
@@ -106871,11 +108847,11 @@ async function writeRecoveredManifest(metaprojectRoot, modules) {
106871
108847
  metaproject: ".metaproject/index.md"
106872
108848
  }
106873
108849
  };
106874
- await writeFile54(path198.join(metaprojectRoot, "metaproject.json"), `${JSON.stringify(manifest, null, 2)}
108850
+ await writeFile54(path201.join(metaprojectRoot, "metaproject.json"), `${JSON.stringify(manifest, null, 2)}
106875
108851
  `, "utf8");
106876
108852
  }
106877
108853
  async function enableTasksInManifest(metaprojectRoot) {
106878
- const manifestPath = path198.join(metaprojectRoot, "metaproject.json");
108854
+ const manifestPath = path201.join(metaprojectRoot, "metaproject.json");
106879
108855
  if (!await pathExists(manifestPath)) {
106880
108856
  return;
106881
108857
  }
@@ -106898,7 +108874,7 @@ async function enableTasksInManifest(metaprojectRoot) {
106898
108874
  `, "utf8");
106899
108875
  }
106900
108876
  async function updateManifestAgentEntrypoints(metaprojectRoot, ruleSources) {
106901
- const manifestPath = path198.join(metaprojectRoot, "metaproject.json");
108877
+ const manifestPath = path201.join(metaprojectRoot, "metaproject.json");
106902
108878
  if (!await pathExists(manifestPath)) {
106903
108879
  return;
106904
108880
  }
@@ -106934,72 +108910,72 @@ async function updateRuntime(projectRoot) {
106934
108910
  }
106935
108911
  }
106936
108912
  async function findRuntimeRoot(projectRoot) {
106937
- const projectRuntime = path198.join(projectRoot, ".metaproject", "runtime", "keryx");
106938
- if (await pathExists(path198.join(projectRuntime, ".git"))) {
108913
+ const projectRuntime = path201.join(projectRoot, ".metaproject", "runtime", "keryx");
108914
+ if (await pathExists(path201.join(projectRuntime, ".git"))) {
106939
108915
  return projectRuntime;
106940
108916
  }
106941
108917
  const home = process.env.HOME;
106942
108918
  if (!home) {
106943
108919
  return null;
106944
108920
  }
106945
- const globalRuntime = path198.join(home, ".keryx", "keryx");
106946
- if (await pathExists(path198.join(globalRuntime, ".git"))) {
108921
+ const globalRuntime = path201.join(home, ".keryx", "keryx");
108922
+ if (await pathExists(path201.join(globalRuntime, ".git"))) {
106947
108923
  return globalRuntime;
106948
108924
  }
106949
108925
  return null;
106950
108926
  }
106951
108927
  async function createServiceDirs(metaprojectRoot, modules) {
106952
108928
  const dirs = [
106953
- path198.join(metaprojectRoot, "core"),
106954
- path198.join(metaprojectRoot, "hooks", "post-update.d"),
106955
- path198.join(metaprojectRoot, "modules"),
106956
- path198.join(metaprojectRoot, "rules"),
106957
- path198.join(metaprojectRoot, "skills", "project-rules"),
108929
+ path201.join(metaprojectRoot, "core"),
108930
+ path201.join(metaprojectRoot, "hooks", "post-update.d"),
108931
+ path201.join(metaprojectRoot, "modules"),
108932
+ path201.join(metaprojectRoot, "rules"),
108933
+ path201.join(metaprojectRoot, "skills", "project-rules"),
106958
108934
  ...modules.enableGdgraph ? [
106959
- path198.join(metaprojectRoot, "core", "gdgraph"),
106960
- path198.join(metaprojectRoot, "skills", "gdgraph")
108935
+ path201.join(metaprojectRoot, "core", "gdgraph"),
108936
+ path201.join(metaprojectRoot, "skills", "gdgraph")
106961
108937
  ] : [],
106962
108938
  ...modules.enableGdctx ? [
106963
- path198.join(metaprojectRoot, "core", "gdctx"),
106964
- path198.join(metaprojectRoot, "skills", "gdctx")
108939
+ path201.join(metaprojectRoot, "core", "gdctx"),
108940
+ path201.join(metaprojectRoot, "skills", "gdctx")
106965
108941
  ] : [],
106966
108942
  ...modules.enableGdwiki ? [
106967
- path198.join(metaprojectRoot, "skills", "gdwiki"),
106968
- path198.join(metaprojectRoot, "wiki", "templates")
108943
+ path201.join(metaprojectRoot, "skills", "gdwiki"),
108944
+ path201.join(metaprojectRoot, "wiki", "templates")
106969
108945
  ] : [],
106970
108946
  ...modules.enableHealth ? [
106971
- path198.join(metaprojectRoot, "core", "health"),
106972
- path198.join(metaprojectRoot, "skills", "health")
108947
+ path201.join(metaprojectRoot, "core", "health"),
108948
+ path201.join(metaprojectRoot, "skills", "health")
106973
108949
  ] : [],
106974
108950
  ...modules.enableTesting ? [
106975
- path198.join(metaprojectRoot, "core", "testing"),
106976
- path198.join(metaprojectRoot, "skills", "testing")
108951
+ path201.join(metaprojectRoot, "core", "testing"),
108952
+ path201.join(metaprojectRoot, "skills", "testing")
106977
108953
  ] : [],
106978
108954
  ...modules.enableMemory ? [
106979
- path198.join(metaprojectRoot, "core", "memory"),
106980
- path198.join(metaprojectRoot, "skills", "memory"),
106981
- path198.join(metaprojectRoot, "memory", "templates")
108955
+ path201.join(metaprojectRoot, "core", "memory"),
108956
+ path201.join(metaprojectRoot, "skills", "memory"),
108957
+ path201.join(metaprojectRoot, "memory", "templates")
106982
108958
  ] : [],
106983
108959
  ...modules.enableTasks ? [
106984
- path198.join(metaprojectRoot, "flows"),
106985
- path198.join(metaprojectRoot, "skills", "flow")
108960
+ path201.join(metaprojectRoot, "flows"),
108961
+ path201.join(metaprojectRoot, "skills", "flow")
106986
108962
  ] : [],
106987
108963
  ...modules.enableSecurity ? [
106988
- path198.join(metaprojectRoot, "core", "security")
108964
+ path201.join(metaprojectRoot, "core", "security")
106989
108965
  ] : [],
106990
108966
  ...modules.enableSac ? [
106991
- path198.join(metaprojectRoot, "skills", "sac")
108967
+ path201.join(metaprojectRoot, "skills", "sac")
106992
108968
  ] : []
106993
108969
  ];
106994
108970
  await Promise.all(dirs.map((dir) => mkdir63(dir, { recursive: true })));
106995
108971
  }
106996
108972
  async function installGdgraphCoreScripts2(metaprojectRoot) {
106997
- const gdgraphCoreRoot = path198.join(metaprojectRoot, "core", "gdgraph");
108973
+ const gdgraphCoreRoot = path201.join(metaprojectRoot, "core", "gdgraph");
106998
108974
  await mkdir63(gdgraphCoreRoot, { recursive: true });
106999
108975
  for (const file of GDGRAPH_CORE_SOURCES) {
107000
- await copyFileIfChanged2(runtimeSourcePath2(`../gdgraph/${file}`), path198.join(gdgraphCoreRoot, file));
108976
+ await copyFileIfChanged2(runtimeSourcePath2(`../gdgraph/${file}`), path201.join(gdgraphCoreRoot, file));
107001
108977
  }
107002
- await writeTextIfChanged4(path198.join(gdgraphCoreRoot, "cli.ts"), renderGdgraphCoreCli());
108978
+ await writeTextIfChanged4(path201.join(gdgraphCoreRoot, "cli.ts"), renderGdgraphCoreCli());
107003
108979
  }
107004
108980
  async function installManagedHook2(projectRoot, hookName, blockId, content) {
107005
108981
  const hooksRoot = await resolveGitHooksRoot(projectRoot);
@@ -107007,7 +108983,7 @@ async function installManagedHook2(projectRoot, hookName, blockId, content) {
107007
108983
  return;
107008
108984
  }
107009
108985
  await mkdir63(hooksRoot, { recursive: true });
107010
- const hookPath = path198.join(hooksRoot, hookName);
108986
+ const hookPath = path201.join(hooksRoot, hookName);
107011
108987
  const blockStart = `# keryx:${blockId}:begin`;
107012
108988
  const blockEnd = `# keryx:${blockId}:end`;
107013
108989
  const managedBlock = `${blockStart}
@@ -107028,7 +109004,7 @@ async function removeManagedHook2(projectRoot, hookName, blockId) {
107028
109004
  if (!hooksRoot) {
107029
109005
  return;
107030
109006
  }
107031
- const hookPath = path198.join(hooksRoot, hookName);
109007
+ const hookPath = path201.join(hooksRoot, hookName);
107032
109008
  if (!await pathExists(hookPath)) {
107033
109009
  return;
107034
109010
  }
@@ -107050,7 +109026,7 @@ async function prePushHasSecurityBlock2(projectRoot) {
107050
109026
  if (!hooksRoot) {
107051
109027
  return false;
107052
109028
  }
107053
- const hookPath = path198.join(hooksRoot, "pre-push");
109029
+ const hookPath = path201.join(hooksRoot, "pre-push");
107054
109030
  if (!await pathExists(hookPath)) {
107055
109031
  return false;
107056
109032
  }
@@ -107065,7 +109041,7 @@ async function agentSettingsHasSecuritySentinel2(projectRoot) {
107065
109041
  return (await readFile100(file, "utf8")).includes(AGENT_HOOKS_SENTINEL);
107066
109042
  }
107067
109043
  async function readManifest5(metaprojectRoot) {
107068
- const manifestPath = path198.join(metaprojectRoot, "metaproject.json");
109044
+ const manifestPath = path201.join(metaprojectRoot, "metaproject.json");
107069
109045
  if (!await pathExists(manifestPath)) {
107070
109046
  return {
107071
109047
  exists: false,
@@ -107134,7 +109110,7 @@ async function inferManifestFromExistingMetaproject(metaprojectRoot) {
107134
109110
  }
107135
109111
  async function anyPathExists(root, candidates) {
107136
109112
  for (const candidate of candidates) {
107137
- if (await pathExists(path198.join(root, candidate))) {
109113
+ if (await pathExists(path201.join(root, candidate))) {
107138
109114
  return true;
107139
109115
  }
107140
109116
  }
@@ -107157,13 +109133,13 @@ function parseUpdateArgs(args) {
107157
109133
  };
107158
109134
  }
107159
109135
  async function runPostUpdateHooks(projectRoot) {
107160
- const hooksDir = path198.join(projectRoot, ".metaproject", "hooks", "post-update.d");
109136
+ const hooksDir = path201.join(projectRoot, ".metaproject", "hooks", "post-update.d");
107161
109137
  if (!await pathExists(hooksDir)) {
107162
109138
  return;
107163
109139
  }
107164
109140
  const entries = (await readdir37(hooksDir)).sort();
107165
109141
  for (const entry of entries) {
107166
- const hookPath = path198.join(hooksDir, entry);
109142
+ const hookPath = path201.join(hooksDir, entry);
107167
109143
  try {
107168
109144
  await accessExecutable(hookPath);
107169
109145
  } catch {
@@ -107175,7 +109151,7 @@ async function runPostUpdateHooks(projectRoot) {
107175
109151
  }
107176
109152
  async function accessExecutable(filePath) {
107177
109153
  await new Promise((resolve, reject) => {
107178
- access4(filePath, constants2.X_OK, (error) => {
109154
+ access4(filePath, constants3.X_OK, (error) => {
107179
109155
  if (error) {
107180
109156
  reject(error);
107181
109157
  return;
@@ -107186,7 +109162,7 @@ async function accessExecutable(filePath) {
107186
109162
  }
107187
109163
  async function run(command, args, cwd) {
107188
109164
  await new Promise((resolve, reject) => {
107189
- const child = spawn6(command, args, {
109165
+ const child = spawn7(command, args, {
107190
109166
  cwd,
107191
109167
  stdio: "inherit"
107192
109168
  });
@@ -107204,14 +109180,14 @@ async function writeTextIfChanged4(filePath, content) {
107204
109180
  if (await pathExists(filePath) && await readFile100(filePath, "utf8") === content) {
107205
109181
  return;
107206
109182
  }
107207
- await mkdir63(path198.dirname(filePath), { recursive: true });
109183
+ await mkdir63(path201.dirname(filePath), { recursive: true });
107208
109184
  await writeFile54(filePath, content, "utf8");
107209
109185
  }
107210
109186
  async function writeTextIfMissing4(filePath, content) {
107211
109187
  if (await pathExists(filePath)) {
107212
109188
  return;
107213
109189
  }
107214
- await mkdir63(path198.dirname(filePath), { recursive: true });
109190
+ await mkdir63(path201.dirname(filePath), { recursive: true });
107215
109191
  await writeFile54(filePath, content, "utf8");
107216
109192
  }
107217
109193
  async function copyFileIfChanged2(from, to) {
@@ -107219,7 +109195,7 @@ async function copyFileIfChanged2(from, to) {
107219
109195
  if (await pathExists(to) && await readFile100(to, "utf8") === next) {
107220
109196
  return;
107221
109197
  }
107222
- await mkdir63(path198.dirname(to), { recursive: true });
109198
+ await mkdir63(path201.dirname(to), { recursive: true });
107223
109199
  await writeFile54(to, next, "utf8");
107224
109200
  }
107225
109201
  function runtimeSourcePath2(relativePath) {
@@ -107228,7 +109204,7 @@ function runtimeSourcePath2(relativePath) {
107228
109204
  return directPath;
107229
109205
  }
107230
109206
  if (relativePath.startsWith("../")) {
107231
- const packagedSourcePath = path198.join(path198.dirname(fileURLToPath8(import.meta.url)), "..", "src", relativePath.slice(3));
109207
+ const packagedSourcePath = path201.join(path201.dirname(fileURLToPath8(import.meta.url)), "..", "src", relativePath.slice(3));
107232
109208
  if (existsSync36(packagedSourcePath)) {
107233
109209
  return packagedSourcePath;
107234
109210
  }
@@ -107262,8 +109238,8 @@ function printHelp20() {
107262
109238
  }
107263
109239
 
107264
109240
  // src/commands/dashboard.ts
107265
- import { spawn as spawn7 } from "child_process";
107266
- import path199 from "path";
109241
+ import { spawn as spawn8 } from "child_process";
109242
+ import path202 from "path";
107267
109243
  init_args();
107268
109244
  async function dashboardCommand(args = []) {
107269
109245
  const options = parseOptions(args);
@@ -107274,7 +109250,7 @@ async function dashboardCommand(args = []) {
107274
109250
  }
107275
109251
  if (subcommand === "build") {
107276
109252
  const result = await buildDashboard();
107277
- const rel = path199.relative(process.cwd(), result.path);
109253
+ const rel = path202.relative(process.cwd(), result.path);
107278
109254
  console.log(` ${style.green(symbols.ok)} Dashboard built ${style.cyan(symbols.arrow)} ${style.cyan(rel)}`);
107279
109255
  note(`Open it: keryx dashboard open`);
107280
109256
  return;
@@ -107282,7 +109258,7 @@ async function dashboardCommand(args = []) {
107282
109258
  if (subcommand === "open") {
107283
109259
  const result = await buildDashboard();
107284
109260
  await openFile(result.path);
107285
- const rel = path199.relative(process.cwd(), result.path);
109261
+ const rel = path202.relative(process.cwd(), result.path);
107286
109262
  console.log(` ${style.green(symbols.ok)} Opened ${style.cyan(rel)}`);
107287
109263
  return;
107288
109264
  }
@@ -107301,7 +109277,7 @@ async function openFile(filePath) {
107301
109277
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
107302
109278
  const args = process.platform === "win32" ? ["/c", "start", "", filePath] : [filePath];
107303
109279
  await new Promise((resolve, reject) => {
107304
- const child = spawn7(command, args, { stdio: "ignore", detached: true });
109280
+ const child = spawn8(command, args, { stdio: "ignore", detached: true });
107305
109281
  child.on("error", reject);
107306
109282
  child.on("close", (code) => {
107307
109283
  if (code === 0) {
@@ -107326,12 +109302,12 @@ function printHelp21() {
107326
109302
  }
107327
109303
 
107328
109304
  // src/commands/agents.ts
107329
- import { readFileSync as readFileSync9 } from "fs";
109305
+ import { readFileSync as readFileSync12 } from "fs";
107330
109306
 
107331
109307
  // src/agents/bootstrap.ts
107332
109308
  import { mkdir as mkdir64, readFile as readFile101, writeFile as writeFile55 } from "fs/promises";
107333
109309
  import { homedir as homedir10 } from "os";
107334
- import path200 from "path";
109310
+ import path203 from "path";
107335
109311
  init_fs();
107336
109312
  var AGENT_BOOTSTRAP_START = "<!-- keryx:global-bootstrap -->";
107337
109313
  var AGENT_BOOTSTRAP_END = "<!-- /keryx:global-bootstrap -->";
@@ -107341,35 +109317,35 @@ var AGENT_BOOTSTRAP_RUNTIMES = [
107341
109317
  aliases: ["claude-code"],
107342
109318
  label: "Claude Code",
107343
109319
  fileName: "CLAUDE.md",
107344
- filePath: (homeRoot) => path200.join(homeRoot, ".claude", "CLAUDE.md")
109320
+ filePath: (homeRoot) => path203.join(homeRoot, ".claude", "CLAUDE.md")
107345
109321
  },
107346
109322
  {
107347
109323
  id: "opencode",
107348
109324
  aliases: ["open-code"],
107349
109325
  label: "OpenCode",
107350
109326
  fileName: "AGENTS.md",
107351
- filePath: (homeRoot) => path200.join(homeRoot, ".config", "opencode", "AGENTS.md")
109327
+ filePath: (homeRoot) => path203.join(homeRoot, ".config", "opencode", "AGENTS.md")
107352
109328
  },
107353
109329
  {
107354
109330
  id: "zcode",
107355
109331
  aliases: ["zed", "zed-code"],
107356
109332
  label: "ZCode",
107357
109333
  fileName: "AGENTS.md",
107358
- filePath: (homeRoot) => path200.join(homeRoot, ".zcode", "AGENTS.md")
109334
+ filePath: (homeRoot) => path203.join(homeRoot, ".zcode", "AGENTS.md")
107359
109335
  },
107360
109336
  {
107361
109337
  id: "codex",
107362
109338
  aliases: [],
107363
109339
  label: "Codex",
107364
109340
  fileName: "AGENTS.md",
107365
- filePath: (homeRoot) => path200.join(homeRoot, ".codex", "AGENTS.md")
109341
+ filePath: (homeRoot) => path203.join(homeRoot, ".codex", "AGENTS.md")
107366
109342
  },
107367
109343
  {
107368
109344
  id: "antigravity",
107369
109345
  aliases: ["antigravuty", "antigravity-code"],
107370
109346
  label: "Antigravity",
107371
109347
  fileName: "AGENTS.md",
107372
- filePath: (homeRoot) => path200.join(homeRoot, ".config", "antigravity", "AGENTS.md")
109348
+ filePath: (homeRoot) => path203.join(homeRoot, ".config", "antigravity", "AGENTS.md")
107373
109349
  }
107374
109350
  ];
107375
109351
  function agentBootstrapRuntimeIds() {
@@ -107417,7 +109393,7 @@ async function installAgentBootstrap(runtime, options = {}) {
107417
109393
  const dryRun = options.dryRun === true;
107418
109394
  const wrote = next !== current;
107419
109395
  if (wrote && !dryRun) {
107420
- await mkdir64(path200.dirname(filePath), { recursive: true });
109396
+ await mkdir64(path203.dirname(filePath), { recursive: true });
107421
109397
  await writeFile55(filePath, next, "utf8");
107422
109398
  }
107423
109399
  const status = dryRun ? statusFromContent(runtime, filePath, exists, next) : await agentBootstrapStatus(runtime, homeRoot);
@@ -107827,7 +109803,7 @@ function monitorCommand(args) {
107827
109803
  }
107828
109804
  let events;
107829
109805
  try {
107830
- events = parseAgentEvents(readFileSync9(source, "utf8"));
109806
+ events = parseAgentEvents(readFileSync12(source, "utf8"));
107831
109807
  } catch (error) {
107832
109808
  console.error(`Failed to read/parse agent-event source "${source}": ${error.message}`);
107833
109809
  process.exitCode = 1;
@@ -107954,7 +109930,7 @@ function printBootstrapHelp() {
107954
109930
  // src/commands/metrics.ts
107955
109931
  init_args();
107956
109932
  import { readFile as readFile102 } from "fs/promises";
107957
- import path202 from "path";
109933
+ import path205 from "path";
107958
109934
 
107959
109935
  // src/metrics/benchmark.ts
107960
109936
  var RELIABILITIES2 = new Set(["exact", "estimated", "unknown"]);
@@ -108771,7 +110747,7 @@ function buildContainmentManifest(inputs, options = {}) {
108771
110747
 
108772
110748
  // src/metrics/oracle-runner.ts
108773
110749
  import { mkdir as mkdir65, writeFile as writeFile56 } from "fs/promises";
108774
- import path201 from "path";
110750
+ import path204 from "path";
108775
110751
 
108776
110752
  // src/metrics/ir.ts
108777
110753
  function toIdSet(ids) {
@@ -109217,9 +111193,9 @@ function buildEvidenceBundle(input, options = {}) {
109217
111193
  async function persistEvidenceBundle(outDir, bundle, ladder = "metastore") {
109218
111194
  const safeTarget = bundle.target.replace(/[^A-Za-z0-9._/-]/g, "_");
109219
111195
  const safeCase = bundle.caseId.replace(/[^A-Za-z0-9._-]/g, "_");
109220
- const dir = path201.join(outDir, "bench", ladder, safeTarget, safeCase, bundle.variant, String(bundle.seed));
111196
+ const dir = path204.join(outDir, "bench", ladder, safeTarget, safeCase, bundle.variant, String(bundle.seed));
109221
111197
  await mkdir65(dir, { recursive: true });
109222
- const write = (name, value) => writeFile56(path201.join(dir, name), `${JSON.stringify(value, null, 2)}
111198
+ const write = (name, value) => writeFile56(path204.join(dir, name), `${JSON.stringify(value, null, 2)}
109223
111199
  `, "utf8");
109224
111200
  await Promise.all([
109225
111201
  write("inputs.json", bundle.inputs),
@@ -109259,7 +111235,7 @@ async function metricsCommand(args = [], projectRoot = process.cwd()) {
109259
111235
  console.log("# metrics status");
109260
111236
  console.log("");
109261
111237
  console.log(`root: ${root}`);
109262
- console.log(`enabled: ${await Bun.file(path202.join(projectRoot, ".metaproject", "metaproject.json")).exists() ? "yes" : "no"}`);
111238
+ console.log(`enabled: ${await Bun.file(path205.join(projectRoot, ".metaproject", "metaproject.json")).exists() ? "yes" : "no"}`);
109263
111239
  const latest = await readLatestPointer(root);
109264
111240
  console.log(`latest: ${latest.status}`);
109265
111241
  return;
@@ -109271,7 +111247,7 @@ async function metricsCommand(args = [], projectRoot = process.cwd()) {
109271
111247
  process.exitCode = 1;
109272
111248
  return;
109273
111249
  }
109274
- const record = JSON.parse(await readFile102(path202.resolve(projectRoot, file), "utf8"));
111250
+ const record = JSON.parse(await readFile102(path205.resolve(projectRoot, file), "utf8"));
109275
111251
  const result = validateRunRecord(record);
109276
111252
  console.log(result.valid ? "valid: yes" : "valid: no");
109277
111253
  for (const error of result.errors)
@@ -109296,7 +111272,7 @@ async function metricsCommand(args = [], projectRoot = process.cwd()) {
109296
111272
  process.exitCode = 1;
109297
111273
  return;
109298
111274
  }
109299
- const file = path202.join(metricsRoot(projectRoot), "runs", `${runId}.json`);
111275
+ const file = path205.join(metricsRoot(projectRoot), "runs", `${runId}.json`);
109300
111276
  if (!await Bun.file(file).exists()) {
109301
111277
  console.error(`Run not found: ${runId}`);
109302
111278
  process.exitCode = 1;
@@ -109313,8 +111289,8 @@ async function metricsCommand(args = [], projectRoot = process.cwd()) {
109313
111289
  process.exitCode = 1;
109314
111290
  return;
109315
111291
  }
109316
- const a = JSON.parse(await readFile102(path202.join(metricsRoot(projectRoot), "runs", `${runA}.json`), "utf8"));
109317
- const b = JSON.parse(await readFile102(path202.join(metricsRoot(projectRoot), "runs", `${runB}.json`), "utf8"));
111292
+ const a = JSON.parse(await readFile102(path205.join(metricsRoot(projectRoot), "runs", `${runA}.json`), "utf8"));
111293
+ const b = JSON.parse(await readFile102(path205.join(metricsRoot(projectRoot), "runs", `${runB}.json`), "utf8"));
109318
111294
  const comparison = compareExecutionRuns(a, b);
109319
111295
  console.log(stableJson(comparison));
109320
111296
  return;
@@ -109348,8 +111324,8 @@ async function metricsCommand(args = [], projectRoot = process.cwd()) {
109348
111324
  return;
109349
111325
  }
109350
111326
  const template = createPairedBenchmarkTemplate(taskIds);
109351
- await Bun.write(path202.resolve(projectRoot, out), stableJson(template));
109352
- console.log(`manifest: ${path202.relative(projectRoot, path202.resolve(projectRoot, out))}`);
111327
+ await Bun.write(path205.resolve(projectRoot, out), stableJson(template));
111328
+ console.log(`manifest: ${path205.relative(projectRoot, path205.resolve(projectRoot, out))}`);
109353
111329
  return;
109354
111330
  }
109355
111331
  if (subcommand === "benchmark" && args[1] === "run") {
@@ -109363,7 +111339,7 @@ async function metricsCommand(args = [], projectRoot = process.cwd()) {
109363
111339
  process.exitCode = 1;
109364
111340
  return;
109365
111341
  }
109366
- const raw = JSON.parse(await readFile102(path202.resolve(projectRoot, file), "utf8"));
111342
+ const raw = JSON.parse(await readFile102(path205.resolve(projectRoot, file), "utf8"));
109367
111343
  const input = Array.isArray(raw) ? raw : raw.runs ?? [];
109368
111344
  const result = validatePairedBenchmark(input);
109369
111345
  console.log(stableJson(result));
@@ -109375,7 +111351,7 @@ async function metricsCommand(args = [], projectRoot = process.cwd()) {
109375
111351
  process.exitCode = 1;
109376
111352
  }
109377
111353
  async function loadAffectedSets(projectRoot, file) {
109378
- const raw = JSON.parse(await readFile102(path202.resolve(projectRoot, file), "utf8"));
111354
+ const raw = JSON.parse(await readFile102(path205.resolve(projectRoot, file), "utf8"));
109379
111355
  const map = new Map;
109380
111356
  for (const entry of raw.targets ?? []) {
109381
111357
  if (typeof entry.target === "string")
@@ -109446,7 +111422,7 @@ async function runHarnessLayer(projectRoot, args, ladder) {
109446
111422
  let tasks;
109447
111423
  let model;
109448
111424
  try {
109449
- const raw = JSON.parse(await readFile102(path202.resolve(projectRoot, resultsPath), "utf8"));
111425
+ const raw = JSON.parse(await readFile102(path205.resolve(projectRoot, resultsPath), "utf8"));
109450
111426
  tasks = raw.tasks ?? [];
109451
111427
  model = raw.model;
109452
111428
  } catch (error) {
@@ -109477,7 +111453,7 @@ async function runSafetyCompletionHonestyLayer(projectRoot, args, ladder) {
109477
111453
  let cases;
109478
111454
  let model;
109479
111455
  try {
109480
- const raw = JSON.parse(await readFile102(path202.resolve(projectRoot, resultsPath), "utf8"));
111456
+ const raw = JSON.parse(await readFile102(path205.resolve(projectRoot, resultsPath), "utf8"));
109481
111457
  cases = raw.cases ?? [];
109482
111458
  model = raw.model;
109483
111459
  } catch (error) {
@@ -109502,7 +111478,7 @@ async function runSafetyFalsePremiseLayer(projectRoot, args, ladder) {
109502
111478
  let cases;
109503
111479
  let model;
109504
111480
  try {
109505
- const raw = JSON.parse(await readFile102(path202.resolve(projectRoot, resultsPath), "utf8"));
111481
+ const raw = JSON.parse(await readFile102(path205.resolve(projectRoot, resultsPath), "utf8"));
109506
111482
  cases = raw.cases ?? [];
109507
111483
  model = raw.model;
109508
111484
  } catch (error) {
@@ -109527,7 +111503,7 @@ async function runSafetyContainmentLayer(projectRoot, args, ladder, caseClass) {
109527
111503
  let cases;
109528
111504
  let model;
109529
111505
  try {
109530
- const raw = JSON.parse(await readFile102(path202.resolve(projectRoot, resultsPath), "utf8"));
111506
+ const raw = JSON.parse(await readFile102(path205.resolve(projectRoot, resultsPath), "utf8"));
109531
111507
  cases = raw.cases ?? [];
109532
111508
  model = raw.model;
109533
111509
  } catch (error) {
@@ -109589,12 +111565,12 @@ async function runGdgraphLayer(projectRoot, args, ladder) {
109589
111565
  }
109590
111566
  const manifests = buildOracleManifestsByGold(inputs, { ladder });
109591
111567
  if (outDir) {
109592
- const resolvedOut = path202.resolve(projectRoot, outDir);
111568
+ const resolvedOut = path205.resolve(projectRoot, outDir);
109593
111569
  for (const input of inputs) {
109594
111570
  for (const named of input.golds) {
109595
111571
  const bundle = buildEvidenceBundle({ target: input.target, system: input.system, gold: named.gold }, { ladder, goldReference: goldPathFor(named.kind), timestamp: new Date().toISOString() });
109596
- const dir = await persistEvidenceBundle(path202.join(resolvedOut, named.kind), bundle, ladder);
109597
- console.error(`bundle[${named.kind}]: ${path202.relative(projectRoot, dir)}`);
111572
+ const dir = await persistEvidenceBundle(path205.join(resolvedOut, named.kind), bundle, ladder);
111573
+ console.error(`bundle[${named.kind}]: ${path205.relative(projectRoot, dir)}`);
109598
111574
  }
109599
111575
  }
109600
111576
  }
@@ -109618,7 +111594,7 @@ async function runGdgraphLayer(projectRoot, args, ladder) {
109618
111594
  return allValid;
109619
111595
  }
109620
111596
  async function loadCoverageMap2(projectRoot, file) {
109621
- const raw = JSON.parse(await readFile102(path202.resolve(projectRoot, file), "utf8"));
111597
+ const raw = JSON.parse(await readFile102(path205.resolve(projectRoot, file), "utf8"));
109622
111598
  return raw.coverageMap ?? {};
109623
111599
  }
109624
111600
  async function runTestingLayer(projectRoot, args, ladder) {
@@ -109655,7 +111631,7 @@ async function runTestingLayer(projectRoot, args, ladder) {
109655
111631
  return result.valid;
109656
111632
  }
109657
111633
  async function loadMemoryGoldK(projectRoot, file) {
109658
- const raw = JSON.parse(await readFile102(path202.resolve(projectRoot, file), "utf8"));
111634
+ const raw = JSON.parse(await readFile102(path205.resolve(projectRoot, file), "utf8"));
109659
111635
  return typeof raw.k === "number" && raw.k > 0 ? raw.k : 3;
109660
111636
  }
109661
111637
  async function runMemoryLayer(projectRoot, args, ladder) {
@@ -109692,11 +111668,11 @@ async function runMemoryLayer(projectRoot, args, ladder) {
109692
111668
  return result.valid;
109693
111669
  }
109694
111670
  async function loadWikiGoldK(projectRoot, file) {
109695
- const raw = JSON.parse(await readFile102(path202.resolve(projectRoot, file), "utf8"));
111671
+ const raw = JSON.parse(await readFile102(path205.resolve(projectRoot, file), "utf8"));
109696
111672
  return typeof raw.k === "number" && raw.k > 0 ? raw.k : 5;
109697
111673
  }
109698
111674
  async function loadWikiGroundedness(projectRoot, file) {
109699
- const raw = JSON.parse(await readFile102(path202.resolve(projectRoot, file), "utf8"));
111675
+ const raw = JSON.parse(await readFile102(path205.resolve(projectRoot, file), "utf8"));
109700
111676
  const map = new Map;
109701
111677
  for (const entry of raw.targets ?? []) {
109702
111678
  if (typeof entry.target !== "string" || !Array.isArray(entry.scores) || entry.scores.length !== 3)
@@ -109752,7 +111728,7 @@ async function runWikiLayer(projectRoot, args, ladder) {
109752
111728
  return result.valid;
109753
111729
  }
109754
111730
  async function loadGdctxFacts(projectRoot, file) {
109755
- const raw = JSON.parse(await readFile102(path202.resolve(projectRoot, file), "utf8"));
111731
+ const raw = JSON.parse(await readFile102(path205.resolve(projectRoot, file), "utf8"));
109756
111732
  const inputs = [];
109757
111733
  for (const entry of raw.inputs ?? []) {
109758
111734
  if (typeof entry.input === "string") {
@@ -109790,7 +111766,7 @@ async function collect(projectRoot, args) {
109790
111766
  process.exitCode = 1;
109791
111767
  return;
109792
111768
  }
109793
- const raw = JSON.parse(await readFile102(path202.resolve(projectRoot, eventFile), "utf8"));
111769
+ const raw = JSON.parse(await readFile102(path205.resolve(projectRoot, eventFile), "utf8"));
109794
111770
  const events = Array.isArray(raw) ? raw : raw.events;
109795
111771
  const startedAt = optionValue(args, "--started-at") ?? events[0]?.timestamp_utc ?? new Date().toISOString();
109796
111772
  const finishedAt = optionValue(args, "--finished-at") ?? events.at(-1)?.timestamp_utc ?? startedAt;
@@ -109806,11 +111782,11 @@ async function collect(projectRoot, args) {
109806
111782
  parentRunId: optionValue(args, "--parent-run-id") ?? null
109807
111783
  });
109808
111784
  const result = await writeRunArtifacts(metricsRoot(projectRoot), record, { cwd: projectRoot });
109809
- console.log(`json: ${path202.relative(projectRoot, result.jsonPath)}`);
109810
- console.log(`markdown: ${path202.relative(projectRoot, result.markdownPath)}`);
111785
+ console.log(`json: ${path205.relative(projectRoot, result.jsonPath)}`);
111786
+ console.log(`markdown: ${path205.relative(projectRoot, result.markdownPath)}`);
109811
111787
  }
109812
111788
  function metricsRoot(projectRoot) {
109813
- return path202.join(projectRoot, ".metaproject", "data", "metrics");
111789
+ return path205.join(projectRoot, ".metaproject", "data", "metrics");
109814
111790
  }
109815
111791
  function printMetricsHelp() {
109816
111792
  console.log(`keryx metrics
@@ -109862,7 +111838,7 @@ async function versionCommand(args, deps = {}) {
109862
111838
  // src/commands/workspace.ts
109863
111839
  init_args();
109864
111840
  init_workspace_service();
109865
- import { randomUUID as randomUUID33 } from "crypto";
111841
+ import { randomUUID as randomUUID34 } from "crypto";
109866
111842
  import { writeFile as writeFile57 } from "fs/promises";
109867
111843
 
109868
111844
  // src/sac/fwk-explain.ts
@@ -109941,7 +111917,7 @@ async function workspaceCommand(args) {
109941
111917
  const component = optionValue(args, "--component");
109942
111918
  if (!title)
109943
111919
  throw new Error("Usage: keryx workspace create --title <title> [--component <workspace-relative-ref>]");
109944
- const workspace = await service6().create({ request: undefined, requestCorrelationId: randomUUID33(), id: newWorkspaceId(), title, ...component ? { component: { kind: "component", uri: component } } : {} });
111920
+ const workspace = await service6().create({ request: undefined, requestCorrelationId: randomUUID34(), id: newWorkspaceId(), title, ...component ? { component: { kind: "component", uri: component } } : {} });
109945
111921
  console.log(JSON.stringify(workspace, null, 2));
109946
111922
  return;
109947
111923
  }
@@ -109971,7 +111947,7 @@ async function workspaceCommand(args) {
109971
111947
  const revision = optionValue(args, "--revision");
109972
111948
  if (!workspaceId || !kind || !uri)
109973
111949
  throw new Error("Usage: keryx workspace add-resource <workspace-id> --kind <kind> --uri <workspace-relative-ref> [--revision <revision>]");
109974
- console.log(JSON.stringify(await service6().addResource({ request: undefined, requestCorrelationId: randomUUID33(), workspaceId, resource: { kind, uri, ...revision ? { revision } : {} } }), null, 2));
111950
+ console.log(JSON.stringify(await service6().addResource({ request: undefined, requestCorrelationId: randomUUID34(), workspaceId, resource: { kind, uri, ...revision ? { revision } : {} } }), null, 2));
109975
111951
  return;
109976
111952
  }
109977
111953
  if (subcommand === "archive") {
@@ -109979,7 +111955,7 @@ async function workspaceCommand(args) {
109979
111955
  const workspaceId = args[1];
109980
111956
  if (!workspaceId)
109981
111957
  throw new Error("Usage: keryx workspace archive <workspace-id>");
109982
- console.log(JSON.stringify(await service6().archive({ request: undefined, requestCorrelationId: randomUUID33(), workspaceId }), null, 2));
111958
+ console.log(JSON.stringify(await service6().archive({ request: undefined, requestCorrelationId: randomUUID34(), workspaceId }), null, 2));
109983
111959
  return;
109984
111960
  }
109985
111961
  if (subcommand === "remove-resource") {
@@ -109988,7 +111964,7 @@ async function workspaceCommand(args) {
109988
111964
  const uri = optionValue(args, "--uri");
109989
111965
  if (!workspaceId || !uri)
109990
111966
  throw new Error("Usage: keryx workspace remove-resource <workspace-id> --uri <workspace-relative-ref>");
109991
- console.log(JSON.stringify(await service6().removeResource({ request: undefined, requestCorrelationId: randomUUID33(), workspaceId, uri }), null, 2));
111967
+ console.log(JSON.stringify(await service6().removeResource({ request: undefined, requestCorrelationId: randomUUID34(), workspaceId, uri }), null, 2));
109992
111968
  return;
109993
111969
  }
109994
111970
  if (subcommand === "rename") {
@@ -109997,7 +111973,7 @@ async function workspaceCommand(args) {
109997
111973
  const title = optionValue(args, "--title");
109998
111974
  if (!workspaceId || !title)
109999
111975
  throw new Error("Usage: keryx workspace rename <workspace-id> --title <title>");
110000
- console.log(JSON.stringify(await service6().rename({ request: undefined, requestCorrelationId: randomUUID33(), workspaceId, title }), null, 2));
111976
+ console.log(JSON.stringify(await service6().rename({ request: undefined, requestCorrelationId: randomUUID34(), workspaceId, title }), null, 2));
110001
111977
  return;
110002
111978
  }
110003
111979
  if (subcommand === "overview") {
@@ -110009,7 +111985,7 @@ async function workspaceCommand(args) {
110009
111985
  const maxTokens = Number(optionValue(args, "--max-tokens") ?? "4096");
110010
111986
  if (!Number.isInteger(maxItems) || !Number.isInteger(maxTokens) || maxItems < 0 || maxTokens < 0)
110011
111987
  throw new Error("--max-items and --max-tokens must be non-negative integers");
110012
- const result = await createLocalFwkReadService(process.cwd()).overview({ workspaceId, request: undefined, requestCorrelationId: randomUUID33(), budget: { maxItems, maxTokens } });
111988
+ const result = await createLocalFwkReadService(process.cwd()).overview({ workspaceId, request: undefined, requestCorrelationId: randomUUID34(), budget: { maxItems, maxTokens } });
110013
111989
  const normalized = normalizeFwkResult(result);
110014
111990
  console.log(JSON.stringify(normalized, null, 2));
110015
111991
  if (args.includes("--explain"))
@@ -110026,7 +112002,7 @@ async function workspaceCommand(args) {
110026
112002
  const maxTokens = Number(optionValue(args, "--max-tokens") ?? "4096");
110027
112003
  if (!Number.isInteger(maxItems) || !Number.isInteger(maxTokens) || maxItems < 0 || maxTokens < 0)
110028
112004
  throw new Error("--max-items and --max-tokens must be non-negative integers");
110029
- const result = await createLocalFwkReadService(process.cwd()).read({ workspaceId, itemId, request: undefined, requestCorrelationId: randomUUID33(), budget: { maxItems, maxTokens } });
112005
+ const result = await createLocalFwkReadService(process.cwd()).read({ workspaceId, itemId, request: undefined, requestCorrelationId: randomUUID34(), budget: { maxItems, maxTokens } });
110030
112006
  const normalized = normalizeFwkResult(result);
110031
112007
  console.log(JSON.stringify(normalized, null, 2));
110032
112008
  if (args.includes("--explain"))
@@ -110048,14 +112024,14 @@ async function workspaceCommand(args) {
110048
112024
  const session = findSession(cwd, sessionRef);
110049
112025
  if (!session)
110050
112026
  throw new Error(`no session matching "${sessionRef}" in this project \u2014 use \`keryx sessions list\``);
110051
- const proposalId = `proposal-${randomUUID33().replace(/-/g, "").slice(0, 16)}`;
112027
+ const proposalId = `proposal-${randomUUID34().replace(/-/g, "").slice(0, 16)}`;
110052
112028
  const notePath = proposalNotePath(cwd, workspaceId, proposalId);
110053
112029
  const materializedNote = note === undefined ? undefined : prepareOutputForPersistence(await guardOutput({ cwd, content: note, target: "task", source: "generated", path: notePath }), note);
110054
112030
  if (materializedNote !== undefined && !materializedNote.allowed)
110055
112031
  throw new Error(materializedNote.reason);
110056
112032
  const persistedNote = materializedNote?.allowed ? materializedNote.content : undefined;
110057
112033
  const { service, wrapUpAuthority, authorizationServer } = createHarnessProposalLifecycleService(cwd, { workspaceId, ...persistedNote ? { note: persistedNote } : {} });
110058
- const requestCorrelationId = randomUUID33();
112034
+ const requestCorrelationId = randomUUID34();
110059
112035
  const actor = await authorizationServer.actorContextFor(undefined, requestCorrelationId);
110060
112036
  if (!actor)
110061
112037
  throw new Error("trusted ActorContext is required");
@@ -110095,13 +112071,13 @@ async function workspaceCommand(args) {
110095
112071
  const proposalId = args[2];
110096
112072
  const decision = optionValue(args, "--decision");
110097
112073
  const reason = optionValue(args, "--reason");
110098
- const idempotencyKey = optionValue(args, "--idempotency-key") ?? randomUUID33();
112074
+ const idempotencyKey = optionValue(args, "--idempotency-key") ?? randomUUID34();
110099
112075
  const confirmToken = optionValue(args, "--confirm-token");
110100
112076
  if (!workspaceId || !proposalId || !decision)
110101
112077
  throw new Error("Usage: keryx workspace review <workspace-id> <proposal-id> --decision <accepted|rejected|dismissed> [--reason <reason>] [--idempotency-key <key>] [--confirm-token <token>]");
110102
112078
  if (decision === "accepted" && !confirmToken)
110103
112079
  throw new Error("--decision accepted requires --confirm-token \u2014 run `keryx workspace confirm-review " + workspaceId + " " + proposalId + "` first");
110104
- const result = await createHarnessProposalLifecycleService(process.cwd(), { workspaceId }).service.review({ request: undefined, requestCorrelationId: randomUUID33(), workspaceId, proposalId, decision, idempotencyKey, interactive: true, ...reason ? { reason } : {}, ...confirmToken ? { confirmToken } : {} });
112080
+ const result = await createHarnessProposalLifecycleService(process.cwd(), { workspaceId }).service.review({ request: undefined, requestCorrelationId: randomUUID34(), workspaceId, proposalId, decision, idempotencyKey, interactive: true, ...reason ? { reason } : {}, ...confirmToken ? { confirmToken } : {} });
110105
112081
  console.log(JSON.stringify(normalizeProposalLifecycleResult(result), null, 2));
110106
112082
  return;
110107
112083
  }
@@ -110126,7 +112102,7 @@ async function workspaceCommand(args) {
110126
112102
  const artifactRef = optionValue(args, "--artifact");
110127
112103
  if (!workspaceId || !to || !artifactRef)
110128
112104
  throw new Error("Usage: keryx workspace handoff <workspace-id> --to <subject> --artifact <ref>");
110129
- const activity = await createLocalCollaborationService(process.cwd()).record({ request: undefined, requestCorrelationId: randomUUID33(), workspaceId, activity: { kind: "handoff-recorded", handoff: { to, artifactRef } } });
112105
+ const activity = await createLocalCollaborationService(process.cwd()).record({ request: undefined, requestCorrelationId: randomUUID34(), workspaceId, activity: { kind: "handoff-recorded", handoff: { to, artifactRef } } });
110130
112106
  console.log(JSON.stringify(activity, null, 2));
110131
112107
  return;
110132
112108
  }
@@ -110135,7 +112111,7 @@ async function workspaceCommand(args) {
110135
112111
  const workspaceId = args[1];
110136
112112
  if (!workspaceId)
110137
112113
  throw new Error("Usage: keryx workspace collaboration <workspace-id>");
110138
- console.log(JSON.stringify(await createLocalCollaborationService(process.cwd()).overview({ workspaceId, request: undefined, requestCorrelationId: randomUUID33() }), null, 2));
112114
+ console.log(JSON.stringify(await createLocalCollaborationService(process.cwd()).overview({ workspaceId, request: undefined, requestCorrelationId: randomUUID34() }), null, 2));
110139
112115
  return;
110140
112116
  }
110141
112117
  if (subcommand === "policy-readiness") {
@@ -110162,7 +112138,7 @@ async function workspaceCommand(args) {
110162
112138
  const workspaceId = args[1];
110163
112139
  const cwd = process.cwd();
110164
112140
  const authorizationServer = localWorkspaceAuthorizationServer();
110165
- const actor = await authorizationServer.actorContextFor(undefined, randomUUID33());
112141
+ const actor = await authorizationServer.actorContextFor(undefined, randomUUID34());
110166
112142
  if (!actor)
110167
112143
  throw new Error("trusted ActorContext is required");
110168
112144
  if (workspaceId) {
@@ -110901,13 +112877,17 @@ Commands:
110901
112877
  forgetting Read the deletion trail \u2014 was this removed, or did it never exist?
110902
112878
  `);
110903
112879
  }
112880
+ function exitCodeForError(error) {
112881
+ return error instanceof ShellFlagError ? error.exitCode : 1;
112882
+ }
110904
112883
  if (import.meta.main) {
110905
112884
  main().catch((error) => {
110906
112885
  console.error(error instanceof Error ? error.message : String(error));
110907
- process.exitCode = 1;
112886
+ process.exitCode = exitCodeForError(error);
110908
112887
  });
110909
112888
  }
110910
112889
  export {
110911
112890
  CLI_ROUTES,
112891
+ exitCodeForError,
110912
112892
  main
110913
112893
  };