@mrciphersmith/keryx 0.2.75 → 0.2.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -11214,9 +11214,9 @@ function resolvePointer(doc, fragment) {
11214
11214
  if (!fragment.startsWith("/")) {
11215
11215
  throw new Error(`Unsupported JSON Pointer fragment (must start with "/"): ${fragment}`);
11216
11216
  }
11217
- const segments2 = fragment.split("/").slice(1).map(decodePointerSegment);
11217
+ const segments = fragment.split("/").slice(1).map(decodePointerSegment);
11218
11218
  let node = doc;
11219
- for (const segment of segments2) {
11219
+ for (const segment of segments) {
11220
11220
  if (node === null || typeof node !== "object" || Array.isArray(node)) {
11221
11221
  throw new Error(`Pointer segment "${segment}" not resolvable in fragment #${fragment}`);
11222
11222
  }
@@ -12495,12 +12495,12 @@ class FakeProvider {
12495
12495
  }
12496
12496
  const { attemptId } = opts;
12497
12497
  let sequence = 0;
12498
- const emit = (body) => ({
12498
+ const emit2 = (body) => ({
12499
12499
  ...body,
12500
12500
  sequence: sequence++,
12501
12501
  attemptId
12502
12502
  });
12503
- yield emit({ kind: "model_start" });
12503
+ yield emit2({ kind: "model_start" });
12504
12504
  for (const raw of transcript.events) {
12505
12505
  const payload = raw.payload ?? {};
12506
12506
  switch (raw.kind) {
@@ -12513,13 +12513,13 @@ class FakeProvider {
12513
12513
  if (extensions !== undefined) {
12514
12514
  body.unknownExtensions = extensions;
12515
12515
  }
12516
- yield emit(body);
12516
+ yield emit2(body);
12517
12517
  break;
12518
12518
  }
12519
12519
  case "tool_call": {
12520
12520
  const input2 = payload.input;
12521
12521
  if (!isPlainObject6(input2)) {
12522
- yield emit({
12522
+ yield emit2({
12523
12523
  kind: "provider_error",
12524
12524
  error: {
12525
12525
  kind: "malformed",
@@ -12538,21 +12538,21 @@ class FakeProvider {
12538
12538
  if (typeof payload.toolName === "string") {
12539
12539
  startBody.toolName = payload.toolName;
12540
12540
  }
12541
- yield emit(startBody);
12542
- yield emit(endBody);
12541
+ yield emit2(startBody);
12542
+ yield emit2(endBody);
12543
12543
  break;
12544
12544
  }
12545
12545
  case "finish": {
12546
12546
  const usage = payload.usage;
12547
12547
  if (isPlainObject6(usage)) {
12548
- yield emit({ kind: "usage_update", usage: mapUsage(usage) });
12548
+ yield emit2({ kind: "usage_update", usage: mapUsage(usage) });
12549
12549
  }
12550
12550
  const body = { kind: "model_end" };
12551
12551
  const extensions = extractUnknownExtensions(payload);
12552
12552
  if (extensions !== undefined) {
12553
12553
  body.unknownExtensions = extensions;
12554
12554
  }
12555
- yield emit(body);
12555
+ yield emit2(body);
12556
12556
  break;
12557
12557
  }
12558
12558
  case "error": {
@@ -12561,7 +12561,7 @@ class FakeProvider {
12561
12561
  retryable: payload.retryable,
12562
12562
  message: payload.message
12563
12563
  };
12564
- yield emit({ kind: "provider_error", error });
12564
+ yield emit2({ kind: "provider_error", error });
12565
12565
  return;
12566
12566
  }
12567
12567
  }
@@ -13660,7 +13660,10 @@ async function runModelTurn(input2) {
13660
13660
  let reasoning = false;
13661
13661
  const startedAt = performance.now();
13662
13662
  let firstByteAt;
13663
- for await (const event of port.stream(request, { attemptId: request.requestId })) {
13663
+ for await (const event of port.stream(request, {
13664
+ attemptId: request.requestId,
13665
+ ...input2.signal !== undefined ? { signal: input2.signal } : {}
13666
+ })) {
13664
13667
  if (firstByteAt === undefined) {
13665
13668
  firstByteAt = performance.now();
13666
13669
  }
@@ -13849,14 +13852,14 @@ function hasUnquotedMetacharacter(command) {
13849
13852
  return state.unbalanced || state.metacharacter;
13850
13853
  }
13851
13854
  function splitSegments(command) {
13852
- const segments2 = [];
13855
+ const segments = [];
13853
13856
  let buf = "";
13854
13857
  let pending;
13855
13858
  let quote;
13856
13859
  const flush = (next) => {
13857
13860
  const raw = buf.trim();
13858
13861
  if (raw.length > 0) {
13859
- segments2.push({
13862
+ segments.push({
13860
13863
  raw,
13861
13864
  words: splitWords(raw),
13862
13865
  ...pending !== undefined ? { precededBy: pending } : {}
@@ -13898,7 +13901,7 @@ function splitSegments(command) {
13898
13901
  buf += ch;
13899
13902
  }
13900
13903
  flush();
13901
- return segments2;
13904
+ return segments;
13902
13905
  }
13903
13906
  function separatorAt(ch, next) {
13904
13907
  if (ch === "&" && next === "&")
@@ -14002,18 +14005,18 @@ function classifySegment(seg) {
14002
14005
  function classifyCommand2(command) {
14003
14006
  if (command.trim().length === 0)
14004
14007
  return "shell";
14005
- const segments2 = splitSegments(command);
14006
- for (const seg of segments2) {
14008
+ const segments = splitSegments(command);
14009
+ for (const seg of segments) {
14007
14010
  if (classifySegment(seg))
14008
14011
  return "destructive";
14009
14012
  }
14010
- for (let i = 1;i < segments2.length; i++) {
14011
- const seg = segments2[i];
14013
+ for (let i = 1;i < segments.length; i++) {
14014
+ const seg = segments[i];
14012
14015
  if (seg.precededBy !== "|")
14013
14016
  continue;
14014
14017
  if (!INTERPRETERS.has(head(seg.words)))
14015
14018
  continue;
14016
- const upstream = segments2.slice(0, i);
14019
+ const upstream = segments.slice(0, i);
14017
14020
  if (upstream.some((s) => DOWNLOADERS.has(head(s.words))))
14018
14021
  return "destructive";
14019
14022
  }
@@ -29245,6 +29248,32 @@ var init_metaproject_operations = __esm(() => {
29245
29248
 
29246
29249
  // src/wiki/deep-enrich.ts
29247
29250
  import { randomUUID as randomUUID15, createHash as createHash22 } from "crypto";
29251
+ function composeAbortSignals(external, timeout) {
29252
+ if (external === undefined) {
29253
+ return { signal: timeout, dispose: () => {} };
29254
+ }
29255
+ const controller = new AbortController;
29256
+ const abortFrom = (signal) => {
29257
+ if (!controller.signal.aborted) {
29258
+ controller.abort(signal.reason);
29259
+ }
29260
+ };
29261
+ const onExternalAbort = () => abortFrom(external);
29262
+ const onTimeoutAbort = () => abortFrom(timeout);
29263
+ external.addEventListener("abort", onExternalAbort, { once: true });
29264
+ timeout.addEventListener("abort", onTimeoutAbort, { once: true });
29265
+ if (external.aborted)
29266
+ abortFrom(external);
29267
+ if (timeout.aborted)
29268
+ abortFrom(timeout);
29269
+ return {
29270
+ signal: controller.signal,
29271
+ dispose: () => {
29272
+ external.removeEventListener("abort", onExternalAbort);
29273
+ timeout.removeEventListener("abort", onTimeoutAbort);
29274
+ }
29275
+ };
29276
+ }
29248
29277
  function errorMessage2(cause) {
29249
29278
  return cause instanceof Error ? cause.message : String(cause);
29250
29279
  }
@@ -29283,6 +29312,9 @@ async function enrichPageDeep(input2) {
29283
29312
  const idSeq = input2.idSeq ?? (() => randomUUID15());
29284
29313
  const clock = input2.clock ?? (() => new Date().toISOString());
29285
29314
  const toolCalls = [];
29315
+ if (input2.signal?.aborted) {
29316
+ return { fallback: true, reason: "deep enrich cancelled", toolCalls };
29317
+ }
29286
29318
  try {
29287
29319
  const env = envWithSavedApiKeys(input2.env ?? process.env);
29288
29320
  const credentialAvailable = hasCredential(input2.provider, env);
@@ -29358,6 +29390,7 @@ async function enrichPageDeep(input2) {
29358
29390
  let assistant = "";
29359
29391
  let pending;
29360
29392
  const abort = new AbortController;
29393
+ const composed = composeAbortSignals(input2.signal, abort.signal);
29361
29394
  const io = {
29362
29395
  write: (s) => {
29363
29396
  assistant += s;
@@ -29389,11 +29422,12 @@ async function enrichPageDeep(input2) {
29389
29422
  };
29390
29423
  const history = [];
29391
29424
  const userLine = buildDeepUserPrompt(input2.page, input2.original, input2.extraInstruction);
29392
- const turn = runAgentTurn(io, deps, history, userLine, { signal: abort.signal });
29425
+ const turn = runAgentTurn(io, deps, history, userLine, { signal: composed.signal });
29393
29426
  const deadlineMs = spawned.reservation.maxRuntimeMs;
29394
29427
  if (deadlineMs <= 0) {
29395
29428
  abort.abort();
29396
29429
  turn.catch(() => {});
29430
+ composed.dispose();
29397
29431
  releaseBudget();
29398
29432
  return {
29399
29433
  fallback: true,
@@ -29405,16 +29439,31 @@ async function enrichPageDeep(input2) {
29405
29439
  const expired = new Promise((resolve2) => {
29406
29440
  timer = setTimeout(() => resolve2("timeout"), deadlineMs);
29407
29441
  });
29442
+ let onExternalAbort;
29443
+ const cancelled = new Promise((resolve2) => {
29444
+ if (input2.signal === undefined)
29445
+ return;
29446
+ onExternalAbort = () => resolve2("cancelled");
29447
+ if (input2.signal.aborted) {
29448
+ onExternalAbort();
29449
+ } else {
29450
+ input2.signal.addEventListener("abort", onExternalAbort, { once: true });
29451
+ }
29452
+ });
29408
29453
  let outcome;
29409
29454
  try {
29410
- outcome = await Promise.race([turn.then(() => "done"), expired]);
29455
+ outcome = await Promise.race([turn.then(() => "done"), expired, cancelled]);
29411
29456
  } finally {
29412
29457
  if (timer !== undefined)
29413
29458
  clearTimeout(timer);
29459
+ if (onExternalAbort !== undefined) {
29460
+ input2.signal?.removeEventListener("abort", onExternalAbort);
29461
+ }
29414
29462
  }
29415
29463
  if (outcome === "timeout") {
29416
29464
  abort.abort();
29417
29465
  turn.catch(() => {});
29466
+ composed.dispose();
29418
29467
  releaseBudget();
29419
29468
  const partial = assistant.trim();
29420
29469
  return {
@@ -29424,7 +29473,15 @@ async function enrichPageDeep(input2) {
29424
29473
  toolCalls
29425
29474
  };
29426
29475
  }
29476
+ composed.dispose();
29427
29477
  releaseBudget();
29478
+ if (outcome === "cancelled" || input2.signal?.aborted) {
29479
+ return {
29480
+ fallback: true,
29481
+ reason: "deep enrich cancelled",
29482
+ toolCalls
29483
+ };
29484
+ }
29428
29485
  const raw = assistant.trim().length > 0 ? assistant.trim() : history.filter((m) => m.role === "assistant").map((m) => m.content).join(`
29429
29486
  `).trim();
29430
29487
  if (raw.length === 0) {
@@ -29611,6 +29668,9 @@ function isWikiEnrichIntent(line) {
29611
29668
  if (t.length === 0) {
29612
29669
  return false;
29613
29670
  }
29671
+ if (/^keryx\s+wiki\s+enrich(?:\s|$)/.test(t)) {
29672
+ return false;
29673
+ }
29614
29674
  const ru = t.includes("\u0432\u0438\u043A\u0438") && (t.includes("\u043E\u0431\u043E\u0433\u0430\u0442") || t.includes("\u043E\u0431\u043E\u0433\u0430\u0449"));
29615
29675
  const en = /\benrich\b/.test(t) && /\bwiki\b/.test(t) || /\bwiki\s+enrich\b/.test(t);
29616
29676
  return ru || en;
@@ -29889,6 +29949,7 @@ async function wikiEnrich(input2) {
29889
29949
  enriched: 0,
29890
29950
  dryRun: 0,
29891
29951
  skipped: 0,
29952
+ cancelled: 0,
29892
29953
  failed: 0
29893
29954
  };
29894
29955
  if (input2.refreshGraph) {
@@ -29929,9 +29990,15 @@ async function wikiEnrich(input2) {
29929
29990
  pageResults = await mapPool(pages, concurrency, async (page, i) => {
29930
29991
  const index = i + 1;
29931
29992
  const status = page.status ?? "draft";
29993
+ if (input2.signal?.aborted) {
29994
+ return { path: page.relativePath, action: "cancelled", reason: "enrichment cancelled" };
29995
+ }
29932
29996
  onPage({ index, total, path: page.relativePath, status, phase: "start" });
29933
29997
  try {
29934
29998
  const originalRaw = await readFile62(page.absolutePath, "utf8");
29999
+ if (input2.signal?.aborted) {
30000
+ return { path: page.relativePath, action: "cancelled", reason: "enrichment cancelled" };
30001
+ }
29935
30002
  const ensured = ensureWikiFrontmatter(originalRaw, {
29936
30003
  title: page.title,
29937
30004
  pageType: page.pageType
@@ -29948,8 +30015,12 @@ async function wikiEnrich(input2) {
29948
30015
  env,
29949
30016
  ...input2.fetch ? { fetch: input2.fetch } : {},
29950
30017
  ...input2.baseUrl !== undefined ? { baseUrl: input2.baseUrl } : {},
29951
- ...input2.providerFactory ? { providerFactory: input2.providerFactory } : {}
30018
+ ...input2.providerFactory ? { providerFactory: input2.providerFactory } : {},
30019
+ ...input2.signal !== undefined ? { signal: input2.signal } : {}
29952
30020
  });
30021
+ if (input2.signal?.aborted) {
30022
+ return { path: page.relativePath, action: "cancelled", reason: "enrichment cancelled" };
30023
+ }
29953
30024
  if (turn.error) {
29954
30025
  onPage({ index, total, path: page.relativePath, status, phase: "failed" });
29955
30026
  return {
@@ -29986,7 +30057,16 @@ async function wikiEnrich(input2) {
29986
30057
  const output2 = enriched.endsWith(`
29987
30058
  `) ? enriched : `${enriched}
29988
30059
  `;
29989
- const materialized = prepareOutputForPersistence(await guardOutput({ cwd: input2.cwd, content: output2, target: "wiki", source: "generated", path: page.relativePath }), output2);
30060
+ const materialized = prepareOutputForPersistence(await (input2.guardOutput ?? guardOutput)({
30061
+ cwd: input2.cwd,
30062
+ content: output2,
30063
+ target: "wiki",
30064
+ source: "generated",
30065
+ path: page.relativePath
30066
+ }), output2);
30067
+ if (input2.signal?.aborted) {
30068
+ return { path: page.relativePath, action: "cancelled", reason: "enrichment cancelled" };
30069
+ }
29990
30070
  if (!materialized.allowed) {
29991
30071
  onPage({ index, total, path: page.relativePath, status, phase: "failed" });
29992
30072
  return { path: page.relativePath, action: "failed", reason: materialized.reason };
@@ -30038,6 +30118,8 @@ async function wikiEnrich(input2) {
30038
30118
  }
30039
30119
  } else if (entry.action === "dry-run") {
30040
30120
  result.dryRun += 1;
30121
+ } else if (entry.action === "cancelled") {
30122
+ result.cancelled += 1;
30041
30123
  } else if (entry.action === "failed") {
30042
30124
  result.failed += 1;
30043
30125
  resumeState.failed.push({ path: entry.path, reason: entry.reason ?? "failed" });
@@ -30108,6 +30190,9 @@ function finalizeEnrichedText(original, rawText, options) {
30108
30190
  async function finishSuccess(ctx, page, originalRaw, content, keyFiles, extra) {
30109
30191
  const index = ctx.pageIndex(page.relativePath);
30110
30192
  const status = page.status ?? "draft";
30193
+ if (ctx.input.signal?.aborted) {
30194
+ return { path: page.relativePath, action: "cancelled", reason: "enrichment cancelled", tier: extra.tier };
30195
+ }
30111
30196
  const nodeHash = keyFiles.length > 0 ? await computePageNodeHash(ctx.cwd, keyFiles, ctx.graph) : undefined;
30112
30197
  if (ctx.input.dryRun) {
30113
30198
  ctx.onPage({ index, total: ctx.total, path: page.relativePath, status, phase: "done" });
@@ -30124,7 +30209,16 @@ async function finishSuccess(ctx, page, originalRaw, content, keyFiles, extra) {
30124
30209
  const output2 = content.endsWith(`
30125
30210
  `) ? content : `${content}
30126
30211
  `;
30127
- const materialized = prepareOutputForPersistence(await guardOutput({ cwd: ctx.cwd, content: output2, target: "wiki", source: "generated", path: page.relativePath }), output2);
30212
+ const materialized = prepareOutputForPersistence(await (ctx.input.guardOutput ?? guardOutput)({
30213
+ cwd: ctx.cwd,
30214
+ content: output2,
30215
+ target: "wiki",
30216
+ source: "generated",
30217
+ path: page.relativePath
30218
+ }), output2);
30219
+ if (ctx.input.signal?.aborted) {
30220
+ return { path: page.relativePath, action: "cancelled", reason: "enrichment cancelled", tier: extra.tier };
30221
+ }
30128
30222
  if (!materialized.allowed) {
30129
30223
  ctx.onPage({ index, total: ctx.total, path: page.relativePath, status, phase: "failed" });
30130
30224
  return {
@@ -30157,6 +30251,9 @@ async function runDeepSingle(ctx, item) {
30157
30251
  const { page, original, originalRaw, keyFiles } = item;
30158
30252
  const index = ctx.pageIndex(page.relativePath);
30159
30253
  const status = page.status ?? "draft";
30254
+ if (ctx.input.signal?.aborted) {
30255
+ return { path: page.relativePath, action: "cancelled", reason: "enrichment cancelled", tier: "deep" };
30256
+ }
30160
30257
  ctx.onPage({ index, total: ctx.total, path: page.relativePath, status, phase: "model" });
30161
30258
  try {
30162
30259
  const deepResult = await enrichPageDeep({
@@ -30169,11 +30266,15 @@ async function runDeepSingle(ctx, item) {
30169
30266
  model: ctx.model,
30170
30267
  maxToolCalls: ctx.wikiConfig.rlm.deep.maxToolCalls,
30171
30268
  maxRuntimeMs: ctx.wikiConfig.rlm.deep.maxRuntimeMs,
30269
+ ...ctx.input.signal !== undefined ? { signal: ctx.input.signal } : {},
30172
30270
  env: ctx.env,
30173
30271
  ...ctx.input.fetch ? { fetch: ctx.input.fetch } : {},
30174
30272
  ...ctx.input.baseUrl !== undefined ? { baseUrl: ctx.input.baseUrl } : {},
30175
30273
  ...ctx.input.providerFactory ? { providerFactory: ctx.input.providerFactory } : {}
30176
30274
  });
30275
+ if (ctx.input.signal?.aborted) {
30276
+ return { path: page.relativePath, action: "cancelled", reason: "enrichment cancelled", tier: "deep" };
30277
+ }
30177
30278
  const deepToolCalls = deepResult.toolCalls.length;
30178
30279
  let rawText = null;
30179
30280
  if ("enriched" in deepResult) {
@@ -30251,9 +30352,9 @@ function batchGroupKey(item) {
30251
30352
  const primaryKeyFile = item.keyFiles[0];
30252
30353
  if (primaryKeyFile !== undefined) {
30253
30354
  const dir = primaryKeyFile.split("/").slice(0, -1).join("/");
30254
- const segments2 = dir.split("/").filter((segment) => segment.length > 0);
30255
- if (segments2.length > 0) {
30256
- return segments2.slice(0, 2).join("/");
30355
+ const segments = dir.split("/").filter((segment) => segment.length > 0);
30356
+ if (segments.length > 0) {
30357
+ return segments.slice(0, 2).join("/");
30257
30358
  }
30258
30359
  }
30259
30360
  return item.page.pageType;
@@ -30290,6 +30391,14 @@ function groupLightPagesIntoBatches(items, batchConfig, tokenBudget) {
30290
30391
  return batches;
30291
30392
  }
30292
30393
  async function runLightBatch(ctx, items) {
30394
+ if (ctx.input.signal?.aborted) {
30395
+ return items.map((item) => ({
30396
+ path: item.page.relativePath,
30397
+ action: "cancelled",
30398
+ reason: "enrichment cancelled",
30399
+ tier: "light"
30400
+ }));
30401
+ }
30293
30402
  for (const item of items) {
30294
30403
  ctx.onPage({
30295
30404
  index: ctx.pageIndex(item.page.relativePath),
@@ -30314,7 +30423,8 @@ async function runLightBatch(ctx, items) {
30314
30423
  env: ctx.env,
30315
30424
  ...ctx.input.fetch ? { fetch: ctx.input.fetch } : {},
30316
30425
  ...ctx.input.baseUrl !== undefined ? { baseUrl: ctx.input.baseUrl } : {},
30317
- ...ctx.input.providerFactory ? { providerFactory: ctx.input.providerFactory } : {}
30426
+ ...ctx.input.providerFactory ? { providerFactory: ctx.input.providerFactory } : {},
30427
+ ...ctx.input.signal !== undefined ? { signal: ctx.input.signal } : {}
30318
30428
  });
30319
30429
  } catch (cause) {
30320
30430
  const reason = cause instanceof Error ? cause.message : String(cause);
@@ -30334,6 +30444,14 @@ async function runLightBatch(ctx, items) {
30334
30444
  };
30335
30445
  });
30336
30446
  }
30447
+ if (ctx.input.signal?.aborted) {
30448
+ return items.map((item) => ({
30449
+ path: item.page.relativePath,
30450
+ action: "cancelled",
30451
+ reason: "enrichment cancelled",
30452
+ tier: "light"
30453
+ }));
30454
+ }
30337
30455
  if (turn.error) {
30338
30456
  const errorReason = `${turn.error.kind}: ${turn.error.message}`;
30339
30457
  return items.map((item) => {
@@ -30397,8 +30515,18 @@ async function runLightBatch(ctx, items) {
30397
30515
  }
30398
30516
  async function runRlmPipeline(ctxInput) {
30399
30517
  const { pages, wikiConfig, input: input2 } = ctxInput;
30518
+ const cancelled = () => pages.map((page) => ({ path: page.relativePath, action: "cancelled", reason: "enrichment cancelled" }));
30519
+ if (input2.signal?.aborted) {
30520
+ return cancelled();
30521
+ }
30400
30522
  const graph = await loadGraph(ctxInput.cwd);
30523
+ if (input2.signal?.aborted) {
30524
+ return cancelled();
30525
+ }
30401
30526
  const gdgraphConfig = await loadGdgraphConfig(ctxInput.cwd);
30527
+ if (input2.signal?.aborted) {
30528
+ return cancelled();
30529
+ }
30402
30530
  const fileNodes = graph.nodes.filter((node) => node.kind === "file").map((node) => node.path);
30403
30531
  const rankEdges = buildRankEdges(graph, gdgraphConfig);
30404
30532
  const ranked = personalizedPageRank(fileNodes, rankEdges, {
@@ -30429,11 +30557,21 @@ async function runRlmPipeline(ctxInput) {
30429
30557
  };
30430
30558
  const prepared = [];
30431
30559
  for (const page of pages) {
30560
+ if (input2.signal?.aborted) {
30561
+ return cancelled();
30562
+ }
30432
30563
  const index = pageIndex(page.relativePath);
30433
30564
  const status = page.status ?? "draft";
30434
30565
  ctx.onPage({ index, total: ctx.total, path: page.relativePath, status, phase: "start" });
30566
+ if (input2.signal?.aborted) {
30567
+ return cancelled();
30568
+ }
30569
+ input2.onRlmPreparation?.(page.relativePath);
30435
30570
  try {
30436
30571
  const originalRaw = await readFile62(page.absolutePath, "utf8");
30572
+ if (input2.signal?.aborted) {
30573
+ return cancelled();
30574
+ }
30437
30575
  const ensured = ensureWikiFrontmatter(originalRaw, { title: page.title, pageType: page.pageType });
30438
30576
  const original = ensured.markdown;
30439
30577
  const keyFiles = keyFilesForPage(moduleKeyFilesIndex, page);
@@ -30441,6 +30579,9 @@ async function runRlmPipeline(ctxInput) {
30441
30579
  let unchanged = false;
30442
30580
  if (previousHash !== undefined && keyFiles.length > 0) {
30443
30581
  const currentHash = await computePageNodeHash(ctx.cwd, keyFiles, graph);
30582
+ if (input2.signal?.aborted) {
30583
+ return cancelled();
30584
+ }
30444
30585
  unchanged = isPageUnchangedSinceLastEnrich(page.relativePath, currentHash, resumeState.completedNodeHashes);
30445
30586
  }
30446
30587
  if (unchanged) {
@@ -30474,6 +30615,9 @@ async function runRlmPipeline(ctxInput) {
30474
30615
  const item = { page, original, originalRaw, keyFiles };
30475
30616
  prepared.push({ kind: tier === "deep" ? "deep" : "light", item });
30476
30617
  } catch (cause) {
30618
+ if (input2.signal?.aborted) {
30619
+ return cancelled();
30620
+ }
30477
30621
  ctx.onPage({ index, total: ctx.total, path: page.relativePath, status, phase: "failed" });
30478
30622
  prepared.push({
30479
30623
  kind: "resolved",
@@ -30485,6 +30629,9 @@ async function runRlmPipeline(ctxInput) {
30485
30629
  });
30486
30630
  }
30487
30631
  }
30632
+ if (input2.signal?.aborted) {
30633
+ return cancelled();
30634
+ }
30488
30635
  const units = [];
30489
30636
  let pendingLight = [];
30490
30637
  const flushLight = () => {
@@ -30514,6 +30661,9 @@ async function runRlmPipeline(ctxInput) {
30514
30661
  }
30515
30662
  }
30516
30663
  flushLight();
30664
+ if (input2.signal?.aborted) {
30665
+ return cancelled();
30666
+ }
30517
30667
  const unitResults = await mapPool(units, ctxInput.concurrency, async (unit) => {
30518
30668
  try {
30519
30669
  if (unit.kind === "resolved") {
@@ -30799,12 +30949,12 @@ async function narrate(opts) {
30799
30949
  });
30800
30950
  if (!turn.credentialAvailable) {
30801
30951
  const message2 = formatMissingCredentialHint(turn.provider);
30802
- emit(asJson, { provider: turn.provider, model: turn.model, credentialAvailable: false, error: message2 }, message2, true);
30952
+ emit2(asJson, { provider: turn.provider, model: turn.model, credentialAvailable: false, error: message2 }, message2, true);
30803
30953
  return;
30804
30954
  }
30805
30955
  if (turn.error) {
30806
30956
  const message2 = `${turn.error.kind}: ${turn.error.message}`;
30807
- emit(asJson, { provider: turn.provider, model: turn.model, error: message2 }, message2, true);
30957
+ emit2(asJson, { provider: turn.provider, model: turn.model, error: message2 }, message2, true);
30808
30958
  return;
30809
30959
  }
30810
30960
  const text = turn.text.trim();
@@ -30814,7 +30964,7 @@ async function narrate(opts) {
30814
30964
  console.log(text);
30815
30965
  }
30816
30966
  }
30817
- function emit(asJson, payload, message2, isError) {
30967
+ function emit2(asJson, payload, message2, isError) {
30818
30968
  if (asJson) {
30819
30969
  console.log(JSON.stringify(payload, null, 2));
30820
30970
  } else if (isError) {
@@ -32904,7 +33054,7 @@ var BUNDLED_GDSKILLS = [
32904
33054
  "Use gdgraph affected context for exported symbols and shared surfaces.",
32905
33055
  "Dispatch specialized review passes conceptually or as separate skill loads.",
32906
33056
  "Report findings first, ordered by severity, with concrete file references."
32907
- ], ["review code", "full review", "review changes"]),
33057
+ ], ["review", "\u0440\u0435\u0432\u044C\u044E", "review code", "full review", "review changes"]),
32908
33058
  skill("review-logic", "review", ["recommended", "full"], "Review logic correctness, contracts, edge cases, nullability, and async behavior.", [
32909
33059
  "Trace behavior through call sites and affected context.",
32910
33060
  "Look for incorrect assumptions, missing branches, race conditions, and error paths.",
@@ -32919,7 +33069,13 @@ var BUNDLED_GDSKILLS = [
32919
33069
  "Map inputs, trust boundaries, and sensitive outputs.",
32920
33070
  "Check injection, auth, crypto, secrets, and unsafe filesystem/network behavior.",
32921
33071
  "Prioritize exploitable findings with concrete remediation."
32922
- ], ["security review", "secure code", "vulnerability"]),
33072
+ ], [
33073
+ "security review",
33074
+ "\u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0441\u0442\u044C",
33075
+ "\u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0441\u0442\u044C \u043A\u043E\u0434\u0430",
33076
+ "secure code",
33077
+ "vulnerability"
33078
+ ]),
32923
33079
  skill("review-performance", "review", ["recommended", "full"], "Review hot paths, unnecessary work, bundle/perf regressions, blocking operations, and memory risk.", [
32924
33080
  "Find changed hot paths and repeated operations.",
32925
33081
  "Check loops, rendering, async blocking, large imports, and caching behavior.",
@@ -33055,7 +33211,12 @@ var BUNDLED_GDSKILLS = [
33055
33211
  "Detect deployment target and required pre-flight checks.",
33056
33212
  "Run verification before deployment unless explicitly skipped.",
33057
33213
  "Summarize deployment status, rollback path, and health checks."
33058
- ], ["deploy", "ship", "release"]),
33214
+ ], [
33215
+ "deploy",
33216
+ "deployment",
33217
+ "ship",
33218
+ "release"
33219
+ ]),
33059
33220
  skill("commit", "quality", ["full"], "Prepare conventional commits with scope, summary, and verification notes.", [
33060
33221
  "Inspect staged and unstaged changes.",
33061
33222
  "Group related changes without staging unrelated work.",
@@ -39526,8 +39687,56 @@ init_guard();
39526
39687
  import { mkdir as mkdir16, readFile as readFile23, writeFile as writeFile18 } from "fs/promises";
39527
39688
  import path46 from "path";
39528
39689
 
39690
+ // src/lib/bounded-stdin.ts
39691
+ async function readStdinBounded(deadlineMs) {
39692
+ if (process.stdin.isTTY) {
39693
+ return null;
39694
+ }
39695
+ const reader = Bun.stdin.stream().getReader();
39696
+ const timer = setTimeout(() => void reader.cancel().catch(() => {}), deadlineMs);
39697
+ timer.unref?.();
39698
+ const chunks = [];
39699
+ try {
39700
+ for (;; ) {
39701
+ const { done, value } = await reader.read();
39702
+ if (done)
39703
+ break;
39704
+ if (value)
39705
+ chunks.push(Buffer.from(value));
39706
+ }
39707
+ } catch {
39708
+ return null;
39709
+ } finally {
39710
+ clearTimeout(timer);
39711
+ reader.releaseLock?.();
39712
+ }
39713
+ return Buffer.concat(chunks).toString("utf8");
39714
+ }
39715
+
39529
39716
  // src/ctx/hook-classify.ts
39530
39717
  var ESCAPE_MARKER = /#\s*keryx:raw\b[ \t]*([^\n]*)/i;
39718
+ function escapeReasonOf(command) {
39719
+ let quote = null;
39720
+ for (let i = 0;i < command.length; i += 1) {
39721
+ const char = command[i];
39722
+ if (quote) {
39723
+ if (char === quote)
39724
+ quote = null;
39725
+ continue;
39726
+ }
39727
+ if (char === "'" || char === '"') {
39728
+ quote = char;
39729
+ continue;
39730
+ }
39731
+ if (char !== "#")
39732
+ continue;
39733
+ const match = ESCAPE_MARKER.exec(command.slice(i));
39734
+ if (match && match.index === 0) {
39735
+ return (match[1] ?? "").trim();
39736
+ }
39737
+ }
39738
+ return null;
39739
+ }
39531
39740
  var LEADING_SKIP = new Set(["sudo", "command", "time", "nice", "env", "builtin"]);
39532
39741
  var ALREADY_ROUTED = new Set(["keryx", "rtk"]);
39533
39742
  var ROUTES = [
@@ -39535,11 +39744,118 @@ var ROUTES = [
39535
39744
  { names: /^(cat|head|tail)$/, suggestion: "keryx ctx read <file> --mode compact" }
39536
39745
  ];
39537
39746
  var GIT_ROUTABLE = /^(diff|log|show)$/;
39538
- function segments(command) {
39539
- return command.split(/\|\||&&|;|\||\n/).map((s) => s.trim()).filter(Boolean);
39747
+ function statements(command) {
39748
+ return command.split(/\|\||&&|;|\n/).map((s) => s.trim()).filter(Boolean);
39749
+ }
39750
+ function stages(command) {
39751
+ const out = [];
39752
+ for (const statement of statements(command)) {
39753
+ const parts = splitPipeline(statement);
39754
+ parts.forEach((text, index) => {
39755
+ const trimmed = text.trim();
39756
+ if (trimmed)
39757
+ out.push({ text: trimmed, isFirst: index === 0 });
39758
+ });
39759
+ }
39760
+ return out;
39761
+ }
39762
+ function splitPipeline(statement) {
39763
+ const parts = [];
39764
+ let current = "";
39765
+ let quote = null;
39766
+ for (const char of statement) {
39767
+ if (quote) {
39768
+ if (char === quote)
39769
+ quote = null;
39770
+ current += char;
39771
+ continue;
39772
+ }
39773
+ if (char === "'" || char === '"') {
39774
+ quote = char;
39775
+ current += char;
39776
+ continue;
39777
+ }
39778
+ if (char === "|") {
39779
+ parts.push(current);
39780
+ current = "";
39781
+ continue;
39782
+ }
39783
+ current += char;
39784
+ }
39785
+ parts.push(current);
39786
+ return parts;
39787
+ }
39788
+ function stageTokens(stage) {
39789
+ return (stage.match(/'[^']*'|"[^"]*"|\S+/g) ?? []).filter(Boolean);
39790
+ }
39791
+ var VALUE_FLAGS = {
39792
+ search: new Set(["-e", "--regexp", "-f", "--file", "-m", "--max-count", "-A", "-B", "-C", "--context"]),
39793
+ head: new Set(["-n", "--lines", "-c", "--bytes"]),
39794
+ tail: new Set(["-n", "--lines", "-c", "--bytes"]),
39795
+ sed: new Set(["-e", "--expression", "-f", "--file"]),
39796
+ awk: new Set(["-f", "-v"])
39797
+ };
39798
+ function valueFlagsFor(command, searchLike) {
39799
+ return searchLike ? VALUE_FLAGS.search : VALUE_FLAGS[command] ?? new Set;
39800
+ }
39801
+ function readsStdin(tokens, isFirst) {
39802
+ const [command, ...rest] = tokens;
39803
+ if (!command) {
39804
+ return true;
39805
+ }
39806
+ if (/^(find|ls|git)$/.test(command)) {
39807
+ return false;
39808
+ }
39809
+ if (/^(rg|ripgrep)$/.test(command) && isFirst) {
39810
+ return false;
39811
+ }
39812
+ const searchLike = /^(rg|grep|egrep|fgrep|ripgrep)$/.test(command);
39813
+ const valueFlags = valueFlagsFor(command, searchLike);
39814
+ let operandsAllowed = searchLike || /^(sed|awk)$/.test(command) ? 1 : 0;
39815
+ for (let i = 0;i < rest.length; i += 1) {
39816
+ const token = rest[i] ?? "";
39817
+ const [flag, attached] = splitFlagValue(token);
39818
+ if (operandsAllowed > 0 && PATTERN_FLAGS.has(flag)) {
39819
+ operandsAllowed = 0;
39820
+ if (attached === null)
39821
+ i += 1;
39822
+ continue;
39823
+ }
39824
+ if (valueFlags.has(flag)) {
39825
+ if (attached === null)
39826
+ i += 1;
39827
+ continue;
39828
+ }
39829
+ if (token.startsWith("-")) {
39830
+ if (searchLike && isRecursiveFlag(token)) {
39831
+ return false;
39832
+ }
39833
+ continue;
39834
+ }
39835
+ if (operandsAllowed > 0) {
39836
+ operandsAllowed -= 1;
39837
+ continue;
39838
+ }
39839
+ return false;
39840
+ }
39841
+ return true;
39842
+ }
39843
+ var PATTERN_FLAGS = new Set(["-e", "--regexp", "-f", "--file", "--expression"]);
39844
+ function splitFlagValue(token) {
39845
+ if (token.startsWith("--")) {
39846
+ const eq = token.indexOf("=");
39847
+ return eq === -1 ? [token, null] : [token.slice(0, eq), token.slice(eq + 1)];
39848
+ }
39849
+ if (token.startsWith("-") && token.length > 2) {
39850
+ return [token.slice(0, 2), token.slice(2)];
39851
+ }
39852
+ return [token, null];
39853
+ }
39854
+ function isRecursiveFlag(token) {
39855
+ return token === "--recursive" || token === "--dereference-recursive" || /^-[a-zA-Z]*[rR]/.test(token);
39540
39856
  }
39541
39857
  function leadingTokens(segment) {
39542
- const tokens = segment.split(/\s+/).filter(Boolean);
39858
+ const tokens = stageTokens(segment);
39543
39859
  let i = 0;
39544
39860
  while (i < tokens.length) {
39545
39861
  const token = tokens[i] ?? "";
@@ -39555,12 +39871,15 @@ function classifyCommand(command) {
39555
39871
  if (!command.trim()) {
39556
39872
  return { block: false };
39557
39873
  }
39558
- const escape = ESCAPE_MARKER.exec(command);
39559
- if (escape) {
39560
- return { block: false, escapeReason: (escape[1] ?? "").trim() };
39874
+ const escapeReason = escapeReasonOf(command);
39875
+ if (escapeReason !== null) {
39876
+ return { block: false, escapeReason };
39561
39877
  }
39562
- for (const segment of segments(command)) {
39563
- const tokens = leadingTokens(segment);
39878
+ for (const stage of stages(command)) {
39879
+ const tokens = leadingTokens(stage.text);
39880
+ if (readsStdin(tokens, stage.isFirst)) {
39881
+ continue;
39882
+ }
39564
39883
  const first = tokens[0];
39565
39884
  if (!first || ALREADY_ROUTED.has(first)) {
39566
39885
  continue;
@@ -39663,18 +39982,30 @@ function stripFromHookArray(settings, key) {
39663
39982
  removeSentinel(settings);
39664
39983
  return settings;
39665
39984
  }
39666
- function hasManagedInArray(settings, key, command) {
39667
- const hooks = settings.hooks;
39668
- const groups = Array.isArray(hooks?.[key]) ? hooks?.[key] : [];
39669
- return groups.some((g) => {
39670
- if (!isManagedGroup2(g))
39985
+ function groupContainer(settings, runtime) {
39986
+ const container = runtime.groupContainer ? settings[runtime.groupContainer] : settings.hooks;
39987
+ const list2 = container?.[runtime.groupKey];
39988
+ return Array.isArray(list2) ? list2 : [];
39989
+ }
39990
+ function managedGroupsFor(settings, runtime) {
39991
+ const command = hookCommand(runtime.id);
39992
+ return groupContainer(settings, runtime).filter((group) => {
39993
+ if (!isManagedGroup2(group))
39671
39994
  return false;
39672
- const inner = g;
39673
- if (inner.command === command)
39674
- return true;
39675
- return Array.isArray(inner.hooks) && inner.hooks.some((h) => h?.command === command);
39995
+ const entry = group;
39996
+ if (runtime.groupShape === "flat") {
39997
+ return entry.command === command;
39998
+ }
39999
+ return Array.isArray(entry.hooks) && entry.hooks.some((hook) => hook?.command === command && hook?.type === "command");
39676
40000
  });
39677
40001
  }
40002
+ function hasRunnableGuard(settings, runtime) {
40003
+ return managedGroupsFor(settings, runtime).length > 0;
40004
+ }
40005
+ function hasStalePreToolUseMatcher(settings, runtime) {
40006
+ const expected = preToolUseMatcher(runtime);
40007
+ return managedGroupsFor(settings, runtime).some((group) => typeof group.matcher !== "string" || group.matcher !== expected);
40008
+ }
39678
40009
  function parseJson(payload) {
39679
40010
  try {
39680
40011
  const parsed = JSON.parse(payload);
@@ -39693,6 +40024,22 @@ function parseToolInputCommand(payload) {
39693
40024
  const command = input2.command;
39694
40025
  return typeof command === "string" ? command : null;
39695
40026
  }
40027
+ function parseToolName(payload) {
40028
+ const record = parseJson(payload);
40029
+ const name = record?.tool_name;
40030
+ return typeof name === "string" ? name : null;
40031
+ }
40032
+ function nativeSearchMessage(tool) {
40033
+ return [
40034
+ `[keryx ctx] The \`${tool}\` tool searches project code without the gdctx routing layer.`,
40035
+ `Use instead: keryx ctx rg "<pattern>" [path]`,
40036
+ `The routed form is compressed and recorded in the routing audit (ctx_used).`,
40037
+ `Structural question (callers, usages, blast radius)? Use gdgraph first.`,
40038
+ `If raw output is genuinely required, run it through Bash with an escape marker:`,
40039
+ ` rg "<pattern>" <path> # keryx:raw <why raw is needed>`
40040
+ ].join(`
40041
+ `);
40042
+ }
39696
40043
  function parseCursorCommand(payload) {
39697
40044
  const record = parseJson(payload);
39698
40045
  const command = record?.command;
@@ -39763,41 +40110,73 @@ function antigravityAllow(_c) {
39763
40110
  return { exitCode: 0, stdout: `${JSON.stringify({ allow_tool: true })}
39764
40111
  ` };
39765
40112
  }
39766
- function preToolUseGroup(id) {
40113
+ function preToolUseMatcher(runtime) {
40114
+ return ["Bash", ...runtime.nativeSearchTools ?? []].join("|");
40115
+ }
40116
+ function validatePreToolUse(settings, runtime) {
40117
+ const command = hookCommand(runtime.id);
40118
+ const expected = preToolUseMatcher(runtime);
40119
+ if (!hasRunnableGuard(settings, runtime)) {
40120
+ return [`${runtime.id}: missing PreToolUse(${expected}) guard`];
40121
+ }
40122
+ if (hasStalePreToolUseMatcher(settings, runtime)) {
40123
+ return [
40124
+ `${runtime.id}: PreToolUse guard does not match ${expected}; a tool it should cover bypasses it. ` + `Re-run \`keryx ctx install-hook --runtime ${runtime.id}\`.`
40125
+ ];
40126
+ }
40127
+ return [];
40128
+ }
40129
+ function describeExistingGuard(settings, runtime) {
40130
+ const command = hookCommand(runtime.id);
40131
+ if (!hasRunnableGuard(settings, runtime))
40132
+ return null;
40133
+ if (!hasStalePreToolUseMatcher(settings, runtime))
40134
+ return null;
40135
+ const found = managedGroupsFor(settings, runtime).map((group) => typeof group.matcher === "string" ? group.matcher : "(no matcher)").join(", ");
40136
+ return `upgraded an existing guard: ${found} -> ${preToolUseMatcher(runtime)}`;
40137
+ }
40138
+ function preToolUseGroup(runtime) {
39767
40139
  return {
39768
- matcher: "Bash",
39769
- hooks: [{ type: "command", command: hookCommand(id) }],
40140
+ matcher: preToolUseMatcher(runtime),
40141
+ hooks: [{ type: "command", command: hookCommand(runtime.id) }],
39770
40142
  [MANAGED_KEY2]: CTX_HOOK_SENTINEL
39771
40143
  };
39772
40144
  }
39773
40145
  var CLAUDE_RUNTIME3 = {
39774
40146
  id: "claude",
39775
- label: ".claude/settings.json (PreToolUse/Bash)",
40147
+ label: ".claude/settings.json (PreToolUse)",
39776
40148
  confidence: "verified",
40149
+ groupShape: "nested",
40150
+ groupKey: "PreToolUse",
40151
+ nativeSearchTools: ["Grep"],
39777
40152
  parseCommand: parseToolInputCommand,
39778
40153
  block: exitCodeBlock,
39779
40154
  allow: exitCodeAllow,
39780
40155
  locate: (root) => path44.join(root, ".claude", "settings.json"),
39781
- merge: (s) => mergeIntoHookArray(s, "PreToolUse", preToolUseGroup("claude")),
40156
+ merge: (s) => mergeIntoHookArray(s, "PreToolUse", preToolUseGroup(CLAUDE_RUNTIME3)),
39782
40157
  strip: (s) => stripFromHookArray(s, "PreToolUse"),
39783
- validate: (s) => hasManagedInArray(s, "PreToolUse", hookCommand("claude")) ? [] : ["claude: missing PreToolUse(Bash) guard"]
40158
+ validate: (s) => validatePreToolUse(s, CLAUDE_RUNTIME3)
39784
40159
  };
39785
40160
  var CODEX_RUNTIME = {
39786
40161
  id: "codex",
39787
- label: ".codex/hooks.json (PreToolUse/Bash)",
40162
+ label: ".codex/hooks.json (PreToolUse)",
39788
40163
  confidence: "verified",
40164
+ groupShape: "nested",
40165
+ groupKey: "PreToolUse",
39789
40166
  parseCommand: parseToolInputCommand,
39790
40167
  block: exitCodeBlock,
39791
40168
  allow: exitCodeAllow,
39792
40169
  locate: (root) => path44.join(root, ".codex", "hooks.json"),
39793
- merge: (s) => mergeIntoHookArray(s, "PreToolUse", preToolUseGroup("codex")),
40170
+ merge: (s) => mergeIntoHookArray(s, "PreToolUse", preToolUseGroup(CODEX_RUNTIME)),
39794
40171
  strip: (s) => stripFromHookArray(s, "PreToolUse"),
39795
- validate: (s) => hasManagedInArray(s, "PreToolUse", hookCommand("codex")) ? [] : ["codex: missing PreToolUse(Bash) guard"]
40172
+ validate: (s) => validatePreToolUse(s, CODEX_RUNTIME)
39796
40173
  };
39797
40174
  var CURSOR_RUNTIME2 = {
39798
40175
  id: "cursor",
39799
40176
  label: ".cursor/hooks.json (beforeShellExecution)",
39800
40177
  confidence: "verified",
40178
+ groupShape: "flat",
40179
+ groupKey: "beforeShellExecution",
39801
40180
  parseCommand: parseCursorCommand,
39802
40181
  block: cursorBlock,
39803
40182
  allow: cursorAllow,
@@ -39810,12 +40189,14 @@ var CURSOR_RUNTIME2 = {
39810
40189
  });
39811
40190
  },
39812
40191
  strip: (s) => stripFromHookArray(s, "beforeShellExecution"),
39813
- validate: (s) => hasManagedInArray(s, "beforeShellExecution", hookCommand("cursor")) ? [] : ["cursor: missing beforeShellExecution guard"]
40192
+ validate: (s) => hasRunnableGuard(s, CURSOR_RUNTIME2) ? [] : ["cursor: missing beforeShellExecution guard"]
39814
40193
  };
39815
40194
  var WINDSURF_RUNTIME = {
39816
40195
  id: "windsurf",
39817
40196
  label: ".windsurf/hooks.json (pre_run_command)",
39818
40197
  confidence: "verified",
40198
+ groupShape: "flat",
40199
+ groupKey: "pre_run_command",
39819
40200
  parseCommand: parseWindsurfCommand,
39820
40201
  block: exitCodeBlock,
39821
40202
  allow: exitCodeAllow,
@@ -39826,12 +40207,15 @@ var WINDSURF_RUNTIME = {
39826
40207
  [MANAGED_KEY2]: CTX_HOOK_SENTINEL
39827
40208
  }),
39828
40209
  strip: (s) => stripFromHookArray(s, "pre_run_command"),
39829
- validate: (s) => hasManagedInArray(s, "pre_run_command", hookCommand("windsurf")) ? [] : ["windsurf: missing pre_run_command guard"]
40210
+ validate: (s) => hasRunnableGuard(s, WINDSURF_RUNTIME) ? [] : ["windsurf: missing pre_run_command guard"]
39830
40211
  };
39831
40212
  var ANTIGRAVITY_RUNTIME = {
39832
40213
  id: "antigravity",
39833
40214
  label: ".agents/hooks.json (PreToolUse/run_command)",
39834
40215
  confidence: "experimental",
40216
+ groupShape: "nested",
40217
+ groupKey: "PreToolUse",
40218
+ groupContainer: "keryx-ctx-guard",
39835
40219
  parseCommand: parseAntigravityCommand,
39836
40220
  block: antigravityBlock,
39837
40221
  allow: antigravityAllow,
@@ -39864,12 +40248,7 @@ var ANTIGRAVITY_RUNTIME = {
39864
40248
  removeSentinel(s);
39865
40249
  return s;
39866
40250
  },
39867
- validate: (s) => {
39868
- const group = s["keryx-ctx-guard"];
39869
- const list2 = Array.isArray(group?.PreToolUse) ? group?.PreToolUse : [];
39870
- const ok = list2.some((g) => isManagedGroup2(g) && Array.isArray(g.hooks) && g.hooks.some((h) => h?.command === hookCommand("antigravity")));
39871
- return ok ? [] : ["antigravity: missing run_command guard"];
39872
- }
40251
+ validate: (s) => hasRunnableGuard(s, ANTIGRAVITY_RUNTIME) ? [] : ["antigravity: missing run_command guard"]
39873
40252
  };
39874
40253
  var OPENCODE_PLUGIN = `// keryx gdctx routing guard \u2014 generated by \`keryx ctx install-hook --runtime opencode\`.
39875
40254
  // Bridges OpenCode's tool.execute.before to \`keryx ctx hook opencode\`.
@@ -39892,6 +40271,8 @@ var OPENCODE_RUNTIME2 = {
39892
40271
  id: "opencode",
39893
40272
  label: ".opencode/plugin/keryx-ctx-guard.js",
39894
40273
  confidence: "experimental",
40274
+ groupShape: "nested",
40275
+ groupKey: "PreToolUse",
39895
40276
  parseCommand: parseToolInputCommand,
39896
40277
  block: exitCodeBlock,
39897
40278
  allow: exitCodeAllow,
@@ -39947,15 +40328,9 @@ function resolveRuntimes2(ids) {
39947
40328
  return { runtimes, unknown, unsupported };
39948
40329
  }
39949
40330
  // src/ctx/hook.ts
40331
+ var STDIN_DEADLINE_MS = 2000;
39950
40332
  async function readStdin() {
39951
- if (process.stdin.isTTY) {
39952
- return "";
39953
- }
39954
- const chunks = [];
39955
- for await (const chunk of process.stdin) {
39956
- chunks.push(Buffer.from(chunk));
39957
- }
39958
- return Buffer.concat(chunks).toString("utf8");
40333
+ return await readStdinBounded(STDIN_DEADLINE_MS) ?? "";
39959
40334
  }
39960
40335
  async function runCtxHook(runtimeId) {
39961
40336
  const runtime = getRuntime2(runtimeId ?? "claude");
@@ -39965,10 +40340,17 @@ async function runCtxHook(runtimeId) {
39965
40340
  const payload = await readStdin();
39966
40341
  const command = runtime.parseCommand(payload);
39967
40342
  if (command === null) {
40343
+ const nativeSearch = matchedNativeSearch(runtime.nativeSearchTools, payload);
40344
+ if (nativeSearch) {
40345
+ emit(refusalAction(runtime.id, nativeSearchMessage(nativeSearch)));
40346
+ }
39968
40347
  return;
39969
40348
  }
39970
40349
  const classification = classifyCommand(command);
39971
40350
  const action = classification.block ? runtime.block(command, classification) : runtime.allow(classification);
40351
+ emit(action);
40352
+ }
40353
+ function emit(action) {
39972
40354
  if (action.stdout)
39973
40355
  process.stdout.write(action.stdout);
39974
40356
  if (action.stderr)
@@ -39976,6 +40358,13 @@ async function runCtxHook(runtimeId) {
39976
40358
  if (action.exitCode)
39977
40359
  process.exitCode = action.exitCode;
39978
40360
  }
40361
+ function matchedNativeSearch(tools, payload) {
40362
+ if (!tools || tools.length === 0) {
40363
+ return null;
40364
+ }
40365
+ const name = parseToolName(payload);
40366
+ return name && tools.includes(name) ? name : null;
40367
+ }
39979
40368
 
39980
40369
  // src/ctx/hook-install.ts
39981
40370
  init_fs();
@@ -40012,9 +40401,10 @@ async function installRuntimeHook(projectRoot, runtime) {
40012
40401
  return { path: file, errors: [`${runtime.id}: no installer defined`] };
40013
40402
  }
40014
40403
  const settings = await readSettings3(file);
40404
+ const upgraded = describeExistingGuard(settings, runtime);
40015
40405
  await writeSettings3(file, merge(settings));
40016
40406
  const errors = validate(await readSettings3(file));
40017
- return { path: file, errors };
40407
+ return { path: file, errors, ...upgraded ? { upgraded } : {} };
40018
40408
  }
40019
40409
  async function uninstallRuntimeHook(projectRoot, runtime) {
40020
40410
  if (runtime.customUninstall) {
@@ -40257,7 +40647,7 @@ async function handleInstallHook(args) {
40257
40647
  console.log("escape: append `# keryx:raw <reason>` to run a raw command anyway");
40258
40648
  console.log("");
40259
40649
  for (const runtime of runtimes) {
40260
- const { path: file, errors } = await installRuntimeHook(cwd, runtime);
40650
+ const { path: file, errors, upgraded } = await installRuntimeHook(cwd, runtime);
40261
40651
  const tag = runtime.confidence === "experimental" ? " (experimental \u2014 verify on a live install)" : "";
40262
40652
  if (errors.length > 0) {
40263
40653
  for (const error of errors)
@@ -40265,6 +40655,8 @@ async function handleInstallHook(args) {
40265
40655
  process.exitCode = 1;
40266
40656
  } else {
40267
40657
  console.log(` \u2713 ${runtime.id} -> ${path46.relative(cwd, file)}${tag}`);
40658
+ if (upgraded)
40659
+ console.log(` ${upgraded}`);
40268
40660
  }
40269
40661
  }
40270
40662
  reportUnsupported(unsupported);
@@ -43719,6 +44111,47 @@ async function routeProjectSkills(args2) {
43719
44111
  }
43720
44112
  console.log(`Next: ${matches2[0]?.next}`);
43721
44113
  }
44114
+ var INFLECTIONS = ["s", "es", "ed", "d", "ing", "er", "ers", "ion", "ions", "ation"];
44115
+ var MIN_STEM = 4;
44116
+ function containsPhrase(text, phrase) {
44117
+ const words = text.split(" ").filter(Boolean);
44118
+ const target = phrase.split(" ").filter(Boolean);
44119
+ if (target.length === 0)
44120
+ return false;
44121
+ for (let i = 0;i + target.length <= words.length; i += 1) {
44122
+ let matched = true;
44123
+ for (let j = 0;j < target.length; j += 1) {
44124
+ const word = words[i + j];
44125
+ const want = target[j];
44126
+ const last = j === target.length - 1;
44127
+ if (word === want)
44128
+ continue;
44129
+ if (last && matchesInflected(word, want))
44130
+ continue;
44131
+ matched = false;
44132
+ break;
44133
+ }
44134
+ if (matched)
44135
+ return true;
44136
+ }
44137
+ return false;
44138
+ }
44139
+ function matchesInflected(word, stem) {
44140
+ if (stem.length < MIN_STEM || !word.startsWith(stem))
44141
+ return false;
44142
+ return INFLECTIONS.includes(word.slice(stem.length));
44143
+ }
44144
+ function triggerFires(trigger, normalizedQuery, queryWords, queryTokens) {
44145
+ if (containsPhrase(normalizedQuery, trigger)) {
44146
+ return true;
44147
+ }
44148
+ const rawWords = trigger.split(" ").filter(Boolean);
44149
+ if (rawWords.length === 0) {
44150
+ return false;
44151
+ }
44152
+ const meaningful = routeTokens(trigger);
44153
+ return rawWords.every((word) => meaningful.has(word) ? queryTokens.has(word) : queryWords.has(word));
44154
+ }
43722
44155
  function scoreBundledSkillRoute(entry, query) {
43723
44156
  const normalizedQuery = normalizeRouteText(query);
43724
44157
  if (!normalizedQuery) {
@@ -43741,18 +44174,13 @@ function scoreBundledSkillRoute(entry, query) {
43741
44174
  score += 100;
43742
44175
  reasons.push("exact skill");
43743
44176
  }
43744
- const triggerHit = triggers.some((trigger) => {
43745
- if (normalizedQuery.includes(trigger)) {
43746
- return true;
43747
- }
43748
- const triggerTokens = [...routeTokens(trigger)];
43749
- return triggerTokens.length > 0 && triggerTokens.every((token) => queryTokens.has(token));
43750
- });
44177
+ const queryWords = new Set(normalizedQuery.split(" ").filter(Boolean));
44178
+ const triggerHit = triggers.some((trigger) => triggerFires(trigger, normalizedQuery, queryWords, queryTokens));
43751
44179
  if (triggerHit) {
43752
44180
  score += 55;
43753
44181
  reasons.push("trigger");
43754
44182
  }
43755
- if (name && normalizedQuery.includes(name)) {
44183
+ if (name && containsPhrase(normalizedQuery, name)) {
43756
44184
  score += 30;
43757
44185
  reasons.push("skill name");
43758
44186
  }
@@ -43858,7 +44286,8 @@ var ROUTE_STOPWORDS = new Set([
43858
44286
  var RU_SYNONYM_PREFIXES = [
43859
44287
  ["\u0440\u0435\u0432\u044C\u044E", ["review"]],
43860
44288
  ["\u0440\u0435\u0432\u044C\u044E\u0435\u0440", ["review", "reviewer"]],
43861
- ["\u043F\u0440\u043E\u0432\u0435\u0440\u043A", ["review", "verify", "check"]],
44289
+ ["\u043F\u0440\u043E\u0432\u0435\u0440\u043A", ["verify", "check"]],
44290
+ ["\u043F\u0440\u043E\u0432\u0435\u0440", ["verify", "check"]],
43862
44291
  ["\u0440\u0435\u0430\u043B\u0438\u0437", ["implement"]],
43863
44292
  ["\u0438\u043C\u043F\u043B\u0435\u043C\u0435\u043D\u0442", ["implement"]],
43864
44293
  ["\u0432\u043D\u0435\u0434\u0440", ["implement"]],
@@ -43894,6 +44323,7 @@ var RU_SYNONYM_PREFIXES = [
43894
44323
  ["\u0441\u043E\u0437\u0434\u0430", ["create"]],
43895
44324
  ["\u0434\u0435\u043F\u043B\u043E\u0439", ["deploy"]],
43896
44325
  ["\u0440\u0430\u0437\u0432\u0435\u0440\u0442", ["deploy"]],
44326
+ ["\u0440\u0430\u0437\u0432\u0435\u0440\u043D", ["deploy"]],
43897
44327
  ["\u0437\u0430\u0432\u0438\u0441\u0438\u043C", ["dependency", "dependencies"]],
43898
44328
  ["\u0438\u043D\u0442\u0435\u0440\u0432\u044C\u044E", ["interview"]],
43899
44329
  ["\u043E\u043F\u0440\u043E\u0441", ["interview"]],
@@ -43903,6 +44333,7 @@ var RU_SYNONYM_PREFIXES = [
43903
44333
  ["\u0444\u043B\u043E", ["flow"]],
43904
44334
  ["\u043E\u0440\u043A\u0435\u0441\u0442\u0440", ["orchestrator", "orchestrate"]],
43905
44335
  ["\u0430\u043D\u0430\u043B\u0438\u0437", ["analyze", "analysis"]],
44336
+ ["\u043F\u0440\u043E\u0430\u043D\u0430\u043B\u0438\u0437", ["analyze", "analysis"]],
43906
44337
  ["\u0440\u0435\u0432\u044C\u044E\u0438\u0440", ["review"]]
43907
44338
  ];
43908
44339
  function routeTokens(normalized, expand = false) {
@@ -46890,9 +47321,9 @@ var SNAPSHOT_DIRECTORIES = new Set(["__snapshots__"]);
46890
47321
  var SNAPSHOT_SUFFIXES = [".snap"];
46891
47322
  function classifyPath(path133, config = DEFAULT_REVIEW_SCOPE_CONFIG) {
46892
47323
  const normalized = path133.replace(/\\/g, "/").replace(/^\.\//, "");
46893
- const segments2 = normalized.split("/").filter((segment) => segment.length > 0);
46894
- const basename = segments2.at(-1) ?? normalized;
46895
- const directories = segments2.slice(0, -1);
47324
+ const segments = normalized.split("/").filter((segment) => segment.length > 0);
47325
+ const basename = segments.at(-1) ?? normalized;
47326
+ const directories = segments.slice(0, -1);
46896
47327
  for (const segment of directories) {
46897
47328
  if (VENDOR_DIRECTORIES.has(segment)) {
46898
47329
  return { reason: "vendored", detail: `vendored: path segment "${segment}/"` };
@@ -53489,9 +53920,9 @@ async function readContent(file) {
53489
53920
  }
53490
53921
  return readStdin2();
53491
53922
  }
53492
- function surfaceWarnings(warnings, emit2 = console.log) {
53923
+ function surfaceWarnings(warnings, emit3 = console.log) {
53493
53924
  for (const warning of warnings) {
53494
- emit2(` ${style.yellow(symbols.bullet)} ${warning}`);
53925
+ emit3(` ${style.yellow(symbols.bullet)} ${warning}`);
53495
53926
  }
53496
53927
  }
53497
53928
  async function handleStatus(cwd) {
@@ -53835,14 +54266,14 @@ async function handleEval(cwd, args2) {
53835
54266
  }
53836
54267
  process.exitCode = gate.status === "fail" ? 1 : 0;
53837
54268
  }
53838
- function renderDecision(decision, emit2 = console.log) {
53839
- emit2("");
53840
- emit2(` gate: ${gateLabel(decision.gate)}`);
53841
- emit2(` action: ${decision.action}`);
53842
- emit2(` findings: ${decision.findings.length}`);
54269
+ function renderDecision(decision, emit3 = console.log) {
54270
+ emit3("");
54271
+ emit3(` gate: ${gateLabel(decision.gate)}`);
54272
+ emit3(` action: ${decision.action}`);
54273
+ emit3(` findings: ${decision.findings.length}`);
53843
54274
  for (const finding of decision.findings.slice(0, 20)) {
53844
54275
  const loc = finding.location?.line ? ` (line ${finding.location.line})` : "";
53845
- emit2(` ${severityMarker(finding.severity)} ${finding.category}/${finding.policyId} \u2192 ${finding.action}${loc}`);
54276
+ emit3(` ${severityMarker(finding.severity)} ${finding.category}/${finding.policyId} \u2192 ${finding.action}${loc}`);
53846
54277
  }
53847
54278
  }
53848
54279
  function severityMarker(severity) {
@@ -53891,7 +54322,7 @@ function applyRuntimeDecision(args2, code, message2, decision, policyIsKnown) {
53891
54322
  if (runtime === undefined) {
53892
54323
  return code;
53893
54324
  }
53894
- const emit2 = (action) => {
54325
+ const emit3 = (action) => {
53895
54326
  if (action.stdout !== undefined) {
53896
54327
  process.stdout.write(action.stdout);
53897
54328
  }
@@ -53903,9 +54334,9 @@ function applyRuntimeDecision(args2, code, message2, decision, policyIsKnown) {
53903
54334
  const outcome = decideHookOutcome(code !== 0, decision, policyIsKnown);
53904
54335
  switch (outcome.kind) {
53905
54336
  case "refuse":
53906
- return emit2(refusalAction(runtime, message2));
54337
+ return emit3(refusalAction(runtime, message2));
53907
54338
  case "approve":
53908
- return emit2(allowAction(runtime));
54339
+ return emit3(allowAction(runtime));
53909
54340
  case "silent":
53910
54341
  return 0;
53911
54342
  }
@@ -63382,7 +63813,7 @@ import { spawnSync as spawnSync2 } from "child_process";
63382
63813
  // package.json
63383
63814
  var package_default = {
63384
63815
  name: "@mrciphersmith/keryx",
63385
- version: "0.2.75",
63816
+ version: "0.2.76",
63386
63817
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
63387
63818
  private: false,
63388
63819
  publishConfig: {
@@ -63878,9 +64309,9 @@ function createAssistantMessageStream(otui, renderer, parent) {
63878
64309
  parent.add(container);
63879
64310
  return { container, segmenter: createStreamSegmenter(), views: [], frozen: 0 };
63880
64311
  };
63881
- const paint = (m, segments2, frozen) => {
63882
- for (let i = m.frozen;i < segments2.length; i++) {
63883
- const segment = segments2[i];
64312
+ const paint = (m, segments, frozen) => {
64313
+ for (let i = m.frozen;i < segments.length; i++) {
64314
+ const segment = segments[i];
63884
64315
  if (segment === undefined) {
63885
64316
  continue;
63886
64317
  }
@@ -63892,11 +64323,11 @@ function createAssistantMessageStream(otui, renderer, parent) {
63892
64323
  existing?.destroy();
63893
64324
  m.views[i] = createSegmentView(otui, renderer, m.container, segment);
63894
64325
  }
63895
- for (const stale of m.views.splice(segments2.length)) {
64326
+ for (const stale of m.views.splice(segments.length)) {
63896
64327
  stale.destroy();
63897
64328
  }
63898
64329
  m.frozen = frozen;
63899
- for (const segment of segments2) {
64330
+ for (const segment of segments) {
63900
64331
  if (segment.kind === "code") {
63901
64332
  lastCode = { lang: segment.lang, body: segment.body };
63902
64333
  }
@@ -63908,8 +64339,8 @@ function createAssistantMessageStream(otui, renderer, parent) {
63908
64339
  return;
63909
64340
  }
63910
64341
  const m = message2 ??= start();
63911
- const { segments: segments2, frozen } = m.segmenter.push(chunk);
63912
- paint(m, segments2, frozen);
64342
+ const { segments, frozen } = m.segmenter.push(chunk);
64343
+ paint(m, segments, frozen);
63913
64344
  },
63914
64345
  finalize: (text) => {
63915
64346
  const m = message2 ?? start();
@@ -63917,9 +64348,9 @@ function createAssistantMessageStream(otui, renderer, parent) {
63917
64348
  view.destroy();
63918
64349
  }
63919
64350
  m.frozen = 0;
63920
- const segments2 = segmentMarkdown(text);
63921
- paint(m, segments2, segments2.length);
63922
- if (segments2.length === 0) {
64351
+ const segments = segmentMarkdown(text);
64352
+ paint(m, segments, segments.length);
64353
+ if (segments.length === 0) {
63923
64354
  try {
63924
64355
  parent.remove(m.container);
63925
64356
  } catch {}
@@ -66702,20 +67133,20 @@ function buildContextUsage(source) {
66702
67133
  const hasEstimate = source.estimateTokens !== undefined;
66703
67134
  const total = hasEstimate ? source.estimateTokens ?? 0 : last ?? 0;
66704
67135
  const estimated = hasEstimate || last === undefined;
66705
- const segments2 = [];
67136
+ const segments = [];
66706
67137
  if (hasEstimate) {
66707
- segments2.push({ id: "history", label: "history (est.)", tokens: source.estimateTokens ?? 0 });
67138
+ segments.push({ id: "history", label: "history (est.)", tokens: source.estimateTokens ?? 0 });
66708
67139
  }
66709
67140
  if (lastIn !== undefined) {
66710
- segments2.push({ id: "last-in", label: "last in", tokens: lastIn });
67141
+ segments.push({ id: "last-in", label: "last in", tokens: lastIn });
66711
67142
  }
66712
67143
  if (lastOut !== undefined) {
66713
- segments2.push({ id: "last-out", label: "last out", tokens: lastOut });
67144
+ segments.push({ id: "last-out", label: "last out", tokens: lastOut });
66714
67145
  }
66715
67146
  return {
66716
67147
  total,
66717
67148
  estimated,
66718
- segments: segments2,
67149
+ segments,
66719
67150
  bar: renderUsageBar(total),
66720
67151
  note: total === 0 ? "No context usage yet." : "No model context window is known. The bar is relative to used tokens, not a billed limit."
66721
67152
  };
@@ -68331,6 +68762,161 @@ function setProjectPermissionMode(projectPath, mode, dir) {
68331
68762
  init_permission_mode();
68332
68763
  init_enrich2();
68333
68764
 
68765
+ // src/tui/foreground-operation.ts
68766
+ class ForegroundOperationOwner {
68767
+ active;
68768
+ disposed = false;
68769
+ get signal() {
68770
+ if (this.active === undefined) {
68771
+ throw new Error("foreground operation has not started");
68772
+ }
68773
+ return this.active.controller.signal;
68774
+ }
68775
+ get isDisposed() {
68776
+ return this.disposed;
68777
+ }
68778
+ get isActive() {
68779
+ return this.active !== undefined;
68780
+ }
68781
+ accepts(token) {
68782
+ return !this.disposed && this.active?.token === token;
68783
+ }
68784
+ begin() {
68785
+ if (this.disposed) {
68786
+ throw new Error("foreground operation owner is disposed");
68787
+ }
68788
+ if (this.active !== undefined) {
68789
+ throw new Error("a foreground operation is already active");
68790
+ }
68791
+ const token = Symbol("foreground-operation");
68792
+ let resolveSettled;
68793
+ const settled = new Promise((resolve3) => {
68794
+ resolveSettled = resolve3;
68795
+ });
68796
+ this.active = { token, controller: new AbortController, settled, resolveSettled };
68797
+ return token;
68798
+ }
68799
+ cancel(reason) {
68800
+ const controller = this.active?.controller;
68801
+ if (controller !== undefined && !controller.signal.aborted) {
68802
+ controller.abort(reason);
68803
+ }
68804
+ }
68805
+ settled() {
68806
+ return this.active?.settled ?? Promise.resolve();
68807
+ }
68808
+ settle(token) {
68809
+ if (this.active?.token !== token) {
68810
+ return;
68811
+ }
68812
+ const active = this.active;
68813
+ this.active = undefined;
68814
+ active.resolveSettled();
68815
+ }
68816
+ dispose() {
68817
+ this.disposed = true;
68818
+ this.cancel("renderer disposed");
68819
+ }
68820
+ }
68821
+ function createForegroundOperationOwner() {
68822
+ return new ForegroundOperationOwner;
68823
+ }
68824
+
68825
+ class ForegroundForceHandoff {
68826
+ pending = [];
68827
+ awaitingSettlement = false;
68828
+ enqueue(item) {
68829
+ this.pending.push(item);
68830
+ if (this.awaitingSettlement)
68831
+ return false;
68832
+ this.awaitingSettlement = true;
68833
+ return true;
68834
+ }
68835
+ get isAwaitingSettlement() {
68836
+ return this.awaitingSettlement;
68837
+ }
68838
+ get hasPending() {
68839
+ return this.pending.length > 0;
68840
+ }
68841
+ takeAfterSettlement() {
68842
+ this.awaitingSettlement = false;
68843
+ return this.pending.shift();
68844
+ }
68845
+ takeNext() {
68846
+ return this.pending.shift();
68847
+ }
68848
+ }
68849
+ function createForegroundForceHandoff() {
68850
+ return new ForegroundForceHandoff;
68851
+ }
68852
+ function finalizeWikiForegroundOperation(state, callbacks) {
68853
+ if (state.disposed)
68854
+ return "disposed";
68855
+ callbacks.stopBusy();
68856
+ if (state.aborted)
68857
+ return "aborted";
68858
+ callbacks.complete();
68859
+ return "completed";
68860
+ }
68861
+ async function runAfterForegroundSettlement(owner, action) {
68862
+ await owner.settled();
68863
+ if (!owner.isDisposed) {
68864
+ action();
68865
+ }
68866
+ }
68867
+ function createForegroundAgentIoFacade(owner, token, io) {
68868
+ const accepts = () => owner.accepts(token);
68869
+ return {
68870
+ write: (text) => {
68871
+ if (accepts())
68872
+ io.write(text);
68873
+ },
68874
+ onHistoryChange: (kind2) => {
68875
+ if (accepts())
68876
+ io.onHistoryChange?.(kind2);
68877
+ },
68878
+ onAssistantText: (text) => {
68879
+ if (accepts())
68880
+ io.onAssistantText?.(text);
68881
+ },
68882
+ onReasoning: (text) => {
68883
+ if (accepts())
68884
+ io.onReasoning?.(text);
68885
+ },
68886
+ onUsage: (usage) => {
68887
+ if (accepts())
68888
+ io.onUsage?.(usage);
68889
+ },
68890
+ onToolCall: (name, input2) => {
68891
+ if (accepts())
68892
+ io.onToolCall?.(name, input2);
68893
+ },
68894
+ onToolResult: (name, result) => {
68895
+ if (accepts())
68896
+ io.onToolResult?.(name, result);
68897
+ },
68898
+ onSystem: (text) => {
68899
+ if (accepts())
68900
+ io.onSystem?.(text);
68901
+ },
68902
+ onTerminalState: (state) => {
68903
+ if (accepts())
68904
+ io.onTerminalState?.(state);
68905
+ },
68906
+ requestApproval: async (tool, input2, meta) => {
68907
+ if (!accepts())
68908
+ return false;
68909
+ const response = await io.requestApproval?.(tool, input2, meta);
68910
+ return accepts() ? response ?? false : false;
68911
+ },
68912
+ onAutoApproved: (tool, input2, meta) => {
68913
+ if (accepts())
68914
+ io.onAutoApproved?.(tool, input2, meta);
68915
+ },
68916
+ permissionMode: () => accepts() ? io.permissionMode?.() ?? "ask" : "ask"
68917
+ };
68918
+ }
68919
+
68334
68920
  // src/tui/herdr-report.ts
68335
68921
  import net from "net";
68336
68922
  var SOURCE = "herdr:keryx";
@@ -72061,14 +72647,14 @@ function shortenCwd(cwd, max) {
72061
72647
  if (full.length <= max) {
72062
72648
  return full;
72063
72649
  }
72064
- const segments2 = full.split("/").filter((s) => s.length > 0);
72065
- for (let i = 1;i < segments2.length; i++) {
72066
- const candidate = `\u2026/${segments2.slice(i).join("/")}`;
72650
+ const segments = full.split("/").filter((s) => s.length > 0);
72651
+ for (let i = 1;i < segments.length; i++) {
72652
+ const candidate = `\u2026/${segments.slice(i).join("/")}`;
72067
72653
  if (candidate.length <= max) {
72068
72654
  return candidate;
72069
72655
  }
72070
72656
  }
72071
- const last = segments2[segments2.length - 1] ?? full;
72657
+ const last = segments[segments.length - 1] ?? full;
72072
72658
  return `\u2026${last.slice(last.length - (max - 1))}`;
72073
72659
  }
72074
72660
  function estimateContextTokens(history) {
@@ -72776,9 +73362,12 @@ async function launchTuiAgentShell(opts) {
72776
73362
  let liveDeps;
72777
73363
  let liveJobs;
72778
73364
  let detachExternal;
73365
+ const foregroundOperation = createForegroundOperationOwner();
72779
73366
  try {
72780
73367
  const r = renderer = await createShellRenderer(otui, {
72781
73368
  onDestroy: () => {
73369
+ foregroundOperation.cancel("renderer destroyed");
73370
+ foregroundOperation.dispose();
72782
73371
  mountedChrome?.destroy();
72783
73372
  setAskUserHost(undefined);
72784
73373
  setSubagentFleetListener(undefined);
@@ -72842,7 +73431,6 @@ async function launchTuiAgentShell(opts) {
72842
73431
  const stopBusy = () => {
72843
73432
  chrome.stopBusy();
72844
73433
  };
72845
- let mainTurnAbortController;
72846
73434
  const sidebar = chrome.sidebarTop;
72847
73435
  sidebar.add(new otui.TextRenderable(r, { id: "sb-title", content: otui.t`${otui.bold("keryx")}` }));
72848
73436
  sidebar.add(new otui.TextRenderable(r, { id: "sb-model-k", content: otui.t`${otui.dim("Model")}`, marginTop: 1 }));
@@ -73873,7 +74461,7 @@ Staying in the current session.
73873
74461
  let mainQueue = [];
73874
74462
  let mainQueueSeq = 0;
73875
74463
  let pendingQueueEdit;
73876
- let priorityMainQuestion;
74464
+ const forceHandoff = createForegroundForceHandoff();
73877
74465
  let sideWorkerRunning = false;
73878
74466
  let sideClearTimeout;
73879
74467
  const showSideQueueStatus = () => {
@@ -74023,15 +74611,19 @@ Staying in the current session.
74023
74611
  return;
74024
74612
  mainQueue = removeMainQueueItem(mainQueue, index);
74025
74613
  paintMainQueue();
74026
- if (mainTurnAbortController !== undefined && !mainTurnAbortController.signal.aborted) {
74027
- priorityMainQuestion = item;
74028
- mainTurnAbortController.abort();
74029
- mainTurnAbortController = undefined;
74614
+ (async () => {
74615
+ const waitsForSettlement = forceHandoff.enqueue(item);
74616
+ foregroundOperation.cancel("queue item forced");
74030
74617
  io.onSystem?.(`\u25C7 main turn interrupted \u2014 q${index + 1} will run next.
74031
74618
  `);
74032
- return;
74033
- }
74034
- runLine(item.question);
74619
+ if (!waitsForSettlement)
74620
+ return;
74621
+ await runAfterForegroundSettlement(foregroundOperation, () => {
74622
+ const next = forceHandoff.takeAfterSettlement();
74623
+ if (next !== undefined)
74624
+ runLine(next.question);
74625
+ });
74626
+ })();
74035
74627
  };
74036
74628
  const handleQueueNavKey = (key) => {
74037
74629
  if (!queueNavActive) {
@@ -74238,6 +74830,8 @@ Staying in the current session.
74238
74830
  });
74239
74831
  switch (decision) {
74240
74832
  case "exit": {
74833
+ foregroundOperation.cancel("shell exit");
74834
+ foregroundOperation.dispose();
74241
74835
  (async () => {
74242
74836
  await closeSlateSession(slateSession, mintTimestampAttemptId);
74243
74837
  await deps.sweepBackgroundJobs?.();
@@ -74258,8 +74852,8 @@ Staying in the current session.
74258
74852
  return;
74259
74853
  }
74260
74854
  case "interrupt": {
74261
- if (mainTurnAbortController !== undefined && !mainTurnAbortController.signal.aborted) {
74262
- mainTurnAbortController.abort();
74855
+ if (foregroundOperation.isActive) {
74856
+ foregroundOperation.cancel("interrupted by user");
74263
74857
  io.onSystem?.(`\u25C7 main turn interrupted.
74264
74858
  `);
74265
74859
  return;
@@ -74686,10 +75280,13 @@ ${formatThemeList(getThemeId())}`);
74686
75280
  }));
74687
75281
  if (isWikiEnrichIntent(line)) {
74688
75282
  const startedAt2 = Date.now();
75283
+ const operation2 = foregroundOperation.begin();
74689
75284
  (async () => {
74690
75285
  try {
74691
75286
  startBusy("planning wiki enrich\u2026");
74692
75287
  const plan = await planWikiEnrich(process.cwd());
75288
+ if (foregroundOperation.signal.aborted || foregroundOperation.isDisposed)
75289
+ return;
74693
75290
  stopBusy();
74694
75291
  const maxList = 40;
74695
75292
  const draftLines = plan.drafts.slice(0, maxList).map((p) => ` \xB7 ${p.relativePath}`);
@@ -74716,6 +75313,8 @@ ${formatThemeList(getThemeId())}`);
74716
75313
  acceptedCount: plan.accepted.length,
74717
75314
  total: plan.forceTargets.length
74718
75315
  });
75316
+ if (foregroundOperation.signal.aborted || foregroundOperation.isDisposed)
75317
+ return;
74719
75318
  input2.focus();
74720
75319
  if (choice2 === "cancel") {
74721
75320
  transcript.add(new otui.TextRenderable(r, {
@@ -74752,8 +75351,11 @@ ${formatThemeList(getThemeId())}`);
74752
75351
  force,
74753
75352
  provider: currentSel.provider,
74754
75353
  model: currentSel.model,
75354
+ signal: foregroundOperation.signal,
74755
75355
  concurrency: 2,
74756
75356
  onPage: (info) => {
75357
+ if (foregroundOperation.signal.aborted || foregroundOperation.isDisposed)
75358
+ return;
74757
75359
  setBusyPhase(`enrich ${info.index}/${info.total} [${info.phase}] ${info.path}`);
74758
75360
  setMainAgent("running", `${info.index}/${info.total}`);
74759
75361
  const status = info.phase === "done" ? "done" : info.phase === "failed" ? "failed" : "running";
@@ -74766,6 +75368,8 @@ ${formatThemeList(getThemeId())}`);
74766
75368
  });
74767
75369
  }
74768
75370
  });
75371
+ if (foregroundOperation.signal.aborted || foregroundOperation.isDisposed)
75372
+ return;
74769
75373
  stopBusy();
74770
75374
  setMainAgent(result.failed > 0 && result.enriched === 0 ? "failed" : "done", `${result.enriched}ok/${result.failed}fail`);
74771
75375
  const lines = [
@@ -74797,20 +75401,34 @@ ${formatThemeList(getThemeId())}`);
74797
75401
  saveSession();
74798
75402
  } catch {}
74799
75403
  } catch (cause) {
75404
+ if (foregroundOperation.signal.aborted || foregroundOperation.isDisposed)
75405
+ return;
74800
75406
  stopBusy();
74801
75407
  transcript.add(new otui.TextRenderable(r, {
74802
75408
  id: `we-err${uid++}`,
74803
75409
  content: otui.t`${otui.red(`wiki enrich failed: ${cause instanceof Error ? cause.message : String(cause)}`)}`
74804
75410
  }));
74805
75411
  } finally {
74806
- const secs = ((Date.now() - startedAt2) / 1000).toFixed(1);
74807
- transcript.add(new otui.TextRenderable(r, {
74808
- id: `w${uid++}`,
74809
- content: otui.t`${otui.dim(`worked for ${secs}s`)}`,
74810
- marginTop: 1
74811
- }));
74812
- stopBusy();
74813
- focusComposer();
75412
+ const operationAborted = foregroundOperation.signal.aborted;
75413
+ foregroundOperation.settle(operation2);
75414
+ finalizeWikiForegroundOperation({ aborted: operationAborted, disposed: foregroundOperation.isDisposed }, {
75415
+ stopBusy,
75416
+ complete: () => {
75417
+ const secs = ((Date.now() - startedAt2) / 1000).toFixed(1);
75418
+ transcript.add(new otui.TextRenderable(r, {
75419
+ id: `w${uid++}`,
75420
+ content: otui.t`${otui.dim(`worked for ${secs}s`)}`,
75421
+ marginTop: 1
75422
+ }));
75423
+ focusComposer();
75424
+ if (!forceHandoff.isAwaitingSettlement) {
75425
+ const next = forceHandoff.takeNext() ?? mainQueue.shift();
75426
+ paintMainQueue();
75427
+ if (next !== undefined)
75428
+ runLine(next.question);
75429
+ }
75430
+ }
75431
+ });
74814
75432
  }
74815
75433
  })();
74816
75434
  return;
@@ -74818,6 +75436,7 @@ ${formatThemeList(getThemeId())}`);
74818
75436
  fleet.clearMatching((w) => w.id !== MAIN_AGENT_ID && !isSideWorkerId(w.id));
74819
75437
  sessions.clear();
74820
75438
  deps.resetSubagentBudget?.();
75439
+ const operation = foregroundOperation.begin();
74821
75440
  setMainAgent("running", "waiting");
74822
75441
  startBusy("waiting for model");
74823
75442
  const startedAt = Date.now();
@@ -74829,8 +75448,6 @@ ${formatThemeList(getThemeId())}`);
74829
75448
  }
74830
75449
  prevOnSystem?.(text);
74831
75450
  };
74832
- const controller = new AbortController;
74833
- mainTurnAbortController = controller;
74834
75451
  const suggestNextStep = async () => {
74835
75452
  try {
74836
75453
  const lastUser = [...history].reverse().find((m) => m.role === "user")?.content ?? "";
@@ -74853,11 +75470,14 @@ ${tail}`,
74853
75470
  chrome.showSuggestion(result.text.trim().split(/\s+/).slice(0, 20).join(" "));
74854
75471
  } catch {}
74855
75472
  };
74856
- runAgentTurn(io, deps, history, line, {
74857
- signal: controller.signal,
75473
+ const foregroundIo = createForegroundAgentIoFacade(foregroundOperation, operation, io);
75474
+ runAgentTurn(foregroundIo, deps, history, line, {
75475
+ signal: foregroundOperation.signal,
74858
75476
  ...slateSession !== undefined ? { slateSession } : {}
74859
75477
  }).finally(() => {
74860
- mainTurnAbortController = undefined;
75478
+ foregroundOperation.settle(operation);
75479
+ if (foregroundOperation.isDisposed)
75480
+ return;
74861
75481
  const secs = ((Date.now() - startedAt) / 1000).toFixed(1);
74862
75482
  stopBusy();
74863
75483
  refreshWorkspaceSidebar();
@@ -74873,15 +75493,11 @@ ${tail}`,
74873
75493
  sbContext.content = otui.t`${otui.dim(`~${est.toLocaleString()} tokens (est)`)}`;
74874
75494
  }
74875
75495
  focusComposer();
74876
- if (priorityMainQuestion === undefined && mainQueue.length === 0 && !turnFailed) {
75496
+ if (!forceHandoff.hasPending && mainQueue.length === 0 && !turnFailed) {
74877
75497
  suggestNextStep();
74878
75498
  }
74879
- if (priorityMainQuestion !== undefined) {
74880
- const next = priorityMainQuestion;
74881
- priorityMainQuestion = undefined;
74882
- runLine(next.question);
74883
- } else if (mainQueue.length > 0) {
74884
- const next = mainQueue.shift();
75499
+ if (!forceHandoff.isAwaitingSettlement) {
75500
+ const next = forceHandoff.takeNext() ?? mainQueue.shift();
74885
75501
  paintMainQueue();
74886
75502
  if (next !== undefined)
74887
75503
  runLine(next.question);
@@ -78037,7 +78653,7 @@ async function runRemoteTurn(input2) {
78037
78653
  let seq = 0;
78038
78654
  createTurnRecord({ turnId, sessionId, project: input2.project, origin: REMOTE_ORIGIN, startedAt }, input2.dir);
78039
78655
  let bounded2 = false;
78040
- const emit2 = (kind2, extra = {}) => {
78656
+ const emit3 = (kind2, extra = {}) => {
78041
78657
  const event = { schemaVersion: "1.0.0", turnId, seq, kind: kind2, at: clock(), ...extra };
78042
78658
  if (appendTurnEvent(event, input2.dir)) {
78043
78659
  seq += 1;
@@ -78076,7 +78692,7 @@ async function runRemoteTurn(input2) {
78076
78692
  }
78077
78693
  return { turnId, sessionId, result };
78078
78694
  };
78079
- emit2("turn.started");
78695
+ emit3("turn.started");
78080
78696
  if (input2.profile.requiredControls.isolation === "required-fail-closed") {
78081
78697
  const available = input2.containmentAvailable ?? (() => false);
78082
78698
  if (!available()) {
@@ -78140,7 +78756,7 @@ async function runRemoteTurn(input2) {
78140
78756
  if (event.kind === "text_delta" && typeof event.text === "string") {
78141
78757
  const redacted = await redactOut(security, event.text);
78142
78758
  assistantText += redacted;
78143
- emit2("assistant.delta", { text: redacted });
78759
+ emit3("assistant.delta", { text: redacted });
78144
78760
  }
78145
78761
  }
78146
78762
  if (asked) {
@@ -78286,12 +78902,12 @@ function matchRoute(pathname) {
78286
78902
  if (FIXED_ROUTES.has(pathname)) {
78287
78903
  return { route: "fixed", pathname };
78288
78904
  }
78289
- const segments2 = pathname.split("/");
78290
- if (segments2.length === 4 && segments2[1] === "v1" && segments2[2] === "turns") {
78291
- return { route: "turn", turnId: segments2[3] ?? "" };
78905
+ const segments = pathname.split("/");
78906
+ if (segments.length === 4 && segments[1] === "v1" && segments[2] === "turns") {
78907
+ return { route: "turn", turnId: segments[3] ?? "" };
78292
78908
  }
78293
- if (segments2.length === 5 && segments2[1] === "v1" && segments2[2] === "turns" && segments2[4] === "events") {
78294
- return { route: "turn-events", turnId: segments2[3] ?? "" };
78909
+ if (segments.length === 5 && segments[1] === "v1" && segments[2] === "turns" && segments[4] === "events") {
78910
+ return { route: "turn-events", turnId: segments[3] ?? "" };
78295
78911
  }
78296
78912
  return { route: "none" };
78297
78913
  }