@kyo-so/cli 0.14.0 → 0.15.0

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/bin/kyoso.js CHANGED
@@ -183896,6 +183896,19 @@ class KyosoRequestError extends Error {
183896
183896
  }
183897
183897
  }
183898
183898
 
183899
+ class KyosoCancellationError extends Error {
183900
+ code = "REQUEST_CANCELLED";
183901
+ constructor(message = "Kyoso review was cancelled.") {
183902
+ super(message);
183903
+ this.name = "KyosoCancellationError";
183904
+ }
183905
+ }
183906
+ function throwIfAborted(signal) {
183907
+ if (!signal?.aborted)
183908
+ return;
183909
+ throw new KyosoCancellationError(typeof signal.reason === "string" ? signal.reason : undefined);
183910
+ }
183911
+
183899
183912
  // src/context/pathPolicy.ts
183900
183913
  function normalizeRelativePath(path) {
183901
183914
  const normalized = normalize(path).replaceAll("\\", "/");
@@ -183951,7 +183964,7 @@ var JUDGE_MAX_OUTPUT_TOKENS = 4096;
183951
183964
  var RAW_OUTPUT_MAX_CHARS = 16384;
183952
183965
  var TRACE_DIR = ".kyoso/traces";
183953
183966
  var KYOSO_CHILD_AGENT = "KYOSO_CHILD_AGENT";
183954
- var KYOSO_VERSION = "0.14.0";
183967
+ var KYOSO_VERSION = "0.15.0";
183955
183968
 
183956
183969
  // src/utils/pathContainment.ts
183957
183970
  import { resolve, sep as sep2 } from "node:path";
@@ -184391,6 +184404,9 @@ var kyosoConfigOverridePaths = [
184391
184404
  "agents.codex.enabled",
184392
184405
  "agents.codex.model",
184393
184406
  "agents.codex.provider",
184407
+ "agents.codex.openRouter.streamIdleTimeoutMs",
184408
+ "agents.codex.openRouter.streamMaxRetries",
184409
+ "agents.codex.openRouter.requestMaxRetries",
184394
184410
  "agents.codex.effort",
184395
184411
  "agents.codex.role",
184396
184412
  "agents.codex.timeoutMs",
@@ -184714,6 +184730,7 @@ ${typeof file2.content === "string" ? file2.content.slice(0, 2000) : ""}`)
184714
184730
  var CODEX_OPENROUTER_PROVIDER = "openrouter";
184715
184731
  var CODEX_DEFAULT_PROVIDER = "default";
184716
184732
  var CODEX_OPENROUTER_MODEL_REQUIRED_ISSUE = "codex_openrouter_model_required";
184733
+ var CODEX_OPENROUTER_POLICY_REQUIRES_PROVIDER_ISSUE = "codex_openrouter_policy_requires_provider";
184717
184734
  var baseAgentSchema = exports_external.object({
184718
184735
  enabled: exports_external.boolean().default(true),
184719
184736
  type: exports_external.literal("acp").default("acp"),
@@ -184736,12 +184753,29 @@ var baseAgentSchema = exports_external.object({
184736
184753
  envWhitelist: exports_external.array(exports_external.string())
184737
184754
  })
184738
184755
  });
184756
+ var codexOpenRouterSchema = exports_external.object({
184757
+ streamIdleTimeoutMs: exports_external.number().int().min(1000).optional(),
184758
+ streamMaxRetries: exports_external.number().int().min(0).max(100).optional(),
184759
+ requestMaxRetries: exports_external.number().int().min(0).max(100).optional()
184760
+ });
184739
184761
  var codexAgentSchema = baseAgentSchema.extend({
184740
184762
  provider: exports_external.enum([CODEX_OPENROUTER_PROVIDER, CODEX_DEFAULT_PROVIDER]).optional(),
184741
184763
  allowProjectProvider: exports_external.array(exports_external.string().min(1).refine(isAbsolute2, {
184742
184764
  message: "must contain only absolute project directory paths for exact matching"
184743
- })).default([])
184765
+ })).default([]),
184766
+ openRouter: codexOpenRouterSchema.default({})
184744
184767
  }).superRefine((agent, context) => {
184768
+ const hasOpenRouterPolicy = Object.values(agent.openRouter).some((value) => value !== undefined);
184769
+ if (hasOpenRouterPolicy && agent.provider !== CODEX_OPENROUTER_PROVIDER) {
184770
+ context.addIssue({
184771
+ code: exports_external.ZodIssueCode.custom,
184772
+ path: ["openRouter"],
184773
+ message: 'agents.codex.openRouter.* requires provider = "openrouter".',
184774
+ params: {
184775
+ kyosoIssue: CODEX_OPENROUTER_POLICY_REQUIRES_PROVIDER_ISSUE
184776
+ }
184777
+ });
184778
+ }
184745
184779
  if (agent.provider !== CODEX_OPENROUTER_PROVIDER || (agent.model?.trim().length ?? 0) > 0) {
184746
184780
  return;
184747
184781
  }
@@ -184867,7 +184901,7 @@ function agentConfigLeafPaths(agent) {
184867
184901
  `agents.${agent}.auth.envWhitelist`
184868
184902
  ];
184869
184903
  if (agent === "codex") {
184870
- paths.push("agents.codex.provider", "agents.codex.allowProjectProvider");
184904
+ paths.push("agents.codex.provider", "agents.codex.allowProjectProvider", "agents.codex.openRouter.streamIdleTimeoutMs", "agents.codex.openRouter.streamMaxRetries", "agents.codex.openRouter.requestMaxRetries");
184871
184905
  }
184872
184906
  return paths;
184873
184907
  }
@@ -186172,7 +186206,7 @@ async function loadProjectConfig(input2) {
186172
186206
  const extension = extname3(requestedPath);
186173
186207
  if (extension === ".toml") {
186174
186208
  const projectTomlConfig = await loadTomlConfigFile(canonicalPath);
186175
- const projectChangesOpenRouterRoute = projectConfigChangesOpenRouterRoute(projectTomlConfig, input2.baseConfig);
186209
+ const projectChangesOpenRouterPolicy = projectConfigChangesOpenRouterPolicy(projectTomlConfig, input2.baseConfig);
186176
186210
  await assertProjectOpenRouterAuthorization({
186177
186211
  projectConfig: projectTomlConfig,
186178
186212
  projectPath: requestedPath,
@@ -186190,7 +186224,7 @@ async function loadProjectConfig(input2) {
186190
186224
  configPath: requestedPath,
186191
186225
  configTrustStatus: "not_found",
186192
186226
  source: { path: requestedPath, layer: "project_toml" },
186193
- warnings: projectChangesOpenRouterRoute ? [openRouterProjectConfigWarning(requestedPath)] : []
186227
+ warnings: projectChangesOpenRouterPolicy ? [openRouterProjectConfigWarning(requestedPath)] : []
186194
186228
  };
186195
186229
  }
186196
186230
  if (extension === ".ts") {
@@ -186207,30 +186241,35 @@ function projectConfigSelectsDefaultProvider(config2) {
186207
186241
  function projectConfigSuppliesCodexModel(config2) {
186208
186242
  return flattenLeaves(config2).some((leaf) => leaf.path.join(".") === "agents.codex.model");
186209
186243
  }
186210
- function projectConfigChangesOpenRouterRoute(projectConfig, baseConfig) {
186211
- return projectConfigSelectsOpenRouter(projectConfig) || configSelectsOpenRouter(baseConfig) && projectConfigSuppliesCodexModel(projectConfig) && !projectConfigSelectsDefaultProvider(projectConfig);
186244
+ function projectConfigSuppliesOpenRouterPolicy(config2) {
186245
+ return flattenLeaves(config2).some((leaf) => leaf.path.join(".").startsWith("agents.codex.openRouter."));
186246
+ }
186247
+ function projectConfigChangesOpenRouterPolicy(projectConfig, baseConfig) {
186248
+ return projectConfigSelectsOpenRouter(projectConfig) || configSelectsOpenRouter(baseConfig) && !projectConfigSelectsDefaultProvider(projectConfig) && (projectConfigSuppliesCodexModel(projectConfig) || projectConfigSuppliesOpenRouterPolicy(projectConfig));
186212
186249
  }
186213
186250
  function configSelectsOpenRouter(config2) {
186214
186251
  return flattenLeaves(config2).some((leaf) => leaf.path.join(".") === "agents.codex.provider" && leaf.value === CODEX_OPENROUTER_PROVIDER);
186215
186252
  }
186216
186253
  function applyProjectCodexProviderReset(baseConfig, projectConfig, mergedConfig) {
186217
- if (!projectConfigSelectsDefaultProvider(projectConfig) || projectConfigSuppliesCodexModel(projectConfig) || !configSelectsOpenRouter(baseConfig) || !isRecord3(mergedConfig) || !isRecord3(mergedConfig.agents) || !isRecord3(mergedConfig.agents.codex)) {
186254
+ if (!projectConfigSelectsDefaultProvider(projectConfig) || !configSelectsOpenRouter(baseConfig) || !isRecord3(mergedConfig) || !isRecord3(mergedConfig.agents) || !isRecord3(mergedConfig.agents.codex)) {
186218
186255
  return mergedConfig;
186219
186256
  }
186220
- const codexWithoutInheritedModel = Object.fromEntries(Object.entries(mergedConfig.agents.codex).filter(([key]) => key !== "model"));
186257
+ const shouldClearInheritedModel = !projectConfigSuppliesCodexModel(projectConfig);
186258
+ const shouldClearInheritedOpenRouterPolicy = !projectConfigSuppliesOpenRouterPolicy(projectConfig);
186259
+ const codexWithoutInheritedOpenRouterConfig = Object.fromEntries(Object.entries(mergedConfig.agents.codex).filter(([key]) => (!shouldClearInheritedModel || key !== "model") && (!shouldClearInheritedOpenRouterPolicy || key !== "openRouter")));
186221
186260
  return {
186222
186261
  ...mergedConfig,
186223
186262
  agents: {
186224
186263
  ...mergedConfig.agents,
186225
- codex: codexWithoutInheritedModel
186264
+ codex: codexWithoutInheritedOpenRouterConfig
186226
186265
  }
186227
186266
  };
186228
186267
  }
186229
186268
  function openRouterProjectConfigWarning(configPath) {
186230
- return `Project config ${sanitizeWarningText(configPath)} changes Codex OpenRouter routing under user-global authorization; it can route Codex review content through OpenRouter.`;
186269
+ return `Project config ${sanitizeWarningText(configPath)} changes Codex OpenRouter routing or transport retry policy under user-global authorization; it can route Codex review content through OpenRouter.`;
186231
186270
  }
186232
186271
  async function assertProjectOpenRouterAuthorization(input2) {
186233
- if (!projectConfigChangesOpenRouterRoute(input2.projectConfig, input2.baseConfig)) {
186272
+ if (!projectConfigChangesOpenRouterPolicy(input2.projectConfig, input2.baseConfig)) {
186234
186273
  return;
186235
186274
  }
186236
186275
  if (await projectProviderIsAuthorized(input2.baseConfig, input2.projectDirectory)) {
@@ -186295,7 +186334,7 @@ async function loadProjectTsConfig(input2) {
186295
186334
  if (trustDecision.execute) {
186296
186335
  const userConfig = await loadUserConfig(canonicalPath, source);
186297
186336
  validateExplicitReviewBudgetThresholds(userConfig, input2.baseConfig);
186298
- const projectChangesOpenRouterRoute = projectConfigChangesOpenRouterRoute(userConfig, input2.baseConfig);
186337
+ const projectChangesOpenRouterPolicy = projectConfigChangesOpenRouterPolicy(userConfig, input2.baseConfig);
186299
186338
  await assertProjectOpenRouterAuthorization({
186300
186339
  projectConfig: userConfig,
186301
186340
  projectPath: requestedPath,
@@ -186308,7 +186347,7 @@ async function loadProjectTsConfig(input2) {
186308
186347
  await trustConfig(trustStorePath, canonicalPath, configHash);
186309
186348
  }
186310
186349
  const projectMergedConfig = deepMerge2(input2.baseConfig, userConfig);
186311
- if (projectChangesOpenRouterRoute) {
186350
+ if (projectChangesOpenRouterPolicy) {
186312
186351
  warnings.push(openRouterProjectConfigWarning(requestedPath));
186313
186352
  }
186314
186353
  return {
@@ -186627,6 +186666,27 @@ function isRecord6(value) {
186627
186666
  return typeof value === "object" && value !== null && !Array.isArray(value);
186628
186667
  }
186629
186668
 
186669
+ // src/judge/signals.ts
186670
+ function linkSignals(timeoutMs, external2) {
186671
+ const controller = new AbortController;
186672
+ const timeout = setTimeout(() => controller.abort(new Error("judge timeout")), timeoutMs);
186673
+ timeout.unref?.();
186674
+ const onExternalAbort = () => controller.abort(external2?.reason);
186675
+ if (external2) {
186676
+ if (external2.aborted)
186677
+ onExternalAbort();
186678
+ else
186679
+ external2.addEventListener("abort", onExternalAbort, { once: true });
186680
+ }
186681
+ return {
186682
+ signal: controller.signal,
186683
+ cleanup() {
186684
+ clearTimeout(timeout);
186685
+ external2?.removeEventListener("abort", onExternalAbort);
186686
+ }
186687
+ };
186688
+ }
186689
+
186630
186690
  // src/judge/anthropic.ts
186631
186691
  var DEFAULT_ANTHROPIC_JUDGE_MODEL = "claude-haiku-4-5";
186632
186692
  function resolveAnthropicJudgeModel(env) {
@@ -186637,39 +186697,45 @@ async function runAnthropicJudge(input2, timeoutMs) {
186637
186697
  if (!apiKey)
186638
186698
  throw new Error("ANTHROPIC_API_KEY is not configured.");
186639
186699
  const requestedModel = resolveAnthropicJudgeModel(input2.env);
186640
- const response = await fetchWithTimeout(`${input2.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com"}/v1/messages`, {
186641
- method: "POST",
186642
- headers: {
186643
- "x-api-key": apiKey,
186644
- "anthropic-version": "2023-06-01",
186645
- "content-type": "application/json"
186646
- },
186647
- body: JSON.stringify({
186648
- model: requestedModel,
186649
- max_tokens: JUDGE_MAX_OUTPUT_TOKENS,
186650
- temperature: 0,
186651
- messages: [
186652
- {
186653
- role: "user",
186654
- content: buildJudgePrompt(input2.tool, input2.result, input2.summaryText, input2.agentFindings)
186655
- }
186656
- ]
186657
- })
186658
- }, timeoutMs);
186659
- if (!response.ok)
186660
- throw new Error(`Anthropic judge failed with HTTP ${response.status}.`);
186661
- const payload = await response.json();
186662
- const content = payload.content?.find((item) => item.type === "text" && item.text)?.text;
186663
- if (!content)
186664
- throw new Error("Anthropic judge response did not include text content.");
186665
- const usage = normalizeUsage(payload.usage);
186666
- const reportedModel = typeof payload.model === "string" ? payload.model : undefined;
186667
- return {
186668
- output: parseJudgeOutput(content, input2.summaryText),
186669
- requestedModel,
186670
- ...reportedModel ? { reportedModel } : {},
186671
- ...usage ? { usage } : {}
186672
- };
186700
+ const { signal, cleanup } = linkSignals(timeoutMs, input2.signal);
186701
+ try {
186702
+ const response = await fetch(`${input2.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com"}/v1/messages`, {
186703
+ method: "POST",
186704
+ headers: {
186705
+ "x-api-key": apiKey,
186706
+ "anthropic-version": "2023-06-01",
186707
+ "content-type": "application/json"
186708
+ },
186709
+ body: JSON.stringify({
186710
+ model: requestedModel,
186711
+ max_tokens: JUDGE_MAX_OUTPUT_TOKENS,
186712
+ temperature: 0,
186713
+ messages: [
186714
+ {
186715
+ role: "user",
186716
+ content: buildJudgePrompt(input2.tool, input2.result, input2.summaryText, input2.agentFindings)
186717
+ }
186718
+ ]
186719
+ }),
186720
+ signal
186721
+ });
186722
+ if (!response.ok)
186723
+ throw new Error(`Anthropic judge failed with HTTP ${response.status}.`);
186724
+ const payload = await response.json();
186725
+ const content = payload.content?.find((item) => item.type === "text" && item.text)?.text;
186726
+ if (!content)
186727
+ throw new Error("Anthropic judge response did not include text content.");
186728
+ const usage = normalizeUsage(payload.usage);
186729
+ const reportedModel = typeof payload.model === "string" ? payload.model : undefined;
186730
+ return {
186731
+ output: parseJudgeOutput(content, input2.summaryText),
186732
+ requestedModel,
186733
+ ...reportedModel ? { reportedModel } : {},
186734
+ ...usage ? { usage } : {}
186735
+ };
186736
+ } finally {
186737
+ cleanup();
186738
+ }
186673
186739
  }
186674
186740
  function normalizeUsage(usage) {
186675
186741
  if (!usage)
@@ -186681,16 +186747,6 @@ function normalizeUsage(usage) {
186681
186747
  cachedWriteTokens: usage.cache_creation_input_tokens
186682
186748
  });
186683
186749
  }
186684
- async function fetchWithTimeout(url2, init, timeoutMs) {
186685
- const controller = new AbortController;
186686
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
186687
- timeout.unref?.();
186688
- try {
186689
- return await fetch(url2, { ...init, signal: controller.signal });
186690
- } finally {
186691
- clearTimeout(timeout);
186692
- }
186693
- }
186694
186750
 
186695
186751
  // src/judge/deterministicFallback.ts
186696
186752
  function runDeterministicJudge(result, summaryText) {
@@ -186713,39 +186769,45 @@ async function runOpenAiJudge(input2, timeoutMs) {
186713
186769
  if (!apiKey)
186714
186770
  throw new Error("OPENAI_API_KEY is not configured.");
186715
186771
  const requestedModel = resolveOpenAiJudgeModel(input2.env);
186716
- const response = await fetchWithTimeout2(`${input2.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"}/chat/completions`, {
186717
- method: "POST",
186718
- headers: {
186719
- authorization: `Bearer ${apiKey}`,
186720
- "content-type": "application/json"
186721
- },
186722
- body: JSON.stringify({
186723
- model: requestedModel,
186724
- response_format: { type: "json_object" },
186725
- messages: [
186726
- {
186727
- role: "user",
186728
- content: buildJudgePrompt(input2.tool, input2.result, input2.summaryText, input2.agentFindings)
186729
- }
186730
- ],
186731
- max_completion_tokens: JUDGE_MAX_OUTPUT_TOKENS,
186732
- temperature: 0
186733
- })
186734
- }, timeoutMs);
186735
- if (!response.ok)
186736
- throw new Error(`OpenAI judge failed with HTTP ${response.status}.`);
186737
- const payload = await response.json();
186738
- const content = payload.choices?.[0]?.message?.content;
186739
- if (!content)
186740
- throw new Error("OpenAI judge response did not include content.");
186741
- const usage = normalizeUsage2(payload.usage);
186742
- const reportedModel = typeof payload.model === "string" ? payload.model : undefined;
186743
- return {
186744
- output: parseJudgeOutput(content, input2.summaryText),
186745
- requestedModel,
186746
- ...reportedModel ? { reportedModel } : {},
186747
- ...usage ? { usage } : {}
186748
- };
186772
+ const { signal, cleanup } = linkSignals(timeoutMs, input2.signal);
186773
+ try {
186774
+ const response = await fetch(`${input2.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"}/chat/completions`, {
186775
+ method: "POST",
186776
+ headers: {
186777
+ authorization: `Bearer ${apiKey}`,
186778
+ "content-type": "application/json"
186779
+ },
186780
+ body: JSON.stringify({
186781
+ model: requestedModel,
186782
+ response_format: { type: "json_object" },
186783
+ messages: [
186784
+ {
186785
+ role: "user",
186786
+ content: buildJudgePrompt(input2.tool, input2.result, input2.summaryText, input2.agentFindings)
186787
+ }
186788
+ ],
186789
+ max_completion_tokens: JUDGE_MAX_OUTPUT_TOKENS,
186790
+ temperature: 0
186791
+ }),
186792
+ signal
186793
+ });
186794
+ if (!response.ok)
186795
+ throw new Error(`OpenAI judge failed with HTTP ${response.status}.`);
186796
+ const payload = await response.json();
186797
+ const content = payload.choices?.[0]?.message?.content;
186798
+ if (!content)
186799
+ throw new Error("OpenAI judge response did not include content.");
186800
+ const usage = normalizeUsage2(payload.usage);
186801
+ const reportedModel = typeof payload.model === "string" ? payload.model : undefined;
186802
+ return {
186803
+ output: parseJudgeOutput(content, input2.summaryText),
186804
+ requestedModel,
186805
+ ...reportedModel ? { reportedModel } : {},
186806
+ ...usage ? { usage } : {}
186807
+ };
186808
+ } finally {
186809
+ cleanup();
186810
+ }
186749
186811
  }
186750
186812
  function normalizeUsage2(usage) {
186751
186813
  if (!usage)
@@ -186758,16 +186820,6 @@ function normalizeUsage2(usage) {
186758
186820
  thoughtTokens: usage.completion_tokens_details?.reasoning_tokens
186759
186821
  });
186760
186822
  }
186761
- async function fetchWithTimeout2(url2, init, timeoutMs) {
186762
- const controller = new AbortController;
186763
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
186764
- timeout.unref?.();
186765
- try {
186766
- return await fetch(url2, { ...init, signal: controller.signal });
186767
- } finally {
186768
- clearTimeout(timeout);
186769
- }
186770
- }
186771
186823
 
186772
186824
  // src/judge/provider.ts
186773
186825
  function resolveJudgeProvider(provider, env) {
@@ -186821,6 +186873,9 @@ async function runJudge(input2) {
186821
186873
  ...output2.usage ? { usage: output2.usage } : {}
186822
186874
  };
186823
186875
  } catch (error51) {
186876
+ if (input2.signal?.aborted) {
186877
+ throw new KyosoCancellationError("Kyoso review was cancelled during judge execution.");
186878
+ }
186824
186879
  return {
186825
186880
  provider,
186826
186881
  status: "failed_fallback",
@@ -186867,13 +186922,18 @@ var OPENROUTER_EXCLUDED_CREDENTIAL_ENV_KEYS = [
186867
186922
  var OPENROUTER_API_KEY_ENV = "OPENROUTER_API_KEY";
186868
186923
  var KYOSO_OPENROUTER_PROVIDER_ID = "kyoso-openrouter";
186869
186924
  var OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
186870
- var OPENROUTER_PROVIDER_PRESET = {
186871
- name: "OpenRouter",
186872
- base_url: OPENROUTER_BASE_URL,
186873
- env_key: OPENROUTER_API_KEY_ENV,
186874
- wire_api: "responses",
186875
- requires_openai_auth: false
186876
- };
186925
+ function buildOpenRouterProviderPreset(options, baseUrl = OPENROUTER_BASE_URL) {
186926
+ return {
186927
+ name: "OpenRouter",
186928
+ base_url: baseUrl,
186929
+ env_key: OPENROUTER_API_KEY_ENV,
186930
+ wire_api: "responses",
186931
+ requires_openai_auth: false,
186932
+ ...options.streamIdleTimeoutMs === undefined ? {} : { stream_idle_timeout_ms: options.streamIdleTimeoutMs },
186933
+ ...options.streamMaxRetries === undefined ? {} : { stream_max_retries: options.streamMaxRetries },
186934
+ ...options.requestMaxRetries === undefined ? {} : { request_max_retries: options.requestMaxRetries }
186935
+ };
186936
+ }
186877
186937
 
186878
186938
  class ChildEnvPreflightError extends Error {
186879
186939
  code;
@@ -186932,7 +186992,7 @@ function buildChildEnvironment(parentEnv, whitelist, explicit, options = {}) {
186932
186992
  }
186933
186993
  if (openRouterSelected) {
186934
186994
  discardOpenRouterExcludedCredentials(env);
186935
- applyOpenRouterConfig(env, parentEnv, options.model, onCredentialPlaceholderDiscarded, options.onOpenRouterProvidersDiscarded ?? warnOpenRouterProvidersDiscarded);
186995
+ applyOpenRouterConfig(env, parentEnv, options.model, options.openRouter, options.openRouterBaseUrlForTest, onCredentialPlaceholderDiscarded, options.onOpenRouterProvidersDiscarded ?? warnOpenRouterProvidersDiscarded);
186936
186996
  } else {
186937
186997
  applyModelConfig(env, options.agent, options.model);
186938
186998
  }
@@ -186989,7 +187049,7 @@ function applyModelConfig(env, agent, model) {
186989
187049
  env.CODEX_CONFIG = JSON.stringify({ model });
186990
187050
  }
186991
187051
  }
186992
- function applyOpenRouterConfig(env, parentEnv, model, onCredentialPlaceholderDiscarded, onOpenRouterProvidersDiscarded) {
187052
+ function applyOpenRouterConfig(env, parentEnv, model, openRouter = {}, baseUrl = OPENROUTER_BASE_URL, onCredentialPlaceholderDiscarded, onOpenRouterProvidersDiscarded) {
186993
187053
  const configuredModel = model?.trim();
186994
187054
  if (!configuredModel) {
186995
187055
  throw new ChildEnvPreflightError("AGENT_CONFIG_INVALID", 'agents.codex.provider="openrouter" requires a non-empty agents.codex.model.');
@@ -187017,7 +187077,7 @@ function applyOpenRouterConfig(env, parentEnv, model, onCredentialPlaceholderDis
187017
187077
  model: configuredModel,
187018
187078
  model_provider: KYOSO_OPENROUTER_PROVIDER_ID,
187019
187079
  model_providers: {
187020
- [KYOSO_OPENROUTER_PROVIDER_ID]: OPENROUTER_PROVIDER_PRESET
187080
+ [KYOSO_OPENROUTER_PROVIDER_ID]: buildOpenRouterProviderPreset(openRouter, baseUrl)
187021
187081
  }
187022
187082
  });
187023
187083
  }
@@ -187096,9 +187156,9 @@ var PLUGIN_RUNTIME_COMPATIBILITY_SCHEMA_VERSION = 2;
187096
187156
  var MINIMUM_SUPPORTED_CODEX_VERSION = "0.144.0-alpha.4";
187097
187157
  var PLUGIN_RUNTIME_EXPECTED_CONTRACT = {
187098
187158
  distribution: {
187099
- pluginVersion: "0.7.2",
187159
+ pluginVersion: "0.7.3",
187100
187160
  mcpCommand: "npx",
187101
- mcpPackagePin: "@kyo-so/cli@0.13.1",
187161
+ mcpPackagePin: "@kyo-so/cli@0.14.0",
187102
187162
  mcpExecutable: "kyoso"
187103
187163
  },
187104
187164
  marketplace: {
@@ -190398,6 +190458,18 @@ async function runDoctor(options) {
190398
190458
  if (agent === "codex" && loaded.config.agents.codex.provider === CODEX_OPENROUTER_PROVIDER) {
190399
190459
  lines.push(` provider: ${CODEX_OPENROUTER_PROVIDER}`);
190400
190460
  lines.push(` model: ${sanitizeTextForDisplay(loaded.config.agents.codex.model ?? "")}`);
190461
+ const openRouter = loaded.config.agents.codex.openRouter;
190462
+ lines.push(" reliability:");
190463
+ lines.push(openRouter.streamIdleTimeoutMs === undefined ? " stream idle timeout: inherited from Codex runtime" : ` stream idle timeout: ${openRouter.streamIdleTimeoutMs} ms (Kyoso config)`);
190464
+ lines.push(openRouter.streamMaxRetries === undefined ? " stream retries: inherited from Codex runtime" : ` stream retries: ${openRouter.streamMaxRetries}`);
190465
+ lines.push(openRouter.requestMaxRetries === undefined ? " request retries: inherited from Codex runtime" : ` request retries: ${openRouter.requestMaxRetries}`);
190466
+ if (openRouter.streamIdleTimeoutMs !== undefined && openRouter.streamMaxRetries !== undefined) {
190467
+ const idleOnlyWindow = openRouter.streamIdleTimeoutMs * (openRouter.streamMaxRetries + 1);
190468
+ lines.push(` maximum idle-only stream window: approximately ${idleOnlyWindow} ms plus backoff`);
190469
+ if (idleOnlyWindow >= config2.timeoutMs) {
190470
+ lines.push(` warning: configured idle-only retry window can consume the entire Codex agent timeout (timeoutMs=${config2.timeoutMs}).`);
190471
+ }
190472
+ }
190401
190473
  const configuredKey = loaded.config.agents.codex.env[OPENROUTER_API_KEY_ENV];
190402
190474
  if (hasUsableEnvValue(loaded.config.agents.codex.env, OPENROUTER_API_KEY_ENV)) {
190403
190475
  lines.push(` auth: detected ${OPENROUTER_API_KEY_ENV} from agents.codex.env`);
@@ -194888,6 +194960,117 @@ function limitAcpNdJsonLineBytes(input2, maxLineBytes = MAX_ACP_NDJSON_LINE_BYTE
194888
194960
  }));
194889
194961
  }
194890
194962
 
194963
+ // src/acp/AgentOutputAccumulator.ts
194964
+ class AgentOutputAccumulator {
194965
+ segments = new Map;
194966
+ messageChunks = [];
194967
+ retryEpoch = 0;
194968
+ observedStreamRetries = 0;
194969
+ discardedRetryMessageBytes = 0;
194970
+ firstOutputAt;
194971
+ lastAcpUpdateAt;
194972
+ nextChunkSequence = 0;
194973
+ addMessageChunk(text, meta3) {
194974
+ this.noteOutput();
194975
+ const id = meta3.messageId ?? `epoch-${this.retryEpoch}`;
194976
+ const key = `${this.retryEpoch}\x00${id}`;
194977
+ const phase = meta3.phase ?? "unknown";
194978
+ let segment = this.segments.get(key);
194979
+ if (!segment) {
194980
+ segment = {
194981
+ id,
194982
+ phase,
194983
+ retryEpoch: this.retryEpoch,
194984
+ text: "",
194985
+ abandoned: false,
194986
+ lastChunkSequence: 0
194987
+ };
194988
+ this.segments.set(key, segment);
194989
+ } else if (segment.phase === "unknown" && phase !== "unknown") {
194990
+ segment.phase = phase;
194991
+ }
194992
+ const sequence = this.nextChunkSequence;
194993
+ this.nextChunkSequence += 1;
194994
+ segment.text += text;
194995
+ segment.lastChunkSequence = sequence;
194996
+ this.messageChunks.push({ segment, text, sequence });
194997
+ }
194998
+ addThoughtChunk(_text) {
194999
+ this.noteOutput();
195000
+ }
195001
+ noteUpdate() {
195002
+ this.lastAcpUpdateAt = new Date().toISOString();
195003
+ }
195004
+ markRetryBoundary() {
195005
+ let discardedMessageBytes = 0;
195006
+ for (const segment of this.segments.values()) {
195007
+ if (segment.retryEpoch !== this.retryEpoch || segment.abandoned)
195008
+ continue;
195009
+ segment.abandoned = true;
195010
+ discardedMessageBytes += Buffer.byteLength(segment.text, "utf8");
195011
+ }
195012
+ this.observedStreamRetries += 1;
195013
+ this.discardedRetryMessageBytes += discardedMessageBytes;
195014
+ this.retryEpoch += 1;
195015
+ return { discardedMessageBytes };
195016
+ }
195017
+ finalRawText() {
195018
+ if (this.observedStreamRetries === 0) {
195019
+ return this.messageChunks.map((chunk) => chunk.text).join("");
195020
+ }
195021
+ const finalAnswer = [...this.segments.values()].filter((segment) => !segment.abandoned && segment.phase === "final_answer").sort((left, right) => left.lastChunkSequence - right.lastChunkSequence).at(-1);
195022
+ if (finalAnswer)
195023
+ return finalAnswer.text;
195024
+ return this.messageChunks.filter((chunk) => !chunk.segment.abandoned && chunk.segment.retryEpoch === this.retryEpoch && chunk.segment.phase === "unknown").sort((left, right) => left.sequence - right.sequence).map((chunk) => chunk.text).join("");
195025
+ }
195026
+ metrics() {
195027
+ return {
195028
+ observedStreamRetries: this.observedStreamRetries,
195029
+ discardedRetryMessageBytes: this.discardedRetryMessageBytes,
195030
+ ...this.firstOutputAt === undefined ? {} : { firstOutputAt: this.firstOutputAt },
195031
+ ...this.lastAcpUpdateAt === undefined ? {} : { lastAcpUpdateAt: this.lastAcpUpdateAt }
195032
+ };
195033
+ }
195034
+ noteOutput() {
195035
+ const timestamp = new Date().toISOString();
195036
+ this.firstOutputAt ??= timestamp;
195037
+ this.lastAcpUpdateAt = timestamp;
195038
+ }
195039
+ }
195040
+
195041
+ // src/acp/codexRetryUpdate.ts
195042
+ var RETRY_ATTEMPT_PATTERN = /Reconnecting\.\.\.\s*(\d+)\/(\d+)/;
195043
+ var DISPLAY_MESSAGE_FIELDS = ["title", "text", "message", "description"];
195044
+ function parseCodexRetryUpdate(update) {
195045
+ if (!isRecord12(update) || update.sessionUpdate !== "session_info_update") {
195046
+ return;
195047
+ }
195048
+ const meta3 = isRecord12(update._meta) ? update._meta : undefined;
195049
+ const codex = meta3 && isRecord12(meta3.codex) ? meta3.codex : undefined;
195050
+ const error51 = codex && isRecord12(codex.error) ? codex.error : undefined;
195051
+ if (error51?.willRetry !== true)
195052
+ return;
195053
+ const rawMessage = typeof error51.message === "string" ? error51.message : findDisplayMessage(update) ?? "model stream retry";
195054
+ const message = sanitizeTextForDisplay(rawMessage) || "model stream retry";
195055
+ const attemptMatch = RETRY_ATTEMPT_PATTERN.exec(message);
195056
+ return {
195057
+ message,
195058
+ ...attemptMatch?.[1] === undefined ? {} : { attempt: Number.parseInt(attemptMatch[1], 10) },
195059
+ ...attemptMatch?.[2] === undefined ? {} : { maxRetries: Number.parseInt(attemptMatch[2], 10) }
195060
+ };
195061
+ }
195062
+ function findDisplayMessage(update) {
195063
+ for (const field of DISPLAY_MESSAGE_FIELDS) {
195064
+ const value = update[field];
195065
+ if (typeof value === "string")
195066
+ return value;
195067
+ }
195068
+ return;
195069
+ }
195070
+ function isRecord12(value) {
195071
+ return value !== null && typeof value === "object" && !Array.isArray(value);
195072
+ }
195073
+
194891
195074
  // src/core/findingAdmission.ts
194892
195075
  import { createHash as createHash4 } from "node:crypto";
194893
195076
  var SAFETY_CATEGORIES = new Set([
@@ -195369,7 +195552,7 @@ function isSeverity(value) {
195369
195552
  return typeof value === "string" && severities.includes(value);
195370
195553
  }
195371
195554
  function normalizeCisaSecureByDesign(value) {
195372
- if (!isRecord12(value))
195555
+ if (!isRecord13(value))
195373
195556
  return;
195374
195557
  const normalized = {};
195375
195558
  const customerSecurityOutcomes = normalizeGateStatus(value.customerSecurityOutcomes);
@@ -195410,7 +195593,7 @@ function isEvidenceQuality(value) {
195410
195593
  return typeof value === "string" && evidenceQualities.includes(value);
195411
195594
  }
195412
195595
  function isStrictAgentOpinion(value) {
195413
- if (!isRecord12(value) || !hasOnlyKeys(value, STRICT_ROOT_KEYS))
195596
+ if (!isRecord13(value) || !hasOnlyKeys(value, STRICT_ROOT_KEYS))
195414
195597
  return false;
195415
195598
  if (typeof value.summary !== "string")
195416
195599
  return false;
@@ -195420,7 +195603,7 @@ function isStrictAgentOpinion(value) {
195420
195603
  return value.cisaSecureByDesign === undefined || isStrictCisaSecureByDesign(value.cisaSecureByDesign);
195421
195604
  }
195422
195605
  function isStrictFinding(value) {
195423
- if (!isRecord12(value) || !hasOnlyKeys(value, STRICT_FINDING_KEYS)) {
195606
+ if (!isRecord13(value) || !hasOnlyKeys(value, STRICT_FINDING_KEYS)) {
195424
195607
  return false;
195425
195608
  }
195426
195609
  if (!isSeverity(value.severity) || !isCategory(value.category) || !isNonEmptyString(value.title) || !isNonEmptyString(value.evidence) || !isNonEmptyString(value.recommendation) || !isConfidence(value.confidence)) {
@@ -195432,11 +195615,11 @@ function isStrictFinding(value) {
195432
195615
  return true;
195433
195616
  }
195434
195617
  function isStrictFindingFiles(value) {
195435
- return Array.isArray(value) && value.every((item) => isRecord12(item) && hasOnlyKeys(item, STRICT_FILE_KEYS) && isNonEmptyString(item.path) && isOptionalLineNumber(item.lineStart) && isOptionalLineNumber(item.lineEnd));
195618
+ return Array.isArray(value) && value.every((item) => isRecord13(item) && hasOnlyKeys(item, STRICT_FILE_KEYS) && isNonEmptyString(item.path) && isOptionalLineNumber(item.lineStart) && isOptionalLineNumber(item.lineEnd));
195436
195619
  }
195437
195620
  function isStrictEvidenceRefs(value) {
195438
195621
  return Array.isArray(value) && value.length <= MAX_EVIDENCE_REFS2 && value.every((item) => {
195439
- if (!isRecord12(item) || !hasOnlyKeys(item, STRICT_EVIDENCE_REF_KEYS)) {
195622
+ if (!isRecord13(item) || !hasOnlyKeys(item, STRICT_EVIDENCE_REF_KEYS)) {
195440
195623
  return false;
195441
195624
  }
195442
195625
  if (item.kind !== "file" && item.kind !== "diff_hunk" && item.kind !== "plan_clause") {
@@ -195452,7 +195635,7 @@ function isStrictEvidenceRefs(value) {
195452
195635
  });
195453
195636
  }
195454
195637
  function isStrictCisaSecureByDesign(value) {
195455
- if (!isRecord12(value) || !hasOnlyKeys(value, STRICT_CISA_KEYS))
195638
+ if (!isRecord13(value) || !hasOnlyKeys(value, STRICT_CISA_KEYS))
195456
195639
  return false;
195457
195640
  for (const key of [
195458
195641
  "customerSecurityOutcomes",
@@ -195491,7 +195674,7 @@ function normalizeFindingFiles(value) {
195491
195674
  if (!Array.isArray(value))
195492
195675
  return;
195493
195676
  const files = value.flatMap((item) => {
195494
- if (!isRecord12(item) || typeof item.path !== "string" || item.path.trim().length === 0) {
195677
+ if (!isRecord13(item) || typeof item.path !== "string" || item.path.trim().length === 0) {
195495
195678
  return [];
195496
195679
  }
195497
195680
  const file2 = {
@@ -195511,7 +195694,7 @@ function normalizeEvidenceRefs2(value) {
195511
195694
  if (!Array.isArray(value))
195512
195695
  return;
195513
195696
  const references = value.slice(0, MAX_EVIDENCE_REFS2).flatMap((item) => {
195514
- if (!isRecord12(item) || item.kind !== "file" && item.kind !== "diff_hunk" && item.kind !== "plan_clause") {
195697
+ if (!isRecord13(item) || item.kind !== "file" && item.kind !== "diff_hunk" && item.kind !== "plan_clause") {
195515
195698
  return [];
195516
195699
  }
195517
195700
  const reference = { kind: item.kind };
@@ -195534,18 +195717,23 @@ function normalizeEvidenceRefs2(value) {
195534
195717
  function normalizeLineNumber(value) {
195535
195718
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= MAX_EVIDENCE_LINE2 ? value : undefined;
195536
195719
  }
195537
- function isRecord12(value) {
195720
+ function isRecord13(value) {
195538
195721
  return typeof value === "object" && value !== null && !Array.isArray(value);
195539
195722
  }
195540
195723
 
195541
195724
  // src/acp/AcpAgentProcess.ts
195725
+ var DEFAULT_PROGRESS_HEARTBEAT_MS = 15000;
195726
+ var ACTIVITY_PROGRESS_THROTTLE_MS = 3000;
195727
+
195542
195728
  class SubprocessAcpAgentManager extends BaseAcpAgentManager {
195543
195729
  config;
195544
195730
  parentEnv;
195545
- constructor(config2, parentEnv = process.env) {
195731
+ internalOptions;
195732
+ constructor(config2, parentEnv = process.env, internalOptions = {}) {
195546
195733
  super();
195547
195734
  this.config = config2;
195548
195735
  this.parentEnv = parentEnv;
195736
+ this.internalOptions = internalOptions;
195549
195737
  }
195550
195738
  async runAgent(input2) {
195551
195739
  const agentConfig = this.config.agents[input2.agent];
@@ -195566,7 +195754,9 @@ class SubprocessAcpAgentManager extends BaseAcpAgentManager {
195566
195754
  agent: input2.agent,
195567
195755
  model: agentConfig.model,
195568
195756
  provider,
195569
- preferApiKey: agentConfig.auth.preferApiKey
195757
+ preferApiKey: agentConfig.auth.preferApiKey,
195758
+ openRouter: input2.agent === "codex" ? this.config.agents.codex.openRouter : undefined,
195759
+ openRouterBaseUrlForTest: input2.agent === "codex" ? this.internalOptions.openRouterBaseUrlForTest : undefined
195570
195760
  });
195571
195761
  } catch (error51) {
195572
195762
  return {
@@ -195581,6 +195771,8 @@ class SubprocessAcpAgentManager extends BaseAcpAgentManager {
195581
195771
  try {
195582
195772
  return await runSubprocessAgent(input2.agent, agentConfig, input2, launchContext.env, launchContext.executionIdentity);
195583
195773
  } catch (error51) {
195774
+ if (error51 instanceof KyosoCancellationError)
195775
+ throw error51;
195584
195776
  return {
195585
195777
  agent: input2.agent,
195586
195778
  role: input2.role,
@@ -195593,6 +195785,7 @@ class SubprocessAcpAgentManager extends BaseAcpAgentManager {
195593
195785
  }
195594
195786
  }
195595
195787
  async function runSubprocessAgent(agent, agentConfig, input2, env, launchExecutionIdentity) {
195788
+ throwIfAborted(input2.signal);
195596
195789
  const startedAt = new Date().toISOString();
195597
195790
  const effectiveTimeoutMs = resolveEffectiveTimeoutMs(input2);
195598
195791
  if (effectiveTimeoutMs <= 0) {
@@ -195608,12 +195801,20 @@ async function runSubprocessAgent(agent, agentConfig, input2, env, launchExecuti
195608
195801
  }
195609
195802
  };
195610
195803
  }
195611
- return new Promise((resolveResult) => {
195804
+ return new Promise((resolveResult, rejectResult) => {
195805
+ const abortController = new AbortController;
195806
+ let cancelSession;
195807
+ let timeout;
195612
195808
  const child = spawn(agentConfig.command, agentConfig.args, {
195613
195809
  cwd: input2.workspaceDir,
195614
195810
  env,
195615
195811
  stdio: ["pipe", "pipe", "pipe"]
195616
195812
  });
195813
+ let termination;
195814
+ const terminate = () => {
195815
+ termination ??= terminateChild(child);
195816
+ return termination;
195817
+ };
195617
195818
  let stdout = "";
195618
195819
  let stderr3 = "";
195619
195820
  let settled = false;
@@ -195629,18 +195830,44 @@ async function runSubprocessAgent(agent, agentConfig, input2, env, launchExecuti
195629
195830
  return;
195630
195831
  });
195631
195832
  });
195632
- const abortController = new AbortController;
195833
+ let onAbort;
195834
+ const cleanup = () => {
195835
+ if (timeout)
195836
+ clearTimeout(timeout);
195837
+ if (onAbort)
195838
+ input2.signal?.removeEventListener("abort", onAbort);
195839
+ };
195633
195840
  const resolveOnce = (result) => {
195634
195841
  if (settled)
195635
195842
  return;
195636
195843
  settled = true;
195637
- clearTimeout(timeout);
195844
+ cleanup();
195638
195845
  const finalResult = spawned && result.executionIdentity === undefined ? { ...result, executionIdentity: launchExecutionIdentity } : result;
195639
195846
  (startedWrite ?? Promise.resolve()).then(() => resolveResult(finalResult));
195640
195847
  };
195641
- const timeout = setTimeout(() => {
195848
+ const rejectOnce = (error51) => {
195849
+ if (settled)
195850
+ return;
195851
+ settled = true;
195852
+ cleanup();
195853
+ rejectResult(error51);
195854
+ };
195855
+ onAbort = () => {
195856
+ const cancellation = cancellationFromSignal(input2.signal);
195857
+ if (timeout)
195858
+ clearTimeout(timeout);
195859
+ abortController.abort(cancellation);
195860
+ cancelSession?.();
195861
+ terminate().then(() => rejectOnce(cancellation));
195862
+ };
195863
+ input2.signal?.addEventListener("abort", onAbort, { once: true });
195864
+ if (input2.signal?.aborted) {
195865
+ onAbort();
195866
+ return;
195867
+ }
195868
+ timeout = setTimeout(() => {
195642
195869
  abortController.abort(new Error("Kyoso agent timeout"));
195643
- terminateChild(child);
195870
+ terminate();
195644
195871
  const deadlineReached = input2.deadlineAtEpochMs !== undefined && Date.now() >= input2.deadlineAtEpochMs;
195645
195872
  resolveOnce({
195646
195873
  agent,
@@ -195669,7 +195896,9 @@ async function runSubprocessAgent(agent, agentConfig, input2, env, launchExecuti
195669
195896
  error: failure
195670
195897
  });
195671
195898
  });
195672
- runAcpClientWorkflow(child, input2, abortController, resolveEffortConfigOption(agent, agentConfig.effort), launchExecutionIdentity).then(({
195899
+ runAcpClientWorkflow(child, input2, abortController, resolveEffortConfigOption(agent, agentConfig.effort), launchExecutionIdentity, (cancel) => {
195900
+ cancelSession = cancel;
195901
+ }).then(({
195673
195902
  rawText,
195674
195903
  warnings,
195675
195904
  usage,
@@ -195677,6 +195906,10 @@ async function runSubprocessAgent(agent, agentConfig, input2, env, launchExecuti
195677
195906
  thoughtBytes,
195678
195907
  outputBytes,
195679
195908
  outputWarningTriggered,
195909
+ observedStreamRetries,
195910
+ discardedRetryMessageBytes,
195911
+ firstOutputAt,
195912
+ lastAcpUpdateAt,
195680
195913
  stopReason,
195681
195914
  executionIdentity
195682
195915
  }) => {
@@ -195694,6 +195927,10 @@ async function runSubprocessAgent(agent, agentConfig, input2, env, launchExecuti
195694
195927
  thoughtBytes,
195695
195928
  outputBytes,
195696
195929
  outputWarningTriggered,
195930
+ observedStreamRetries,
195931
+ ...discardedRetryMessageBytes === 0 ? {} : { discardedRetryMessageBytes },
195932
+ ...firstOutputAt === undefined ? {} : { firstOutputAt },
195933
+ ...lastAcpUpdateAt === undefined ? {} : { lastAcpUpdateAt },
195697
195934
  stopReason,
195698
195935
  executionIdentity,
195699
195936
  ...usage ? { usage } : {},
@@ -195706,6 +195943,10 @@ async function runSubprocessAgent(agent, agentConfig, input2, env, launchExecuti
195706
195943
  }
195707
195944
  });
195708
195945
  }).catch((error51) => {
195946
+ if (error51 instanceof KyosoCancellationError) {
195947
+ terminate().then(() => rejectOnce(error51));
195948
+ return;
195949
+ }
195709
195950
  const outputLimitError = findOutputLimitError(error51, abortController);
195710
195951
  if (outputLimitError) {
195711
195952
  stdout = outputLimitError.rawText;
@@ -195720,6 +195961,12 @@ async function runSubprocessAgent(agent, agentConfig, input2, env, launchExecuti
195720
195961
  thoughtBytes: outputLimitError.thoughtBytes,
195721
195962
  outputBytes: outputLimitError.outputBytes,
195722
195963
  outputWarningTriggered: outputLimitError.outputWarningTriggered,
195964
+ observedStreamRetries: outputLimitError.metrics.observedStreamRetries,
195965
+ ...outputLimitError.metrics.discardedRetryMessageBytes === 0 ? {} : {
195966
+ discardedRetryMessageBytes: outputLimitError.metrics.discardedRetryMessageBytes
195967
+ },
195968
+ ...outputLimitError.metrics.firstOutputAt === undefined ? {} : { firstOutputAt: outputLimitError.metrics.firstOutputAt },
195969
+ ...outputLimitError.metrics.lastAcpUpdateAt === undefined ? {} : { lastAcpUpdateAt: outputLimitError.metrics.lastAcpUpdateAt },
195723
195970
  stopReason: "cancelled",
195724
195971
  startedAt,
195725
195972
  completedAt: new Date().toISOString(),
@@ -195730,6 +195977,31 @@ async function runSubprocessAgent(agent, agentConfig, input2, env, launchExecuti
195730
195977
  });
195731
195978
  return;
195732
195979
  }
195980
+ if (error51 instanceof CodexTerminalSystemError) {
195981
+ stdout = error51.rawText;
195982
+ const failureText2 = [stderr3, formatAgentErrorDetail(error51)].filter((part) => part.trim().length > 0).join(`
195983
+ `);
195984
+ resolveOnce({
195985
+ agent,
195986
+ role: input2.role,
195987
+ status: "failed",
195988
+ rawText: stdout,
195989
+ messageBytes: error51.messageBytes,
195990
+ thoughtBytes: error51.thoughtBytes,
195991
+ outputBytes: error51.outputBytes,
195992
+ outputWarningTriggered: error51.outputWarningTriggered,
195993
+ observedStreamRetries: error51.metrics.observedStreamRetries,
195994
+ ...error51.metrics.discardedRetryMessageBytes === 0 ? {} : {
195995
+ discardedRetryMessageBytes: error51.metrics.discardedRetryMessageBytes
195996
+ },
195997
+ ...error51.metrics.firstOutputAt === undefined ? {} : { firstOutputAt: error51.metrics.firstOutputAt },
195998
+ ...error51.metrics.lastAcpUpdateAt === undefined ? {} : { lastAcpUpdateAt: error51.metrics.lastAcpUpdateAt },
195999
+ startedAt,
196000
+ completedAt: new Date().toISOString(),
196001
+ error: buildAgentFailure(failureText2, "Agent process failed.")
196002
+ });
196003
+ return;
196004
+ }
195733
196005
  if (error51 instanceof AcpNdJsonLineLimitError) {
195734
196006
  abortController.abort(error51);
195735
196007
  resolveOnce({
@@ -195761,7 +196033,7 @@ async function runSubprocessAgent(agent, agentConfig, input2, env, launchExecuti
195761
196033
  error: buildAgentFailure(failureText, "Agent process failed.")
195762
196034
  });
195763
196035
  }).finally(() => {
195764
- terminateChild(child);
196036
+ terminate();
195765
196037
  });
195766
196038
  child.on("close", (code) => {
195767
196039
  if (settled || code === 0 || abortController.signal.aborted)
@@ -195779,7 +196051,7 @@ async function runSubprocessAgent(agent, agentConfig, input2, env, launchExecuti
195779
196051
  });
195780
196052
  });
195781
196053
  }
195782
- async function runAcpClientWorkflow(child, input2, abortController, configOption, launchExecutionIdentity) {
196054
+ async function runAcpClientWorkflow(child, input2, abortController, configOption, launchExecutionIdentity, onSessionReady) {
195783
196055
  if (!child.stdin || !child.stdout) {
195784
196056
  throw new Error("Agent process did not expose stdio streams.");
195785
196057
  }
@@ -195827,6 +196099,15 @@ async function runAcpClientWorkflow(child, input2, abortController, configOption
195827
196099
  kyosoReadOnly: true
195828
196100
  }
195829
196101
  }).withSession(async (session) => {
196102
+ const cancelSession = () => {
196103
+ ctx.notify(methods.agent.session.cancel, {
196104
+ sessionId: session.sessionId
196105
+ }).catch(() => {
196106
+ return;
196107
+ });
196108
+ };
196109
+ onSessionReady(cancelSession);
196110
+ throwIfAborted(input2.signal);
195830
196111
  const warnings = [];
195831
196112
  if (configOption) {
195832
196113
  await ctx.request(methods.agent.session.setConfigOption, { sessionId: session.sessionId, ...configOption }, { cancellationSignal: abortController.signal }).catch((error51) => {
@@ -195839,61 +196120,154 @@ async function runAcpClientWorkflow(child, input2, abortController, configOption
195839
196120
  console.error(`kyoso: ${warning}`);
195840
196121
  });
195841
196122
  }
195842
- const promptResponse = session.prompt(input2.prompt, {
195843
- cancellationSignal: abortController.signal
195844
- });
195845
- promptResponse.catch(() => {
195846
- return;
195847
- });
195848
- let rawText = "";
196123
+ const accumulator = new AgentOutputAccumulator;
195849
196124
  let messageBytes = 0;
195850
196125
  let thoughtBytes = 0;
195851
196126
  let outputBytes = 0;
195852
196127
  let outputWarningTriggered = false;
195853
- for (;; ) {
195854
- const message = await session.nextUpdate();
195855
- if (message.kind === "stop") {
195856
- const usage = normalizeUsage3(message.response.usage);
195857
- return {
195858
- rawText,
195859
- warnings,
195860
- ...usage ? { usage } : {},
195861
- messageBytes,
195862
- thoughtBytes,
195863
- outputBytes,
195864
- outputWarningTriggered,
195865
- stopReason: message.stopReason,
195866
- executionIdentity: withReportedExecutionIdentity(launchExecutionIdentity, message.response._meta)
195867
- };
195868
- }
195869
- const update = message.update;
195870
- if (update.sessionUpdate !== "agent_message_chunk" && update.sessionUpdate !== "agent_thought_chunk" || update.content.type !== "text") {
195871
- continue;
195872
- }
195873
- const chunkBytes = Buffer.byteLength(update.content.text, "utf8");
195874
- const isMessage = update.sessionUpdate === "agent_message_chunk";
195875
- const nextMessageBytes = messageBytes + (isMessage ? chunkBytes : 0);
195876
- const nextThoughtBytes = thoughtBytes + (isMessage ? 0 : chunkBytes);
195877
- const nextOutputBytes = nextMessageBytes + nextThoughtBytes;
195878
- const nextOutputWarningTriggered = outputWarningTriggered || input2.warnOutputBytes !== undefined && nextOutputBytes >= input2.warnOutputBytes;
195879
- if (input2.maxOutputBytes !== undefined && nextOutputBytes > input2.maxOutputBytes) {
195880
- const retainedRawText = isMessage ? `${rawText}${utf8Prefix(update.content.text, input2.maxOutputBytes - outputBytes)}` : rawText;
195881
- await ctx.notify(methods.agent.session.cancel, {
195882
- sessionId: session.sessionId
195883
- }).catch(() => {
196128
+ let codexReportedSystemError = false;
196129
+ const sessionStartedAtEpochMs = Date.now();
196130
+ let lastActivityAtEpochMs = 0;
196131
+ const emitProgress = (event) => {
196132
+ try {
196133
+ const progress = input2.onProgress?.(event);
196134
+ Promise.resolve(progress).catch(() => {
195884
196135
  return;
195885
196136
  });
195886
- const error51 = new AgentOutputLimitError(retainedRawText, nextMessageBytes, nextThoughtBytes, nextOutputBytes, input2.maxOutputBytes, nextOutputWarningTriggered);
195887
- abortController.abort(error51);
196137
+ } catch {}
196138
+ };
196139
+ const heartbeatMs = input2.heartbeatMs ?? DEFAULT_PROGRESS_HEARTBEAT_MS;
196140
+ const heartbeat = input2.onProgress && heartbeatMs > 0 ? setInterval(() => {
196141
+ const now = Date.now();
196142
+ const lastAcpUpdateAt = accumulator.metrics().lastAcpUpdateAt;
196143
+ const lastUpdateEpochMs = lastAcpUpdateAt ? Date.parse(lastAcpUpdateAt) : sessionStartedAtEpochMs;
196144
+ emitProgress({
196145
+ type: "agent_waiting",
196146
+ agent: input2.agent,
196147
+ elapsedMs: Math.max(0, now - sessionStartedAtEpochMs),
196148
+ sinceLastAcpUpdateMs: Math.max(0, now - (Number.isFinite(lastUpdateEpochMs) ? lastUpdateEpochMs : sessionStartedAtEpochMs)),
196149
+ ...input2.streamIdleTimeoutMs === undefined ? {} : { streamIdleTimeoutMs: input2.streamIdleTimeoutMs },
196150
+ timestamp: new Date(now).toISOString()
196151
+ });
196152
+ }, heartbeatMs) : undefined;
196153
+ heartbeat?.unref?.();
196154
+ const stopHeartbeat = () => {
196155
+ if (heartbeat)
196156
+ clearInterval(heartbeat);
196157
+ };
196158
+ input2.signal?.addEventListener("abort", stopHeartbeat, { once: true });
196159
+ try {
196160
+ const promptResponse = session.prompt(input2.prompt, {
196161
+ cancellationSignal: abortController.signal
196162
+ });
196163
+ const promptCompletion = promptResponse.catch((error51) => {
196164
+ if (input2.signal?.aborted) {
196165
+ throw cancellationFromSignal(input2.signal);
196166
+ }
196167
+ if (abortController.signal.aborted) {
196168
+ const reason = abortController.signal.reason;
196169
+ if (reason !== undefined)
196170
+ throw reason;
196171
+ }
195888
196172
  throw error51;
196173
+ });
196174
+ const promptFailure = promptCompletion.then(() => new Promise(() => {
196175
+ return;
196176
+ }));
196177
+ for (;; ) {
196178
+ const message = await Promise.race([
196179
+ session.nextUpdate(),
196180
+ promptFailure
196181
+ ]);
196182
+ if (message.kind === "stop") {
196183
+ await promptCompletion;
196184
+ if (codexReportedSystemError) {
196185
+ throw new CodexTerminalSystemError(accumulator.finalRawText(), messageBytes, thoughtBytes, outputBytes, outputWarningTriggered, accumulator.metrics());
196186
+ }
196187
+ const usage = normalizeUsage3(message.response.usage);
196188
+ return {
196189
+ rawText: accumulator.finalRawText(),
196190
+ warnings,
196191
+ ...usage ? { usage } : {},
196192
+ messageBytes,
196193
+ thoughtBytes,
196194
+ outputBytes,
196195
+ outputWarningTriggered,
196196
+ ...accumulator.metrics(),
196197
+ stopReason: message.stopReason,
196198
+ executionIdentity: withReportedExecutionIdentity(launchExecutionIdentity, message.response._meta)
196199
+ };
196200
+ }
196201
+ const update = message.update;
196202
+ accumulator.noteUpdate();
196203
+ const retry = parseCodexRetryUpdate(update);
196204
+ if (retry) {
196205
+ codexReportedSystemError = false;
196206
+ const boundary = accumulator.markRetryBoundary();
196207
+ emitProgress({
196208
+ type: "agent_retrying",
196209
+ agent: input2.agent,
196210
+ observedRetry: accumulator.metrics().observedStreamRetries,
196211
+ ...retry.attempt === undefined ? {} : { attempt: retry.attempt },
196212
+ ...retry.maxRetries === undefined ? {} : { maxRetries: retry.maxRetries },
196213
+ reason: retry.message,
196214
+ discardedMessageBytes: boundary.discardedMessageBytes,
196215
+ timestamp: new Date().toISOString()
196216
+ });
196217
+ continue;
196218
+ }
196219
+ const codexThreadStatus = readCodexThreadStatus(update);
196220
+ if (codexThreadStatus !== undefined) {
196221
+ codexReportedSystemError = codexThreadStatus === "systemError";
196222
+ continue;
196223
+ }
196224
+ if (update.sessionUpdate !== "agent_message_chunk" && update.sessionUpdate !== "agent_thought_chunk" || update.content.type !== "text") {
196225
+ continue;
196226
+ }
196227
+ const chunkBytes = Buffer.byteLength(update.content.text, "utf8");
196228
+ const isMessage = update.sessionUpdate === "agent_message_chunk";
196229
+ const nextMessageBytes = messageBytes + (isMessage ? chunkBytes : 0);
196230
+ const nextThoughtBytes = thoughtBytes + (isMessage ? 0 : chunkBytes);
196231
+ const nextOutputBytes = nextMessageBytes + nextThoughtBytes;
196232
+ const nextOutputWarningTriggered = outputWarningTriggered || input2.warnOutputBytes !== undefined && nextOutputBytes >= input2.warnOutputBytes;
196233
+ if (input2.maxOutputBytes !== undefined && nextOutputBytes > input2.maxOutputBytes) {
196234
+ if (isMessage) {
196235
+ accumulator.addMessageChunk(utf8Prefix(update.content.text, input2.maxOutputBytes - outputBytes), readChunkMeta(update));
196236
+ }
196237
+ const retainedRawText = accumulator.finalRawText();
196238
+ await ctx.notify(methods.agent.session.cancel, {
196239
+ sessionId: session.sessionId
196240
+ }).catch(() => {
196241
+ return;
196242
+ });
196243
+ const error51 = new AgentOutputLimitError(retainedRawText, nextMessageBytes, nextThoughtBytes, nextOutputBytes, input2.maxOutputBytes, nextOutputWarningTriggered, accumulator.metrics());
196244
+ abortController.abort(error51);
196245
+ throw error51;
196246
+ }
196247
+ if (isMessage) {
196248
+ accumulator.addMessageChunk(update.content.text, readChunkMeta(update));
196249
+ } else {
196250
+ accumulator.addThoughtChunk(update.content.text);
196251
+ }
196252
+ messageBytes = nextMessageBytes;
196253
+ thoughtBytes = nextThoughtBytes;
196254
+ outputBytes = nextOutputBytes;
196255
+ outputWarningTriggered = nextOutputWarningTriggered;
196256
+ const now = Date.now();
196257
+ if (now - lastActivityAtEpochMs >= ACTIVITY_PROGRESS_THROTTLE_MS) {
196258
+ lastActivityAtEpochMs = now;
196259
+ emitProgress({
196260
+ type: "agent_activity",
196261
+ agent: input2.agent,
196262
+ activity: isMessage ? "message" : "thought",
196263
+ totalOutputBytes: outputBytes,
196264
+ timestamp: new Date(now).toISOString()
196265
+ });
196266
+ }
195889
196267
  }
195890
- if (isMessage) {
195891
- rawText += update.content.text;
195892
- }
195893
- messageBytes = nextMessageBytes;
195894
- thoughtBytes = nextThoughtBytes;
195895
- outputBytes = nextOutputBytes;
195896
- outputWarningTriggered = nextOutputWarningTriggered;
196268
+ } finally {
196269
+ stopHeartbeat();
196270
+ input2.signal?.removeEventListener("abort", stopHeartbeat);
195897
196271
  }
195898
196272
  });
195899
196273
  });
@@ -195917,7 +196291,8 @@ class AgentOutputLimitError extends Error {
195917
196291
  outputBytes;
195918
196292
  maxOutputBytes;
195919
196293
  outputWarningTriggered;
195920
- constructor(rawText, messageBytes, thoughtBytes, outputBytes, maxOutputBytes, outputWarningTriggered) {
196294
+ metrics;
196295
+ constructor(rawText, messageBytes, thoughtBytes, outputBytes, maxOutputBytes, outputWarningTriggered, metrics) {
195921
196296
  super(`Agent output exceeded ${maxOutputBytes} bytes.`);
195922
196297
  this.rawText = rawText;
195923
196298
  this.messageBytes = messageBytes;
@@ -195925,15 +196300,40 @@ class AgentOutputLimitError extends Error {
195925
196300
  this.outputBytes = outputBytes;
195926
196301
  this.maxOutputBytes = maxOutputBytes;
195927
196302
  this.outputWarningTriggered = outputWarningTriggered;
196303
+ this.metrics = metrics;
195928
196304
  this.name = "AgentOutputLimitError";
195929
196305
  }
195930
196306
  }
196307
+
196308
+ class CodexTerminalSystemError extends Error {
196309
+ rawText;
196310
+ messageBytes;
196311
+ thoughtBytes;
196312
+ outputBytes;
196313
+ outputWarningTriggered;
196314
+ metrics;
196315
+ constructor(rawText, messageBytes, thoughtBytes, outputBytes, outputWarningTriggered, metrics) {
196316
+ super(sanitizeTextForDisplay(rawText) || "Codex ACP reported a terminal system error.");
196317
+ this.rawText = rawText;
196318
+ this.messageBytes = messageBytes;
196319
+ this.thoughtBytes = thoughtBytes;
196320
+ this.outputBytes = outputBytes;
196321
+ this.outputWarningTriggered = outputWarningTriggered;
196322
+ this.metrics = metrics;
196323
+ this.name = "CodexTerminalSystemError";
196324
+ }
196325
+ }
195931
196326
  function findOutputLimitError(error51, abortController) {
195932
196327
  if (error51 instanceof AgentOutputLimitError)
195933
196328
  return error51;
195934
196329
  const reason = abortController.signal.reason;
195935
196330
  return reason instanceof AgentOutputLimitError ? reason : undefined;
195936
196331
  }
196332
+ function cancellationFromSignal(signal) {
196333
+ if (signal?.reason instanceof KyosoCancellationError)
196334
+ return signal.reason;
196335
+ return new KyosoCancellationError(typeof signal?.reason === "string" ? signal.reason : undefined);
196336
+ }
195937
196337
  function resolveEffectiveTimeoutMs(input2) {
195938
196338
  const deadlineRemaining = input2.deadlineAtEpochMs === undefined ? Number.POSITIVE_INFINITY : input2.deadlineAtEpochMs - Date.now();
195939
196339
  return Math.max(0, Math.min(input2.timeoutMs, deadlineRemaining));
@@ -195942,7 +196342,7 @@ function normalizeUsage3(usage) {
195942
196342
  return normalizeModelTokenUsage(usage);
195943
196343
  }
195944
196344
  function withReportedExecutionIdentity(identity, metadata) {
195945
- const record2 = isRecord13(metadata) ? metadata : {};
196345
+ const record2 = isRecord14(metadata) ? metadata : {};
195946
196346
  return createModelExecutionIdentity({
195947
196347
  providerRoute: identity.providerRoute,
195948
196348
  requestedModel: identity.requestedModel,
@@ -195950,7 +196350,26 @@ function withReportedExecutionIdentity(identity, metadata) {
195950
196350
  reportedModel: record2.model
195951
196351
  });
195952
196352
  }
195953
- function isRecord13(value) {
196353
+ function readChunkMeta(update) {
196354
+ const record2 = isRecord14(update) ? update : {};
196355
+ const metadata = isRecord14(record2._meta) ? record2._meta : {};
196356
+ const codex = isRecord14(metadata.codex) ? metadata.codex : {};
196357
+ const phase = codex.phase === "commentary" || codex.phase === "final_answer" ? codex.phase : "unknown";
196358
+ return {
196359
+ ...typeof record2.messageId === "string" ? { messageId: record2.messageId } : {},
196360
+ phase
196361
+ };
196362
+ }
196363
+ function readCodexThreadStatus(update) {
196364
+ const record2 = isRecord14(update) ? update : {};
196365
+ if (record2.sessionUpdate !== "session_info_update")
196366
+ return;
196367
+ const metadata = isRecord14(record2._meta) ? record2._meta : {};
196368
+ const codex = isRecord14(metadata.codex) ? metadata.codex : {};
196369
+ const threadStatus = isRecord14(codex.threadStatus) ? codex.threadStatus : {};
196370
+ return typeof threadStatus.type === "string" ? threadStatus.type : undefined;
196371
+ }
196372
+ function isRecord14(value) {
195954
196373
  return value !== null && typeof value === "object" && !Array.isArray(value);
195955
196374
  }
195956
196375
  function resolveEffortConfigOption(agent, effort) {
@@ -196016,14 +196435,40 @@ function assertWithinWorkspace(workspaceRoot, absolute) {
196016
196435
  }
196017
196436
  }
196018
196437
  function terminateChild(child) {
196019
- if (child.exitCode !== null || child.signalCode !== null)
196020
- return;
196021
- child.kill("SIGTERM");
196022
- const killTimer = setTimeout(() => {
196023
- if (child.exitCode === null && child.signalCode === null)
196438
+ if (child.exitCode !== null || child.signalCode !== null) {
196439
+ return Promise.resolve();
196440
+ }
196441
+ return new Promise((resolve10) => {
196442
+ let settled = false;
196443
+ let killTimer;
196444
+ let escalationTimer;
196445
+ let onClose;
196446
+ const settle = () => {
196447
+ if (settled)
196448
+ return;
196449
+ settled = true;
196450
+ if (killTimer)
196451
+ clearTimeout(killTimer);
196452
+ if (escalationTimer)
196453
+ clearTimeout(escalationTimer);
196454
+ if (onClose)
196455
+ child.off("close", onClose);
196456
+ resolve10();
196457
+ };
196458
+ onClose = () => settle();
196459
+ child.once("close", onClose);
196460
+ child.kill("SIGTERM");
196461
+ killTimer = setTimeout(() => {
196462
+ if (child.exitCode !== null || child.signalCode !== null) {
196463
+ settle();
196464
+ return;
196465
+ }
196024
196466
  child.kill("SIGKILL");
196025
- }, 2000);
196026
- killTimer.unref();
196467
+ escalationTimer = setTimeout(settle, 500);
196468
+ escalationTimer.unref();
196469
+ }, 2000);
196470
+ killTimer.unref();
196471
+ });
196027
196472
  }
196028
196473
  function isMissingPathError6(error51) {
196029
196474
  return typeof error51 === "object" && error51 !== null && "code" in error51 && error51.code === "ENOENT";
@@ -196207,6 +196652,128 @@ function assertUsableEnvironmentValue(env, key) {
196207
196652
  return value;
196208
196653
  }
196209
196654
 
196655
+ // src/cli/progress.ts
196656
+ function resolveCliProgressMode(value, stderrIsTty) {
196657
+ const mode = value ?? "auto";
196658
+ if (mode === "auto")
196659
+ return stderrIsTty ? "plain" : "off";
196660
+ if (mode === "plain" || mode === "jsonl" || mode === "off")
196661
+ return mode;
196662
+ throw new Error(`Invalid --progress value "${value}". Expected auto|plain|jsonl|off.`);
196663
+ }
196664
+ function createCliProgressSink(mode, stderrWrite) {
196665
+ if (mode === "off" || mode === "auto")
196666
+ return;
196667
+ if (mode === "jsonl") {
196668
+ return (event) => writeSafely(stderrWrite, `${JSON.stringify(event)}
196669
+ `);
196670
+ }
196671
+ const startedAtEpochMs = Date.now();
196672
+ return (event) => {
196673
+ const elapsedMs = Math.max(0, Date.now() - startedAtEpochMs);
196674
+ writeSafely(stderrWrite, `[${formatElapsed(elapsedMs)}] ${formatPlainProgressMessage(event)}
196675
+ `);
196676
+ };
196677
+ }
196678
+ function writeSafely(write, line) {
196679
+ try {
196680
+ write(line);
196681
+ } catch {}
196682
+ }
196683
+ function formatElapsed(elapsedMs) {
196684
+ const totalSeconds = Math.floor(elapsedMs / 1000);
196685
+ const minutes = Math.floor(totalSeconds / 60);
196686
+ const seconds = totalSeconds % 60;
196687
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
196688
+ }
196689
+ function formatPlainProgressMessage(event) {
196690
+ switch (event.type) {
196691
+ case "review_started":
196692
+ return `Kyoso review started (${event.tool})`;
196693
+ case "phase_started":
196694
+ return phaseStartedMessage(event);
196695
+ case "phase_completed":
196696
+ return phaseCompletedMessage(event);
196697
+ case "phase_skipped":
196698
+ return `${phaseLabel(event.phase)} skipped`;
196699
+ case "agent_started":
196700
+ return event.role === "finding_verifier" ? `Finding verifier started: ${event.agent}` : `Primary reviewers started: ${event.agent}`;
196701
+ case "agent_activity":
196702
+ return `${event.agent}: received ${event.totalOutputBytes} output bytes`;
196703
+ case "agent_waiting":
196704
+ return waitingMessage(event);
196705
+ case "agent_retrying":
196706
+ return retryMessage(event);
196707
+ case "agent_completed":
196708
+ return completedMessage(event);
196709
+ case "review_completed":
196710
+ return `Review completed: ${event.decision}`;
196711
+ case "review_failed":
196712
+ return "Review failed";
196713
+ case "review_cancelled":
196714
+ return "Review cancelled";
196715
+ }
196716
+ }
196717
+ function phaseStartedMessage(event) {
196718
+ switch (event.phase) {
196719
+ case "context":
196720
+ return "Preparing context";
196721
+ case "snapshot":
196722
+ return "Creating read-only snapshot";
196723
+ case "primary":
196724
+ return "Starting primary reviewers";
196725
+ case "verification":
196726
+ return "Cross-agent verification started";
196727
+ case "judge":
196728
+ return "Cross-model judge started";
196729
+ case "finalize":
196730
+ return "Finalizing review";
196731
+ case "preflight":
196732
+ return "Checking review configuration";
196733
+ case "aggregation":
196734
+ return "Aggregating findings";
196735
+ }
196736
+ }
196737
+ function phaseCompletedMessage(event) {
196738
+ switch (event.phase) {
196739
+ case "context":
196740
+ return "Context prepared";
196741
+ case "snapshot":
196742
+ return "Read-only snapshot created";
196743
+ case "primary":
196744
+ return "Primary reviewers completed";
196745
+ case "verification":
196746
+ return "Cross-agent verification completed";
196747
+ case "judge":
196748
+ return "Cross-model judge completed";
196749
+ case "finalize":
196750
+ return "Review finalized";
196751
+ case "preflight":
196752
+ return "Review configuration checked";
196753
+ case "aggregation":
196754
+ return "Findings aggregated";
196755
+ }
196756
+ }
196757
+ function phaseLabel(phase) {
196758
+ return phase === "preflight" ? "Preflight" : phase;
196759
+ }
196760
+ function waitingMessage(event) {
196761
+ const seconds = Math.floor(event.sinceLastAcpUpdateMs / 1000);
196762
+ const threshold = event.streamIdleTimeoutMs === undefined ? "" : `; stream idle retry threshold is ${Math.floor(event.streamIdleTimeoutMs / 1000)}s`;
196763
+ return `${event.agent}: process alive; no ACP update for ${seconds}s${threshold}`;
196764
+ }
196765
+ function retryMessage(event) {
196766
+ const attempt = event.maxRetries === undefined ? String(event.observedRetry) : `${event.observedRetry}/${event.maxRetries}`;
196767
+ return `${event.agent}: retrying model stream (${attempt}); discarded ${event.discardedMessageBytes} bytes of incomplete output`;
196768
+ }
196769
+ function completedMessage(event) {
196770
+ if (event.observedStreamRetries === undefined) {
196771
+ return `${event.agent} ${event.status}`;
196772
+ }
196773
+ const suffix = event.observedStreamRetries === 1 ? "retry" : "retries";
196774
+ return `${event.agent} completed after ${event.observedStreamRetries} observed stream ${suffix}`;
196775
+ }
196776
+
196210
196777
  // node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
196211
196778
  var __create2 = Object.create;
196212
196779
  var __defProp2 = Object.defineProperty;
@@ -210174,6 +210741,11 @@ import { resolve as resolve10 } from "node:path";
210174
210741
 
210175
210742
  // src/config/configOverrides.ts
210176
210743
  var NUMBER_VALUE = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
210744
+ var UNSET_NUMBER_OVERRIDE_PATHS = new Set([
210745
+ "agents.codex.openRouter.streamIdleTimeoutMs",
210746
+ "agents.codex.openRouter.streamMaxRetries",
210747
+ "agents.codex.openRouter.requestMaxRetries"
210748
+ ]);
210177
210749
  function applyConfigOverrides(config2, assignments) {
210178
210750
  if (assignments.length === 0)
210179
210751
  return config2;
@@ -210181,9 +210753,9 @@ function applyConfigOverrides(config2, assignments) {
210181
210753
  const baseConfig = config2;
210182
210754
  const overridden = structuredClone(config2);
210183
210755
  for (const override of overrides) {
210184
- writePath2(overridden, override.path, parseConfigOverrideValue(override.value, readPath2(baseConfig, override.path)));
210756
+ writePath2(overridden, override.path, parseConfigOverrideValue(override.value, readPath2(baseConfig, override.path), override.path));
210185
210757
  }
210186
- clearInheritedOpenRouterModelForProviderReset(baseConfig, overridden, overrides);
210758
+ clearInheritedOpenRouterConfigForProviderReset(baseConfig, overridden, overrides);
210187
210759
  assertOpenRouterProviderOverrideIncludesModel(baseConfig, overridden, overrides);
210188
210760
  const parsed = kyosoConfigSchema.safeParse(overridden);
210189
210761
  if (parsed.success)
@@ -210204,17 +210776,23 @@ function assertOpenRouterProviderOverrideIncludesModel(baseConfig, overridden, o
210204
210776
  const assignment = findAssignmentForPath(overrides, providerPath.join(".")) ?? "agents.codex.provider=openrouter";
210205
210777
  throw new Error(`Invalid --set value ${JSON.stringify(assignment)}: selecting agents.codex.provider=openrouter requires agents.codex.model in the same --set invocation.`);
210206
210778
  }
210207
- function clearInheritedOpenRouterModelForProviderReset(baseConfig, overridden, overrides) {
210779
+ function clearInheritedOpenRouterConfigForProviderReset(baseConfig, overridden, overrides) {
210208
210780
  const providerPath = ["agents", "codex", "provider"];
210209
210781
  const modelPath = ["agents", "codex", "model"];
210782
+ const openRouterPath = ["agents", "codex", "openRouter"];
210210
210783
  const selectsDefaultProvider = overrides.some((override) => override.path.join(".") === providerPath.join(".") && override.value === CODEX_DEFAULT_PROVIDER);
210211
210784
  const suppliesModel = overrides.some((override) => override.path.join(".") === modelPath.join("."));
210212
- if (!selectsDefaultProvider || suppliesModel || readPath2(baseConfig, providerPath) !== CODEX_OPENROUTER_PROVIDER || readPath2(overridden, providerPath) !== CODEX_DEFAULT_PROVIDER) {
210785
+ const suppliesOpenRouterPolicy = overrides.some((override) => override.path.join(".").startsWith(`${openRouterPath.join(".")}.`));
210786
+ if (!selectsDefaultProvider || readPath2(baseConfig, providerPath) !== CODEX_OPENROUTER_PROVIDER || readPath2(overridden, providerPath) !== CODEX_DEFAULT_PROVIDER) {
210213
210787
  return;
210214
210788
  }
210215
210789
  const codex = readPath2(overridden, ["agents", "codex"]);
210216
- if (isRecord14(codex))
210790
+ if (!isRecord15(codex))
210791
+ return;
210792
+ if (!suppliesModel)
210217
210793
  delete codex.model;
210794
+ if (!suppliesOpenRouterPolicy)
210795
+ delete codex.openRouter;
210218
210796
  }
210219
210797
  function findAssignmentForPath(overrides, path) {
210220
210798
  for (let index = overrides.length - 1;index >= 0; index -= 1) {
@@ -210240,7 +210818,7 @@ function parseConfigOverride(assignment) {
210240
210818
  value: assignment.slice(separator + 1)
210241
210819
  };
210242
210820
  }
210243
- function parseConfigOverrideValue(value, currentValue) {
210821
+ function parseConfigOverrideValue(value, currentValue, path) {
210244
210822
  if (typeof currentValue === "boolean") {
210245
210823
  if (value === "true")
210246
210824
  return true;
@@ -210248,7 +210826,7 @@ function parseConfigOverrideValue(value, currentValue) {
210248
210826
  return false;
210249
210827
  return value;
210250
210828
  }
210251
- if (typeof currentValue === "number" && NUMBER_VALUE.test(value)) {
210829
+ if ((typeof currentValue === "number" || currentValue === undefined && UNSET_NUMBER_OVERRIDE_PATHS.has(path.join("."))) && NUMBER_VALUE.test(value)) {
210252
210830
  const parsed = Number(value);
210253
210831
  if (Number.isFinite(parsed))
210254
210832
  return parsed;
@@ -210258,7 +210836,7 @@ function parseConfigOverrideValue(value, currentValue) {
210258
210836
  function readPath2(target, path) {
210259
210837
  let current = target;
210260
210838
  for (const key of path) {
210261
- if (!isRecord14(current))
210839
+ if (!isRecord15(current))
210262
210840
  return;
210263
210841
  current = current[key];
210264
210842
  }
@@ -210268,7 +210846,7 @@ function writePath2(target, path, value) {
210268
210846
  let current = target;
210269
210847
  for (const key of path.slice(0, -1)) {
210270
210848
  const child = current[key];
210271
- if (!isRecord14(child)) {
210849
+ if (!isRecord15(child)) {
210272
210850
  throw new Error(`Cannot apply --set key ${JSON.stringify(path.join("."))}.`);
210273
210851
  }
210274
210852
  current = child;
@@ -210277,7 +210855,7 @@ function writePath2(target, path, value) {
210277
210855
  if (leaf)
210278
210856
  current[leaf] = value;
210279
210857
  }
210280
- function isRecord14(value) {
210858
+ function isRecord15(value) {
210281
210859
  return typeof value === "object" && value !== null && !Array.isArray(value);
210282
210860
  }
210283
210861
 
@@ -210292,6 +210870,7 @@ class FakeAgentManager extends BaseAcpAgentManager {
210292
210870
  this.verifierScenarios = verifierScenarios;
210293
210871
  }
210294
210872
  async runAgent(input2) {
210873
+ throwIfAborted(input2.signal);
210295
210874
  this.calls.push(input2);
210296
210875
  const startedAt = new Date().toISOString();
210297
210876
  if (input2.role === "finding_verifier") {
@@ -211344,29 +211923,184 @@ function buildContext(request, options) {
211344
211923
  }
211345
211924
  }
211346
211925
 
211347
- // src/core/validateRequest.ts
211348
- function validateReviewRequest(tool, request) {
211349
- if (typeof request.goal !== "string" || request.goal.trim().length === 0) {
211350
- throw new KyosoRequestError("goal is required", "VALIDATION_ERROR");
211351
- }
211352
- validateReviewContract(request);
211353
- validateSelectedFiles(request);
211354
- if (tool === "diff_review" && !request.diff?.unifiedDiff) {
211355
- throw new KyosoRequestError("diff_review requires diff.unifiedDiff in MCP/core mode", "DIFF_REQUIRED");
211926
+ // src/core/progressDispatcher.ts
211927
+ var DEFAULT_SINK_TIMEOUT_MS = 2000;
211928
+ var DEFAULT_MAX_QUEUE = 128;
211929
+ function isTransientEvent(event) {
211930
+ return event.type === "agent_activity" || event.type === "agent_waiting";
211931
+ }
211932
+ function isSameCoalescibleEvent(previous, next) {
211933
+ if (!previous || !isTransientEvent(previous) || !isTransientEvent(next)) {
211934
+ return false;
211356
211935
  }
211936
+ return previous.type === next.type && previous.agent === next.agent;
211357
211937
  }
211358
- function validateReviewContract(request) {
211359
- const contract = request.reviewContract;
211360
- if (contract === undefined)
211361
- return;
211362
- if (!isRecord15(contract)) {
211363
- throw new KyosoRequestError("reviewContract must be an object", "VALIDATION_ERROR");
211364
- }
211365
- const allowedKeys = new Set(["focus", "nonGoals", "acceptedRisks"]);
211366
- const unknownKeys = Object.keys(contract).filter((key) => !allowedKeys.has(key));
211367
- if (unknownKeys.length > 0) {
211368
- throw new KyosoRequestError(`reviewContract contains unknown keys: ${unknownKeys.join(", ")}`, "VALIDATION_ERROR");
211369
- }
211938
+ function withoutThrowing(callback) {
211939
+ try {
211940
+ callback?.();
211941
+ } catch {}
211942
+ }
211943
+ function createProgressDispatcher(sink, options = {}) {
211944
+ if (!sink) {
211945
+ return {
211946
+ emit: () => {
211947
+ return;
211948
+ },
211949
+ flush: async () => {
211950
+ return;
211951
+ }
211952
+ };
211953
+ }
211954
+ const sinkTimeoutMs = options.sinkTimeoutMs ?? DEFAULT_SINK_TIMEOUT_MS;
211955
+ const maxQueue = options.maxQueue ?? DEFAULT_MAX_QUEUE;
211956
+ const queue = [];
211957
+ const idleResolvers = new Set;
211958
+ let disabled = false;
211959
+ let processing = false;
211960
+ const notifyIdle = () => {
211961
+ if (processing || queue.length > 0)
211962
+ return;
211963
+ for (const resolve10 of idleResolvers)
211964
+ resolve10();
211965
+ idleResolvers.clear();
211966
+ };
211967
+ const waitForIdle = () => {
211968
+ if (!processing && queue.length === 0)
211969
+ return Promise.resolve();
211970
+ return new Promise((resolve10) => idleResolvers.add(resolve10));
211971
+ };
211972
+ const disable = (reason) => {
211973
+ if (disabled)
211974
+ return;
211975
+ disabled = true;
211976
+ queue.length = 0;
211977
+ withoutThrowing(() => options.onSinkDisabled?.(reason));
211978
+ notifyIdle();
211979
+ };
211980
+ const deliver = async (event) => {
211981
+ let timeout;
211982
+ try {
211983
+ const delivery = Promise.resolve().then(() => sink(event));
211984
+ const timeoutResult = new Promise((resolve10) => {
211985
+ timeout = setTimeout(() => resolve10("timeout"), sinkTimeoutMs);
211986
+ timeout.unref?.();
211987
+ });
211988
+ const result = await Promise.race([
211989
+ delivery.then(() => "delivered"),
211990
+ timeoutResult
211991
+ ]);
211992
+ if (result === "timeout") {
211993
+ disable(`Progress sink timed out after ${sinkTimeoutMs}ms.`);
211994
+ return false;
211995
+ }
211996
+ return true;
211997
+ } catch {
211998
+ disable("Progress sink threw while handling an event.");
211999
+ return false;
212000
+ } finally {
212001
+ if (timeout)
212002
+ clearTimeout(timeout);
212003
+ }
212004
+ };
212005
+ const drain = async () => {
212006
+ try {
212007
+ while (!disabled && queue.length > 0) {
212008
+ const event = queue.shift();
212009
+ if (!event)
212010
+ continue;
212011
+ if (!await deliver(event))
212012
+ break;
212013
+ }
212014
+ } finally {
212015
+ processing = false;
212016
+ if (!disabled && queue.length > 0) {
212017
+ startDrain();
212018
+ } else {
212019
+ notifyIdle();
212020
+ }
212021
+ }
212022
+ };
212023
+ const startDrain = () => {
212024
+ if (processing || disabled || queue.length === 0)
212025
+ return;
212026
+ processing = true;
212027
+ drain();
212028
+ };
212029
+ const makeRoomForMilestone = () => {
212030
+ const transientIndex = queue.findIndex(isTransientEvent);
212031
+ if (transientIndex >= 0) {
212032
+ queue.splice(transientIndex, 1);
212033
+ return true;
212034
+ }
212035
+ if (queue.length < maxQueue)
212036
+ return true;
212037
+ queue.shift();
212038
+ return true;
212039
+ };
212040
+ return {
212041
+ emit(event) {
212042
+ try {
212043
+ if (disabled)
212044
+ return;
212045
+ const previous = queue.at(-1);
212046
+ if (isSameCoalescibleEvent(previous, event)) {
212047
+ queue[queue.length - 1] = event;
212048
+ } else if (queue.length < maxQueue) {
212049
+ queue.push(event);
212050
+ } else if (isTransientEvent(event)) {
212051
+ return;
212052
+ } else if (makeRoomForMilestone()) {
212053
+ queue.push(event);
212054
+ }
212055
+ startDrain();
212056
+ } catch {}
212057
+ },
212058
+ async flush(maxWaitMs = DEFAULT_SINK_TIMEOUT_MS) {
212059
+ try {
212060
+ const idle = waitForIdle();
212061
+ if (maxWaitMs <= 0)
212062
+ return;
212063
+ let timeout;
212064
+ try {
212065
+ await Promise.race([
212066
+ idle,
212067
+ new Promise((resolve10) => {
212068
+ timeout = setTimeout(resolve10, maxWaitMs);
212069
+ timeout.unref?.();
212070
+ })
212071
+ ]);
212072
+ } finally {
212073
+ if (timeout)
212074
+ clearTimeout(timeout);
212075
+ }
212076
+ } catch {}
212077
+ }
212078
+ };
212079
+ }
212080
+
212081
+ // src/core/validateRequest.ts
212082
+ function validateReviewRequest(tool, request) {
212083
+ if (typeof request.goal !== "string" || request.goal.trim().length === 0) {
212084
+ throw new KyosoRequestError("goal is required", "VALIDATION_ERROR");
212085
+ }
212086
+ validateReviewContract(request);
212087
+ validateSelectedFiles(request);
212088
+ if (tool === "diff_review" && !request.diff?.unifiedDiff) {
212089
+ throw new KyosoRequestError("diff_review requires diff.unifiedDiff in MCP/core mode", "DIFF_REQUIRED");
212090
+ }
212091
+ }
212092
+ function validateReviewContract(request) {
212093
+ const contract = request.reviewContract;
212094
+ if (contract === undefined)
212095
+ return;
212096
+ if (!isRecord16(contract)) {
212097
+ throw new KyosoRequestError("reviewContract must be an object", "VALIDATION_ERROR");
212098
+ }
212099
+ const allowedKeys = new Set(["focus", "nonGoals", "acceptedRisks"]);
212100
+ const unknownKeys = Object.keys(contract).filter((key) => !allowedKeys.has(key));
212101
+ if (unknownKeys.length > 0) {
212102
+ throw new KyosoRequestError(`reviewContract contains unknown keys: ${unknownKeys.join(", ")}`, "VALIDATION_ERROR");
212103
+ }
211370
212104
  const focus = contract.focus;
211371
212105
  if (focus !== undefined && (!Array.isArray(focus) || focus.length > REVIEW_LENSES.length || focus.some((lens) => !isReviewLens(lens)))) {
211372
212106
  throw new KyosoRequestError("reviewContract.focus contains an invalid review lens", "VALIDATION_ERROR");
@@ -211376,7 +212110,7 @@ function validateReviewContract(request) {
211376
212110
  throw new KyosoRequestError("reviewContract.nonGoals must contain at most 20 non-empty strings of 500 characters or fewer", "VALIDATION_ERROR");
211377
212111
  }
211378
212112
  const acceptedRisks = contract.acceptedRisks;
211379
- if (acceptedRisks !== undefined && (!Array.isArray(acceptedRisks) || acceptedRisks.length > 20 || acceptedRisks.some((risk) => !isRecord15(risk) || typeof risk.findingFingerprint !== "string" || !/^sha256:[0-9a-f]{64}$/.test(risk.findingFingerprint) || typeof risk.rationale !== "string" || risk.rationale.trim().length === 0 || risk.rationale.length > 500))) {
212113
+ if (acceptedRisks !== undefined && (!Array.isArray(acceptedRisks) || acceptedRisks.length > 20 || acceptedRisks.some((risk) => !isRecord16(risk) || typeof risk.findingFingerprint !== "string" || !/^sha256:[0-9a-f]{64}$/.test(risk.findingFingerprint) || typeof risk.rationale !== "string" || risk.rationale.trim().length === 0 || risk.rationale.length > 500))) {
211380
212114
  throw new KyosoRequestError("reviewContract.acceptedRisks must contain valid finding fingerprints and bounded rationales", "VALIDATION_ERROR");
211381
212115
  }
211382
212116
  }
@@ -211388,13 +212122,13 @@ function validateSelectedFiles(request) {
211388
212122
  throw new KyosoRequestError("selectedFiles must be an array", "VALIDATION_ERROR");
211389
212123
  }
211390
212124
  for (const file2 of selectedFiles) {
211391
- if (!isRecord15(file2) || typeof file2.path !== "string" || file2.path.trim().length === 0 || typeof file2.content !== "string" || file2.language !== undefined && typeof file2.language !== "string" || file2.truncated !== undefined && typeof file2.truncated !== "boolean") {
212125
+ if (!isRecord16(file2) || typeof file2.path !== "string" || file2.path.trim().length === 0 || typeof file2.content !== "string" || file2.language !== undefined && typeof file2.language !== "string" || file2.truncated !== undefined && typeof file2.truncated !== "boolean") {
211392
212126
  throw new KyosoRequestError("selectedFiles entries require string path/content and valid optional metadata", "VALIDATION_ERROR");
211393
212127
  }
211394
212128
  normalizeRelativePath(file2.path);
211395
212129
  }
211396
212130
  }
211397
- function isRecord15(value) {
212131
+ function isRecord16(value) {
211398
212132
  return typeof value === "object" && value !== null && !Array.isArray(value);
211399
212133
  }
211400
212134
 
@@ -211925,11 +212659,11 @@ function canonicalJson(value) {
211925
212659
  function canonicalize(value) {
211926
212660
  if (Array.isArray(value))
211927
212661
  return value.map(canonicalize);
211928
- if (!isRecord16(value))
212662
+ if (!isRecord17(value))
211929
212663
  return value;
211930
212664
  return Object.fromEntries(Object.entries(value).filter(([, child]) => child !== undefined).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => [key, canonicalize(child)]));
211931
212665
  }
211932
- function isRecord16(value) {
212666
+ function isRecord17(value) {
211933
212667
  return typeof value === "object" && value !== null && !Array.isArray(value);
211934
212668
  }
211935
212669
 
@@ -211943,7 +212677,7 @@ var REVIEW_BUDGET_KEYS = new Set([
211943
212677
  ]);
211944
212678
  var MODEL_CALL_KINDS = ["primary", "verifier", "judge"];
211945
212679
  function resolveReviewBudget(ceiling, requested) {
211946
- if (requested !== undefined && !isRecord17(requested)) {
212680
+ if (requested !== undefined && !isRecord18(requested)) {
211947
212681
  throw new KyosoRequestError("options.reviewBudget must be an object.", "REVIEW_BUDGET_INVALID");
211948
212682
  }
211949
212683
  for (const [key, value] of Object.entries(requested ?? {})) {
@@ -212133,6 +212867,10 @@ class ReviewBudgetTracker {
212133
212867
  current.thoughtBytes = values.thoughtBytes;
212134
212868
  current.outputBytes = values.outputBytes;
212135
212869
  current.outputWarningTriggered = values.outputWarningTriggered;
212870
+ current.observedStreamRetries = values.observedStreamRetries;
212871
+ current.discardedRetryMessageBytes = values.discardedRetryMessageBytes;
212872
+ current.firstOutputAt = values.firstOutputAt;
212873
+ current.lastAcpUpdateAt = values.lastAcpUpdateAt;
212136
212874
  current.salvaged = values.salvaged;
212137
212875
  current.reportedFindings = values.reportedFindings;
212138
212876
  current.findingsTargetExceeded = values.findingsTargetExceeded;
@@ -212247,6 +212985,12 @@ class ReviewBudgetTracker {
212247
212985
  ...reservation.thoughtBytes !== undefined ? { thoughtBytes: reservation.thoughtBytes } : {},
212248
212986
  ...reservation.outputBytes !== undefined ? { outputBytes: reservation.outputBytes } : {},
212249
212987
  ...reservation.outputWarningTriggered !== undefined ? { outputWarningTriggered: reservation.outputWarningTriggered } : {},
212988
+ ...reservation.observedStreamRetries !== undefined ? { observedStreamRetries: reservation.observedStreamRetries } : {},
212989
+ ...reservation.discardedRetryMessageBytes !== undefined ? {
212990
+ discardedRetryMessageBytes: reservation.discardedRetryMessageBytes
212991
+ } : {},
212992
+ ...reservation.firstOutputAt !== undefined ? { firstOutputAt: reservation.firstOutputAt } : {},
212993
+ ...reservation.lastAcpUpdateAt !== undefined ? { lastAcpUpdateAt: reservation.lastAcpUpdateAt } : {},
212250
212994
  ...reservation.salvaged !== undefined ? { salvaged: reservation.salvaged } : {},
212251
212995
  ...reservation.reportedFindings !== undefined ? { reportedFindings: reservation.reportedFindings } : {},
212252
212996
  ...reservation.findingsTargetExceeded !== undefined ? { findingsTargetExceeded: reservation.findingsTargetExceeded } : {},
@@ -212287,7 +213031,7 @@ function emptyReviewModelCallPlan() {
212287
213031
  ceilingEffects: []
212288
213032
  };
212289
213033
  }
212290
- function isRecord17(value) {
213034
+ function isRecord18(value) {
212291
213035
  return typeof value === "object" && value !== null && !Array.isArray(value);
212292
213036
  }
212293
213037
 
@@ -212349,7 +213093,7 @@ function parseVerificationVerdicts(rawText) {
212349
213093
  if (!Array.isArray(parsed.verdicts))
212350
213094
  return;
212351
213095
  return parsed.verdicts.flatMap((item) => {
212352
- if (!isRecord18(item))
213096
+ if (!isRecord19(item))
212353
213097
  return [];
212354
213098
  if (typeof item.findingId !== "string")
212355
213099
  return [];
@@ -212427,11 +213171,12 @@ function verificationNote(reasoning) {
212427
213171
  function isVerdict(value) {
212428
213172
  return value === "confirmed" || value === "refuted" || value === "uncertain";
212429
213173
  }
212430
- function isRecord18(value) {
213174
+ function isRecord19(value) {
212431
213175
  return typeof value === "object" && value !== null && !Array.isArray(value);
212432
213176
  }
212433
213177
 
212434
213178
  // src/core/runReview.ts
213179
+ var MAX_AGENT_RETRY_PROGRESS_EVENTS = 100;
212435
213180
  function requestForRecursionFingerprint(request) {
212436
213181
  try {
212437
213182
  return scanAndRedactSecrets(request).redactedRequest;
@@ -212447,138 +213192,368 @@ async function runReview(tool, request, options = {}) {
212447
213192
  const auditEnv = { ...process.env, ...options.env };
212448
213193
  const traceWriterFactory = options.traceWriterFactory ?? createTraceWriter;
212449
213194
  let snapshot;
213195
+ let activeTrace;
213196
+ let progressDeliveryFailure;
213197
+ let writeProgressDeliveryFailure;
213198
+ let reviewFailureReported = false;
213199
+ const dispatcher = createProgressDispatcher(options.onProgress, {
213200
+ onSinkDisabled: (reason) => {
213201
+ if (progressDeliveryFailure !== undefined)
213202
+ return;
213203
+ progressDeliveryFailure = reason;
213204
+ writeProgressDeliveryFailure?.(reason);
213205
+ }
213206
+ });
213207
+ const phaseStartedAt = new Map;
213208
+ const startPhase = (phase) => {
213209
+ throwIfAborted(options.signal);
213210
+ phaseStartedAt.set(phase, Date.now());
213211
+ dispatcher.emit({
213212
+ type: "phase_started",
213213
+ traceId,
213214
+ phase,
213215
+ timestamp: new Date().toISOString()
213216
+ });
213217
+ };
213218
+ const completePhase = (phase) => {
213219
+ dispatcher.emit({
213220
+ type: "phase_completed",
213221
+ traceId,
213222
+ phase,
213223
+ durationMs: Math.max(0, Date.now() - (phaseStartedAt.get(phase) ?? Date.now())),
213224
+ timestamp: new Date().toISOString()
213225
+ });
213226
+ };
213227
+ const skipPhase = (phase, reason) => {
213228
+ dispatcher.emit({
213229
+ type: "phase_skipped",
213230
+ traceId,
213231
+ phase,
213232
+ reason,
213233
+ timestamp: new Date().toISOString()
213234
+ });
213235
+ };
213236
+ const reportReviewCompleted = async (result) => {
213237
+ dispatcher.emit({
213238
+ type: "review_completed",
213239
+ traceId,
213240
+ decision: result.decision,
213241
+ completionStatus: result.completion.status,
213242
+ durationMs: Math.max(0, Date.now() - startedAtEpochMs),
213243
+ timestamp: new Date().toISOString()
213244
+ });
213245
+ await dispatcher.flush();
213246
+ if (progressDeliveryFailure !== undefined) {
213247
+ result.audit.warnings = Array.from(new Set([
213248
+ ...result.audit.warnings ?? [],
213249
+ `PROGRESS_DELIVERY_FAILED: ${progressDeliveryFailure}`
213250
+ ]));
213251
+ }
213252
+ };
213253
+ const completeReview = async (result) => {
213254
+ await reportReviewCompleted(result);
213255
+ return result;
213256
+ };
213257
+ const reportReviewFailure = async (error51, trace) => {
213258
+ if (reviewFailureReported)
213259
+ return;
213260
+ reviewFailureReported = true;
213261
+ const timestamp = new Date().toISOString();
213262
+ if (error51 instanceof KyosoCancellationError) {
213263
+ dispatcher.emit({ type: "review_cancelled", traceId, timestamp });
213264
+ await trace?.write({ type: "review_cancelled", traceId, timestamp }).catch(() => {
213265
+ return;
213266
+ });
213267
+ } else {
213268
+ dispatcher.emit({
213269
+ type: "review_failed",
213270
+ traceId,
213271
+ ...error51 instanceof KyosoRequestError ? { errorCode: error51.code } : {},
213272
+ timestamp
213273
+ });
213274
+ await trace?.write({
213275
+ type: "review_failed",
213276
+ traceId,
213277
+ ...error51 instanceof KyosoRequestError ? { errorCode: error51.code } : {},
213278
+ timestamp
213279
+ }).catch(() => {
213280
+ return;
213281
+ });
213282
+ }
213283
+ await dispatcher.flush();
213284
+ };
213285
+ dispatcher.emit({
213286
+ type: "review_started",
213287
+ traceId,
213288
+ tool,
213289
+ timestamp: new Date().toISOString()
213290
+ });
212450
213291
  try {
212451
- assertNotChildAgent(options.env ?? process.env);
212452
- } catch (error51) {
212453
- if (error51 instanceof KyosoRequestError) {
212454
- const config2 = kyosoConfigSchema.parse(defaultConfig);
212455
- const reviewBudget = resolveReviewBudget(config2.reviewBudget, undefined);
212456
- const budgetTracker = new ReviewBudgetTracker(reviewBudget, startedAtEpochMs, configuredReviewModelCallPlan(config2, reviewBudget, options.env ?? process.env));
212457
- const requestFingerprint = createRequestFingerprint({
213292
+ try {
213293
+ throwIfAborted(options.signal);
213294
+ assertNotChildAgent(options.env ?? process.env);
213295
+ } catch (error51) {
213296
+ if (error51 instanceof KyosoRequestError) {
213297
+ const config2 = kyosoConfigSchema.parse(defaultConfig);
213298
+ const reviewBudget = resolveReviewBudget(config2.reviewBudget, undefined);
213299
+ const budgetTracker = new ReviewBudgetTracker(reviewBudget, startedAtEpochMs, configuredReviewModelCallPlan(config2, reviewBudget, options.env ?? process.env));
213300
+ const requestFingerprint = createRequestFingerprint({
213301
+ tool,
213302
+ request: requestForRecursionFingerprint(request),
213303
+ config: config2,
213304
+ roles: resolveAgentRoles(config2),
213305
+ budget: reviewBudget,
213306
+ entrypoint: options.entrypoint
213307
+ });
213308
+ const trace2 = traceWriterFactory({
213309
+ enabled: config2.audit.enabled,
213310
+ directory: config2.audit.directory,
213311
+ traceId,
213312
+ cwd,
213313
+ env: auditEnv
213314
+ });
213315
+ activeTrace = trace2;
213316
+ writeProgressDeliveryFailure = (reason) => {
213317
+ trace2.write({
213318
+ type: "progress_delivery_failed",
213319
+ traceId,
213320
+ reason,
213321
+ timestamp: new Date().toISOString()
213322
+ }).catch(() => {
213323
+ return;
213324
+ });
213325
+ };
213326
+ if (progressDeliveryFailure !== undefined) {
213327
+ writeProgressDeliveryFailure(progressDeliveryFailure);
213328
+ }
213329
+ try {
213330
+ await trace2.write({
213331
+ type: "request_received",
213332
+ traceId,
213333
+ tool,
213334
+ timestamp: new Date().toISOString()
213335
+ });
213336
+ await writeReviewBudgetPlanned({
213337
+ trace: trace2,
213338
+ traceId,
213339
+ budgetTracker,
213340
+ requestFingerprint
213341
+ });
213342
+ return await completeReview(await buildPolicyBlockResult({
213343
+ tool,
213344
+ trace: trace2,
213345
+ traceId,
213346
+ startedAt,
213347
+ networkMode: config2.network.defaultMode,
213348
+ cisaPolicy: config2.securityReview.cisaSecureByDesign,
213349
+ warning: error51.message,
213350
+ budgetTracker,
213351
+ requestFingerprint,
213352
+ coverage: unavailableReviewCoverage(requestForRecursionFingerprint(request), "recursive invocation blocked before agent execution", config2.reviewPolicy.additionalLenses),
213353
+ finding: {
213354
+ id: "KYOSO-1",
213355
+ severity: "critical",
213356
+ category: "other",
213357
+ title: "Recursive Kyoso invocation blocked",
213358
+ evidence: error51.message,
213359
+ recommendation: "Do not expose Kyoso MCP tools to Kyoso child agents.",
213360
+ disposition: "gate",
213361
+ changeRelation: "unknown",
213362
+ evidenceQuality: "concrete",
213363
+ evidenceRefs: [],
213364
+ policyReasons: ["kyoso_policy", "recursive_invocation"],
213365
+ fingerprint: "",
213366
+ sourceAgents: ["kyoso_policy"],
213367
+ confidence: "high"
213368
+ },
213369
+ redactionsApplied: 0
213370
+ }));
213371
+ } finally {
213372
+ await trace2.finalize();
213373
+ }
213374
+ }
213375
+ throw error51;
213376
+ }
213377
+ startPhase("preflight");
213378
+ const baseLoaded = options.config !== undefined ? {
213379
+ config: options.config,
213380
+ configHash: options.configHash,
213381
+ configTrustStatus: "trusted",
213382
+ sources: [],
213383
+ warnings: []
213384
+ } : await loadConfig({
213385
+ cwd,
213386
+ configPath: options.configPath,
213387
+ ignoreConfig: options.ignoreConfig,
213388
+ trustConfig: options.trustConfig,
213389
+ allowUnknownConfig: options.allowUnknownConfig,
213390
+ promptForTrust: options.promptForTrust,
213391
+ trustStorePath: options.trustStorePath,
213392
+ env: options.env,
213393
+ trustPrompt: options.trustPrompt
213394
+ });
213395
+ const loaded = options.configOverrides && options.configOverrides.length > 0 ? {
213396
+ ...baseLoaded,
213397
+ config: applyConfigOverrides(baseLoaded.config, options.configOverrides)
213398
+ } : baseLoaded;
213399
+ completePhase("preflight");
213400
+ const trace = traceWriterFactory({
213401
+ enabled: loaded.config.audit.enabled,
213402
+ directory: loaded.config.audit.directory,
213403
+ traceId,
213404
+ cwd,
213405
+ includeRawAgentOutput: loaded.config.audit.includeRawAgentOutput,
213406
+ env: auditEnv
213407
+ });
213408
+ activeTrace = trace;
213409
+ const warnings = [...loaded.warnings, ...trace.warnings];
213410
+ writeProgressDeliveryFailure = (reason) => {
213411
+ warnings.push(`PROGRESS_DELIVERY_FAILED: ${reason}`);
213412
+ trace.write({
213413
+ type: "progress_delivery_failed",
213414
+ traceId,
213415
+ reason,
213416
+ timestamp: new Date().toISOString()
213417
+ }).catch(() => {
213418
+ return;
213419
+ });
213420
+ };
213421
+ if (progressDeliveryFailure !== undefined) {
213422
+ writeProgressDeliveryFailure(progressDeliveryFailure);
213423
+ }
213424
+ try {
213425
+ await trace.write({
213426
+ type: "request_received",
213427
+ traceId,
212458
213428
  tool,
212459
- request: requestForRecursionFingerprint(request),
212460
- config: config2,
212461
- roles: resolveAgentRoles(config2),
212462
- budget: reviewBudget,
212463
- entrypoint: options.entrypoint
213429
+ timestamp: new Date().toISOString()
212464
213430
  });
212465
- const trace2 = traceWriterFactory({
212466
- enabled: config2.audit.enabled,
212467
- directory: config2.audit.directory,
213431
+ await trace.write({
213432
+ type: "config_loaded",
212468
213433
  traceId,
212469
- cwd,
212470
- env: auditEnv
213434
+ configHash: loaded.configHash,
213435
+ configPath: loaded.configPath,
213436
+ configSources: loaded.sources,
213437
+ configTrustStatus: loaded.configTrustStatus,
213438
+ timestamp: new Date().toISOString()
212471
213439
  });
212472
- try {
212473
- await trace2.write({
212474
- type: "request_received",
212475
- traceId,
213440
+ validateReviewRequest(tool, request);
213441
+ const reviewBudget = resolveReviewBudget(loaded.config.reviewBudget, request.options?.reviewBudget);
213442
+ const budgetTracker = new ReviewBudgetTracker(reviewBudget, startedAtEpochMs, configuredReviewModelCallPlan(loaded.config, reviewBudget, options.env ?? process.env, request.options?.judgeProvider));
213443
+ assertTrustedWorkspaceRoot(request.workspace?.root, loaded.config.workspace.root, cwd);
213444
+ const networkMode = resolveNetworkMode(request.options?.network, loaded.config.network.defaultMode, options.mcpNetworkMode);
213445
+ if (networkMode === "unrestricted" && !loaded.config.network.allowUnrestricted) {
213446
+ throw new KyosoRequestError("unrestricted network mode is disabled by config", "NETWORK_MODE_DISABLED");
213447
+ }
213448
+ const disabledPolicy = disabledReviewPolicy(tool, loaded.config, options.entrypoint);
213449
+ if (disabledPolicy) {
213450
+ const redactedRequest = requestForRecursionFingerprint(request);
213451
+ const requestFingerprint2 = createRequestFingerprint({
212476
213452
  tool,
212477
- timestamp: new Date().toISOString()
213453
+ request: redactedRequest,
213454
+ config: loaded.config,
213455
+ roles: resolveAgentRoles(loaded.config),
213456
+ budget: reviewBudget,
213457
+ entrypoint: options.entrypoint
212478
213458
  });
212479
213459
  await writeReviewBudgetPlanned({
212480
- trace: trace2,
213460
+ trace,
212481
213461
  traceId,
212482
213462
  budgetTracker,
212483
- requestFingerprint
213463
+ requestFingerprint: requestFingerprint2
212484
213464
  });
212485
- return await buildPolicyBlockResult({
213465
+ const warning = disabledPolicy.warning;
213466
+ return await completeReview(await buildPolicyBlockResult({
212486
213467
  tool,
212487
- trace: trace2,
213468
+ trace,
212488
213469
  traceId,
212489
213470
  startedAt,
212490
- networkMode: config2.network.defaultMode,
212491
- cisaPolicy: config2.securityReview.cisaSecureByDesign,
212492
- warning: error51.message,
213471
+ configHash: loaded.configHash,
213472
+ networkMode,
213473
+ cisaPolicy: loaded.config.securityReview.cisaSecureByDesign,
213474
+ warning,
212493
213475
  budgetTracker,
212494
- requestFingerprint,
212495
- coverage: unavailableReviewCoverage(requestForRecursionFingerprint(request), "recursive invocation blocked before agent execution", config2.reviewPolicy.additionalLenses),
213476
+ requestFingerprint: requestFingerprint2,
213477
+ coverage: unavailableReviewCoverage(redactedRequest, disabledPolicy.coverageReason, loaded.config.reviewPolicy.additionalLenses),
212496
213478
  finding: {
212497
213479
  id: "KYOSO-1",
212498
213480
  severity: "critical",
212499
213481
  category: "other",
212500
- title: "Recursive Kyoso invocation blocked",
212501
- evidence: error51.message,
212502
- recommendation: "Do not expose Kyoso MCP tools to Kyoso child agents.",
213482
+ title: disabledPolicy.title,
213483
+ evidence: warning,
213484
+ recommendation: disabledPolicy.recommendation,
212503
213485
  disposition: "gate",
212504
213486
  changeRelation: "unknown",
212505
213487
  evidenceQuality: "concrete",
212506
213488
  evidenceRefs: [],
212507
- policyReasons: ["kyoso_policy", "recursive_invocation"],
213489
+ policyReasons: ["kyoso_policy", disabledPolicy.policyReason],
212508
213490
  fingerprint: "",
212509
213491
  sourceAgents: ["kyoso_policy"],
212510
213492
  confidence: "high"
212511
213493
  },
212512
213494
  redactionsApplied: 0
213495
+ }));
213496
+ }
213497
+ if (networkMode === "unrestricted" && loaded.config.network.warnOnUnrestricted) {
213498
+ warnings.push("Network mode is unrestricted; write policy remains denied.");
213499
+ }
213500
+ startPhase("context");
213501
+ const secretScan = scanAndRedactSecrets(request);
213502
+ await trace.write({
213503
+ type: "secret_scan_completed",
213504
+ traceId,
213505
+ detected: secretScan.detected,
213506
+ redactions: secretScan.redactions,
213507
+ timestamp: new Date().toISOString()
213508
+ });
213509
+ const allowSecretOverride = loaded.config.secrets.allowOverride && request.options?.allowSecretRedaction === true;
213510
+ if (secretScan.detected && loaded.config.secrets.blockOnDetectedSecret && !allowSecretOverride) {
213511
+ const requestFingerprint2 = createRequestFingerprint({
213512
+ tool,
213513
+ request: secretScan.redactedRequest,
213514
+ config: loaded.config,
213515
+ roles: resolveAgentRoles(loaded.config),
213516
+ budget: reviewBudget,
213517
+ entrypoint: options.entrypoint
212513
213518
  });
212514
- } finally {
212515
- await trace2.finalize();
213519
+ await writeReviewBudgetPlanned({
213520
+ trace,
213521
+ traceId,
213522
+ budgetTracker,
213523
+ requestFingerprint: requestFingerprint2
213524
+ });
213525
+ skipPhase("context", "secret_detected");
213526
+ return await completeReview(await buildSecretBlockResult({
213527
+ tool,
213528
+ trace,
213529
+ traceId,
213530
+ startedAt,
213531
+ configHash: loaded.configHash,
213532
+ networkMode,
213533
+ cisaPolicy: loaded.config.securityReview.cisaSecureByDesign,
213534
+ additionalLenses: loaded.config.reviewPolicy.additionalLenses,
213535
+ secretScan,
213536
+ warnings,
213537
+ budgetTracker,
213538
+ requestFingerprint: requestFingerprint2
213539
+ }));
212516
213540
  }
212517
- }
212518
- throw error51;
212519
- }
212520
- const baseLoaded = options.config !== undefined ? {
212521
- config: options.config,
212522
- configHash: options.configHash,
212523
- configTrustStatus: "trusted",
212524
- sources: [],
212525
- warnings: []
212526
- } : await loadConfig({
212527
- cwd,
212528
- configPath: options.configPath,
212529
- ignoreConfig: options.ignoreConfig,
212530
- trustConfig: options.trustConfig,
212531
- allowUnknownConfig: options.allowUnknownConfig,
212532
- promptForTrust: options.promptForTrust,
212533
- trustStorePath: options.trustStorePath,
212534
- env: options.env,
212535
- trustPrompt: options.trustPrompt
212536
- });
212537
- const loaded = options.configOverrides && options.configOverrides.length > 0 ? {
212538
- ...baseLoaded,
212539
- config: applyConfigOverrides(baseLoaded.config, options.configOverrides)
212540
- } : baseLoaded;
212541
- const trace = traceWriterFactory({
212542
- enabled: loaded.config.audit.enabled,
212543
- directory: loaded.config.audit.directory,
212544
- traceId,
212545
- cwd,
212546
- includeRawAgentOutput: loaded.config.audit.includeRawAgentOutput,
212547
- env: auditEnv
212548
- });
212549
- const warnings = [...loaded.warnings, ...trace.warnings];
212550
- try {
212551
- await trace.write({
212552
- type: "request_received",
212553
- traceId,
212554
- tool,
212555
- timestamp: new Date().toISOString()
212556
- });
212557
- await trace.write({
212558
- type: "config_loaded",
212559
- traceId,
212560
- configHash: loaded.configHash,
212561
- configPath: loaded.configPath,
212562
- configSources: loaded.sources,
212563
- configTrustStatus: loaded.configTrustStatus,
212564
- timestamp: new Date().toISOString()
212565
- });
212566
- validateReviewRequest(tool, request);
212567
- const reviewBudget = resolveReviewBudget(loaded.config.reviewBudget, request.options?.reviewBudget);
212568
- const budgetTracker = new ReviewBudgetTracker(reviewBudget, startedAtEpochMs, configuredReviewModelCallPlan(loaded.config, reviewBudget, options.env ?? process.env, request.options?.judgeProvider));
212569
- assertTrustedWorkspaceRoot(request.workspace?.root, loaded.config.workspace.root, cwd);
212570
- const networkMode = resolveNetworkMode(request.options?.network, loaded.config.network.defaultMode, options.mcpNetworkMode);
212571
- if (networkMode === "unrestricted" && !loaded.config.network.allowUnrestricted) {
212572
- throw new KyosoRequestError("unrestricted network mode is disabled by config", "NETWORK_MODE_DISABLED");
212573
- }
212574
- const disabledPolicy = disabledReviewPolicy(tool, loaded.config, options.entrypoint);
212575
- if (disabledPolicy) {
212576
- const redactedRequest = requestForRecursionFingerprint(request);
212577
- const requestFingerprint2 = createRequestFingerprint({
213541
+ const denyPatterns = mergeDenyPatterns(loaded.config.workspace.deny, secretScan.redactedRequest.workspace?.denyRead);
213542
+ const allowPatterns = secretScan.redactedRequest.workspace?.allowRead ?? [];
213543
+ const built = buildContext(secretScan.redactedRequest, {
213544
+ maxContextBytes: loaded.config.workspace.maxContextBytes,
213545
+ maxDiffBytes: loaded.config.workspace.maxDiffBytes,
213546
+ denyPatterns,
213547
+ allowPatterns
213548
+ });
213549
+ warnings.push(...built.warnings);
213550
+ completePhase("context");
213551
+ const agentRoles = resolveAgentRoles(loaded.config);
213552
+ const requestFingerprint = createRequestFingerprint({
212578
213553
  tool,
212579
- request: redactedRequest,
213554
+ request: built.request,
212580
213555
  config: loaded.config,
212581
- roles: resolveAgentRoles(loaded.config),
213556
+ roles: agentRoles,
212582
213557
  budget: reviewBudget,
212583
213558
  entrypoint: options.entrypoint
212584
213559
  });
@@ -212586,370 +213561,308 @@ async function runReview(tool, request, options = {}) {
212586
213561
  trace,
212587
213562
  traceId,
212588
213563
  budgetTracker,
212589
- requestFingerprint: requestFingerprint2
213564
+ requestFingerprint
212590
213565
  });
212591
- const warning = disabledPolicy.warning;
212592
- return await buildPolicyBlockResult({
212593
- tool,
212594
- trace,
212595
- traceId,
212596
- startedAt,
212597
- configHash: loaded.configHash,
212598
- networkMode,
212599
- cisaPolicy: loaded.config.securityReview.cisaSecureByDesign,
212600
- warning,
212601
- budgetTracker,
212602
- requestFingerprint: requestFingerprint2,
212603
- coverage: unavailableReviewCoverage(redactedRequest, disabledPolicy.coverageReason, loaded.config.reviewPolicy.additionalLenses),
212604
- finding: {
212605
- id: "KYOSO-1",
212606
- severity: "critical",
212607
- category: "other",
212608
- title: disabledPolicy.title,
212609
- evidence: warning,
212610
- recommendation: disabledPolicy.recommendation,
212611
- disposition: "gate",
212612
- changeRelation: "unknown",
212613
- evidenceQuality: "concrete",
212614
- evidenceRefs: [],
212615
- policyReasons: ["kyoso_policy", disabledPolicy.policyReason],
212616
- fingerprint: "",
212617
- sourceAgents: ["kyoso_policy"],
212618
- confidence: "high"
212619
- },
212620
- redactionsApplied: 0
212621
- });
212622
- }
212623
- if (networkMode === "unrestricted" && loaded.config.network.warnOnUnrestricted) {
212624
- warnings.push("Network mode is unrestricted; write policy remains denied.");
212625
- }
212626
- const secretScan = scanAndRedactSecrets(request);
212627
- await trace.write({
212628
- type: "secret_scan_completed",
212629
- traceId,
212630
- detected: secretScan.detected,
212631
- redactions: secretScan.redactions,
212632
- timestamp: new Date().toISOString()
212633
- });
212634
- const allowSecretOverride = loaded.config.secrets.allowOverride && request.options?.allowSecretRedaction === true;
212635
- if (secretScan.detected && loaded.config.secrets.blockOnDetectedSecret && !allowSecretOverride) {
212636
- const requestFingerprint2 = createRequestFingerprint({
212637
- tool,
212638
- request: secretScan.redactedRequest,
212639
- config: loaded.config,
212640
- roles: resolveAgentRoles(loaded.config),
212641
- budget: reviewBudget,
212642
- entrypoint: options.entrypoint
213566
+ warnings.push(...plannedBudgetWarnings(budgetTracker));
213567
+ startPhase("snapshot");
213568
+ snapshot = await createSnapshot(traceId, tool, built.request, {
213569
+ denyPatterns,
213570
+ allowPatterns,
213571
+ agentRoles
212643
213572
  });
212644
- await writeReviewBudgetPlanned({
212645
- trace,
213573
+ await trace.write({
213574
+ type: "snapshot_created",
212646
213575
  traceId,
212647
- budgetTracker,
212648
- requestFingerprint: requestFingerprint2
213576
+ path: snapshot.root,
213577
+ fileCount: snapshot.fileCount,
213578
+ timestamp: new Date().toISOString()
212649
213579
  });
212650
- return await buildSecretBlockResult({
213580
+ completePhase("snapshot");
213581
+ const manager = options.agentManager ?? defaultAgentManager(loaded.config, options.env ?? process.env);
213582
+ startPhase("primary");
213583
+ const agentResults = await runAgents({
212651
213584
  tool,
212652
- trace,
213585
+ request: built.request,
213586
+ config: loaded.config,
212653
213587
  traceId,
212654
- startedAt,
212655
- configHash: loaded.configHash,
213588
+ workspaceDir: snapshot.root,
212656
213589
  networkMode,
212657
- cisaPolicy: loaded.config.securityReview.cisaSecureByDesign,
212658
- additionalLenses: loaded.config.reviewPolicy.additionalLenses,
212659
- secretScan,
213590
+ manager,
213591
+ trace,
212660
213592
  warnings,
212661
213593
  budgetTracker,
212662
- requestFingerprint: requestFingerprint2
213594
+ progressDispatcher: dispatcher,
213595
+ signal: options.signal,
213596
+ progressHeartbeatMs: options.progressHeartbeatMs
212663
213597
  });
212664
- }
212665
- const denyPatterns = mergeDenyPatterns(loaded.config.workspace.deny, secretScan.redactedRequest.workspace?.denyRead);
212666
- const allowPatterns = secretScan.redactedRequest.workspace?.allowRead ?? [];
212667
- const built = buildContext(secretScan.redactedRequest, {
212668
- maxContextBytes: loaded.config.workspace.maxContextBytes,
212669
- maxDiffBytes: loaded.config.workspace.maxDiffBytes,
212670
- denyPatterns,
212671
- allowPatterns
212672
- });
212673
- warnings.push(...built.warnings);
212674
- const agentRoles = resolveAgentRoles(loaded.config);
212675
- const requestFingerprint = createRequestFingerprint({
212676
- tool,
212677
- request: built.request,
212678
- config: loaded.config,
212679
- roles: agentRoles,
212680
- budget: reviewBudget,
212681
- entrypoint: options.entrypoint
212682
- });
212683
- await writeReviewBudgetPlanned({
212684
- trace,
212685
- traceId,
212686
- budgetTracker,
212687
- requestFingerprint
212688
- });
212689
- warnings.push(...plannedBudgetWarnings(budgetTracker));
212690
- snapshot = await createSnapshot(traceId, tool, built.request, {
212691
- denyPatterns,
212692
- allowPatterns,
212693
- agentRoles
212694
- });
212695
- await trace.write({
212696
- type: "snapshot_created",
212697
- traceId,
212698
- path: snapshot.root,
212699
- fileCount: snapshot.fileCount,
212700
- timestamp: new Date().toISOString()
212701
- });
212702
- const manager = options.agentManager ?? defaultAgentManager(loaded.config, options.env ?? process.env);
212703
- const agentResults = await runAgents({
212704
- tool,
212705
- request: built.request,
212706
- config: loaded.config,
212707
- traceId,
212708
- workspaceDir: snapshot.root,
212709
- networkMode,
212710
- manager,
212711
- trace,
212712
- warnings,
212713
- budgetTracker
212714
- });
212715
- warnings.push(...agentResults.flatMap((result) => (result.warnings ?? []).map((warning) => `Agent ${result.agent} ${warning}`)));
212716
- const normalizedAgentResults = agentResults.map((result) => normalizeAgentRunResult(result, reviewBudget.maxFindingsPerAgent));
212717
- for (const result of normalizedAgentResults.filter((item) => item.findingsTargetExceeded)) {
212718
- warnings.push(`Agent ${result.agent} reported ${result.reportedFindings} findings, above the soft target of ${reviewBudget.maxFindingsPerAgent}; all findings were retained.`);
212719
- }
212720
- const enabledAgents = ["codex", "claude"].filter((agent) => loaded.config.agents[agent].enabled);
212721
- const agentsUsed = normalizedAgentResults.filter((result) => result.status !== "skipped").map((result) => result.agent);
212722
- const reviewMode = enabledAgents.length === 1 ? "single_agent" : "multi_agent";
212723
- const completed = normalizedAgentResults.filter((result) => result.status === "completed");
212724
- const attempted = normalizedAgentResults.filter((result) => result.status !== "skipped");
212725
- const degraded = completed.length !== attempted.length || enabledAgents.length === 0;
212726
- const coverage = buildReviewCoverage({
212727
- request: built.request,
212728
- additionalLenses: loaded.config.reviewPolicy.additionalLenses,
212729
- agentResults: normalizedAgentResults
212730
- });
212731
- if (isCoverageIncomplete(coverage, {
212732
- multiAgentRequired: loaded.config.reviewPolicy.multiAgentRequired
212733
- })) {
212734
- budgetTracker.markIncomplete("coverage_incomplete");
212735
- warnings.push(formatCoverageWarning(coverage, loaded.config));
212736
- }
212737
- let aggregate = aggregateAgentResults(normalizedAgentResults, {
212738
- reviewMode
212739
- });
212740
- if (secretScan.detected && allowSecretOverride) {
213598
+ completePhase("primary");
213599
+ warnings.push(...agentResults.flatMap((result) => (result.warnings ?? []).map((warning) => `Agent ${result.agent} ${warning}`)));
213600
+ startPhase("aggregation");
213601
+ const normalizedAgentResults = agentResults.map((result) => normalizeAgentRunResult(result, reviewBudget.maxFindingsPerAgent));
213602
+ for (const result of normalizedAgentResults.filter((item) => item.findingsTargetExceeded)) {
213603
+ warnings.push(`Agent ${result.agent} reported ${result.reportedFindings} findings, above the soft target of ${reviewBudget.maxFindingsPerAgent}; all findings were retained.`);
213604
+ }
213605
+ const enabledAgents = ["codex", "claude"].filter((agent) => loaded.config.agents[agent].enabled);
213606
+ const agentsUsed = normalizedAgentResults.filter((result) => result.status !== "skipped").map((result) => result.agent);
213607
+ const reviewMode = enabledAgents.length === 1 ? "single_agent" : "multi_agent";
213608
+ const completed = normalizedAgentResults.filter((result) => result.status === "completed");
213609
+ const attempted = normalizedAgentResults.filter((result) => result.status !== "skipped");
213610
+ const degraded = completed.length !== attempted.length || enabledAgents.length === 0;
213611
+ const coverage = buildReviewCoverage({
213612
+ request: built.request,
213613
+ additionalLenses: loaded.config.reviewPolicy.additionalLenses,
213614
+ agentResults: normalizedAgentResults
213615
+ });
213616
+ if (isCoverageIncomplete(coverage, {
213617
+ multiAgentRequired: loaded.config.reviewPolicy.multiAgentRequired
213618
+ })) {
213619
+ budgetTracker.markIncomplete("coverage_incomplete");
213620
+ warnings.push(formatCoverageWarning(coverage, loaded.config));
213621
+ }
213622
+ let aggregate = aggregateAgentResults(normalizedAgentResults, {
213623
+ reviewMode
213624
+ });
213625
+ if (secretScan.detected && allowSecretOverride) {
213626
+ aggregate = {
213627
+ ...aggregate,
213628
+ findings: reindexFindings([
213629
+ buildSecretFinding(secretScan, {
213630
+ id: "KYOSO-1",
213631
+ blocked: false
213632
+ }),
213633
+ ...aggregate.findings
213634
+ ])
213635
+ };
213636
+ }
213637
+ if (completed.length === 0 && (attempted.length > 0 || enabledAgents.length === 0)) {
213638
+ const noPrimaryAgents = enabledAgents.length === 0;
213639
+ if (noPrimaryAgents) {
213640
+ budgetTracker.markIncomplete("coverage_incomplete");
213641
+ warnings.push("No primary review agents are enabled; review coverage is incomplete.");
213642
+ }
213643
+ aggregate = {
213644
+ ...aggregate,
213645
+ findings: [
213646
+ ...aggregate.findings,
213647
+ {
213648
+ id: `KYOSO-${aggregate.findings.length + 1}`,
213649
+ severity: "critical",
213650
+ category: "other",
213651
+ title: noPrimaryAgents ? "No primary review agents enabled" : "All backend agents failed",
213652
+ evidence: noPrimaryAgents ? "Both configured primary reviewers are disabled." : normalizedAgentResults.map((result) => `${result.agent}: ${result.error?.code ?? result.status}`).join("; "),
213653
+ recommendation: noPrimaryAgents ? "Enable at least one primary reviewer before running Kyoso." : "Run kyoso doctor and retry after agent authentication or adapter issues are fixed.",
213654
+ disposition: "gate",
213655
+ changeRelation: "unknown",
213656
+ evidenceQuality: "concrete",
213657
+ evidenceRefs: [],
213658
+ policyReasons: ["kyoso_policy", "coverage_incomplete"],
213659
+ fingerprint: "",
213660
+ sourceAgents: ["kyoso_policy"],
213661
+ confidence: "high"
213662
+ }
213663
+ ]
213664
+ };
213665
+ }
212741
213666
  aggregate = {
212742
213667
  ...aggregate,
212743
- findings: reindexFindings([
212744
- buildSecretFinding(secretScan, {
212745
- id: "KYOSO-1",
212746
- blocked: false
212747
- }),
212748
- ...aggregate.findings
212749
- ])
213668
+ findings: admitFindings({
213669
+ tool,
213670
+ request: built.request,
213671
+ findings: aggregate.findings,
213672
+ reviewMode
213673
+ })
212750
213674
  };
212751
- }
212752
- if (completed.length === 0 && (attempted.length > 0 || enabledAgents.length === 0)) {
212753
- const noPrimaryAgents = enabledAgents.length === 0;
212754
- if (noPrimaryAgents) {
212755
- budgetTracker.markIncomplete("coverage_incomplete");
212756
- warnings.push("No primary review agents are enabled; review coverage is incomplete.");
213675
+ await trace.write({
213676
+ type: "aggregation_completed",
213677
+ traceId,
213678
+ findingCount: aggregate.findings.length,
213679
+ timestamp: new Date().toISOString()
213680
+ });
213681
+ completePhase("aggregation");
213682
+ const verificationMode = loaded.config.verification.enabled && reviewMode === "single_agent" ? "skipped_single_agent" : loaded.config.verification.enabled && enabledAgents.length > 1 ? "cross_agent" : undefined;
213683
+ if (verificationMode === "cross_agent") {
213684
+ startPhase("verification");
213685
+ warnings.push(...await runFindingVerification({
213686
+ tool,
213687
+ request: built.request,
213688
+ config: loaded.config,
213689
+ traceId,
213690
+ workspaceDir: snapshot.root,
213691
+ networkMode,
213692
+ manager,
213693
+ trace,
213694
+ findings: aggregate.findings,
213695
+ budgetTracker,
213696
+ signal: options.signal
213697
+ }));
213698
+ completePhase("verification");
213699
+ } else {
213700
+ skipPhase("verification", verificationMode === "skipped_single_agent" ? "single_agent_review" : "verification_disabled");
212757
213701
  }
212758
213702
  aggregate = {
212759
213703
  ...aggregate,
212760
- findings: [
212761
- ...aggregate.findings,
212762
- {
212763
- id: `KYOSO-${aggregate.findings.length + 1}`,
212764
- severity: "critical",
212765
- category: "other",
212766
- title: noPrimaryAgents ? "No primary review agents enabled" : "All backend agents failed",
212767
- evidence: noPrimaryAgents ? "Both configured primary reviewers are disabled." : normalizedAgentResults.map((result) => `${result.agent}: ${result.error?.code ?? result.status}`).join("; "),
212768
- recommendation: noPrimaryAgents ? "Enable at least one primary reviewer before running Kyoso." : "Run kyoso doctor and retry after agent authentication or adapter issues are fixed.",
212769
- disposition: "gate",
212770
- changeRelation: "unknown",
212771
- evidenceQuality: "concrete",
212772
- evidenceRefs: [],
212773
- policyReasons: ["kyoso_policy", "coverage_incomplete"],
212774
- fingerprint: "",
212775
- sourceAgents: ["kyoso_policy"],
212776
- confidence: "high"
212777
- }
212778
- ]
213704
+ findings: admitFindings({
213705
+ tool,
213706
+ request: built.request,
213707
+ findings: aggregate.findings,
213708
+ reviewMode
213709
+ })
212779
213710
  };
212780
- }
212781
- aggregate = {
212782
- ...aggregate,
212783
- findings: admitFindings({
213711
+ if (aggregate.findings.some((finding) => finding.disposition === "disputed")) {
213712
+ budgetTracker.markIncomplete("disputed_finding");
213713
+ }
213714
+ const cisaPolicy = loaded.config.securityReview.cisaSecureByDesign;
213715
+ const cisa = tool === "security_review" && cisaPolicy.enabled ? computeCisaGate(aggregate.findings, normalizedAgentResults, cisaPolicy) : undefined;
213716
+ const budgetBeforeJudge = budgetTracker.snapshot();
213717
+ const decision = budgetBeforeJudge.completion.status === "incomplete" ? "block" : decide({
212784
213718
  tool,
212785
- request: built.request,
212786
213719
  findings: aggregate.findings,
212787
- reviewMode
212788
- })
212789
- };
212790
- await trace.write({
212791
- type: "aggregation_completed",
212792
- traceId,
212793
- findingCount: aggregate.findings.length,
212794
- timestamp: new Date().toISOString()
212795
- });
212796
- const verificationMode = loaded.config.verification.enabled && reviewMode === "single_agent" ? "skipped_single_agent" : loaded.config.verification.enabled && enabledAgents.length > 1 ? "cross_agent" : undefined;
212797
- if (verificationMode === "cross_agent") {
212798
- warnings.push(...await runFindingVerification({
213720
+ cisa: cisaPolicy.gate ? cisa : undefined,
213721
+ degraded,
213722
+ secretScan: { detected: secretScan.detected, blocked: false }
213723
+ });
213724
+ const completedAt = new Date().toISOString();
213725
+ const resultWithoutMarkdown = {
213726
+ decision,
213727
+ completion: budgetBeforeJudge.completion,
213728
+ executionBudget: budgetBeforeJudge.executionBudget,
213729
+ requestFingerprint,
213730
+ degraded,
213731
+ agentsUsed,
213732
+ reviewMode,
213733
+ coverage,
213734
+ ...verificationMode ? { verificationMode } : {},
213735
+ findings: aggregate.findings,
213736
+ cisaSecureByDesign: cisa,
213737
+ disagreements: aggregate.disagreements,
213738
+ testsToAdd: selectRegressionTests(aggregate.testsToAdd),
213739
+ residualRisks: tool === "security_review" && aggregate.residualRisks.length === 0 ? [
213740
+ "No residual risks were reported by completed agents; verify security assumptions before release."
213741
+ ] : aggregate.residualRisks,
213742
+ openQuestions: Array.from(new Set([
213743
+ ...aggregate.openQuestions,
213744
+ ...buildAdmissionOpenQuestions(aggregate.findings)
213745
+ ])),
213746
+ agentOpinions: normalizedAgentResults.map((result) => agentOpinionSummary(result, request.options?.includeAgentRawOutputs === true)),
213747
+ audit: {
213748
+ traceId,
213749
+ startedAt,
213750
+ completedAt,
213751
+ agentsUsed,
213752
+ redactionsApplied: secretScan.redactions,
213753
+ networkMode,
213754
+ workspaceMode: "temp_snapshot",
213755
+ configHash: loaded.configHash,
213756
+ warnings: Array.from(new Set([...warnings, ...trace.warnings])),
213757
+ modelCalls: budgetBeforeJudge.modelCalls
213758
+ }
213759
+ };
213760
+ const summaryText = defaultSummaryText(resultWithoutMarkdown);
213761
+ startPhase("judge");
213762
+ const judge = await runBudgetedJudge({
212799
213763
  tool,
212800
- request: built.request,
212801
- config: loaded.config,
212802
- traceId,
212803
- workspaceDir: snapshot.root,
212804
- networkMode,
212805
- manager,
213764
+ result: resultWithoutMarkdown,
213765
+ summaryText,
213766
+ agentFindings: buildJudgeAgentFindings(normalizedAgentResults),
213767
+ config: loaded.config.judge,
213768
+ requestedProvider: request.options?.judgeProvider,
213769
+ env: options.env ?? process.env,
213770
+ budgetTracker,
212806
213771
  trace,
212807
- findings: aggregate.findings,
212808
- budgetTracker
213772
+ traceId,
213773
+ signal: options.signal
213774
+ });
213775
+ completePhase("judge");
213776
+ startPhase("finalize");
213777
+ const judgeComments = new Map(judge.output.disagreementComments.map((comment) => [
213778
+ comment.topic,
213779
+ comment.judgeComment
213780
+ ]));
213781
+ const disagreements = resultWithoutMarkdown.disagreements.map((disagreement) => ({
213782
+ ...disagreement,
213783
+ judgeComment: judgeComments.get(disagreement.topic) ?? disagreement.judgeComment
212809
213784
  }));
212810
- }
212811
- aggregate = {
212812
- ...aggregate,
212813
- findings: admitFindings({
213785
+ const crossModelAnalysis = buildCrossModelAnalysis(judge, reviewMode);
213786
+ const budgetAfterJudge = budgetTracker.snapshot();
213787
+ const finalWarnings = Array.from(new Set([
213788
+ ...resultWithoutMarkdown.audit.warnings ?? [],
213789
+ ...outputWarningMessages(budgetAfterJudge),
213790
+ ...tokenUsageWarningMessages(budgetTracker, budgetAfterJudge)
213791
+ ]));
213792
+ const finalDecision = budgetAfterJudge.completion.status === "incomplete" ? "block" : decision;
213793
+ const resultAfterJudge = {
213794
+ ...resultWithoutMarkdown,
213795
+ decision: finalDecision,
213796
+ completion: budgetAfterJudge.completion,
213797
+ executionBudget: budgetAfterJudge.executionBudget,
213798
+ disagreements,
213799
+ ...crossModelAnalysis ? { crossModelAnalysis } : {},
213800
+ audit: {
213801
+ ...resultWithoutMarkdown.audit,
213802
+ completedAt: new Date().toISOString(),
213803
+ warnings: finalWarnings,
213804
+ modelCalls: budgetAfterJudge.modelCalls
213805
+ }
213806
+ };
213807
+ const judgeEvent = {
213808
+ type: "judge_completed",
213809
+ traceId,
213810
+ provider: judge.provider,
213811
+ status: judge.status,
213812
+ ...judge.executionIdentity ? { executionIdentity: judge.executionIdentity } : {},
213813
+ timestamp: new Date().toISOString()
213814
+ };
213815
+ if (judge.error)
213816
+ judgeEvent.error = judge.error;
213817
+ await trace.write(judgeEvent);
213818
+ resultAfterJudge.audit.completedAt = new Date().toISOString();
213819
+ await writeReviewBudgetCompleted({
213820
+ trace,
213821
+ traceId,
213822
+ budgetTracker,
213823
+ requestFingerprint
213824
+ });
213825
+ await trace.write({
213826
+ type: "decision_completed",
213827
+ traceId,
213828
+ decision: finalDecision,
213829
+ timestamp: new Date().toISOString()
213830
+ });
213831
+ await trace.write({
213832
+ type: "response_sent",
213833
+ traceId,
213834
+ timestamp: new Date().toISOString()
213835
+ });
213836
+ completePhase("finalize");
213837
+ await reportReviewCompleted(resultAfterJudge);
213838
+ return await finalizeReviewResult({
212814
213839
  tool,
212815
- request: built.request,
212816
- findings: aggregate.findings,
212817
- reviewMode
212818
- })
212819
- };
212820
- if (aggregate.findings.some((finding) => finding.disposition === "disputed")) {
212821
- budgetTracker.markIncomplete("disputed_finding");
213840
+ trace,
213841
+ result: resultAfterJudge,
213842
+ summaryText: resultAfterJudge.completion.status === "incomplete" ? defaultSummaryText(resultAfterJudge) : judge.output.summaryText
213843
+ });
213844
+ } catch (error51) {
213845
+ await reportReviewFailure(error51, trace);
213846
+ throw error51;
213847
+ } finally {
213848
+ await trace.finalize();
213849
+ if (snapshot)
213850
+ await cleanupSnapshot(snapshot.root);
212822
213851
  }
212823
- const cisaPolicy = loaded.config.securityReview.cisaSecureByDesign;
212824
- const cisa = tool === "security_review" && cisaPolicy.enabled ? computeCisaGate(aggregate.findings, normalizedAgentResults, cisaPolicy) : undefined;
212825
- const budgetBeforeJudge = budgetTracker.snapshot();
212826
- const decision = budgetBeforeJudge.completion.status === "incomplete" ? "block" : decide({
212827
- tool,
212828
- findings: aggregate.findings,
212829
- cisa: cisaPolicy.gate ? cisa : undefined,
212830
- degraded,
212831
- secretScan: { detected: secretScan.detected, blocked: false }
212832
- });
212833
- const completedAt = new Date().toISOString();
212834
- const resultWithoutMarkdown = {
212835
- decision,
212836
- completion: budgetBeforeJudge.completion,
212837
- executionBudget: budgetBeforeJudge.executionBudget,
212838
- requestFingerprint,
212839
- degraded,
212840
- agentsUsed,
212841
- reviewMode,
212842
- coverage,
212843
- ...verificationMode ? { verificationMode } : {},
212844
- findings: aggregate.findings,
212845
- cisaSecureByDesign: cisa,
212846
- disagreements: aggregate.disagreements,
212847
- testsToAdd: selectRegressionTests(aggregate.testsToAdd),
212848
- residualRisks: tool === "security_review" && aggregate.residualRisks.length === 0 ? [
212849
- "No residual risks were reported by completed agents; verify security assumptions before release."
212850
- ] : aggregate.residualRisks,
212851
- openQuestions: Array.from(new Set([
212852
- ...aggregate.openQuestions,
212853
- ...buildAdmissionOpenQuestions(aggregate.findings)
212854
- ])),
212855
- agentOpinions: normalizedAgentResults.map((result) => agentOpinionSummary(result, request.options?.includeAgentRawOutputs === true)),
212856
- audit: {
212857
- traceId,
212858
- startedAt,
212859
- completedAt,
212860
- agentsUsed,
212861
- redactionsApplied: secretScan.redactions,
212862
- networkMode,
212863
- workspaceMode: "temp_snapshot",
212864
- configHash: loaded.configHash,
212865
- warnings: Array.from(new Set([...warnings, ...trace.warnings])),
212866
- modelCalls: budgetBeforeJudge.modelCalls
212867
- }
212868
- };
212869
- const summaryText = defaultSummaryText(resultWithoutMarkdown);
212870
- const judge = await runBudgetedJudge({
212871
- tool,
212872
- result: resultWithoutMarkdown,
212873
- summaryText,
212874
- agentFindings: buildJudgeAgentFindings(normalizedAgentResults),
212875
- config: loaded.config.judge,
212876
- requestedProvider: request.options?.judgeProvider,
212877
- env: options.env ?? process.env,
212878
- budgetTracker,
212879
- trace,
212880
- traceId
212881
- });
212882
- const judgeComments = new Map(judge.output.disagreementComments.map((comment) => [
212883
- comment.topic,
212884
- comment.judgeComment
212885
- ]));
212886
- const disagreements = resultWithoutMarkdown.disagreements.map((disagreement) => ({
212887
- ...disagreement,
212888
- judgeComment: judgeComments.get(disagreement.topic) ?? disagreement.judgeComment
212889
- }));
212890
- const crossModelAnalysis = buildCrossModelAnalysis(judge, reviewMode);
212891
- const budgetAfterJudge = budgetTracker.snapshot();
212892
- const finalWarnings = Array.from(new Set([
212893
- ...resultWithoutMarkdown.audit.warnings ?? [],
212894
- ...outputWarningMessages(budgetAfterJudge),
212895
- ...tokenUsageWarningMessages(budgetTracker, budgetAfterJudge)
212896
- ]));
212897
- const finalDecision = budgetAfterJudge.completion.status === "incomplete" ? "block" : decision;
212898
- const resultAfterJudge = {
212899
- ...resultWithoutMarkdown,
212900
- decision: finalDecision,
212901
- completion: budgetAfterJudge.completion,
212902
- executionBudget: budgetAfterJudge.executionBudget,
212903
- disagreements,
212904
- ...crossModelAnalysis ? { crossModelAnalysis } : {},
212905
- audit: {
212906
- ...resultWithoutMarkdown.audit,
212907
- completedAt: new Date().toISOString(),
212908
- warnings: finalWarnings,
212909
- modelCalls: budgetAfterJudge.modelCalls
212910
- }
212911
- };
212912
- const judgeEvent = {
212913
- type: "judge_completed",
212914
- traceId,
212915
- provider: judge.provider,
212916
- status: judge.status,
212917
- ...judge.executionIdentity ? { executionIdentity: judge.executionIdentity } : {},
212918
- timestamp: new Date().toISOString()
212919
- };
212920
- if (judge.error)
212921
- judgeEvent.error = judge.error;
212922
- await trace.write(judgeEvent);
212923
- resultAfterJudge.audit.completedAt = new Date().toISOString();
212924
- await writeReviewBudgetCompleted({
212925
- trace,
212926
- traceId,
212927
- budgetTracker,
212928
- requestFingerprint
212929
- });
212930
- await trace.write({
212931
- type: "decision_completed",
212932
- traceId,
212933
- decision: finalDecision,
212934
- timestamp: new Date().toISOString()
212935
- });
212936
- await trace.write({
212937
- type: "response_sent",
212938
- traceId,
212939
- timestamp: new Date().toISOString()
212940
- });
212941
- return await finalizeReviewResult({
212942
- tool,
212943
- trace,
212944
- result: resultAfterJudge,
212945
- summaryText: resultAfterJudge.completion.status === "incomplete" ? defaultSummaryText(resultAfterJudge) : judge.output.summaryText
212946
- });
212947
- } finally {
212948
- await trace.finalize();
212949
- if (snapshot)
212950
- await cleanupSnapshot(snapshot.root);
213852
+ } catch (error51) {
213853
+ await reportReviewFailure(error51, activeTrace);
213854
+ throw error51;
212951
213855
  }
212952
213856
  }
213857
+ function durationMsBetween(startedAt, completedAt) {
213858
+ if (!startedAt || !completedAt)
213859
+ return 0;
213860
+ const start = Date.parse(startedAt);
213861
+ const end = Date.parse(completedAt);
213862
+ if (!Number.isFinite(start) || !Number.isFinite(end))
213863
+ return 0;
213864
+ return Math.max(0, end - start);
213865
+ }
212953
213866
  async function runFindingVerification(input2) {
212954
213867
  const allowDemotionRequested = input2.config.verification.allowDemotion;
212955
213868
  const selection = selectVerificationTargets(input2.findings, input2.config.verification.maxFindings);
@@ -213111,6 +214024,7 @@ async function runFindingVerification(input2) {
213111
214024
  warnOutputBytes: input2.budgetTracker.budget.effectiveWarnAgentOutputBytes,
213112
214025
  maxOutputBytes: input2.budgetTracker.budget.maxAgentOutputBytes,
213113
214026
  networkMode: input2.networkMode,
214027
+ signal: input2.signal,
213114
214028
  onStarted: (executionIdentity) => {
213115
214029
  input2.budgetTracker.markStarted(group.reservation, executionIdentity);
213116
214030
  const event = buildAgentStartedEvent({
@@ -213128,6 +214042,8 @@ async function runFindingVerification(input2) {
213128
214042
  try {
213129
214043
  results = await input2.manager.runAll(agentInputs);
213130
214044
  } catch (error51) {
214045
+ if (error51 instanceof KyosoCancellationError)
214046
+ throw error51;
213131
214047
  for (const group of scheduledGroups) {
213132
214048
  applyVerificationVerdicts(group.targets, group.verifier, undefined);
213133
214049
  await finalizeModelCallResult({
@@ -213352,6 +214268,19 @@ async function recordSkippedJudgeCall(input2, reason) {
213352
214268
  async function runAgents(input2) {
213353
214269
  const agentRoles = resolveAgentRoles(input2.config);
213354
214270
  const enabledAgents = ["codex", "claude"].filter((agent) => input2.config.agents[agent].enabled);
214271
+ const openRouter = input2.config.agents.codex.openRouter;
214272
+ const hasOpenRouterRetryPolicy = Object.values(openRouter).some((value) => value !== undefined);
214273
+ if (enabledAgents.includes("codex") && input2.config.agents.codex.provider === CODEX_OPENROUTER_PROVIDER && hasOpenRouterRetryPolicy) {
214274
+ await input2.trace.write({
214275
+ type: "openrouter_retry_policy_resolved",
214276
+ traceId: input2.traceId,
214277
+ streamIdleTimeoutMs: openRouter.streamIdleTimeoutMs,
214278
+ streamMaxRetries: openRouter.streamMaxRetries,
214279
+ requestMaxRetries: openRouter.requestMaxRetries,
214280
+ source: "kyoso_config",
214281
+ timestamp: new Date().toISOString()
214282
+ });
214283
+ }
213355
214284
  if (enabledAgents.length === 0)
213356
214285
  return [];
213357
214286
  const reservationResult = input2.budgetTracker.reserveMany(enabledAgents.map((agent) => ({ kind: "primary", agent })));
@@ -213437,6 +214366,8 @@ async function runAgents(input2) {
213437
214366
  const agentInputs = enabledAgents.map((agent) => {
213438
214367
  const agentConfig = input2.config.agents[agent];
213439
214368
  const role = agentRoles[agent] ?? agentConfig.role;
214369
+ let emittedRetryProgressEvents = 0;
214370
+ let retryProgressLimitWarned = false;
213440
214371
  const reservation = reservations.get(agent);
213441
214372
  if (!reservation) {
213442
214373
  throw new Error(`Missing primary budget reservation for ${agent}.`);
@@ -213457,15 +214388,27 @@ async function runAgents(input2) {
213457
214388
  warnOutputBytes: input2.budgetTracker.budget.effectiveWarnAgentOutputBytes,
213458
214389
  maxOutputBytes: input2.budgetTracker.budget.maxAgentOutputBytes,
213459
214390
  networkMode: input2.networkMode,
214391
+ signal: input2.signal,
214392
+ heartbeatMs: input2.progressHeartbeatMs,
214393
+ ...agent === "codex" && input2.config.agents.codex.provider === CODEX_OPENROUTER_PROVIDER && openRouter.streamIdleTimeoutMs !== undefined ? { streamIdleTimeoutMs: openRouter.streamIdleTimeoutMs } : {},
213460
214394
  onStarted: (executionIdentity) => {
213461
214395
  input2.budgetTracker.markStarted(reservation, executionIdentity);
213462
214396
  if (!acceptingStartedEvents)
213463
214397
  return Promise.resolve();
214398
+ const progressExecutionIdentity = input2.budgetTracker.executionIdentity(reservation);
214399
+ input2.progressDispatcher.emit({
214400
+ type: "agent_started",
214401
+ traceId: input2.traceId,
214402
+ agent,
214403
+ role,
214404
+ ...progressExecutionIdentity ? { executionIdentity: progressExecutionIdentity } : {},
214405
+ timestamp: new Date().toISOString()
214406
+ });
213464
214407
  const event = buildAgentStartedEvent({
213465
214408
  traceId: input2.traceId,
213466
214409
  agent,
213467
214410
  role,
213468
- executionIdentity: input2.budgetTracker.executionIdentity(reservation)
214411
+ executionIdentity: progressExecutionIdentity
213469
214412
  });
213470
214413
  const write = (async () => {
213471
214414
  try {
@@ -213476,6 +214419,33 @@ async function runAgents(input2) {
213476
214419
  })();
213477
214420
  startedWrites.push(write);
213478
214421
  return write;
214422
+ },
214423
+ onProgress: (event) => {
214424
+ if (!acceptingStartedEvents)
214425
+ return;
214426
+ input2.progressDispatcher.emit({ ...event, traceId: input2.traceId });
214427
+ if (event.type !== "agent_retrying")
214428
+ return;
214429
+ if (emittedRetryProgressEvents >= MAX_AGENT_RETRY_PROGRESS_EVENTS) {
214430
+ if (!retryProgressLimitWarned) {
214431
+ retryProgressLimitWarned = true;
214432
+ input2.warnings.push(`AGENT_RETRY_PROGRESS_LIMIT: ${agent} emitted more than ${MAX_AGENT_RETRY_PROGRESS_EVENTS} retry progress events; later events were omitted from the audit trace.`);
214433
+ }
214434
+ return;
214435
+ }
214436
+ emittedRetryProgressEvents += 1;
214437
+ const write = (async () => {
214438
+ try {
214439
+ await input2.trace.write({
214440
+ ...event,
214441
+ traceId: input2.traceId,
214442
+ type: "agent_retrying"
214443
+ });
214444
+ } catch {
214445
+ input2.warnings.push("AUDIT_WRITE_FAILED: agent_retrying event could not be recorded.");
214446
+ }
214447
+ })();
214448
+ startedWrites.push(write);
213479
214449
  }
213480
214450
  };
213481
214451
  });
@@ -213483,6 +214453,8 @@ async function runAgents(input2) {
213483
214453
  try {
213484
214454
  results = await input2.manager.runAll(agentInputs);
213485
214455
  } catch (error51) {
214456
+ if (error51 instanceof KyosoCancellationError)
214457
+ throw error51;
213486
214458
  const detail = sanitizeTextForDisplay(error51 instanceof Error ? error51.message : String(error51));
213487
214459
  input2.warnings.push(`Primary-agent execution failed: ${detail}`);
213488
214460
  results = agentInputs.map((agentInput) => ({
@@ -213533,6 +214505,16 @@ async function runAgents(input2) {
213533
214505
  if (result.status !== "completed") {
213534
214506
  input2.budgetTracker.markIncomplete("coverage_incomplete");
213535
214507
  }
214508
+ input2.progressDispatcher.emit({
214509
+ type: "agent_completed",
214510
+ traceId: input2.traceId,
214511
+ agent: result.agent,
214512
+ status: result.status,
214513
+ durationMs: durationMsBetween(result.startedAt, result.completedAt),
214514
+ ...result.outputBytes === undefined ? {} : { outputBytes: result.outputBytes },
214515
+ ...result.observedStreamRetries === undefined ? {} : { observedStreamRetries: result.observedStreamRetries },
214516
+ timestamp: new Date().toISOString()
214517
+ });
213536
214518
  }
213537
214519
  await Promise.all(normalizedResults.map((result) => {
213538
214520
  const event = {
@@ -213623,6 +214605,12 @@ async function finalizeModelCallResult(input2) {
213623
214605
  ...thoughtBytes === undefined ? {} : { thoughtBytes },
213624
214606
  ...outputBytes === undefined ? {} : { outputBytes },
213625
214607
  ...outputWarningTriggered === undefined ? {} : { outputWarningTriggered },
214608
+ ...input2.result.observedStreamRetries === undefined ? {} : { observedStreamRetries: input2.result.observedStreamRetries },
214609
+ ...input2.result.discardedRetryMessageBytes === undefined ? {} : {
214610
+ discardedRetryMessageBytes: input2.result.discardedRetryMessageBytes
214611
+ },
214612
+ ...input2.result.firstOutputAt === undefined ? {} : { firstOutputAt: input2.result.firstOutputAt },
214613
+ ...input2.result.lastAcpUpdateAt === undefined ? {} : { lastAcpUpdateAt: input2.result.lastAcpUpdateAt },
213626
214614
  ...input2.result.salvaged === undefined ? {} : { salvaged: input2.result.salvaged },
213627
214615
  ...input2.result.reportedFindings === undefined ? {} : { reportedFindings: input2.result.reportedFindings },
213628
214616
  ...input2.result.findingsTargetExceeded === undefined ? {} : { findingsTargetExceeded: input2.result.findingsTargetExceeded },
@@ -213661,6 +214649,12 @@ async function finalizeModelCallResult(input2) {
213661
214649
  ...thoughtBytes === undefined ? {} : { thoughtBytes },
213662
214650
  ...outputBytes === undefined ? {} : { outputBytes },
213663
214651
  ...outputWarningTriggered === undefined ? {} : { outputWarningTriggered },
214652
+ ...input2.result.observedStreamRetries === undefined ? {} : { observedStreamRetries: input2.result.observedStreamRetries },
214653
+ ...input2.result.discardedRetryMessageBytes === undefined ? {} : {
214654
+ discardedRetryMessageBytes: input2.result.discardedRetryMessageBytes
214655
+ },
214656
+ ...input2.result.firstOutputAt === undefined ? {} : { firstOutputAt: input2.result.firstOutputAt },
214657
+ ...input2.result.lastAcpUpdateAt === undefined ? {} : { lastAcpUpdateAt: input2.result.lastAcpUpdateAt },
213664
214658
  ...input2.result.salvaged === undefined ? {} : { salvaged: input2.result.salvaged },
213665
214659
  ...input2.result.reportedFindings === undefined ? {} : { reportedFindings: input2.result.reportedFindings },
213666
214660
  ...input2.result.findingsTargetExceeded === undefined ? {} : { findingsTargetExceeded: input2.result.findingsTargetExceeded },
@@ -214093,6 +215087,27 @@ function formatMcpResponse(result) {
214093
215087
  };
214094
215088
  }
214095
215089
 
215090
+ // src/mcp/progress.ts
215091
+ function createMcpProgressSink(mcpReq) {
215092
+ const progressToken = mcpReq._meta?.progressToken;
215093
+ if (progressToken === undefined)
215094
+ return;
215095
+ let sequence = 0;
215096
+ return async (event) => {
215097
+ await mcpReq.notify({
215098
+ method: "notifications/progress",
215099
+ params: {
215100
+ progressToken,
215101
+ progress: ++sequence,
215102
+ message: formatMcpProgressMessage(event)
215103
+ }
215104
+ });
215105
+ };
215106
+ }
215107
+ function formatMcpProgressMessage(event) {
215108
+ return formatPlainProgressMessage(event);
215109
+ }
215110
+
214096
215111
  // src/mcp/schemas.ts
214097
215112
  var kyosoReviewRequestSchema = object({
214098
215113
  goal: string2().min(1),
@@ -214141,23 +215156,29 @@ var kyosoReviewRequestSchema = object({
214141
215156
 
214142
215157
  // src/mcp/server.ts
214143
215158
  var KYOSO_MCP_INSTRUCTIONS = "Kyoso is a multi-agent planning and review gate. Use it only when the user explicitly asks for Kyoso, multi-agent review, plan review, security review, CISA Secure by Design review, or diff review. Kyoso does not apply code changes. It returns structured review results and Markdown summaries.";
215159
+ var REVIEW_TOOL_DESCRIPTIONS = {
215160
+ plan_review: "Review an implementation plan before coding. Kyoso does not modify files. Sends MCP progress notifications when the client provides a progressToken.",
215161
+ security_review: "Review a security-sensitive plan, selected files, or diff with CISA Secure by Design gates. Sends MCP progress notifications when the client provides a progressToken.",
215162
+ diff_review: "Review a provided unified diff after implementation. Kyoso does not apply patches. Sends MCP progress notifications when the client provides a progressToken."
215163
+ };
214144
215164
  function createMcpServer(options = {}) {
214145
215165
  const reviewOptions = { ...options, entrypoint: "mcp" };
214146
215166
  const server2 = new McpServer({ name: "kyoso", version: KYOSO_VERSION }, { instructions: KYOSO_MCP_INSTRUCTIONS });
214147
- server2.registerTool("plan_review", {
214148
- description: "Review an implementation plan before coding. Kyoso does not modify files.",
214149
- inputSchema: kyosoReviewRequestSchema
214150
- }, async (request) => formatMcpResponse(await runReview("plan_review", request, reviewOptions)));
214151
- server2.registerTool("security_review", {
214152
- description: "Review a security-sensitive plan, selected files, or diff with CISA Secure by Design gates.",
214153
- inputSchema: kyosoReviewRequestSchema
214154
- }, async (request) => formatMcpResponse(await runReview("security_review", request, reviewOptions)));
214155
- server2.registerTool("diff_review", {
214156
- description: "Review a provided unified diff after implementation. Kyoso does not apply patches.",
214157
- inputSchema: kyosoReviewRequestSchema
214158
- }, async (request) => formatMcpResponse(await runReview("diff_review", request, reviewOptions)));
215167
+ registerReviewTool(server2, "plan_review", reviewOptions);
215168
+ registerReviewTool(server2, "security_review", reviewOptions);
215169
+ registerReviewTool(server2, "diff_review", reviewOptions);
214159
215170
  return server2;
214160
215171
  }
215172
+ function registerReviewTool(server2, tool, reviewOptions) {
215173
+ server2.registerTool(tool, {
215174
+ description: REVIEW_TOOL_DESCRIPTIONS[tool],
215175
+ inputSchema: kyosoReviewRequestSchema
215176
+ }, async (request, ctx) => formatMcpResponse(await runReview(tool, request, {
215177
+ ...reviewOptions,
215178
+ signal: ctx.mcpReq.signal,
215179
+ onProgress: createMcpProgressSink(ctx.mcpReq)
215180
+ })));
215181
+ }
214161
215182
  async function startMcpServer(options = {}) {
214162
215183
  const server2 = createMcpServer(options);
214163
215184
  const transport = new StdioServerTransport;
@@ -214221,17 +215242,49 @@ async function main() {
214221
215242
  if (parsed.command === "plan" || parsed.command === "security" || parsed.command === "diff") {
214222
215243
  const tool = commandToTool(parsed.command);
214223
215244
  const request = await buildReviewRequest(tool, parsed.flags);
214224
- const result = await runReview(tool, request, {
214225
- cwd,
214226
- configPath,
214227
- ignoreConfig,
214228
- trustConfig: trustConfig2,
214229
- allowUnknownConfig,
214230
- configOverrides: configOverrideFlags(parsed.flags),
214231
- entrypoint: "cli",
214232
- promptForTrust: canPromptForConfigTrust()
215245
+ const progressMode = resolveCliProgressMode(stringFlag(parsed.flags, "progress"), process.stderr.isTTY === true);
215246
+ const progressSink = createCliProgressSink(progressMode, (line) => {
215247
+ process.stderr.write(line);
214233
215248
  });
214234
- console.log(booleanFlag(parsed.flags, "json") ? JSON.stringify(result, null, 2) : result.summaryMarkdown);
215249
+ const controller = new AbortController;
215250
+ let interruptCount = 0;
215251
+ const onSigint = () => {
215252
+ interruptCount += 1;
215253
+ if (interruptCount > 1) {
215254
+ process.exit(130);
215255
+ }
215256
+ if (progressMode !== "jsonl") {
215257
+ process.stderr.write(`Cancelling... (press Ctrl-C again to force quit)
215258
+ `);
215259
+ }
215260
+ controller.abort(new KyosoCancellationError("Interrupted by SIGINT."));
215261
+ };
215262
+ process.on("SIGINT", onSigint);
215263
+ try {
215264
+ const result = await runReview(tool, request, {
215265
+ cwd,
215266
+ configPath,
215267
+ ignoreConfig,
215268
+ trustConfig: trustConfig2,
215269
+ allowUnknownConfig,
215270
+ configOverrides: configOverrideFlags(parsed.flags),
215271
+ entrypoint: "cli",
215272
+ promptForTrust: canPromptForConfigTrust(),
215273
+ onProgress: progressSink,
215274
+ signal: controller.signal
215275
+ });
215276
+ console.log(booleanFlag(parsed.flags, "json") ? JSON.stringify(result, null, 2) : result.summaryMarkdown);
215277
+ } catch (error51) {
215278
+ if (!isCancellationError(error51))
215279
+ throw error51;
215280
+ if (progressMode !== "jsonl") {
215281
+ process.stderr.write(`Review cancelled.
215282
+ `);
215283
+ }
215284
+ process.exitCode = 130;
215285
+ } finally {
215286
+ process.off("SIGINT", onSigint);
215287
+ }
214235
215288
  return;
214236
215289
  }
214237
215290
  console.log(HELP);
@@ -214335,13 +215388,20 @@ Usage:
214335
215388
  kyoso setup [codex|claude-code] [--write] [--with-openrouter] [--runner npx|bunx] [--command <command>] [--global] [--force]
214336
215389
  kyoso setup codex|claude-code --skill-only [--write] [--global] [--force]
214337
215390
  kyoso openrouter-acp-smoke
214338
- kyoso plan --goal <text> [--plan <path-or-text>] [--file <path>] [--focus <lens>]... [--set <key>=<value>]... [--json] [--trust-config] [--allow-unknown-config]
214339
- kyoso security --goal <text> [--diff <path>] [--file <path>] [--focus <lens>]... [--set <key>=<value>]... [--json] [--allow-secret-redaction] [--trust-config] [--allow-unknown-config]
214340
- kyoso diff --base main --head HEAD [--focus <lens>]... [--set <key>=<value>]... [--json] [--trust-config] [--allow-unknown-config]
215391
+ kyoso plan --goal <text> [--plan <path-or-text>] [--file <path>] [--focus <lens>]... [--set <key>=<value>]... [--json] [--progress auto|plain|jsonl|off] [--trust-config] [--allow-unknown-config]
215392
+ kyoso security --goal <text> [--diff <path>] [--file <path>] [--focus <lens>]... [--set <key>=<value>]... [--json] [--progress auto|plain|jsonl|off] [--allow-secret-redaction] [--trust-config] [--allow-unknown-config]
215393
+ kyoso diff --base main --head HEAD [--focus <lens>]... [--set <key>=<value>]... [--json] [--progress auto|plain|jsonl|off] [--trust-config] [--allow-unknown-config]
214341
215394
  kyoso doctor [--trust-config] [--allow-unknown-config]
214342
215395
  kyoso init [--force]
214343
215396
  `;
214344
215397
  main().catch((error51) => {
215398
+ if (isCancellationError(error51)) {
215399
+ console.error("Review cancelled.");
215400
+ process.exit(130);
215401
+ }
214345
215402
  console.error(error51 instanceof Error ? error51.message : String(error51));
214346
215403
  process.exit(1);
214347
215404
  });
215405
+ function isCancellationError(error51) {
215406
+ return error51 instanceof KyosoCancellationError || typeof error51 === "object" && error51 !== null && "code" in error51 && error51.code === "REQUEST_CANCELLED";
215407
+ }