@mrciphersmith/keryx 0.2.74 → 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
  }
@@ -16448,7 +16451,13 @@ function writeJsonl(file, history, ts) {
16448
16451
  function redactHistory(history) {
16449
16452
  return history.map((message) => ({
16450
16453
  ...message,
16451
- content: redactSensitiveText(message.content)
16454
+ content: redactSensitiveText(message.content),
16455
+ ...message.toolCalls === undefined ? {} : {
16456
+ toolCalls: message.toolCalls.map((call) => ({
16457
+ ...call,
16458
+ arguments: redactSensitiveText(call.arguments)
16459
+ }))
16460
+ }
16452
16461
  }));
16453
16462
  }
16454
16463
  function readJsonl2(file) {
@@ -17613,6 +17622,9 @@ async function createProjectSkill(projectRoot, options) {
17613
17622
  if (!await pathExists(metaprojectRoot)) {
17614
17623
  throw new Error("Metaproject is not initialized. Run: keryx init");
17615
17624
  }
17625
+ if (!isRoutableSkillTarget(options.target)) {
17626
+ throw new Error(`keryx skills create: target ${JSON.stringify(options.target)} reads as prose, not a routing key. ` + 'A target is what `keryx skills route` matches against \u2014 a concept ("auth flow"), a symbol (`IResultDqReport`), ' + "or a path (`src/dq/DqScoreCard.tsx`). A sentence matches nothing and verifies as permanently stale.");
17627
+ }
17616
17628
  const moduleName = slugify3(options.module ?? inferModule(options.target));
17617
17629
  const skillName = slugify3(options.name ?? inferSkillName(options.target));
17618
17630
  const format = options.format ?? "auto";
@@ -18605,6 +18617,23 @@ function authorityFor(manifest, subject) {
18605
18617
  function normalizeProposalLifecycleResult(value) {
18606
18618
  return JSON.parse(JSON.stringify(value));
18607
18619
  }
18620
+ async function readProposalSecurityGate(cwd, workspaceId, proposalId) {
18621
+ const file = path86.join(cwd, ".metaproject", "workspaces", workspaceId, "proposals", `${proposalId}.json`);
18622
+ let parsed;
18623
+ try {
18624
+ parsed = JSON.parse(await readFile44(file, "utf8"));
18625
+ } catch {
18626
+ return;
18627
+ }
18628
+ if (typeof parsed !== "object" || parsed === null)
18629
+ return;
18630
+ const record = parsed;
18631
+ const gate = record.security?.gate;
18632
+ if (gate !== "pass" && gate !== "needs-approval")
18633
+ return;
18634
+ const evidenceRefs = Array.isArray(record.evidence) ? record.evidence.map((entry) => typeof entry === "object" && entry !== null ? entry.uri : undefined).filter((uri) => typeof uri === "string") : [];
18635
+ return { gate, evidenceRefs };
18636
+ }
18608
18637
  var ProposalLifecycleError;
18609
18638
  var init_proposal_lifecycle = __esm(() => {
18610
18639
  init_fs();
@@ -18788,13 +18817,16 @@ async function digestLedgerPrefix(handle, byteLength) {
18788
18817
  return digest2;
18789
18818
  const buffer = Buffer.alloc(Math.min(ledgerDigestChunkBytes, byteLength));
18790
18819
  let position = 0;
18791
- while (position < byteLength) {
18820
+ const maxChunks = Math.ceil(byteLength / buffer.length);
18821
+ for (let chunk = 0;chunk < maxChunks && position < byteLength; chunk += 1) {
18792
18822
  const result = await handle.read(buffer, 0, Math.min(buffer.length, byteLength - position), position);
18793
18823
  if (result.bytesRead === 0)
18794
18824
  return;
18795
18825
  digest2.update(buffer.subarray(0, result.bytesRead));
18796
18826
  position += result.bytesRead;
18797
18827
  }
18828
+ if (position < byteLength)
18829
+ return;
18798
18830
  return digest2;
18799
18831
  }
18800
18832
  function parseCheckpoint(value) {
@@ -29216,6 +29248,32 @@ var init_metaproject_operations = __esm(() => {
29216
29248
 
29217
29249
  // src/wiki/deep-enrich.ts
29218
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
+ }
29219
29277
  function errorMessage2(cause) {
29220
29278
  return cause instanceof Error ? cause.message : String(cause);
29221
29279
  }
@@ -29254,6 +29312,9 @@ async function enrichPageDeep(input2) {
29254
29312
  const idSeq = input2.idSeq ?? (() => randomUUID15());
29255
29313
  const clock = input2.clock ?? (() => new Date().toISOString());
29256
29314
  const toolCalls = [];
29315
+ if (input2.signal?.aborted) {
29316
+ return { fallback: true, reason: "deep enrich cancelled", toolCalls };
29317
+ }
29257
29318
  try {
29258
29319
  const env = envWithSavedApiKeys(input2.env ?? process.env);
29259
29320
  const credentialAvailable = hasCredential(input2.provider, env);
@@ -29329,6 +29390,7 @@ async function enrichPageDeep(input2) {
29329
29390
  let assistant = "";
29330
29391
  let pending;
29331
29392
  const abort = new AbortController;
29393
+ const composed = composeAbortSignals(input2.signal, abort.signal);
29332
29394
  const io = {
29333
29395
  write: (s) => {
29334
29396
  assistant += s;
@@ -29360,11 +29422,12 @@ async function enrichPageDeep(input2) {
29360
29422
  };
29361
29423
  const history = [];
29362
29424
  const userLine = buildDeepUserPrompt(input2.page, input2.original, input2.extraInstruction);
29363
- const turn = runAgentTurn(io, deps, history, userLine, { signal: abort.signal });
29425
+ const turn = runAgentTurn(io, deps, history, userLine, { signal: composed.signal });
29364
29426
  const deadlineMs = spawned.reservation.maxRuntimeMs;
29365
29427
  if (deadlineMs <= 0) {
29366
29428
  abort.abort();
29367
29429
  turn.catch(() => {});
29430
+ composed.dispose();
29368
29431
  releaseBudget();
29369
29432
  return {
29370
29433
  fallback: true,
@@ -29376,16 +29439,31 @@ async function enrichPageDeep(input2) {
29376
29439
  const expired = new Promise((resolve2) => {
29377
29440
  timer = setTimeout(() => resolve2("timeout"), deadlineMs);
29378
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
+ });
29379
29453
  let outcome;
29380
29454
  try {
29381
- outcome = await Promise.race([turn.then(() => "done"), expired]);
29455
+ outcome = await Promise.race([turn.then(() => "done"), expired, cancelled]);
29382
29456
  } finally {
29383
29457
  if (timer !== undefined)
29384
29458
  clearTimeout(timer);
29459
+ if (onExternalAbort !== undefined) {
29460
+ input2.signal?.removeEventListener("abort", onExternalAbort);
29461
+ }
29385
29462
  }
29386
29463
  if (outcome === "timeout") {
29387
29464
  abort.abort();
29388
29465
  turn.catch(() => {});
29466
+ composed.dispose();
29389
29467
  releaseBudget();
29390
29468
  const partial = assistant.trim();
29391
29469
  return {
@@ -29395,7 +29473,15 @@ async function enrichPageDeep(input2) {
29395
29473
  toolCalls
29396
29474
  };
29397
29475
  }
29476
+ composed.dispose();
29398
29477
  releaseBudget();
29478
+ if (outcome === "cancelled" || input2.signal?.aborted) {
29479
+ return {
29480
+ fallback: true,
29481
+ reason: "deep enrich cancelled",
29482
+ toolCalls
29483
+ };
29484
+ }
29399
29485
  const raw = assistant.trim().length > 0 ? assistant.trim() : history.filter((m) => m.role === "assistant").map((m) => m.content).join(`
29400
29486
  `).trim();
29401
29487
  if (raw.length === 0) {
@@ -29582,6 +29668,9 @@ function isWikiEnrichIntent(line) {
29582
29668
  if (t.length === 0) {
29583
29669
  return false;
29584
29670
  }
29671
+ if (/^keryx\s+wiki\s+enrich(?:\s|$)/.test(t)) {
29672
+ return false;
29673
+ }
29585
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"));
29586
29675
  const en = /\benrich\b/.test(t) && /\bwiki\b/.test(t) || /\bwiki\s+enrich\b/.test(t);
29587
29676
  return ru || en;
@@ -29860,6 +29949,7 @@ async function wikiEnrich(input2) {
29860
29949
  enriched: 0,
29861
29950
  dryRun: 0,
29862
29951
  skipped: 0,
29952
+ cancelled: 0,
29863
29953
  failed: 0
29864
29954
  };
29865
29955
  if (input2.refreshGraph) {
@@ -29900,9 +29990,15 @@ async function wikiEnrich(input2) {
29900
29990
  pageResults = await mapPool(pages, concurrency, async (page, i) => {
29901
29991
  const index = i + 1;
29902
29992
  const status = page.status ?? "draft";
29993
+ if (input2.signal?.aborted) {
29994
+ return { path: page.relativePath, action: "cancelled", reason: "enrichment cancelled" };
29995
+ }
29903
29996
  onPage({ index, total, path: page.relativePath, status, phase: "start" });
29904
29997
  try {
29905
29998
  const originalRaw = await readFile62(page.absolutePath, "utf8");
29999
+ if (input2.signal?.aborted) {
30000
+ return { path: page.relativePath, action: "cancelled", reason: "enrichment cancelled" };
30001
+ }
29906
30002
  const ensured = ensureWikiFrontmatter(originalRaw, {
29907
30003
  title: page.title,
29908
30004
  pageType: page.pageType
@@ -29919,8 +30015,12 @@ async function wikiEnrich(input2) {
29919
30015
  env,
29920
30016
  ...input2.fetch ? { fetch: input2.fetch } : {},
29921
30017
  ...input2.baseUrl !== undefined ? { baseUrl: input2.baseUrl } : {},
29922
- ...input2.providerFactory ? { providerFactory: input2.providerFactory } : {}
30018
+ ...input2.providerFactory ? { providerFactory: input2.providerFactory } : {},
30019
+ ...input2.signal !== undefined ? { signal: input2.signal } : {}
29923
30020
  });
30021
+ if (input2.signal?.aborted) {
30022
+ return { path: page.relativePath, action: "cancelled", reason: "enrichment cancelled" };
30023
+ }
29924
30024
  if (turn.error) {
29925
30025
  onPage({ index, total, path: page.relativePath, status, phase: "failed" });
29926
30026
  return {
@@ -29957,7 +30057,16 @@ async function wikiEnrich(input2) {
29957
30057
  const output2 = enriched.endsWith(`
29958
30058
  `) ? enriched : `${enriched}
29959
30059
  `;
29960
- 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
+ }
29961
30070
  if (!materialized.allowed) {
29962
30071
  onPage({ index, total, path: page.relativePath, status, phase: "failed" });
29963
30072
  return { path: page.relativePath, action: "failed", reason: materialized.reason };
@@ -30009,6 +30118,8 @@ async function wikiEnrich(input2) {
30009
30118
  }
30010
30119
  } else if (entry.action === "dry-run") {
30011
30120
  result.dryRun += 1;
30121
+ } else if (entry.action === "cancelled") {
30122
+ result.cancelled += 1;
30012
30123
  } else if (entry.action === "failed") {
30013
30124
  result.failed += 1;
30014
30125
  resumeState.failed.push({ path: entry.path, reason: entry.reason ?? "failed" });
@@ -30079,6 +30190,9 @@ function finalizeEnrichedText(original, rawText, options) {
30079
30190
  async function finishSuccess(ctx, page, originalRaw, content, keyFiles, extra) {
30080
30191
  const index = ctx.pageIndex(page.relativePath);
30081
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
+ }
30082
30196
  const nodeHash = keyFiles.length > 0 ? await computePageNodeHash(ctx.cwd, keyFiles, ctx.graph) : undefined;
30083
30197
  if (ctx.input.dryRun) {
30084
30198
  ctx.onPage({ index, total: ctx.total, path: page.relativePath, status, phase: "done" });
@@ -30095,7 +30209,16 @@ async function finishSuccess(ctx, page, originalRaw, content, keyFiles, extra) {
30095
30209
  const output2 = content.endsWith(`
30096
30210
  `) ? content : `${content}
30097
30211
  `;
30098
- 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
+ }
30099
30222
  if (!materialized.allowed) {
30100
30223
  ctx.onPage({ index, total: ctx.total, path: page.relativePath, status, phase: "failed" });
30101
30224
  return {
@@ -30128,6 +30251,9 @@ async function runDeepSingle(ctx, item) {
30128
30251
  const { page, original, originalRaw, keyFiles } = item;
30129
30252
  const index = ctx.pageIndex(page.relativePath);
30130
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
+ }
30131
30257
  ctx.onPage({ index, total: ctx.total, path: page.relativePath, status, phase: "model" });
30132
30258
  try {
30133
30259
  const deepResult = await enrichPageDeep({
@@ -30140,11 +30266,15 @@ async function runDeepSingle(ctx, item) {
30140
30266
  model: ctx.model,
30141
30267
  maxToolCalls: ctx.wikiConfig.rlm.deep.maxToolCalls,
30142
30268
  maxRuntimeMs: ctx.wikiConfig.rlm.deep.maxRuntimeMs,
30269
+ ...ctx.input.signal !== undefined ? { signal: ctx.input.signal } : {},
30143
30270
  env: ctx.env,
30144
30271
  ...ctx.input.fetch ? { fetch: ctx.input.fetch } : {},
30145
30272
  ...ctx.input.baseUrl !== undefined ? { baseUrl: ctx.input.baseUrl } : {},
30146
30273
  ...ctx.input.providerFactory ? { providerFactory: ctx.input.providerFactory } : {}
30147
30274
  });
30275
+ if (ctx.input.signal?.aborted) {
30276
+ return { path: page.relativePath, action: "cancelled", reason: "enrichment cancelled", tier: "deep" };
30277
+ }
30148
30278
  const deepToolCalls = deepResult.toolCalls.length;
30149
30279
  let rawText = null;
30150
30280
  if ("enriched" in deepResult) {
@@ -30222,9 +30352,9 @@ function batchGroupKey(item) {
30222
30352
  const primaryKeyFile = item.keyFiles[0];
30223
30353
  if (primaryKeyFile !== undefined) {
30224
30354
  const dir = primaryKeyFile.split("/").slice(0, -1).join("/");
30225
- const segments2 = dir.split("/").filter((segment) => segment.length > 0);
30226
- if (segments2.length > 0) {
30227
- 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("/");
30228
30358
  }
30229
30359
  }
30230
30360
  return item.page.pageType;
@@ -30261,6 +30391,14 @@ function groupLightPagesIntoBatches(items, batchConfig, tokenBudget) {
30261
30391
  return batches;
30262
30392
  }
30263
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
+ }
30264
30402
  for (const item of items) {
30265
30403
  ctx.onPage({
30266
30404
  index: ctx.pageIndex(item.page.relativePath),
@@ -30285,7 +30423,8 @@ async function runLightBatch(ctx, items) {
30285
30423
  env: ctx.env,
30286
30424
  ...ctx.input.fetch ? { fetch: ctx.input.fetch } : {},
30287
30425
  ...ctx.input.baseUrl !== undefined ? { baseUrl: ctx.input.baseUrl } : {},
30288
- ...ctx.input.providerFactory ? { providerFactory: ctx.input.providerFactory } : {}
30426
+ ...ctx.input.providerFactory ? { providerFactory: ctx.input.providerFactory } : {},
30427
+ ...ctx.input.signal !== undefined ? { signal: ctx.input.signal } : {}
30289
30428
  });
30290
30429
  } catch (cause) {
30291
30430
  const reason = cause instanceof Error ? cause.message : String(cause);
@@ -30305,6 +30444,14 @@ async function runLightBatch(ctx, items) {
30305
30444
  };
30306
30445
  });
30307
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
+ }
30308
30455
  if (turn.error) {
30309
30456
  const errorReason = `${turn.error.kind}: ${turn.error.message}`;
30310
30457
  return items.map((item) => {
@@ -30368,8 +30515,18 @@ async function runLightBatch(ctx, items) {
30368
30515
  }
30369
30516
  async function runRlmPipeline(ctxInput) {
30370
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
+ }
30371
30522
  const graph = await loadGraph(ctxInput.cwd);
30523
+ if (input2.signal?.aborted) {
30524
+ return cancelled();
30525
+ }
30372
30526
  const gdgraphConfig = await loadGdgraphConfig(ctxInput.cwd);
30527
+ if (input2.signal?.aborted) {
30528
+ return cancelled();
30529
+ }
30373
30530
  const fileNodes = graph.nodes.filter((node) => node.kind === "file").map((node) => node.path);
30374
30531
  const rankEdges = buildRankEdges(graph, gdgraphConfig);
30375
30532
  const ranked = personalizedPageRank(fileNodes, rankEdges, {
@@ -30400,11 +30557,21 @@ async function runRlmPipeline(ctxInput) {
30400
30557
  };
30401
30558
  const prepared = [];
30402
30559
  for (const page of pages) {
30560
+ if (input2.signal?.aborted) {
30561
+ return cancelled();
30562
+ }
30403
30563
  const index = pageIndex(page.relativePath);
30404
30564
  const status = page.status ?? "draft";
30405
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);
30406
30570
  try {
30407
30571
  const originalRaw = await readFile62(page.absolutePath, "utf8");
30572
+ if (input2.signal?.aborted) {
30573
+ return cancelled();
30574
+ }
30408
30575
  const ensured = ensureWikiFrontmatter(originalRaw, { title: page.title, pageType: page.pageType });
30409
30576
  const original = ensured.markdown;
30410
30577
  const keyFiles = keyFilesForPage(moduleKeyFilesIndex, page);
@@ -30412,6 +30579,9 @@ async function runRlmPipeline(ctxInput) {
30412
30579
  let unchanged = false;
30413
30580
  if (previousHash !== undefined && keyFiles.length > 0) {
30414
30581
  const currentHash = await computePageNodeHash(ctx.cwd, keyFiles, graph);
30582
+ if (input2.signal?.aborted) {
30583
+ return cancelled();
30584
+ }
30415
30585
  unchanged = isPageUnchangedSinceLastEnrich(page.relativePath, currentHash, resumeState.completedNodeHashes);
30416
30586
  }
30417
30587
  if (unchanged) {
@@ -30445,6 +30615,9 @@ async function runRlmPipeline(ctxInput) {
30445
30615
  const item = { page, original, originalRaw, keyFiles };
30446
30616
  prepared.push({ kind: tier === "deep" ? "deep" : "light", item });
30447
30617
  } catch (cause) {
30618
+ if (input2.signal?.aborted) {
30619
+ return cancelled();
30620
+ }
30448
30621
  ctx.onPage({ index, total: ctx.total, path: page.relativePath, status, phase: "failed" });
30449
30622
  prepared.push({
30450
30623
  kind: "resolved",
@@ -30456,6 +30629,9 @@ async function runRlmPipeline(ctxInput) {
30456
30629
  });
30457
30630
  }
30458
30631
  }
30632
+ if (input2.signal?.aborted) {
30633
+ return cancelled();
30634
+ }
30459
30635
  const units = [];
30460
30636
  let pendingLight = [];
30461
30637
  const flushLight = () => {
@@ -30485,6 +30661,9 @@ async function runRlmPipeline(ctxInput) {
30485
30661
  }
30486
30662
  }
30487
30663
  flushLight();
30664
+ if (input2.signal?.aborted) {
30665
+ return cancelled();
30666
+ }
30488
30667
  const unitResults = await mapPool(units, ctxInput.concurrency, async (unit) => {
30489
30668
  try {
30490
30669
  if (unit.kind === "resolved") {
@@ -30770,12 +30949,12 @@ async function narrate(opts) {
30770
30949
  });
30771
30950
  if (!turn.credentialAvailable) {
30772
30951
  const message2 = formatMissingCredentialHint(turn.provider);
30773
- 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);
30774
30953
  return;
30775
30954
  }
30776
30955
  if (turn.error) {
30777
30956
  const message2 = `${turn.error.kind}: ${turn.error.message}`;
30778
- 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);
30779
30958
  return;
30780
30959
  }
30781
30960
  const text = turn.text.trim();
@@ -30785,7 +30964,7 @@ async function narrate(opts) {
30785
30964
  console.log(text);
30786
30965
  }
30787
30966
  }
30788
- function emit(asJson, payload, message2, isError) {
30967
+ function emit2(asJson, payload, message2, isError) {
30789
30968
  if (asJson) {
30790
30969
  console.log(JSON.stringify(payload, null, 2));
30791
30970
  } else if (isError) {
@@ -32875,7 +33054,7 @@ var BUNDLED_GDSKILLS = [
32875
33054
  "Use gdgraph affected context for exported symbols and shared surfaces.",
32876
33055
  "Dispatch specialized review passes conceptually or as separate skill loads.",
32877
33056
  "Report findings first, ordered by severity, with concrete file references."
32878
- ], ["review code", "full review", "review changes"]),
33057
+ ], ["review", "\u0440\u0435\u0432\u044C\u044E", "review code", "full review", "review changes"]),
32879
33058
  skill("review-logic", "review", ["recommended", "full"], "Review logic correctness, contracts, edge cases, nullability, and async behavior.", [
32880
33059
  "Trace behavior through call sites and affected context.",
32881
33060
  "Look for incorrect assumptions, missing branches, race conditions, and error paths.",
@@ -32890,7 +33069,13 @@ var BUNDLED_GDSKILLS = [
32890
33069
  "Map inputs, trust boundaries, and sensitive outputs.",
32891
33070
  "Check injection, auth, crypto, secrets, and unsafe filesystem/network behavior.",
32892
33071
  "Prioritize exploitable findings with concrete remediation."
32893
- ], ["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
+ ]),
32894
33079
  skill("review-performance", "review", ["recommended", "full"], "Review hot paths, unnecessary work, bundle/perf regressions, blocking operations, and memory risk.", [
32895
33080
  "Find changed hot paths and repeated operations.",
32896
33081
  "Check loops, rendering, async blocking, large imports, and caching behavior.",
@@ -33026,7 +33211,12 @@ var BUNDLED_GDSKILLS = [
33026
33211
  "Detect deployment target and required pre-flight checks.",
33027
33212
  "Run verification before deployment unless explicitly skipped.",
33028
33213
  "Summarize deployment status, rollback path, and health checks."
33029
- ], ["deploy", "ship", "release"]),
33214
+ ], [
33215
+ "deploy",
33216
+ "deployment",
33217
+ "ship",
33218
+ "release"
33219
+ ]),
33030
33220
  skill("commit", "quality", ["full"], "Prepare conventional commits with scope, summary, and verification notes.", [
33031
33221
  "Inspect staged and unstaged changes.",
33032
33222
  "Group related changes without staging unrelated work.",
@@ -33530,6 +33720,28 @@ async function validateValue(value, schema, valuePath, errors, rootSchema, schem
33530
33720
  message: `Expected array length >= ${schema.minItems}`
33531
33721
  });
33532
33722
  }
33723
+ if (Array.isArray(value) && schema.maxItems !== undefined && value.length > schema.maxItems) {
33724
+ errors.push({
33725
+ path: valuePath,
33726
+ message: `Expected array length <= ${schema.maxItems}`
33727
+ });
33728
+ }
33729
+ if (typeof value === "number" && schema.exclusiveMinimum !== undefined && value <= schema.exclusiveMinimum) {
33730
+ errors.push({
33731
+ path: valuePath,
33732
+ message: `Expected number > ${schema.exclusiveMinimum}`
33733
+ });
33734
+ }
33735
+ if (schema.not !== undefined) {
33736
+ const negated = [];
33737
+ await validateValue(value, schema.not, valuePath, negated, rootSchema, schemaCache);
33738
+ if (negated.length === 0) {
33739
+ errors.push({
33740
+ path: valuePath,
33741
+ message: "Expected value NOT to match the prohibited subschema"
33742
+ });
33743
+ }
33744
+ }
33533
33745
  if (typeof value === "string") {
33534
33746
  if (schema.minLength !== undefined && value.length < schema.minLength) {
33535
33747
  errors.push({
@@ -39475,8 +39687,56 @@ init_guard();
39475
39687
  import { mkdir as mkdir16, readFile as readFile23, writeFile as writeFile18 } from "fs/promises";
39476
39688
  import path46 from "path";
39477
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
+
39478
39716
  // src/ctx/hook-classify.ts
39479
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
+ }
39480
39740
  var LEADING_SKIP = new Set(["sudo", "command", "time", "nice", "env", "builtin"]);
39481
39741
  var ALREADY_ROUTED = new Set(["keryx", "rtk"]);
39482
39742
  var ROUTES = [
@@ -39484,11 +39744,118 @@ var ROUTES = [
39484
39744
  { names: /^(cat|head|tail)$/, suggestion: "keryx ctx read <file> --mode compact" }
39485
39745
  ];
39486
39746
  var GIT_ROUTABLE = /^(diff|log|show)$/;
39487
- function segments(command) {
39488
- 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);
39489
39856
  }
39490
39857
  function leadingTokens(segment) {
39491
- const tokens = segment.split(/\s+/).filter(Boolean);
39858
+ const tokens = stageTokens(segment);
39492
39859
  let i = 0;
39493
39860
  while (i < tokens.length) {
39494
39861
  const token = tokens[i] ?? "";
@@ -39504,12 +39871,15 @@ function classifyCommand(command) {
39504
39871
  if (!command.trim()) {
39505
39872
  return { block: false };
39506
39873
  }
39507
- const escape = ESCAPE_MARKER.exec(command);
39508
- if (escape) {
39509
- return { block: false, escapeReason: (escape[1] ?? "").trim() };
39874
+ const escapeReason = escapeReasonOf(command);
39875
+ if (escapeReason !== null) {
39876
+ return { block: false, escapeReason };
39510
39877
  }
39511
- for (const segment of segments(command)) {
39512
- 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
+ }
39513
39883
  const first = tokens[0];
39514
39884
  if (!first || ALREADY_ROUTED.has(first)) {
39515
39885
  continue;
@@ -39612,18 +39982,30 @@ function stripFromHookArray(settings, key) {
39612
39982
  removeSentinel(settings);
39613
39983
  return settings;
39614
39984
  }
39615
- function hasManagedInArray(settings, key, command) {
39616
- const hooks = settings.hooks;
39617
- const groups = Array.isArray(hooks?.[key]) ? hooks?.[key] : [];
39618
- return groups.some((g) => {
39619
- 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))
39620
39994
  return false;
39621
- const inner = g;
39622
- if (inner.command === command)
39623
- return true;
39624
- 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");
39625
40000
  });
39626
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
+ }
39627
40009
  function parseJson(payload) {
39628
40010
  try {
39629
40011
  const parsed = JSON.parse(payload);
@@ -39642,6 +40024,22 @@ function parseToolInputCommand(payload) {
39642
40024
  const command = input2.command;
39643
40025
  return typeof command === "string" ? command : null;
39644
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
+ }
39645
40043
  function parseCursorCommand(payload) {
39646
40044
  const record = parseJson(payload);
39647
40045
  const command = record?.command;
@@ -39712,41 +40110,73 @@ function antigravityAllow(_c) {
39712
40110
  return { exitCode: 0, stdout: `${JSON.stringify({ allow_tool: true })}
39713
40111
  ` };
39714
40112
  }
39715
- 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) {
39716
40139
  return {
39717
- matcher: "Bash",
39718
- hooks: [{ type: "command", command: hookCommand(id) }],
40140
+ matcher: preToolUseMatcher(runtime),
40141
+ hooks: [{ type: "command", command: hookCommand(runtime.id) }],
39719
40142
  [MANAGED_KEY2]: CTX_HOOK_SENTINEL
39720
40143
  };
39721
40144
  }
39722
40145
  var CLAUDE_RUNTIME3 = {
39723
40146
  id: "claude",
39724
- label: ".claude/settings.json (PreToolUse/Bash)",
40147
+ label: ".claude/settings.json (PreToolUse)",
39725
40148
  confidence: "verified",
40149
+ groupShape: "nested",
40150
+ groupKey: "PreToolUse",
40151
+ nativeSearchTools: ["Grep"],
39726
40152
  parseCommand: parseToolInputCommand,
39727
40153
  block: exitCodeBlock,
39728
40154
  allow: exitCodeAllow,
39729
40155
  locate: (root) => path44.join(root, ".claude", "settings.json"),
39730
- merge: (s) => mergeIntoHookArray(s, "PreToolUse", preToolUseGroup("claude")),
40156
+ merge: (s) => mergeIntoHookArray(s, "PreToolUse", preToolUseGroup(CLAUDE_RUNTIME3)),
39731
40157
  strip: (s) => stripFromHookArray(s, "PreToolUse"),
39732
- validate: (s) => hasManagedInArray(s, "PreToolUse", hookCommand("claude")) ? [] : ["claude: missing PreToolUse(Bash) guard"]
40158
+ validate: (s) => validatePreToolUse(s, CLAUDE_RUNTIME3)
39733
40159
  };
39734
40160
  var CODEX_RUNTIME = {
39735
40161
  id: "codex",
39736
- label: ".codex/hooks.json (PreToolUse/Bash)",
40162
+ label: ".codex/hooks.json (PreToolUse)",
39737
40163
  confidence: "verified",
40164
+ groupShape: "nested",
40165
+ groupKey: "PreToolUse",
39738
40166
  parseCommand: parseToolInputCommand,
39739
40167
  block: exitCodeBlock,
39740
40168
  allow: exitCodeAllow,
39741
40169
  locate: (root) => path44.join(root, ".codex", "hooks.json"),
39742
- merge: (s) => mergeIntoHookArray(s, "PreToolUse", preToolUseGroup("codex")),
40170
+ merge: (s) => mergeIntoHookArray(s, "PreToolUse", preToolUseGroup(CODEX_RUNTIME)),
39743
40171
  strip: (s) => stripFromHookArray(s, "PreToolUse"),
39744
- validate: (s) => hasManagedInArray(s, "PreToolUse", hookCommand("codex")) ? [] : ["codex: missing PreToolUse(Bash) guard"]
40172
+ validate: (s) => validatePreToolUse(s, CODEX_RUNTIME)
39745
40173
  };
39746
40174
  var CURSOR_RUNTIME2 = {
39747
40175
  id: "cursor",
39748
40176
  label: ".cursor/hooks.json (beforeShellExecution)",
39749
40177
  confidence: "verified",
40178
+ groupShape: "flat",
40179
+ groupKey: "beforeShellExecution",
39750
40180
  parseCommand: parseCursorCommand,
39751
40181
  block: cursorBlock,
39752
40182
  allow: cursorAllow,
@@ -39759,12 +40189,14 @@ var CURSOR_RUNTIME2 = {
39759
40189
  });
39760
40190
  },
39761
40191
  strip: (s) => stripFromHookArray(s, "beforeShellExecution"),
39762
- validate: (s) => hasManagedInArray(s, "beforeShellExecution", hookCommand("cursor")) ? [] : ["cursor: missing beforeShellExecution guard"]
40192
+ validate: (s) => hasRunnableGuard(s, CURSOR_RUNTIME2) ? [] : ["cursor: missing beforeShellExecution guard"]
39763
40193
  };
39764
40194
  var WINDSURF_RUNTIME = {
39765
40195
  id: "windsurf",
39766
40196
  label: ".windsurf/hooks.json (pre_run_command)",
39767
40197
  confidence: "verified",
40198
+ groupShape: "flat",
40199
+ groupKey: "pre_run_command",
39768
40200
  parseCommand: parseWindsurfCommand,
39769
40201
  block: exitCodeBlock,
39770
40202
  allow: exitCodeAllow,
@@ -39775,12 +40207,15 @@ var WINDSURF_RUNTIME = {
39775
40207
  [MANAGED_KEY2]: CTX_HOOK_SENTINEL
39776
40208
  }),
39777
40209
  strip: (s) => stripFromHookArray(s, "pre_run_command"),
39778
- 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"]
39779
40211
  };
39780
40212
  var ANTIGRAVITY_RUNTIME = {
39781
40213
  id: "antigravity",
39782
40214
  label: ".agents/hooks.json (PreToolUse/run_command)",
39783
40215
  confidence: "experimental",
40216
+ groupShape: "nested",
40217
+ groupKey: "PreToolUse",
40218
+ groupContainer: "keryx-ctx-guard",
39784
40219
  parseCommand: parseAntigravityCommand,
39785
40220
  block: antigravityBlock,
39786
40221
  allow: antigravityAllow,
@@ -39813,12 +40248,7 @@ var ANTIGRAVITY_RUNTIME = {
39813
40248
  removeSentinel(s);
39814
40249
  return s;
39815
40250
  },
39816
- validate: (s) => {
39817
- const group = s["keryx-ctx-guard"];
39818
- const list2 = Array.isArray(group?.PreToolUse) ? group?.PreToolUse : [];
39819
- const ok = list2.some((g) => isManagedGroup2(g) && Array.isArray(g.hooks) && g.hooks.some((h) => h?.command === hookCommand("antigravity")));
39820
- return ok ? [] : ["antigravity: missing run_command guard"];
39821
- }
40251
+ validate: (s) => hasRunnableGuard(s, ANTIGRAVITY_RUNTIME) ? [] : ["antigravity: missing run_command guard"]
39822
40252
  };
39823
40253
  var OPENCODE_PLUGIN = `// keryx gdctx routing guard \u2014 generated by \`keryx ctx install-hook --runtime opencode\`.
39824
40254
  // Bridges OpenCode's tool.execute.before to \`keryx ctx hook opencode\`.
@@ -39841,6 +40271,8 @@ var OPENCODE_RUNTIME2 = {
39841
40271
  id: "opencode",
39842
40272
  label: ".opencode/plugin/keryx-ctx-guard.js",
39843
40273
  confidence: "experimental",
40274
+ groupShape: "nested",
40275
+ groupKey: "PreToolUse",
39844
40276
  parseCommand: parseToolInputCommand,
39845
40277
  block: exitCodeBlock,
39846
40278
  allow: exitCodeAllow,
@@ -39896,15 +40328,9 @@ function resolveRuntimes2(ids) {
39896
40328
  return { runtimes, unknown, unsupported };
39897
40329
  }
39898
40330
  // src/ctx/hook.ts
40331
+ var STDIN_DEADLINE_MS = 2000;
39899
40332
  async function readStdin() {
39900
- if (process.stdin.isTTY) {
39901
- return "";
39902
- }
39903
- const chunks = [];
39904
- for await (const chunk of process.stdin) {
39905
- chunks.push(Buffer.from(chunk));
39906
- }
39907
- return Buffer.concat(chunks).toString("utf8");
40333
+ return await readStdinBounded(STDIN_DEADLINE_MS) ?? "";
39908
40334
  }
39909
40335
  async function runCtxHook(runtimeId) {
39910
40336
  const runtime = getRuntime2(runtimeId ?? "claude");
@@ -39914,10 +40340,17 @@ async function runCtxHook(runtimeId) {
39914
40340
  const payload = await readStdin();
39915
40341
  const command = runtime.parseCommand(payload);
39916
40342
  if (command === null) {
40343
+ const nativeSearch = matchedNativeSearch(runtime.nativeSearchTools, payload);
40344
+ if (nativeSearch) {
40345
+ emit(refusalAction(runtime.id, nativeSearchMessage(nativeSearch)));
40346
+ }
39917
40347
  return;
39918
40348
  }
39919
40349
  const classification = classifyCommand(command);
39920
40350
  const action = classification.block ? runtime.block(command, classification) : runtime.allow(classification);
40351
+ emit(action);
40352
+ }
40353
+ function emit(action) {
39921
40354
  if (action.stdout)
39922
40355
  process.stdout.write(action.stdout);
39923
40356
  if (action.stderr)
@@ -39925,6 +40358,13 @@ async function runCtxHook(runtimeId) {
39925
40358
  if (action.exitCode)
39926
40359
  process.exitCode = action.exitCode;
39927
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
+ }
39928
40368
 
39929
40369
  // src/ctx/hook-install.ts
39930
40370
  init_fs();
@@ -39961,9 +40401,10 @@ async function installRuntimeHook(projectRoot, runtime) {
39961
40401
  return { path: file, errors: [`${runtime.id}: no installer defined`] };
39962
40402
  }
39963
40403
  const settings = await readSettings3(file);
40404
+ const upgraded = describeExistingGuard(settings, runtime);
39964
40405
  await writeSettings3(file, merge(settings));
39965
40406
  const errors = validate(await readSettings3(file));
39966
- return { path: file, errors };
40407
+ return { path: file, errors, ...upgraded ? { upgraded } : {} };
39967
40408
  }
39968
40409
  async function uninstallRuntimeHook(projectRoot, runtime) {
39969
40410
  if (runtime.customUninstall) {
@@ -40206,7 +40647,7 @@ async function handleInstallHook(args) {
40206
40647
  console.log("escape: append `# keryx:raw <reason>` to run a raw command anyway");
40207
40648
  console.log("");
40208
40649
  for (const runtime of runtimes) {
40209
- const { path: file, errors } = await installRuntimeHook(cwd, runtime);
40650
+ const { path: file, errors, upgraded } = await installRuntimeHook(cwd, runtime);
40210
40651
  const tag = runtime.confidence === "experimental" ? " (experimental \u2014 verify on a live install)" : "";
40211
40652
  if (errors.length > 0) {
40212
40653
  for (const error of errors)
@@ -40214,6 +40655,8 @@ async function handleInstallHook(args) {
40214
40655
  process.exitCode = 1;
40215
40656
  } else {
40216
40657
  console.log(` \u2713 ${runtime.id} -> ${path46.relative(cwd, file)}${tag}`);
40658
+ if (upgraded)
40659
+ console.log(` ${upgraded}`);
40217
40660
  }
40218
40661
  }
40219
40662
  reportUnsupported(unsupported);
@@ -40350,7 +40793,7 @@ ${renderUntrackedFiles(untracked, config)}
40350
40793
  ` : ""}
40351
40794
  ## Risk Hints
40352
40795
 
40353
- ${renderRiskHints(risky, shape)}
40796
+ ${renderRiskHints(risky, shape, shape.files)}
40354
40797
 
40355
40798
  ## Hunks
40356
40799
 
@@ -40723,13 +41166,19 @@ function renderDiffCounts(file) {
40723
41166
  }
40724
41167
  return file.changed !== undefined ? `: ~${file.changed} changed` : "";
40725
41168
  }
40726
- function renderRiskHints(risky, shape) {
41169
+ function renderRiskHints(risky, shape, files) {
40727
41170
  const hints = risky.map((file) => `- ${file.path}`);
40728
41171
  if (shape.pathsAbbreviated) {
40729
41172
  hints.push("- (paths are abbreviated by `--stat`; risk matching is unreliable \u2014 re-run with `--numstat` or `--name-only` to check)");
40730
41173
  }
40731
41174
  if (hints.length === 0) {
40732
- return shape.mode === "unenumerable" ? "- unknown: no file list in this output shape" : "- none";
41175
+ if (shape.mode === "unenumerable") {
41176
+ return "- unknown: no file list in this output shape";
41177
+ }
41178
+ if (files.length === 0 && (shape.reportedFileCount ?? 0) > 0) {
41179
+ return `- unknown: ${shape.reportedFileCount} file(s) changed but this output shape lists no per-file rows \u2014 nothing was checked. Re-run with \`--numstat\` or \`--name-only\`.`;
41180
+ }
41181
+ return "- none";
40733
41182
  }
40734
41183
  return hints.join(`
40735
41184
  `);
@@ -43662,6 +44111,47 @@ async function routeProjectSkills(args2) {
43662
44111
  }
43663
44112
  console.log(`Next: ${matches2[0]?.next}`);
43664
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
+ }
43665
44155
  function scoreBundledSkillRoute(entry, query) {
43666
44156
  const normalizedQuery = normalizeRouteText(query);
43667
44157
  if (!normalizedQuery) {
@@ -43684,18 +44174,13 @@ function scoreBundledSkillRoute(entry, query) {
43684
44174
  score += 100;
43685
44175
  reasons.push("exact skill");
43686
44176
  }
43687
- const triggerHit = triggers.some((trigger) => {
43688
- if (normalizedQuery.includes(trigger)) {
43689
- return true;
43690
- }
43691
- const triggerTokens = [...routeTokens(trigger)];
43692
- return triggerTokens.length > 0 && triggerTokens.every((token) => queryTokens.has(token));
43693
- });
44177
+ const queryWords = new Set(normalizedQuery.split(" ").filter(Boolean));
44178
+ const triggerHit = triggers.some((trigger) => triggerFires(trigger, normalizedQuery, queryWords, queryTokens));
43694
44179
  if (triggerHit) {
43695
44180
  score += 55;
43696
44181
  reasons.push("trigger");
43697
44182
  }
43698
- if (name && normalizedQuery.includes(name)) {
44183
+ if (name && containsPhrase(normalizedQuery, name)) {
43699
44184
  score += 30;
43700
44185
  reasons.push("skill name");
43701
44186
  }
@@ -43801,7 +44286,8 @@ var ROUTE_STOPWORDS = new Set([
43801
44286
  var RU_SYNONYM_PREFIXES = [
43802
44287
  ["\u0440\u0435\u0432\u044C\u044E", ["review"]],
43803
44288
  ["\u0440\u0435\u0432\u044C\u044E\u0435\u0440", ["review", "reviewer"]],
43804
- ["\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"]],
43805
44291
  ["\u0440\u0435\u0430\u043B\u0438\u0437", ["implement"]],
43806
44292
  ["\u0438\u043C\u043F\u043B\u0435\u043C\u0435\u043D\u0442", ["implement"]],
43807
44293
  ["\u0432\u043D\u0435\u0434\u0440", ["implement"]],
@@ -43837,6 +44323,7 @@ var RU_SYNONYM_PREFIXES = [
43837
44323
  ["\u0441\u043E\u0437\u0434\u0430", ["create"]],
43838
44324
  ["\u0434\u0435\u043F\u043B\u043E\u0439", ["deploy"]],
43839
44325
  ["\u0440\u0430\u0437\u0432\u0435\u0440\u0442", ["deploy"]],
44326
+ ["\u0440\u0430\u0437\u0432\u0435\u0440\u043D", ["deploy"]],
43840
44327
  ["\u0437\u0430\u0432\u0438\u0441\u0438\u043C", ["dependency", "dependencies"]],
43841
44328
  ["\u0438\u043D\u0442\u0435\u0440\u0432\u044C\u044E", ["interview"]],
43842
44329
  ["\u043E\u043F\u0440\u043E\u0441", ["interview"]],
@@ -43846,6 +44333,7 @@ var RU_SYNONYM_PREFIXES = [
43846
44333
  ["\u0444\u043B\u043E", ["flow"]],
43847
44334
  ["\u043E\u0440\u043A\u0435\u0441\u0442\u0440", ["orchestrator", "orchestrate"]],
43848
44335
  ["\u0430\u043D\u0430\u043B\u0438\u0437", ["analyze", "analysis"]],
44336
+ ["\u043F\u0440\u043E\u0430\u043D\u0430\u043B\u0438\u0437", ["analyze", "analysis"]],
43849
44337
  ["\u0440\u0435\u0432\u044C\u044E\u0438\u0440", ["review"]]
43850
44338
  ];
43851
44339
  function routeTokens(normalized, expand = false) {
@@ -46833,9 +47321,9 @@ var SNAPSHOT_DIRECTORIES = new Set(["__snapshots__"]);
46833
47321
  var SNAPSHOT_SUFFIXES = [".snap"];
46834
47322
  function classifyPath(path133, config = DEFAULT_REVIEW_SCOPE_CONFIG) {
46835
47323
  const normalized = path133.replace(/\\/g, "/").replace(/^\.\//, "");
46836
- const segments2 = normalized.split("/").filter((segment) => segment.length > 0);
46837
- const basename = segments2.at(-1) ?? normalized;
46838
- 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);
46839
47327
  for (const segment of directories) {
46840
47328
  if (VENDOR_DIRECTORIES.has(segment)) {
46841
47329
  return { reason: "vendored", detail: `vendored: path segment "${segment}/"` };
@@ -49574,8 +50062,11 @@ init_project_skills();
49574
50062
  import { readFile as readFile72, readdir as readdir24 } from "fs/promises";
49575
50063
  import path135 from "path";
49576
50064
  var PROJECT_REVIEWER_MODULE = "review";
50065
+ function escapeRegexLiteral(literal) {
50066
+ return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
50067
+ }
49577
50068
  function metadataLine(content, label) {
49578
- const match = content.match(new RegExp(`^${label.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&")}:\\s*(.+)$`, "m"));
50069
+ const match = content.match(new RegExp(`^${escapeRegexLiteral(label)}:\\s*(.+)$`, "m"));
49579
50070
  return match?.[1]?.trim();
49580
50071
  }
49581
50072
  async function skillDirs(root) {
@@ -53429,9 +53920,9 @@ async function readContent(file) {
53429
53920
  }
53430
53921
  return readStdin2();
53431
53922
  }
53432
- function surfaceWarnings(warnings, emit2 = console.log) {
53923
+ function surfaceWarnings(warnings, emit3 = console.log) {
53433
53924
  for (const warning of warnings) {
53434
- emit2(` ${style.yellow(symbols.bullet)} ${warning}`);
53925
+ emit3(` ${style.yellow(symbols.bullet)} ${warning}`);
53435
53926
  }
53436
53927
  }
53437
53928
  async function handleStatus(cwd) {
@@ -53775,14 +54266,14 @@ async function handleEval(cwd, args2) {
53775
54266
  }
53776
54267
  process.exitCode = gate.status === "fail" ? 1 : 0;
53777
54268
  }
53778
- function renderDecision(decision, emit2 = console.log) {
53779
- emit2("");
53780
- emit2(` gate: ${gateLabel(decision.gate)}`);
53781
- emit2(` action: ${decision.action}`);
53782
- 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}`);
53783
54274
  for (const finding of decision.findings.slice(0, 20)) {
53784
54275
  const loc = finding.location?.line ? ` (line ${finding.location.line})` : "";
53785
- 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}`);
53786
54277
  }
53787
54278
  }
53788
54279
  function severityMarker(severity) {
@@ -53831,7 +54322,7 @@ function applyRuntimeDecision(args2, code, message2, decision, policyIsKnown) {
53831
54322
  if (runtime === undefined) {
53832
54323
  return code;
53833
54324
  }
53834
- const emit2 = (action) => {
54325
+ const emit3 = (action) => {
53835
54326
  if (action.stdout !== undefined) {
53836
54327
  process.stdout.write(action.stdout);
53837
54328
  }
@@ -53843,9 +54334,9 @@ function applyRuntimeDecision(args2, code, message2, decision, policyIsKnown) {
53843
54334
  const outcome = decideHookOutcome(code !== 0, decision, policyIsKnown);
53844
54335
  switch (outcome.kind) {
53845
54336
  case "refuse":
53846
- return emit2(refusalAction(runtime, message2));
54337
+ return emit3(refusalAction(runtime, message2));
53847
54338
  case "approve":
53848
- return emit2(allowAction(runtime));
54339
+ return emit3(allowAction(runtime));
53849
54340
  case "silent":
53850
54341
  return 0;
53851
54342
  }
@@ -63322,7 +63813,7 @@ import { spawnSync as spawnSync2 } from "child_process";
63322
63813
  // package.json
63323
63814
  var package_default = {
63324
63815
  name: "@mrciphersmith/keryx",
63325
- version: "0.2.74",
63816
+ version: "0.2.76",
63326
63817
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
63327
63818
  private: false,
63328
63819
  publishConfig: {
@@ -63818,9 +64309,9 @@ function createAssistantMessageStream(otui, renderer, parent) {
63818
64309
  parent.add(container);
63819
64310
  return { container, segmenter: createStreamSegmenter(), views: [], frozen: 0 };
63820
64311
  };
63821
- const paint = (m, segments2, frozen) => {
63822
- for (let i = m.frozen;i < segments2.length; i++) {
63823
- 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];
63824
64315
  if (segment === undefined) {
63825
64316
  continue;
63826
64317
  }
@@ -63832,11 +64323,11 @@ function createAssistantMessageStream(otui, renderer, parent) {
63832
64323
  existing?.destroy();
63833
64324
  m.views[i] = createSegmentView(otui, renderer, m.container, segment);
63834
64325
  }
63835
- for (const stale of m.views.splice(segments2.length)) {
64326
+ for (const stale of m.views.splice(segments.length)) {
63836
64327
  stale.destroy();
63837
64328
  }
63838
64329
  m.frozen = frozen;
63839
- for (const segment of segments2) {
64330
+ for (const segment of segments) {
63840
64331
  if (segment.kind === "code") {
63841
64332
  lastCode = { lang: segment.lang, body: segment.body };
63842
64333
  }
@@ -63848,8 +64339,8 @@ function createAssistantMessageStream(otui, renderer, parent) {
63848
64339
  return;
63849
64340
  }
63850
64341
  const m = message2 ??= start();
63851
- const { segments: segments2, frozen } = m.segmenter.push(chunk);
63852
- paint(m, segments2, frozen);
64342
+ const { segments, frozen } = m.segmenter.push(chunk);
64343
+ paint(m, segments, frozen);
63853
64344
  },
63854
64345
  finalize: (text) => {
63855
64346
  const m = message2 ?? start();
@@ -63857,9 +64348,9 @@ function createAssistantMessageStream(otui, renderer, parent) {
63857
64348
  view.destroy();
63858
64349
  }
63859
64350
  m.frozen = 0;
63860
- const segments2 = segmentMarkdown(text);
63861
- paint(m, segments2, segments2.length);
63862
- if (segments2.length === 0) {
64351
+ const segments = segmentMarkdown(text);
64352
+ paint(m, segments, segments.length);
64353
+ if (segments.length === 0) {
63863
64354
  try {
63864
64355
  parent.remove(m.container);
63865
64356
  } catch {}
@@ -66642,20 +67133,20 @@ function buildContextUsage(source) {
66642
67133
  const hasEstimate = source.estimateTokens !== undefined;
66643
67134
  const total = hasEstimate ? source.estimateTokens ?? 0 : last ?? 0;
66644
67135
  const estimated = hasEstimate || last === undefined;
66645
- const segments2 = [];
67136
+ const segments = [];
66646
67137
  if (hasEstimate) {
66647
- segments2.push({ id: "history", label: "history (est.)", tokens: source.estimateTokens ?? 0 });
67138
+ segments.push({ id: "history", label: "history (est.)", tokens: source.estimateTokens ?? 0 });
66648
67139
  }
66649
67140
  if (lastIn !== undefined) {
66650
- segments2.push({ id: "last-in", label: "last in", tokens: lastIn });
67141
+ segments.push({ id: "last-in", label: "last in", tokens: lastIn });
66651
67142
  }
66652
67143
  if (lastOut !== undefined) {
66653
- segments2.push({ id: "last-out", label: "last out", tokens: lastOut });
67144
+ segments.push({ id: "last-out", label: "last out", tokens: lastOut });
66654
67145
  }
66655
67146
  return {
66656
67147
  total,
66657
67148
  estimated,
66658
- segments: segments2,
67149
+ segments,
66659
67150
  bar: renderUsageBar(total),
66660
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."
66661
67152
  };
@@ -68271,6 +68762,161 @@ function setProjectPermissionMode(projectPath, mode, dir) {
68271
68762
  init_permission_mode();
68272
68763
  init_enrich2();
68273
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
+
68274
68920
  // src/tui/herdr-report.ts
68275
68921
  import net from "net";
68276
68922
  var SOURCE = "herdr:keryx";
@@ -72001,14 +72647,14 @@ function shortenCwd(cwd, max) {
72001
72647
  if (full.length <= max) {
72002
72648
  return full;
72003
72649
  }
72004
- const segments2 = full.split("/").filter((s) => s.length > 0);
72005
- for (let i = 1;i < segments2.length; i++) {
72006
- 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("/")}`;
72007
72653
  if (candidate.length <= max) {
72008
72654
  return candidate;
72009
72655
  }
72010
72656
  }
72011
- const last = segments2[segments2.length - 1] ?? full;
72657
+ const last = segments[segments.length - 1] ?? full;
72012
72658
  return `\u2026${last.slice(last.length - (max - 1))}`;
72013
72659
  }
72014
72660
  function estimateContextTokens(history) {
@@ -72716,9 +73362,12 @@ async function launchTuiAgentShell(opts) {
72716
73362
  let liveDeps;
72717
73363
  let liveJobs;
72718
73364
  let detachExternal;
73365
+ const foregroundOperation = createForegroundOperationOwner();
72719
73366
  try {
72720
73367
  const r = renderer = await createShellRenderer(otui, {
72721
73368
  onDestroy: () => {
73369
+ foregroundOperation.cancel("renderer destroyed");
73370
+ foregroundOperation.dispose();
72722
73371
  mountedChrome?.destroy();
72723
73372
  setAskUserHost(undefined);
72724
73373
  setSubagentFleetListener(undefined);
@@ -72782,7 +73431,6 @@ async function launchTuiAgentShell(opts) {
72782
73431
  const stopBusy = () => {
72783
73432
  chrome.stopBusy();
72784
73433
  };
72785
- let mainTurnAbortController;
72786
73434
  const sidebar = chrome.sidebarTop;
72787
73435
  sidebar.add(new otui.TextRenderable(r, { id: "sb-title", content: otui.t`${otui.bold("keryx")}` }));
72788
73436
  sidebar.add(new otui.TextRenderable(r, { id: "sb-model-k", content: otui.t`${otui.dim("Model")}`, marginTop: 1 }));
@@ -73813,7 +74461,7 @@ Staying in the current session.
73813
74461
  let mainQueue = [];
73814
74462
  let mainQueueSeq = 0;
73815
74463
  let pendingQueueEdit;
73816
- let priorityMainQuestion;
74464
+ const forceHandoff = createForegroundForceHandoff();
73817
74465
  let sideWorkerRunning = false;
73818
74466
  let sideClearTimeout;
73819
74467
  const showSideQueueStatus = () => {
@@ -73963,15 +74611,19 @@ Staying in the current session.
73963
74611
  return;
73964
74612
  mainQueue = removeMainQueueItem(mainQueue, index);
73965
74613
  paintMainQueue();
73966
- if (mainTurnAbortController !== undefined && !mainTurnAbortController.signal.aborted) {
73967
- priorityMainQuestion = item;
73968
- mainTurnAbortController.abort();
73969
- mainTurnAbortController = undefined;
74614
+ (async () => {
74615
+ const waitsForSettlement = forceHandoff.enqueue(item);
74616
+ foregroundOperation.cancel("queue item forced");
73970
74617
  io.onSystem?.(`\u25C7 main turn interrupted \u2014 q${index + 1} will run next.
73971
74618
  `);
73972
- return;
73973
- }
73974
- 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
+ })();
73975
74627
  };
73976
74628
  const handleQueueNavKey = (key) => {
73977
74629
  if (!queueNavActive) {
@@ -74178,6 +74830,8 @@ Staying in the current session.
74178
74830
  });
74179
74831
  switch (decision) {
74180
74832
  case "exit": {
74833
+ foregroundOperation.cancel("shell exit");
74834
+ foregroundOperation.dispose();
74181
74835
  (async () => {
74182
74836
  await closeSlateSession(slateSession, mintTimestampAttemptId);
74183
74837
  await deps.sweepBackgroundJobs?.();
@@ -74198,8 +74852,8 @@ Staying in the current session.
74198
74852
  return;
74199
74853
  }
74200
74854
  case "interrupt": {
74201
- if (mainTurnAbortController !== undefined && !mainTurnAbortController.signal.aborted) {
74202
- mainTurnAbortController.abort();
74855
+ if (foregroundOperation.isActive) {
74856
+ foregroundOperation.cancel("interrupted by user");
74203
74857
  io.onSystem?.(`\u25C7 main turn interrupted.
74204
74858
  `);
74205
74859
  return;
@@ -74626,10 +75280,13 @@ ${formatThemeList(getThemeId())}`);
74626
75280
  }));
74627
75281
  if (isWikiEnrichIntent(line)) {
74628
75282
  const startedAt2 = Date.now();
75283
+ const operation2 = foregroundOperation.begin();
74629
75284
  (async () => {
74630
75285
  try {
74631
75286
  startBusy("planning wiki enrich\u2026");
74632
75287
  const plan = await planWikiEnrich(process.cwd());
75288
+ if (foregroundOperation.signal.aborted || foregroundOperation.isDisposed)
75289
+ return;
74633
75290
  stopBusy();
74634
75291
  const maxList = 40;
74635
75292
  const draftLines = plan.drafts.slice(0, maxList).map((p) => ` \xB7 ${p.relativePath}`);
@@ -74656,6 +75313,8 @@ ${formatThemeList(getThemeId())}`);
74656
75313
  acceptedCount: plan.accepted.length,
74657
75314
  total: plan.forceTargets.length
74658
75315
  });
75316
+ if (foregroundOperation.signal.aborted || foregroundOperation.isDisposed)
75317
+ return;
74659
75318
  input2.focus();
74660
75319
  if (choice2 === "cancel") {
74661
75320
  transcript.add(new otui.TextRenderable(r, {
@@ -74692,8 +75351,11 @@ ${formatThemeList(getThemeId())}`);
74692
75351
  force,
74693
75352
  provider: currentSel.provider,
74694
75353
  model: currentSel.model,
75354
+ signal: foregroundOperation.signal,
74695
75355
  concurrency: 2,
74696
75356
  onPage: (info) => {
75357
+ if (foregroundOperation.signal.aborted || foregroundOperation.isDisposed)
75358
+ return;
74697
75359
  setBusyPhase(`enrich ${info.index}/${info.total} [${info.phase}] ${info.path}`);
74698
75360
  setMainAgent("running", `${info.index}/${info.total}`);
74699
75361
  const status = info.phase === "done" ? "done" : info.phase === "failed" ? "failed" : "running";
@@ -74706,6 +75368,8 @@ ${formatThemeList(getThemeId())}`);
74706
75368
  });
74707
75369
  }
74708
75370
  });
75371
+ if (foregroundOperation.signal.aborted || foregroundOperation.isDisposed)
75372
+ return;
74709
75373
  stopBusy();
74710
75374
  setMainAgent(result.failed > 0 && result.enriched === 0 ? "failed" : "done", `${result.enriched}ok/${result.failed}fail`);
74711
75375
  const lines = [
@@ -74737,20 +75401,34 @@ ${formatThemeList(getThemeId())}`);
74737
75401
  saveSession();
74738
75402
  } catch {}
74739
75403
  } catch (cause) {
75404
+ if (foregroundOperation.signal.aborted || foregroundOperation.isDisposed)
75405
+ return;
74740
75406
  stopBusy();
74741
75407
  transcript.add(new otui.TextRenderable(r, {
74742
75408
  id: `we-err${uid++}`,
74743
75409
  content: otui.t`${otui.red(`wiki enrich failed: ${cause instanceof Error ? cause.message : String(cause)}`)}`
74744
75410
  }));
74745
75411
  } finally {
74746
- const secs = ((Date.now() - startedAt2) / 1000).toFixed(1);
74747
- transcript.add(new otui.TextRenderable(r, {
74748
- id: `w${uid++}`,
74749
- content: otui.t`${otui.dim(`worked for ${secs}s`)}`,
74750
- marginTop: 1
74751
- }));
74752
- stopBusy();
74753
- 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
+ });
74754
75432
  }
74755
75433
  })();
74756
75434
  return;
@@ -74758,6 +75436,7 @@ ${formatThemeList(getThemeId())}`);
74758
75436
  fleet.clearMatching((w) => w.id !== MAIN_AGENT_ID && !isSideWorkerId(w.id));
74759
75437
  sessions.clear();
74760
75438
  deps.resetSubagentBudget?.();
75439
+ const operation = foregroundOperation.begin();
74761
75440
  setMainAgent("running", "waiting");
74762
75441
  startBusy("waiting for model");
74763
75442
  const startedAt = Date.now();
@@ -74769,8 +75448,6 @@ ${formatThemeList(getThemeId())}`);
74769
75448
  }
74770
75449
  prevOnSystem?.(text);
74771
75450
  };
74772
- const controller = new AbortController;
74773
- mainTurnAbortController = controller;
74774
75451
  const suggestNextStep = async () => {
74775
75452
  try {
74776
75453
  const lastUser = [...history].reverse().find((m) => m.role === "user")?.content ?? "";
@@ -74793,11 +75470,14 @@ ${tail}`,
74793
75470
  chrome.showSuggestion(result.text.trim().split(/\s+/).slice(0, 20).join(" "));
74794
75471
  } catch {}
74795
75472
  };
74796
- runAgentTurn(io, deps, history, line, {
74797
- signal: controller.signal,
75473
+ const foregroundIo = createForegroundAgentIoFacade(foregroundOperation, operation, io);
75474
+ runAgentTurn(foregroundIo, deps, history, line, {
75475
+ signal: foregroundOperation.signal,
74798
75476
  ...slateSession !== undefined ? { slateSession } : {}
74799
75477
  }).finally(() => {
74800
- mainTurnAbortController = undefined;
75478
+ foregroundOperation.settle(operation);
75479
+ if (foregroundOperation.isDisposed)
75480
+ return;
74801
75481
  const secs = ((Date.now() - startedAt) / 1000).toFixed(1);
74802
75482
  stopBusy();
74803
75483
  refreshWorkspaceSidebar();
@@ -74813,15 +75493,11 @@ ${tail}`,
74813
75493
  sbContext.content = otui.t`${otui.dim(`~${est.toLocaleString()} tokens (est)`)}`;
74814
75494
  }
74815
75495
  focusComposer();
74816
- if (priorityMainQuestion === undefined && mainQueue.length === 0 && !turnFailed) {
75496
+ if (!forceHandoff.hasPending && mainQueue.length === 0 && !turnFailed) {
74817
75497
  suggestNextStep();
74818
75498
  }
74819
- if (priorityMainQuestion !== undefined) {
74820
- const next = priorityMainQuestion;
74821
- priorityMainQuestion = undefined;
74822
- runLine(next.question);
74823
- } else if (mainQueue.length > 0) {
74824
- const next = mainQueue.shift();
75499
+ if (!forceHandoff.isAwaitingSettlement) {
75500
+ const next = forceHandoff.takeNext() ?? mainQueue.shift();
74825
75501
  paintMainQueue();
74826
75502
  if (next !== undefined)
74827
75503
  runLine(next.question);
@@ -77977,7 +78653,7 @@ async function runRemoteTurn(input2) {
77977
78653
  let seq = 0;
77978
78654
  createTurnRecord({ turnId, sessionId, project: input2.project, origin: REMOTE_ORIGIN, startedAt }, input2.dir);
77979
78655
  let bounded2 = false;
77980
- const emit2 = (kind2, extra = {}) => {
78656
+ const emit3 = (kind2, extra = {}) => {
77981
78657
  const event = { schemaVersion: "1.0.0", turnId, seq, kind: kind2, at: clock(), ...extra };
77982
78658
  if (appendTurnEvent(event, input2.dir)) {
77983
78659
  seq += 1;
@@ -78016,7 +78692,7 @@ async function runRemoteTurn(input2) {
78016
78692
  }
78017
78693
  return { turnId, sessionId, result };
78018
78694
  };
78019
- emit2("turn.started");
78695
+ emit3("turn.started");
78020
78696
  if (input2.profile.requiredControls.isolation === "required-fail-closed") {
78021
78697
  const available = input2.containmentAvailable ?? (() => false);
78022
78698
  if (!available()) {
@@ -78080,7 +78756,7 @@ async function runRemoteTurn(input2) {
78080
78756
  if (event.kind === "text_delta" && typeof event.text === "string") {
78081
78757
  const redacted = await redactOut(security, event.text);
78082
78758
  assistantText += redacted;
78083
- emit2("assistant.delta", { text: redacted });
78759
+ emit3("assistant.delta", { text: redacted });
78084
78760
  }
78085
78761
  }
78086
78762
  if (asked) {
@@ -78226,12 +78902,12 @@ function matchRoute(pathname) {
78226
78902
  if (FIXED_ROUTES.has(pathname)) {
78227
78903
  return { route: "fixed", pathname };
78228
78904
  }
78229
- const segments2 = pathname.split("/");
78230
- if (segments2.length === 4 && segments2[1] === "v1" && segments2[2] === "turns") {
78231
- 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] ?? "" };
78232
78908
  }
78233
- if (segments2.length === 5 && segments2[1] === "v1" && segments2[2] === "turns" && segments2[4] === "events") {
78234
- 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] ?? "" };
78235
78911
  }
78236
78912
  return { route: "none" };
78237
78913
  }
@@ -82895,13 +83571,26 @@ async function workspaceCommand(args2) {
82895
83571
  return;
82896
83572
  }
82897
83573
  if (subcommand === "confirm-review") {
82898
- rejectUnknownOptions(args2.slice(3), new Set);
83574
+ rejectUnknownOptions(args2.slice(3), new Set(["--acknowledge-security"]));
82899
83575
  const workspaceId = args2[1];
82900
83576
  const proposalId = args2[2];
82901
83577
  if (!workspaceId || !proposalId)
82902
- throw new Error("Usage: keryx workspace confirm-review <workspace-id> <proposal-id>");
82903
- const { token, expiresAt } = await mintConfirmToken(process.cwd(), workspaceId, proposalId, { securityAcknowledged: true });
82904
- console.log(JSON.stringify({ token, expiresAt }, null, 2));
83578
+ throw new Error("Usage: keryx workspace confirm-review <workspace-id> <proposal-id> [--acknowledge-security]");
83579
+ const security = await readProposalSecurityGate(process.cwd(), workspaceId, proposalId);
83580
+ if (!security)
83581
+ throw new Error(`keryx workspace confirm-review: cannot read proposal ${proposalId} in workspace ${workspaceId} \u2014 refusing to mint a token for a proposal whose security gate is unknown`);
83582
+ const acknowledgeSecurity = booleanFlag(args2, "--acknowledge-security");
83583
+ if (security.gate === "needs-approval" && !acknowledgeSecurity) {
83584
+ throw new Error([
83585
+ `keryx workspace confirm-review: proposal ${proposalId} is gated \`needs-approval\` \u2014 its evidence tripped the security scan.`,
83586
+ ...security.evidenceRefs.length > 0 ? [`Evidence: ${security.evidenceRefs.join(", ")}`] : [],
83587
+ "Read the evidence, then re-run with --acknowledge-security to state that you have.",
83588
+ "Accepting without reading it is the thing this gate exists to prevent."
83589
+ ].join(`
83590
+ `));
83591
+ }
83592
+ const { token, expiresAt } = await mintConfirmToken(process.cwd(), workspaceId, proposalId, { securityAcknowledged: security.gate === "needs-approval" && acknowledgeSecurity });
83593
+ console.log(JSON.stringify({ token, expiresAt, securityGate: security.gate, securityAcknowledged: security.gate === "needs-approval" && acknowledgeSecurity }, null, 2));
82905
83594
  return;
82906
83595
  }
82907
83596
  if (subcommand === "review") {